06 — Paper Trading Engine¶
1. Purpose and honesty standard¶
The paper trading engine exists to answer one question: would this system have made money, after everything that costs money? A simulator that fills every order at the last printed price at zero cost answers a different and useless question, and produces track records that evaporate on contact with a real broker.
The design standard is therefore pessimistic realism: where a modelling choice is uncertain, choose the assumption less favourable to the strategy. An overstated backtest is not a neutral error — it is the error that causes real money to be lost later.
The engine also implements the interface a live broker adapter would satisfy in Phase 5, so the strategy code path is identical. But the live implementation does not exist. There is no LiveBroker class, no credential storage for one, and no order-routing code. See 12.
2. Account model¶
Double-entry, Decimal throughout, no floats anywhere in the ledger.
Account
├── cash_balance settled cash
├── pending_settlement[] proceeds not yet settled (T+1)
├── positions[]
│ ├── instrument_id
│ ├── quantity fractional supported
│ ├── lots[] per-lot cost basis, acquisition date → tax lot + holding period
│ ├── avg_cost
│ └── realised_pnl
├── orders[] full state machine history
├── transactions[] append-only, hash-chained
└── purification_liability accrued per [05 §6]
Invariants, enforced as property-based tests with Hypothesis rather than as examples:
- Cash + market value of positions + pending settlement = total equity, at every point in time.
- Position quantity is never negative. Shorting is not "disallowed by a check" — the fill engine has no code path that produces a negative quantity, and attempting one is a domain error.
- Every transaction references the order that caused it, and every order references its recommendation or manual origin.
- The transaction log is append-only and hash-chained; corrections are compensating entries, never mutations.
Cash earns 0%. This is a compliance requirement (05 §7), and it is also a meaningful drag that an honest simulation must include. A strategy that holds 40% cash for a year is genuinely giving up nothing in this model — and that is the correct representation of the constraint the user actually lives under.
3. Order lifecycle¶
CREATED → VALIDATED → ACCEPTED → [PARTIALLY_FILLED] → FILLED
│ │
├── REJECTED ├── CANCELLED
└── EXPIRED └── EXPIRED
Validation, in order, with the first failure terminal:
- Compliance — the instrument's current verdict must be
COMPLIANT. A stale verdict triggers recomputation before proceeding. - Risk — position size, exposure, and loss-limit checks per 07. The risk engine can reject or reduce.
- Buying power — settled cash only. Trading on unsettled proceeds is a form of credit and is not simulated.
- Instrument state — halted, delisted, or outside session hours.
- Sanity — limit price within a plausible band of last trade, quantity within liquidity bounds.
Supported types: market, limit, stop, stop-limit, and trailing stop. Time in force: DAY, GTC, IOC, FOK. No margin types, no short types.
4. Fill simulation¶
This is where realism is won or lost.
Timing¶
Orders are matched against subsequent bars, never the bar that triggered the decision. An order created from a signal computed on the 2026-08-03 close is eligible from the 2026-08-04 open. This one rule removes the single most common backtest fantasy.
Price¶
| Order type | Simulated fill |
|---|---|
| Market | Next bar's open, plus spread cost, plus market impact |
| Limit (buy) | Fills only if the bar's low ≤ limit; fill at min(limit, open) |
| Limit (sell) | Fills only if the bar's high ≥ limit; fill at max(limit, open) |
| Stop | Becomes market when the bar's range crosses the stop; fills at the stop price plus gap slippage — and if the bar gapped through the stop, at the open, which is the realistic and unfavourable outcome |
Intraday bars are used where available; daily-only data uses conservative OHLC bounds and the engine records that fills are lower-fidelity.
Spread¶
Half-spread charged on entry and exit, estimated per instrument from its liquidity bucket, with a floor. Illiquid names are charged materially more, which is what actually happens.
Market impact¶
Square-root law, the standard empirical form:
with c ≈ 0.5–1.0 calibrated per venue class, σ_daily the instrument's realised volatility, and ADV_20 its 20-day average dollar volume. The practical consequence is that the simulator makes size expensive in small caps, which correctly discourages strategies whose backtested returns depend on trading more size than the market can absorb.
Participation cap¶
An order may not consume more than a configured share of a bar's volume (default 10%). Excess is either carried to the next bar or cancelled per the order's TIF. Without this cap, a backtest can "buy" a company's entire day of volume, which is both impossible and the source of some spectacular fictional returns.
Partial fills¶
Modelled for limit orders in thin instruments, driven by the participation cap and the traded volume at or through the limit.
Latency¶
A configurable decision-to-market delay (default 1 second live, one bar in backtest). For a daily-horizon system this is nearly immaterial, and it is modelled anyway so the assumption is explicit rather than hidden.
5. Costs¶
| Cost | Model |
|---|---|
| Commission | Per-share, per-trade, or percentage — configurable per venue profile; supports zero-commission with a wider effective spread, which is the honest representation of payment-for-order-flow economics |
| Spread | Per §4 |
| Impact | Per §4 |
| Exchange / regulatory fees | Per-venue schedule where applicable |
| FX | Spread applied on cross-currency settlement, plus a configurable conversion fee |
| Custody / platform | Optional periodic fee |
| Interest | Never earned. Any modelled interest is routed to purification |
Every cost is recorded as a distinct transaction line, so the Performance screen can show gross return, cost drag decomposed by type, and net return. Cost drag is one of the most under-appreciated numbers in retail investing, and showing it prominently is a deliberate product decision.
6. Corporate actions¶
Processed on ex-date from the corporate_action table, and this is one of the highest-bug-density areas in any trading system:
| Action | Handling |
|---|---|
| Cash dividend | Cash credited on pay date; purification accrued at the instrument's rate; dividend recorded separately from price return so total return decomposes correctly |
| Stock dividend / split | Quantity and per-lot cost basis adjusted; holding period preserved on each lot |
| Reverse split | Same, with fractional handling per the configured policy (cash-in-lieu by default) |
| Merger — cash | Position closed at terms, realised P&L booked, holding period recorded |
| Merger — stock | Position converted at the exchange ratio, basis carried over per lot |
| Spinoff | Basis allocated across parent and spun entity by relative market value; the spun entity is immediately compliance-screened, since it may not inherit the parent's status |
| Delisting | Position marked, valued at last reliable price, flagged for user resolution rather than silently zeroed |
| Symbol change | Reference data updated; the instrument's FIGI-based identity is unchanged, which is why identity is not the ticker |
Open orders are cancelled and reissued at adjusted prices on split ex-dates. A stop at $100 on a 4:1 split must become $25, and forgetting this liquidates positions in a backtest for no reason.
7. Analytics¶
Computed daily and on demand, all point-in-time reproducible:
Return — time-weighted return (strategy skill, flow-independent) and money-weighted/IRR (the user's actual experience). Both shown, because they diverge and each answers a different question. Daily, monthly, since-inception, and net-of-purification.
Risk — realised volatility, downside deviation, max drawdown and current drawdown, drawdown duration, Ulcer index, VaR and CVaR at 95/99 (historical and parametric), beta and downside beta vs benchmark.
Risk-adjusted — Sharpe, Sortino, Calmar, information ratio vs a Shariah-compliant benchmark. Note that conventional Sharpe subtracts a risk-free rate; since the user's actual alternative is 0% cash, the engine reports Sharpe against zero by default and documents this clearly. Using T-bill yields as the baseline would be internally inconsistent with a portfolio that cannot hold T-bills.
Behavioural — hit rate, average win vs average loss, profit factor, expectancy per trade, average holding period, turnover, time in market.
Attribution — by instrument, sector, geography, and by originating agent (via the SHAP contribution recorded on each recommendation). Agent-level attribution is the input to the learning loop and the most genuinely novel number in the product: it answers "which of my analysts is actually any good?"
Benchmarking — against a Shariah-compliant index where licensing permits, otherwise against a transparently constructed internal compliant benchmark, plus a naive equal-weight compliant-universe portfolio. The naive benchmark is the important one: it is the honest bar the whole system must clear to justify itself.
8. Simulation fidelity levels¶
Declared on every result, because a number's meaning depends on how it was produced:
| Level | Data | Use |
|---|---|---|
| L1 | Daily OHLCV, estimated spreads | Fast screening, long backtests |
| L2 | Intraday bars, per-instrument spread estimates, full impact model | Reference. The default for anything reported |
| L3 | Quote-level replay where data permits | Validation of L2 assumptions on a sample |
The gap between L1 and L2 results on the same strategy is itself a diagnostic: a strategy whose returns collapse under better fill modelling was never real.
9. What this engine deliberately cannot do¶
- Short, borrow, or use margin — no code path exists.
- Trade instruments that are not
COMPLIANTat order time. - Earn interest.
- Fill on the same bar that generated the signal.
- Trade delisted instruments or backfill a position into the past.
- Reach a real brokerage. There is no adapter, no credential store, and no network route to one in Phases 1–4.