Features

Workflow automation

Schedule data updates, send Telegram alerts, run tasks in parallel, and track progress

How do you run research and data updates unattended and hear when something happens? VBT schedules Python jobs for trading research, such as an hourly data update that sends a Telegram alert when a signal fires, and adds parallel tasks, nested progress bars, and a command-line interface.

Update BTCUSDT hourly and alert on moving average crossovers, replayed over one week
def signal_message(data):
    close = data.close.iloc[:-1]  
    fast = vbt.MA.run(close, window=20)
    slow = vbt.MA.run(close, window=50)
    bar = f"{close.index[-1]:%Y-%m-%d %H:%M} BTCUSDT 20h MA crossed"
    if fast.ma_crossed_above(slow).iloc[-1]:
        return f"{bar} above 50h MA at {close.iloc[-1]:,.0f}"
    if fast.ma_crossed_below(slow).iloc[-1]:
        return f"{bar} below 50h MA at {close.iloc[-1]:,.0f}"

class AlertUpdater(vbt.DataUpdater):
    def __init__(self, data, notify, **kwargs):
        vbt.DataUpdater.__init__(self, data, notify=notify, **kwargs)
        self._notify = notify

    def update(self, **kwargs):
        vbt.DataUpdater.update(self, **kwargs)  
        message = signal_message(self.data)
        if message is not None:
            self._notify(message)

data = vbt.BinanceData.pull(
    "BTCUSDT", timeframe="1h", start="2024-11-24", end="2024-12-01 00:01"
)
updater = AlertUpdater(data, notify=print)
for hour in pd.date_range("2024-12-01 01:00", periods=7 * 24, freq="h", tz="UTC"):
    updater.update(end=hour + pd.Timedelta(minutes=1))  
2024-12-01 20:00 BTCUSDT 20h MA crossed above 50h MA at 97,100
2024-12-02 12:00 BTCUSDT 20h MA crossed below 50h MA at 94,972
2024-12-04 07:00 BTCUSDT 20h MA crossed above 50h MA at 96,589
2024-12-06 09:00 BTCUSDT 20h MA crossed below 50h MA at 98,000
2024-12-07 07:00 BTCUSDT 20h MA crossed above 50h MA at 99,405

Five crossovers fired in that week. Each update fetched only the newest bars from Binance, so the same object can run for months without downloading the history again. To run it live, send the messages through a Telegram bot instead of print and let the scheduler call update:

Run the same job every hour and send alerts to Telegram
bot = vbt.TelegramBot(token="YOUR_BOT_TOKEN")  
bot.start(in_background=True)

updater = AlertUpdater(data, notify=bot.send_message_to_all)  
updater.update_every("hour", "01:00")  

Choose a recurring workflow

The same tools can run a data feed, a batch of backtests, or a regular report. Your Python function defines the work, and VBT handles when it runs and how repeated calls are collected:

What you want to automateTools to use
Refresh prices and check the latest signalDataUpdater with a scheduled update and your own signal function.
Keep market data on disk as new bars arriveA data saver with scheduled updates.
Repeat research across symbols, dates, or configurationsTask, execute, or the iterated decorator.
Run a daily analysis and save the resultScheduleManager calling a function that builds and saves your research objects.

Scheduling

vbt.ScheduleManager runs any function on a schedule, with the interval, unit, weekday, and time in plain words, such as every(10, "minutes"), every("hour", "01:00"), or every("monday", "09:30"). Jobs run in a blocking loop or as an asyncio task in the background, which keeps a notebook usable while they run. Tags group jobs so they can be replaced or removed together. vbt.DataUpdater and the data savers on the Data storage page use the same scheduler, so updating a dataset, saving it to disk, and checking signals can share one job.

Run the blocking scheduler in a Python process for an unattended job, or use start_in_background in a notebook with an active event loop and stop it with stop. The process or notebook session keeps the schedule running. For intervals aligned to clock boundaries, zero_offset=True can align recurring jobs, such as checks every ten minutes.

A scheduled function can also rebuild indicators, run a parameter search, and save the resulting portfolios for later inspection. Configuration and persistence shows how to keep inputs, settings, and results together between runs.

Tasks and iteration

vbt.Task packages a function with its arguments without running it, and vbt.execute runs a list of tasks one after another, in threads, or in processes, with a progress bar. @vbt.iterated turns a loop over one argument into a parallel run with labeled results, as the Iterated decorator and Tasks highlights below show. For reproducible random runs, vbt.set_seed seeds Python, NumPy, and Numba. Give independent randomized tasks their own explicit seeds so the experiment records which random inputs each task used.

Use tasks for separate experiments that do not depend on each other's results. Use an iterated function when the same calculation repeats over symbols, time periods, or another argument. The execution and result-merging choices let you keep that calculation as an ordinary Python function. Parallel execution and caching covers the available engines and ways to reuse completed work.

Chain the steps of a job

Data objects also support pipe and chain for steps that depend on earlier outputs. A sequence can pull data, select symbols, add indicators, and pass the result to your own function. This keeps the data preparation you use interactively available to the same scheduled job. The Data pipelines page covers the preparation side.

Progress

Long VBT operations, such as data downloads, parameter sweeps, and task execution, show a progress bar once they run longer than two seconds, and it can be turned off or customized. Bars nest, so a loop over symbols, then parameters, then splits shows one line per level, as the Nested progress bars highlight below shows. Bars drawn from inside parallel workers interfere with each other, so pass show_progress=False to VBT calls that run inside workers, such as when another library parallelizes them.

Command-line workflows

The vbt command runs in a terminal without a Python script. It can ask VBT's documentation assistant a question, search its knowledge base, and start or call the MCP server, as the CLI highlight below shows.

Notifications

vbt.TelegramBot connects to Telegram through python-telegram-bot. Besides sending messages, it can answer commands you define, so a chat can ask the bot for the latest price or a portfolio summary. send_giphy attaches a GIF found by text through GIPHY. The CCXT and Polygon data classes retry requests that fail with network errors, and the TradingView class retries responses it cannot parse. These retries can recover from temporary failures before the configured attempts are exhausted. Custom HTTP integrations can use requests_retry_session to set retry counts, backoff, and retryable status codes.

Alerts can report completed research as well as trading signals. Use your job's results to send a short summary, or define a bot command that reads a saved portfolio when you ask for it. The Live simulation page shows how a portfolio can continue as new data arrives.

CLI

✅ VBT now ships with a Typer-based command-line interface that lets you chat, search, and invoke MCP tools directly from the terminal. No Python script required. After installing the package, you can run the vbt command in your terminal to see all available commands.

Ask VBT a question from the terminal
vbt chat "How to backtest a weekly rebalancing strategy?"  

The first time you run most of these commands, it may take a while to prepare documents. However, most of the preparation steps are cached and stored, so future calls will be much faster and will not require repeating the process.

Iterated decorator

✅ Thinking about parallelizing a for-loop? VBT has a decorator for that.

Emulate a parallelized nested loop to get Sharpe by year and month
import calendar

data = vbt.YFData.pull("BTC-USD")

@vbt.iterated(over_arg="year", merge_func="column_stack", engine="pathos")  
@vbt.iterated(over_arg="month", merge_func="concat")  
def get_year_month_sharpe(data, year, month):  
    mask = (data.index.year == year) & (data.index.month == month)
    if not mask.any():
        return np.nan
    year_returns = data.loc[mask].returns
    return year_returns.vbt.returns.sharpe_ratio()

years = data.index.year.unique().sort_values().rename("year")
months = data.index.month.unique().sort_values().rename("month")
sharpe_matrix = get_year_month_sharpe(
    data,
    years,
    {calendar.month_abbr[month]: month for month in months},  
)
sharpe_matrix.transpose().vbt.heatmap(
    trace_kwargs=dict(colorscale="RdBu", zmid=0),
    yaxis=dict(autorange="reversed")
).show()
Monthly Sharpe ratio heatmap for Bitcoin grouped by year Figure data (JSON)

Tasks

✅ Testing multiple parameter combinations usually involves using the @vbt.parameterized decorator. But what if you want to test entirely uncorrelated configurations or even different functions? The latest addition to VBT lets you execute any sequence of unrelated tests in parallel by assigning each test to a task.

Simulate SL, TSL, and TP parameters in three separate processes and compare their expectancy
data = vbt.YFData.pull("BTC-USD")

task1 = vbt.Task(  
    vbt.PF.from_random_signals,
    data,
    n=100, seed=42,
    sl_stop=vbt.Param(np.arange(1, 51) / 100)
)
task2 = vbt.Task(
    vbt.PF.from_random_signals,
    data,
    n=100, seed=42,
    tsl_stop=vbt.Param(np.arange(1, 51) / 100)
)
task3 = vbt.Task(
    vbt.PF.from_random_signals,
    data,
    n=100, seed=42,
    tp_stop=vbt.Param(np.arange(1, 51) / 100)
)
pf1, pf2, pf3 = vbt.execute([task1, task2, task3], engine="pathos")  

fig = pf1.trades.expectancy.rename("SL").vbt.plot()
pf2.trades.expectancy.rename("TSL").vbt.plot(fig=fig)
pf3.trades.expectancy.rename("TP").vbt.plot(fig=fig)
fig.show()
Expectancy across stop-loss, trailing stop-loss, and take-profit levels Figure data (JSON)

Nested progress bars

✅ Progress bars are now aware of each other. When a new progress bar starts, it checks whether another progress bar with the same identifier has already finished its task. If so, the new progress bar will close itself and delegate its progress to the existing one.

Display progress of three parameters using nested progress bars
symbols = ["BTC-USD", "ETH-USD"]
fast_windows = range(5, 105, 5)
slow_windows = range(5, 105, 5)
sharpe_ratios = dict()

with vbt.ProgressBar(total=len(symbols), bar_id="pbar1") as pbar1:  
    for symbol in symbols:
        pbar1.set_description(dict(symbol=symbol), refresh=True)
        data = vbt.YFData.pull(symbol)

        with vbt.ProgressBar(total=len(fast_windows), bar_id="pbar2") as pbar2:  
            for fast_window in fast_windows:
                pbar2.set_description(dict(fast_window=fast_window), refresh=True)

                with vbt.ProgressBar(total=len(slow_windows), bar_id="pbar3") as pbar3:  
                    for slow_window in slow_windows:
                        if fast_window < slow_window:
                            pbar3.set_description(dict(slow_window=slow_window), refresh=True)
                            fast_sma = data.run("talib_func:sma", fast_window)
                            slow_sma = data.run("talib_func:sma", slow_window)
                            entries = fast_sma.vbt.crossed_above(slow_sma)
                            exits = fast_sma.vbt.crossed_below(slow_sma)
                            pf = vbt.PF.from_signals(data, entries, exits)
                            sharpe_ratios[(symbol, fast_window, slow_window)] = pf.sharpe_ratio
                        pbar3.update()

                pbar2.update()

        pbar1.update()
Symbol 2/2
Fast window 20/20
Slow window 20/20
sharpe_ratios = pd.Series(sharpe_ratios)
sharpe_ratios.index.names = ["symbol", "fast_window", "slow_window"]
sharpe_ratios
symbol   fast_window  slow_window
BTC-USD  5            10             1.063616
                      15             1.218345
                      20             1.273154
                      25             1.365664
                      30             1.394469
                                          ...
ETH-USD  80           90             0.582995
                      95             0.617568
         85           90             0.701215
                      95             0.616037
         90           95             0.566650
Length: 342, dtype: float64

Copyright © 2021–2026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.