Simulator¶
The fundcloud.sim module is the execution engine: a single Simulator class with four entry points (run_strategy, run_weights, run_signals, run_orders), plus the small protocol classes that govern costs (FixedBps, PerShare, NoCost), slippage (HalfSpread, NoSlippage), and fill timing (NextBarOpen, NextBarClose). All paths return a SimResult that carries the post-run Portfolio, executed Trades, full Order history, and per-bar equity curve. See the Simulator guide for when to pick each entry point.
fundcloud.sim
¶
The simulator engine.
Simulator and SimResult are lazy-loaded to avoid a circular import
against fundcloud.strategies — strategies need Order from this package
and the simulator needs BaseStrategy from fundcloud.strategies.
OrderSide
module-attribute
¶
Direction of an :class:Order.
"buy" opens or adds to a long (and closes / reduces a short).
"sell" opens or adds to a short (and closes / reduces a long).
The simulator routes the same side through both flat→position
opens and position→opposite closes — what matters is the sign of the
resulting position-delta, not whether the trader thinks of it as
"entry" or "exit".
OrderKind
module-attribute
¶
Kind of order. "market" fills at the reference price; "limit" requires a limit_price.
TradeReason
module-attribute
¶
Why a :class:Trade was emitted.
"signal" — a strategy-emitted :class:Order that filled normally.
"stop_loss" — a forced exit synthesised by the simulator's intra-bar
stop-loss check (long stop tripped on bar low, short stop on bar high).
"take_profit" — a forced exit synthesised by the simulator's
intra-bar take-profit check (long TP tripped on bar high, short TP on
bar low).
"trailing_stop" — a forced exit synthesised by the simulator's
intra-bar trailing-stop check (the trail level ratchets in the
favourable direction with each bar's high/low, then triggers like a
fixed stop on the unfavourable side).
When several stops could fire on the same bar, the conservative
arbitration is: any stop (fixed or trailing) beats take-profit; among
sl_stop and tsl_stop the tighter effective level wins (the
one that's closer to current price). The simulator records the source
so analytics can split realised P&L between discretionary exits,
defensive stop-outs, profit-taking, and trail-following.
Simulator
¶
Simulator(
data: Backend | DataFrame,
*,
costs: CostModel | None = None,
slippage: SlippageModel | None = None,
cash: float = 1000000.0,
execution: ExecutionModel | None = None,
)
Discrete-time backtest engine.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Backend | DataFrame
|
A :class: |
required |
costs
|
CostModel | None
|
Swap-in friction/execution models. Defaults: :class: |
None
|
slippage
|
CostModel | None
|
Swap-in friction/execution models. Defaults: :class: |
None
|
execution
|
CostModel | None
|
Swap-in friction/execution models. Defaults: :class: |
None
|
cash
|
float
|
Starting cash balance. |
1000000.0
|
Examples:
Drive a weekly DCA on a synthetic single-asset Bars frame:
>>> import pandas as pd
>>> from fundcloud.sim import Simulator
>>> from fundcloud.strategies import DCA
>>> bars = pd.DataFrame({ # tiny, flat-price dummy
... ("open", "SPY"): 100.0,
... ("high", "SPY"): 100.0,
... ("low", "SPY"): 100.0,
... ("close", "SPY"): 100.0,
... ("volume", "SPY"): 1_000.0,
... }, index=pd.date_range("2024-01-02", periods=60, freq="B"))
>>> bars.columns = pd.MultiIndex.from_tuples(bars.columns)
>>> result = Simulator(bars, cash=10_000.0).run_strategy(
... DCA(100.0, horizon="weekly", weights={"SPY": 1.0}),
... )
>>> result.equity_curve.iloc[-1] >= 0.0
True
Source code in python/fundcloud/sim/simulator.py
run_orders
¶
Execute an explicit long-format orders DataFrame.
Optional columns sl_stop / tp_stop attach intra-bar
stop-loss / take-profit fractions to each order. They are
honoured by the dispatcher (Python fallback today; Rust kernel
once parity lands).
Source code in python/fundcloud/sim/simulator.py
run_signals
¶
Convert boolean entry/exit panels into market orders.
size is a fraction of current cash to allocate per entry.
.. note::
Signal panels emit market orders without attached brackets.
For intra-bar stop-loss / take-profit support, drive the
same logic through :meth:run_strategy with a custom
:class:BaseStrategy that emits Order(..., sl_stop=...,
tp_stop=...).
Source code in python/fundcloud/sim/simulator.py
run_strategy
¶
Drive a :class:BaseStrategy bar by bar and return a :class:SimResult.
The strategy sees one :class:~fundcloud.strategies.base.Context per
bar and returns zero or more :class:~fundcloud.sim.Order instances.
The simulator applies the execution / cost / slippage models and
updates a single live :class:~fundcloud.portfolio.Portfolio.
Source code in python/fundcloud/sim/simulator.py
run_weights
¶
At each row of target_weights, rebalance toward those weights.
target_weights is a dense DataFrame indexed by timestamp with
one column per asset; missing values are forward-filled.
Source code in python/fundcloud/sim/simulator.py
SimResult
dataclass
¶
SimResult(
portfolio: Portfolio,
trades: DataFrame,
orders: DataFrame,
equity_curve: Series,
bars: DataFrame,
)
Output of :meth:Simulator.run_*.
Examples:
>>> # Given ``result = Simulator(bars).run_strategy(strategy)`` —
>>> # ``result.pf`` is a shortcut for ``result.portfolio``:
>>> # result.pf.sharpe()
>>> # result.pf.max_drawdown()
Order
dataclass
¶
Order(
ts: Timestamp,
asset: str,
side: OrderSide,
qty: float | None = None,
notional: float | None = None,
kind: OrderKind = "market",
limit_price: float | None = None,
sl_stop: float | None = None,
tp_stop: float | None = None,
tsl_stop: float | None = None,
)
Instruction to trade. Frozen so strategies can hand one up safely.
Exactly one of qty or notional must be set (qty wins if
both are). qty is unsigned; the :attr:side field carries
direction. The :class:~fundcloud.sim.Simulator resolves
notional-only orders to a quantity at fill time using the
reference price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ts
|
Timestamp
|
Timestamp at which the strategy emitted the order. |
required |
asset
|
str
|
Asset ticker. |
required |
side
|
OrderSide
|
|
required |
qty
|
float | None
|
Unsigned share count. Mutually exclusive with |
None
|
notional
|
float | None
|
Unsigned dollar amount; the simulator divides by the fill price
at execution time. Mutually exclusive with |
None
|
kind
|
OrderKind
|
|
'market'
|
limit_price
|
float | None
|
Price ceiling (buy) or floor (sell). Required when
|
None
|
sl_stop
|
float | None
|
Stop-loss attached to the entry, expressed as a fraction in
|
None
|
tp_stop
|
float | None
|
Take-profit attached to the entry, expressed as a fraction
|
None
|
tsl_stop
|
float | None
|
Trailing stop-loss attached to the entry, expressed as a
fraction in On accumulating entries, the existing trail is retained —
the high-water mark continues ratcheting from the first
entry's price, regardless of the new entry's fill price or
Forced exits tag :attr: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither |
Examples:
>>> import pandas as pd
>>> from fundcloud.sim import Order
>>> Order(ts=pd.Timestamp("2024-01-02"), asset="SPY", side="buy", qty=10.0)
Order(ts=Timestamp('2024-01-02 00:00:00'), asset='SPY', side='buy', qty=10.0, ...)
Bracket order — long with 5% stop-loss and 10% take-profit:
>>> Order(
... ts=pd.Timestamp("2024-01-02"), asset="SPY", side="buy",
... qty=10.0, sl_stop=0.05, tp_stop=0.10,
... )
Order(ts=..., sl_stop=0.05, tp_stop=0.1, ...)
Full bracket — fixed stop-loss, take-profit, and trailing stop on the same entry. The fixed SL caps the worst-case loss at entry, the take-profit caps the best-case gain, and the trail rides the middle:
>>> Order(
... ts=pd.Timestamp("2024-01-02"), asset="SPY", side="buy",
... qty=10.0, sl_stop=0.10, tp_stop=0.30, tsl_stop=0.05,
... )
Order(ts=..., sl_stop=0.1, tp_stop=0.3, tsl_stop=0.05)
signed_qty
¶
Position-delta: positive for buys (long-bias), negative for sells (short-bias).
Returns the change this order applies to the asset's position
when it fills — +qty for "buy", -qty for "sell".
A short-cover order (buy on top of a short) still has a positive
signed quantity; the resulting net position is what indicates
a close.
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in python/fundcloud/sim/orders.py
with_qty
¶
Return a new :class:Order with qty set and notional cleared.
Useful when the simulator resolves a notional-only order to an explicit share count using the fill price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
qty
|
float
|
Unsigned share count for the resolved order. |
required |
Returns:
| Type | Description |
|---|---|
Order
|
Copy of |
Source code in python/fundcloud/sim/orders.py
Trade
dataclass
¶
Trade(
order: Order,
ts: Timestamp,
asset: str,
qty: float,
price: float,
fee: float = 0.0,
slippage_bps: float = 0.0,
reason: TradeReason = "signal",
)
A filled :class:Order. The portfolio applies these to mutate state.
Trades are the simulator's output unit: each one books a quantity of an asset at a fill price, charges a fee against cash, and records the slippage applied vs the reference price.
Attributes:
| Name | Type | Description |
|---|---|---|
order |
Order
|
The original :class: |
ts |
Timestamp
|
Timestamp at which the fill executed. |
asset |
str
|
Asset being traded (mirrors |
qty |
float
|
Signed quantity. Positive for buys, negative for sells. |
price |
float
|
Fill price after slippage is applied. |
fee |
float
|
Commission / exchange fee charged to cash. Always non-negative. |
slippage_bps |
float
|
Slippage applied vs the reference price, in basis points
(positive number). |
reason |
TradeReason
|
Why the trade fired — see :data: |
notional
property
¶
Signed dollar value of the fill: qty * price.
Positive for buys (cash outflow), negative for sells (cash
inflow). Note this excludes fees — the simulator subtracts
fee from cash separately.
CostModel
¶
Bases: Protocol
Fee charged to cash for a fill.
Implement fee(price, qty) returning a non-negative dollar
amount. The simulator subtracts the result from cash on every fill.
fee
¶
Return the commission for a fill of qty shares at price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price
|
float
|
Fill price (after slippage). |
required |
qty
|
float
|
Signed quantity. Implementations should treat the magnitude; buys and sells are charged symmetrically. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Non-negative dollar fee. |
Source code in python/fundcloud/sim/costs.py
FixedBps
dataclass
¶
Proportional-to-notional fee, in basis points (1 bp = 0.01 %).
The :class:~fundcloud.sim.Simulator default. Charges
max(minimum, |price * qty| * bps * 1e-4) per fill, so a 5 bps
model on a $10,000 trade costs $5.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bps
|
float
|
Basis points charged on notional. Default |
5.0
|
minimum
|
float
|
Floor in dollars. Useful when modelling broker minimums.
Default |
0.0
|
Examples:
>>> from fundcloud.sim import FixedBps
>>> FixedBps(bps=10).fee(price=100.0, qty=50.0)
5.0
>>> FixedBps(bps=5, minimum=1.0).fee(price=10.0, qty=1.0) # tiny trade hits the floor
1.0
PerShare
dataclass
¶
Flat per-share commission (typical US-equity broker pricing).
Charges max(minimum, |qty| * rate) per fill — independent of
price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Dollars per share. Default |
0.005
|
minimum
|
float
|
Floor in dollars. Default |
1.0
|
Examples:
>>> from fundcloud.sim import PerShare
>>> PerShare(rate=0.005).fee(price=50.0, qty=400.0)
2.0
>>> PerShare(rate=0.005, minimum=1.0).fee(price=50.0, qty=10.0) # below floor
1.0
NoCost
dataclass
¶
SlippageModel
¶
Bases: Protocol
Adjust the raw reference price into an achievable fill price.
apply
¶
Return (fill_price, slippage_bps).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price
|
float
|
Reference price from the :class: |
required |
side
|
Literal['buy', 'sell']
|
|
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
|
Source code in python/fundcloud/sim/slippage.py
HalfSpread
dataclass
¶
Pay half the bid-ask spread (in basis points) on every fill.
Buys execute at price * (1 + half_spread_bps * 1e-4); sells at
the symmetric discount. The recorded slippage is half the spread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spread_bps
|
float
|
Full bid-ask spread in basis points. Default |
2.0
|
Examples:
>>> from fundcloud.sim import HalfSpread
>>> HalfSpread(spread_bps=10.0).apply(price=100.0, side="buy")
(100.05, 5.0)
>>> HalfSpread(spread_bps=10.0).apply(price=100.0, side="sell")
(99.95, 5.0)
NoSlippage
dataclass
¶
ExecutionModel
¶
Bases: Protocol
Decide when an order submitted at bar t fills.
fill_at
¶
Resolve the bar index where an order fills.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_index
|
int
|
Integer position of the bar that emitted the order. |
required |
bars_index_size
|
int
|
Total number of bars in the simulator's index. |
required |
Returns:
| Type | Description |
|---|---|
int or None
|
Either |
Source code in python/fundcloud/sim/execution.py
reference_price
¶
Return the reference price for a fill at fill_index.
The simulator hands this price to the
:class:~fundcloud.sim.SlippageModel, which nudges it to the
achievable fill price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bars
|
DataFrame
|
Bars frame ( |
required |
fill_index
|
int
|
Bar index returned by :meth: |
required |
asset
|
str
|
Asset ticker. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Reference price (typically open or close of the fill bar). |
Source code in python/fundcloud/sim/execution.py
NextBarOpen
dataclass
¶
Orders submitted at bar t fill at the open of bar t + 1.
The honest default: a strategy that decides on bar t's close
(using bar-t prices and history) gets the next available
executable price, with no look-ahead. Orders emitted on the final
bar can't fill — the simulator records them as filled=False.
Examples:
>>> from fundcloud.sim import NextBarOpen
>>> NextBarOpen().fill_at(signal_index=4, bars_index_size=10)
5
>>> NextBarOpen().fill_at(signal_index=9, bars_index_size=10) is None
True
NextBarClose
dataclass
¶
Orders submitted at bar t fill at the close of bar t + 1.
Like :class:NextBarOpen but uses the close of the fill bar as
the reference price — convenient when modelling end-of-day desks
or a full bar of participation between signal and execution.
Look-ahead-free: the fill bar is strictly later than the signal
bar. Orders emitted on the final bar can't fill — the simulator
records them as filled=False.
Examples:
>>> from fundcloud.sim import NextBarClose
>>> NextBarClose().fill_at(signal_index=4, bars_index_size=10)
5
>>> NextBarClose().fill_at(signal_index=9, bars_index_size=10) is None
True