Quant Buffet API
Engine API
PortfolioEngine, EngineConfig, the exact fill algorithm, Trade, and BacktestResult.
`backtest.engine` implements a daily, long-only simulator with cash, commission, and slippage. Strategies interact with it almost exclusively through `set_target_weights`.
EngineConfig
python
@dataclass
class EngineConfig:
initial_cash: float = 100_000.0
commission_bps: float = 5.0 # basis points of notional, per fill
slippage_bps: float = 2.0 # basis points of price impact, per sideThe lab always constructs EngineConfig(initial_cash=100_000, commission_bps=5.0, slippage_bps=2.0). Local scripts can override any of the three.
PortfolioEngine
python
config = EngineConfig(
initial_cash=100_000.0,
commission_bps=5.0, # 5 bps of notional per fill
slippage_bps=2.0, # 2 bps price impact per side
)
engine = PortfolioEngine(prices, config)
def on_day(engine: PortfolioEngine, dt: pd.Timestamp) -> None:
engine.set_target_weights(dt, {"SPY": 0.6, "TLT": 0.4})
result = engine.run(on_day, start=pd.Timestamp("2015-01-01"), end=None)
# result.equity, result.holdings, result.trades, result.cash| Member | Type | Description |
|---|---|---|
prices | pd.DataFrame | The panel, sorted by date. |
symbols | list[str] | Panel columns — the only tradable names. |
cash | float | Uninvested cash, updated on every fill. |
positions | dict[str, float] | Share count per symbol (fractional allowed). |
trades | list[Trade] | Every fill, in chronological order. |
config | EngineConfig | Cost assumptions in force. |
set_target_weights(dt, weights) | method | The rebalance entry point. |
run(on_day, *, start, end) | method | Walks the calendar and returns a BacktestResult. |
python
def on_day(engine: PortfolioEngine, dt: pd.Timestamp) -> None:
engine.cash # float: uninvested cash right now
engine.positions # dict[str, float]: share count per symbol
engine.symbols # list[str]: columns of the price panel
engine.config.commission_bps # cost assumptions in force
len(engine.trades) # fills so far
# There is no current_weights() helper — derive it when you need it
px = engine.prices.loc[dt]
mv = {s: engine.positions[s] * float(px[s])
for s in engine.symbols if pd.notna(px[s])}
equity = engine.cash + sum(mv.values())
weights = {s: v / equity for s, v in mv.items()} if equity > 0 else {}set_target_weights: the exact rules
- Symbols not in the panel are dropped, silently. A typo means that leg simply never trades.
- Each weight is clamped with `max(0.0, w)`, so negatives become zero — the engine is long-only.
- If the weights sum to more than 1.0, every weight is divided by the total, normalising the book to exactly 100% invested. Leverage is impossible.
- If the new targets match the previous ones within 1e-6, the call returns immediately — no trades, no cost.
- Current equity is marked as
cash + Σ shares × price, skipping any symbol whose price isNaN. - Target share counts are
equity × weight / price; aNaNor non-positive price yields a target of zero shares. - All sells execute first, freeing cash, then all buys.
- If a buy exceeds available cash, it is scaled down to what the cash can afford after commission.
Fill pricing
python
# Slippage is applied against you on both sides
buy_price = close * (1 + slippage_bps / 10_000) # you pay more
sell_price = close * (1 - slippage_bps / 10_000) # you receive less
notional = shares * fill_price
commission = notional * (commission_bps / 10_000)
# Buy: cash -= notional + commission
# Sell: cash += notional - commissionWith the lab defaults, one full round trip therefore costs about 14 bps: 5 bps commission plus 2 bps slippage on the sell, and the same again on the buy. Positions smaller than 1e-8 shares are snapped to exactly zero to prevent dust accumulating.
run(on_day, *, start=None, end=None)
startandendare keyword-only and inclusive;Nonemeans the panel boundary.- For each date,
on_day(engine, dt)is called first, then the book is marked to that day's close. - Because trading happens at the same close used to mark, the equity curve reflects your new weights immediately.
- Returns a
BacktestResultwhosebenchmarkfield isNone— the caller attaches a benchmark if it wants one.
BacktestResult
| Field | Type | Description |
|---|---|---|
equity | pd.Series | Total portfolio value per date. |
holdings | pd.DataFrame | Share counts per symbol, indexed by date. |
trades | list[Trade] | Every fill with side, shares, price, and commission. |
cash | pd.Series | Cash balance per date. |
benchmark | pd.Series | None | Always None from run(); set it yourself. |
meta | dict | Free-form metadata bag. |
Trade
python
@dataclass
class Trade:
date: str # "YYYY-MM-DD" string, not a Timestamp
symbol: str
side: str # "buy" | "sell"
shares: float # rounded to 6 dp
price: float # after slippage, rounded to 6 dp
value: float # notional, rounded to 2 dp
commission: float # rounded to 4 dppython
# Post-run analysis you can do in a local script
import pandas as pd
tr = pd.DataFrame([t.__dict__ for t in result.trades])
tr.groupby("symbol")["commission"].sum() # cost by symbol
tr.groupby("side")["value"].sum() # gross traded notional
tr["value"].sum() / result.equity.mean() # crude turnover measure