Skip to main content

Migrating from vectorbt

You have a vectorbt backtest and want to migrate to MTS1B for live trading.

High-level

vectorbtMTS1B
vbt.Portfolio.from_signals(...)run_single(factor=...)
vbt.IF (indicator factory)@register("f_X") factor function
data.run(...)lake.build_panel(...)
signals.dropdown_above(...)factor returning thresholded ranking
portfolio.stats()BacktestResult metrics
vbt.Portfolio.live(...)mts1b-oms + mts1b-research/live_executor
GPU via cupyGPU via mts1b-GPUbacktester

Example: simple cross-sectional momentum port

In vectorbt

import vectorbt as vbt
import numpy as np
import pandas as pd

# Data
price = vbt.YFData.download(["AAPL", "MSFT", "GOOG", "META"],
start="2014-01-01").get("Close")

# Momentum factor (12-1)
returns_12_1 = price.shift(21) / price.shift(252+21) - 1
signal = returns_12_1.rank(axis=1, ascending=False) <= 2 # top 2 each day

# Portfolio
portfolio = vbt.Portfolio.from_signals(
close=price,
entries=signal,
exits=~signal,
init_cash=100_000,
fees=0.001,
freq="1D",
)

print(portfolio.stats())

In MTS1B

import numpy as np
from mts1b_quantkit.factors import register, zscore_cross_sectional
from mts1b_foundation.market_data import UniversePanel
from mts1b_datalake import lake


# Factor
@register("f_momentum_12_1")
def f_momentum_12_1(panel: UniversePanel, /, h_long: int = 252, h_skip: int = 21) -> np.ndarray:
close = panel.close
ret = close[-h_skip-1] / close[-h_long-h_skip-1] - 1
return zscore_cross_sectional(ret)


# Panel from data lake
panel = lake.build_panel(
universe=["AAPL", "MSFT", "GOOG", "META"],
interval="daily",
start="2014-01-01", end="2024-01-01",
)


# Backtest
from mts1b_GPUbacktester import run_single
from mts1b_quantkit.factors import get

result = run_single(
factor=get("f_momentum_12_1"),
params={"h_long": 252, "h_skip": 21},
universe=["AAPL", "MSFT", "GOOG", "META"],
start="2014-01-01", end="2024-01-01",
rebal="daily",
sizing={"method": "equal_weight_long", "n": 2, "gross": 1.0},
cost_bps=10,
)

print(f"Sharpe: {result.sharpe:.2f}")
print(f"CAGR: {result.cagr:.2%}")
print(f"MaxDD: {result.max_drawdown:.2%}")

Key differences

vectorbt is fully vectorized; MTS1B is bar-driven

vectorbt creates the full (T, A) matrix of all decisions at once and post-processes. MTS1B processes bars sequentially (with vectorized math inside).

For pure backtest performance:

  • vectorbt is slightly faster on small universes
  • MTS1B is faster on large universes (GPU)

For live trading:

  • vectorbt has no native live execution (vectorbt PRO has limited live support, paid)
  • MTS1B has full live OMS + risk + treasury

Indicator factory → factor function

# vectorbt
my_ind = vbt.IndicatorFactory(
class_name="MyIndicator",
input_names=["price"],
param_names=["window"],
output_names=["sma"],
).from_apply_func(lambda price, window: price.rolling(window).mean())

result = my_ind.run(price, window=20)
print(result.sma)

# MTS1B
@register("f_sma")
def f_sma(panel: UniversePanel, /, window: int = 20) -> np.ndarray:
close = panel.close
return np.array([
close[max(0, t-window):t].mean(axis=0) for t in range(len(close))
])

from_signals → factor + sizer

vectorbt's Portfolio.from_signals(entries, exits) is binary in/out. MTS1B factors return continuous rankings; the sizer turns them into weights.

# vectorbt — binary signals
portfolio = vbt.Portfolio.from_signals(close=price, entries=long_entries, exits=long_exits)

# MTS1B — continuous ranking + sizer
@register("f_my_signal")
def f_my_signal(panel, /, **params):
raw = ... # your logic
return zscore_cross_sectional(raw)

# Sizer applies in run_single via sizing={"method": "equal_weight_ls", "n_long": 5, "n_short": 5}

If you have binary signals, set gross=1.0 and use equal_weight_long with n equal to your active position count.

Live trading transition

vectorbt → live requires manual wiring (or vectorbt PRO). MTS1B has the full chain:

# After backtest validation
await StrategyRegistry.register(
StrategySpec(
strategy_id="vectorbt_port_v1",
factor="f_my_signal",
params={},
universe="us-large-cap",
rebal="daily",
sizing="kelly_voltarget_12",
cost_bps=10,
enabled=True,
)
)

# Spin up live executor
await live_executor.run(fund_id="paper-vbt-port",
strategies_from="registry")

Stats

# vectorbt
portfolio.stats()
# Includes: Sharpe, Sortino, MaxDD, Calmar, etc

# MTS1B
result = run_single(...)
result.sharpe
result.sortino # via mts1b_quantkit.metrics.sortino(result.returns)
result.max_drawdown
result.calmar

What MTS1B does better

  • Live trading from day 1 (vectorbt requires PRO)
  • Per-strategy risk envelopes
  • Multi-fund treasury
  • 17-pattern dedupe contract (no scattered HRP / Sharpe variants)
  • 56 doc pages + 10 reference pages

What vectorbt does better

  • Faster on small/medium universes (very-tight inner loops)
  • Built-in plotly visualization
  • More "out of the box" indicators (700+)
  • Smaller install footprint

Migration checklist

  • Convert each indicator into a @register("f_X") factor function
  • Replace Portfolio.from_signals with run_single
  • Replace data fetch with lake.build_panel
  • If you used GPU (cupy directly), use mts1b-GPUbacktester instead
  • If you want live trading, register strategy + use live_executor
  • Set up FundConfig + RiskEnvelope
  • Test on paper

See also