Concept · The simulator
The set of dimensions on which the backtest accounting in this repo matches — and does not match — what a real account on a real perp venue would experience for the same trade signals.
When the leaderboard reports a strategy's return, that number is what the simulator's accounting produced. Whether a real perp account, fed the identical entry/exit signals, would have produced the same outcome depends on which aspects of "real trading" the simulator faithfully models and which it does not. This concept catalogs both columns so headline numbers can be interpreted correctly.
The short version, which the rest of this page expands:
Funding is the load-bearing gap. Funding penalizes hold time — the longer you sit in a position, the more funding ticks you pay. And the only rows in this dossier with any measurable edge are the long-hold 4h and 1h swing strategies (slow timeframes, positions held for many hours or days). Those are exactly the rows a missing funding model flatters most: the simulator lets them hold for free, when a real account would bleed funding the whole time. So the strategies that look best are the ones whose results are most inflated by the gap.
| Aspect | Where | Notes |
|---|---|---|
| Per-(strategy, symbol) equity ledger | apps/backend/src/evaluation/equity-ledger.ts:14 |
Each pair starts at initialEquity ($200), evolves via apply(netPnl) on each closed trade. Independent per pair. |
| Equity-scaled position sizing | evaluator.ts:1340-1342 |
positionSizeUsd = equity × positionSizePct/100 in equityMode. Losses shrink next position; wins grow it. Real compounding. |
| Soft margin floor | evaluator.ts:1343-1379, MIN_POSITION_USD = $1 |
If computed position < $1, the entry signal is written with fillStatus: 'margin_insufficient' and no position opens. Triggers when equity drops below $50. |
| Liquidation distance | evaluator.ts:143-158 |
At leverage L, liquidation triggers on a 1/L adverse intra-candle move (long: low ≤ entry × (1 − 1/L); short: high ≥ entry × (1 + 1/L)). |
| Trading fees | feeBps on both entry and exit legs (packages/shared/src/index.ts:304-318) |
A flat 0.17% per leg (charged on entry AND exit), applied to gross PnL. This is the simulator's only modeled friction. |
| Position lifecycle | evaluation/position/position.ts |
Entry → exit (signal or TP/SL or liquidation), with frozen entry primitives. |
A common worry is that the simulator might "ignore" leverage. It does not. Leverage is applied correctly: the pure helper computePnl (packages/shared/src/index.ts:304-318) takes its size argument as the leveraged notional (the full position size, not the cash margin behind it), and the leverage multiply happens at the caller boundary in apps/backend/src/evaluation/position/position.ts:198. A position at higher leverage produces proportionally larger P&L on the same price move, as expected.
The subtle and important fact is what leverage does not touch: the set of trades that fire. Leverage scales the size of each win and loss, and therefore the drawdown — but it never moves when the EMA crossover triggers, so it never changes which trades happen.
The 1×≡2× identity pair (from this dossier) proves it. Two strategies in run 83 are the same EMA 21/50 SOL 1h long, differing only in leverage:
They are the same 436 trades: identical 139 wins and 297 losses, identical 31.9% win rate (the share of trades that closed in profit). Leverage changed only the drawdown (the deepest peak-to-trough equity drop): −5.06% at 1× versus −9.9% at 2×. Same trades, scaled losses. This is the cleanest demonstration in the fleet that leverage is a magnitude knob, not a selection knob — it never altered the strategy's actual decisions, only how hard each decision hit the account.
Real perps progressively deleverage as equity approaches the maintenance margin floor (typically 0.5–2% of notional remaining). Simulator triggers only the binary 1/L liquidation rule.
Perps charge or credit funding (a periodic payment between long and short holders of a perpetual future) on a regular cadence, typically every 8 hours. Direction depends on the perp-vs-spot price gap. The simulator charges zero funding cost.
This is the single most consequential omission in the whole dossier, and the reason is structural. Funding penalizes hold time — every funding tick a position sits through is another payment. The only strategies in the EMA fleet with any measurable edge are the long-hold 4h and 1h swings, which hold positions for many hours or days and so would absorb the most funding. So the missing funding model flatters exactly the rows that look best. A fast scalp that's in and out within a single funding window barely cares about funding; a multi-day swing cares enormously — and the swings are the dossier's headline performers. Read every edge in this dossier knowing its real-account version would be dragged down by funding the simulator never charged.
The backtest fills at the candle's close price with no slippage (no gap between the expected and actual fill price) and no maker/taker split (it does not distinguish the cheaper "maker" fee for adding liquidity from the pricier "taker" fee for removing it). Both push real-account costs above what the simulator charges, in the same direction as funding.
Refined 2026-06-29 — maker/taker matters most for scalpers. The maker/taker spread (~3 bps round-trip) is load-bearing only for strategies with small per-trade targets — a scalper hunting a 0.2% move keeps 0.08% taker-both-ways vs 0.16% maker-both-ways, an effective doubling of profit. For the 1h–4h swing strategies, where winning trades move 2–10%+, the spread is noise. No hosted perp-capable competitor currently models this either (as of 2026-06-10 market research) — it is not a catch-up gap. The higher-priority gap for this user base is funding simulation (section 3 above), which penalizes the exact long-hold swing rows that look best in the current results. See
growth/research/market-competitors.md §4and maker taker fees.
Refined 2026-06-29 — slippage is NOT missing; it is bypassed. A working VWAP (volume-weighted average price) depth-walk model already exists in
apps/backend/src/orderbook/fill-calculator.ts— it walks real order-book levels and computes realistic slippage. It is called on thesource === 'live'branch infill-resolver.ts, but live strategy evaluation has been inert since Phase 213. Every backtest result returnsslippageBps: 0. The machinery is not absent — it is dead code on the live path. See.planning/threads/cost-model-fidelity.md.
Refined 2026-06-29 — real historical order-book replay is not possible, but parametric simulation is. No historical L2 (level-2 order book — the full bids/asks ladder at every timestamp) data was ever captured, so the VWAP model cannot replay history. However, a three-rung parametric ladder using only the OHLCV candle data already in Postgres can adequately model slippage without any new data pipeline. The dead VWAP model becomes the calibration oracle: collect live books going forward, fit the
kparameter, apply it historically. The three rungs: (1) config-knob flat rate — kills zero-slippage in an afternoon; (2) volatility-scaled (k × candleRange/close) — charges more on the wide breakout candles where EMA-cross entries cluster; (3) Almgren square-root market impact (k × σ × √(orderNotional / candleVolumeNotional)) — size-aware, industry-standard, uses candle volume already stored. Build Rung 1 now; target Rung 3.
Refined 2026-06-29 — the three-path model: backtest / forward-test / calibration. The
OrderBookStore+calculateFill/ VWAP depth-walk machinery inapps/backend/src/orderbook/was built for the now-retired live trading path. It will be reused for forward-testing (future feature: paper-trading against a live market feed without real orders). It is not relevant to backtesting. The three paths use different slippage models by design:
Path Data source Slippage model Backtest (historical) OHLCV candles Parametric — Almgren uses candleVolumeNotionalForward-test (future) OrderBookStore+@depth20WScalculateFill/ VWAP depth walk — the real bookCalibration bridge Forward-test fills Fit Almgren kfrom real fills → retro-apply to historicalThe candle-based Almgren model is the right tool for historical backtesting even if full L2 history existed. Real L2 replay is path-dependent — order book state at any moment is the cumulative result of every prior trade in the session; reconstructing it from snapshots is lossy. Almgren treats
candleVolumeNotionalas a liquidity proxy — deliberately coarser, but auditable, reproducible, and calibratable. TheOrderBookStore's own 5-second stale threshold is a hard design signal: it was built to fail gracefully when the live feed goes cold, a property that makes no sense in a replay context. See slippage.
Refined 2026-06-29 — zero-slippage bias compounds with leaderboard selection. Slippage is paid per fill, so it penalises high-turnover strategies most. The leaderboard crowns the best of many spawned variants — the winner is disproportionately high-frequency, high-fill-count. That is exactly the cohort zero-slippage inflates most. Adding realistic slippage will reorder the leaderboard, knocking down churners toward low-turnover survivors. That reordering is the honest result, not a side effect — the same epistemological move as the significance classifier (boring low-turnover survivor beats thrilling churner that only wins when fills are free). See slippage and
.planning/threads/cost-model-fidelity.md.
Refined 2026-06-22 — funding does NOT flatter the buy-and-hold comparison; it cuts the other way. A tempting argument is "we don't charge funding, so we're unfairly inflating buy-and-hold." It's backwards. The buy-and-hold benchmark (buy and hold) is a spot-like 1× hold, which pays no funding in reality — so its number is honest. The strategies hold perp positions across funding ticks but are charged nothing, so in reality a leveraged perp strategy held through a hot-funding bull would do worse than the sim shows, while spot hold is unchanged. Net: real funding widens the gap against hold-heavy strategies, not narrows it. The funding gap is also non-uniform — it scales with average holding time, hitting slow trend-followers hardest and fast scalpers least. The "missing variable" that actually makes the buy-and-hold comparison less stark is risk adjusted return (hold's hidden 70–95% drawdown), not funding. Full reasoning:
wiki/qa-sessions/2026-06-22-session.md#q1; concept: funding rate.
The ultimate resolution to the fill-ambiguity problem in §3a is tick data (every individual exchange trade, timestamped to the millisecond) instead of per-candle OHLCV aggregates. The tradeoff is stark.
| OHLCV candles (current) | 1-minute sub-bars | Tick data | |
|---|---|---|---|
| TP/SL simultaneous trigger | ❌ Unresolvable | ✅ Resolves ~95% of cases | ✅ Exact |
| Limit-order fill timing | ❌ Unknown within candle | ✓ Within 1-minute window | ✅ Millisecond-precise |
| Liquidation sequencing | ❌ Per-candle only | ✓ Per-minute | ✅ Exact |
| Realistic slippage | ❌ Not modeled | ❌ Not modeled | ❌ Still requires L2 order book depth (different dataset) |
| Storage (20 symbols, 6 years) | ~100 MB | ~6 GB | ~1 TB |
| Compute multiplier vs 1h | 1× | ~60× | ~42,000× |
| Infrastructure required | Postgres | Postgres | ClickHouse / QuestDB class |
Key nuance: tick data does not solve slippage. Tick data shows the price path — every price a trade occurred at. It does not show the order book depth — how much liquidity existed at each level. Realistic slippage modeling requires L2 snapshots (the full bid/ask ladder at every point in time), a fundamentally different and larger dataset that no hosted perp backtest tool currently maintains.
Verdict for this fleet: the 1h/4h swing strategies with measured edge have targets of 2–10%. Intra-candle fill timing ambiguity is noise at that scale. Tick data earns its cost only for sub-1m scalpers or strategies with very tight TP/SL that frequently both land inside a single candle. The practical middle ground is 1-minute sub-bars for the fill-resolution pass — resolves TP/SL ordering at ~60× current storage, no specialized infrastructure. See tick data.
Resolved in phase 121.1 — see bankruptcy. The simulator now enforces a configurable
equityBankruptcyFloor(default$0, with percent-of-initial-equity support) per(strategyId, symbol). A closing trade whose debit crosses the floor is taggedexit_reason: 'bankruptcy', and subsequent entry signals are recorded withfillStatus: 'account_bankrupt'. The original gap description below is retained as historical record.
The $1 floor blocks new entries when 2% of equity drops below $1, but it does not prevent equity from being driven negative by debits from already-open positions or by accumulated losses. A real perp account would have been force-closed long before equity reached zero.
Each (strategy, symbol) pair has its own independent $200. No shared bankroll, no correlated drawdown, no cross-pair margin call. Real account: all positions draw from the same equity pool, so losses on one strategy reduce capital available to others.
No size-down after drawdown, no capitulation, no margin top-up, no human discretion overriding signal at panic moments. The simulator continues executing programmatic signals until the $1 floor stops new entries.
Real venues see liquidity vanish in fast markets (flash crashes, news events), so fills slip badly exactly when it hurts most. The simulator fills at the candle close with no slippage at all, so it never reflects order-book collapse during high-volatility moments.
Different fleet metrics have different fidelity-sensitivity:
| Metric | Affected by simulator gaps? |
|---|---|
| Win Rate (WR) | Mostly accurate — entry/exit logic is faithful. |
| Profit Factor (PF) | Mostly accurate — same trades, same per-trade PnL ratios. |
| Sharpe Ratio | Mostly accurate — unitless ratio of return to volatility. |
| Trade Count (N) | Accurate. |
| Net PnL ($) | Correctly leveraged at the trade level. Distorted only by missing funding, maintenance-margin cascades, and venue minimums — not by leverage accounting. |
| Max DD$ | Correctly leveraged. Real DD$ at high leverage is amplified by funding drag and partial-liquidation cascades, which the simulator does not model. |
/equity ratio (RoR proxy) |
Useful as a lower bound on risk — real risk is at least this bad, primarily due to venue-mechanics gaps (funding, maintenance margin, min order size). |
| Expectancy / R-multiple | Sizing-dependent; interpret only at active config snapshot. |
wiki/qa-sessions/2026-05-17-session.md#q12 (first asked here)apps/backend/src/evaluation/equity-ledger.tsapps/backend/src/evaluation/evaluator.ts:143-158, 1340-1379packages/shared/src/index.ts:304-318 (computePnl)apps/backend/src/evaluation/position/position.ts:198,291 (caller-side leverage multiplication).planning/debug/resolved/leverage-ignored-in-pnl.md (Resolved 2026-05-17 — section 1 above corrected)Related concepts
See it in a real result →Put it to the test
Spawn your variant, run it on the same engine, and read the edge-significance verdict before you risk real money.