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.
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,405Five 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:
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 automate | Tools to use |
|---|---|
| Refresh prices and check the latest signal | DataUpdater with a scheduled update and your own signal function. |
| Keep market data on disk as new bars arrive | A data saver with scheduled updates. |
| Repeat research across symbols, dates, or configurations | Task, execute, or the iterated decorator. |
| Run a daily analysis and save the result | ScheduleManager 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.
✅ 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.
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.
✅ Thinking about parallelizing a for-loop? VBT has a decorator for that.
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()✅ 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.
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()✅ 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.
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()sharpe_ratios = pd.Series(sharpe_ratios)
sharpe_ratios.index.names = ["symbol", "fast_window", "slow_window"]
sharpe_ratiossymbol 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: float64Related pages
- Performance › Parallel execution and cachingChunk work across threads, processes, or Ray, cache results, and keep memory bounded
- Optimization and validation › Parameter optimizationSweep millions of parameter combinations with grids, conditions, and random search
- Backtesting › Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
- Developer toolsSearch the API, read source, draw reference graphs, and extend classes with factories
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.