Skip to content

Simulator

Simulator is the single object that turns an intention — whether expressed as a strategy, a target-weights matrix, a pair of entry/exit boolean panels, or a raw order blotter — into an executed Portfolio, and records every fill, order, and intra-bar cost along the way. Four entry points share one execution loop, one set of cost/slippage/timing assumptions, and one result shape, so swapping how you specify the strategy never changes how you read the output.

Why four entry points, not four simulators

In practice, researchers move back and forth between specification styles: a target-weights matrix from an optimiser, then an explicit order list for a what-if, then a BaseStrategy for production. Keeping all four paths on the same simulator core means the execution semantics (fill timing, slippage, fees, cash accounting) are guaranteed to match, so comparing results across specifications is apples-to-apples.

from fundcloud.sim import Simulator, FixedBps, HalfSpread, NextBarOpen

sim = Simulator(
    bars,
    cash=100_000,
    costs=FixedBps(bps=5),
    slippage=HalfSpread(spread_bps=2),
    execution=NextBarOpen(),
)

Entry points

Method Use when
run_strategy(strategy) You have a BaseStrategy. The simulator calls init once, decide per bar, close at the end.
run_weights(weights_df) You have a wide (date × asset) target-weights frame. The simulator rebalances to the target row by row.
run_signals(entries, exits, size=1.0) You have two boolean panels. On entry, allocate size × cash to the asset; on exit, close the position.
run_orders(orders_df) You have an explicit long-format [ts, asset, side, qty] frame. Executes them as-is.

Every entry point returns a SimResult with:

  • portfolio — the post-sim snapshot (Portfolio).
  • trades — long-format executed trades DataFrame.
  • orders — long-format order history (including unfilled).
  • equity_curve — per-bar equity in dollars.
  • .metrics() / .summary() — shortcut to portfolio.summary().

Execution models

  • NextBarOpen (default) — orders fire at the open of bar t+1.
  • NextBarClose — orders fire at the close of bar t+1. Same fill bar as NextBarOpen; just uses the close price instead of the open. Useful for end-of-day desks or when modelling a full bar of participation between signal and execution.

Both are strictly look-ahead-free — the fill bar is always strictly later than the signal bar.

Cost + slippage

  • Costs: FixedBps(bps, minimum), PerShare(rate, minimum), NoCost.
  • Slippage: NoSlippage, HalfSpread(spread_bps).

Both are simple protocols — supply your own class with the matching fee / apply method to get custom behaviour.

Bracket orders

Attach a stop-loss and/or take-profit to any entry Order via the sl_stop / tp_stop fractions and the simulator handles the intra-bar exit check on every subsequent bar — long stops trip on bar.low, take-profits on bar.high (mirrored for shorts), with realistic gap behaviour and the conservative SL-wins-ties default. Forced fills appear in the trades DataFrame tagged reason="stop_loss" or "take_profit". Full reference + worked examples in Bracket orders.

Worked example

from fundcloud.sim import Simulator, FixedBps
from fundcloud.strategies import DCA

result = Simulator(bars, cash=100_000, costs=FixedBps(10)).run_strategy(
    DCA(amount=500, horizon="weekly", weights={"AAPL": 0.5, "MSFT": 0.5})
)

print(result.portfolio.sharpe())
print(result.trades[["ts", "asset", "qty", "price", "fee"]].head())

Feed the same result.portfolio into Tear sheets for polished HTML/PDF/Excel output.