Shadow Optimization
Shadow optimization runs thousands of parameter variants alongside your live strategy in real-time. Each variant processes the same market data but with different parameters — entries, take-profits, stop-losses — all on paper. When a variant proves itself profitable, it can be promoted to live trading automatically.
How It Works
Live market data (tickers + trades)
├── Live strategy → real orders
└── Shadow engine → 19,000+ paper variants
├── Variant A: eD=0.5, TP=0.3, SL=0.5 → 3 trades, +0.78%
├── Variant B: eD=0.8, TP=0.5, SL=1.0 → 2 trades, +0.52%
└── ...
Winner detected → burst promotion → live ordersThe shadow engine runs in its own tokio task with zero overhead on the live strategy's event loop. Market data is forwarded via channels.
Execution Modes
Batch Mode (default)
Uses a Structure of Arrays (SoA) layout where all variants share flat arrays for positions, entries, and metrics. Processes ~0.002us per variant per event — handles 20,000+ variants across 100+ pairs on a single core.
Requires the strategy plugin to export a BatchFactory via declare_batch_strategy!.
Generic Mode (fallback)
Creates individual Strategy + InMemoryExchange instances per variant. ~3us per variant per event — ~1000x slower than batch, but works with any strategy plugin.
Enabling Shadow
Add a shadow block to your strategy config:
{
"strats": [{
"name": "my-strategy",
"type": "shot",
"entryDistance": 3.0,
"chaseSensitivity": 1,
"takeProfit": 0.3,
"stopLoss": 0.5,
"orderSize": 0.00001,
"shadow": {
"enabled": true,
"entryDistance": { "min": 0.1, "max": 3.0, "step": 0.2 },
"chaseSensitivity": { "min": 0.1, "max": 1, "step": 0.2 },
"takeProfit": { "min": 0.3, "max": 2, "step": 0.1 },
"stopLoss": { "min": 0.5, "max": 1, "step": 0.1 },
"evaluationWindowSecs": 600,
"minTrades": 1,
"reportIntervalSecs": 30
}
}]
}Each parameter with a { min, max, step } range generates variants. The total variant count is the product of all range sizes. Use constraints and idle pruning to keep the count manageable.
Shadow-Only Mode
When shadowOnly: true, the live strategy suppresses all entries until a variant is promoted. This lets you safely discover which parameters work on a pair before committing real orders.
"shadow": {
"enabled": true,
"shadowOnly": true,
...
}Constraints
Filter out invalid parameter combinations before they consume resources:
"constraints": [
{ "left": "chaseSensitivity", "op": "<", "right": "entryDistance" },
{ "left": "takeProfit", "op": "<", "right": "entryDistance" }
]Supported operators: <, <=, >, >=.
Constraints are evaluated during variant generation. In the example above, a variant with chaseSensitivity=0.8, entryDistance=0.5 would be pruned because chase sensitivity must be less than entry distance.
Idle Pruning
Start all variants at the minimum value of a parameter, then prune higher values that never get a chance to trade:
"pruneWhenIdle": ["takeProfit"]With pruneWhenIdle: ["takeProfit"], all variants initially use the minimum TP (e.g., 0.3). When a variant group (same entry params) completes a trade, idle higher-TP variants in that group are pruned — they share the same entry so they would have entered identically, but the lowest TP proved the entry works first.
Burst Promotion
Burst mode detects variants that complete trades quickly and promotes them to live trading in real-time — no waiting for evaluation windows.
"shadow": {
"promotion": {
"mode": "burst",
"burstTrades": 3,
"burstWindowMs": 300000,
"maxConcurrentPromotions": 3,
"maxLosses": 3,
"lossPenaltySecs": 600,
"minSharpe": 0.3,
"minScore": 0.5,
"minMargin": 0.2,
"maxDrawdownPct": 10.0,
"requireNoPosition": true,
"cooldownSecs": 300,
"trackLiveAsVariant": true,
"maxPromotionsPerWindow": 99,
"minTradesForPromotion": 3
}
}Burst Criteria
A variant is promoted when ALL of these are met:
| Criteria | Config Key | Description |
|---|---|---|
| Trade count | burstTrades | Completed N round-trips |
| Time window | burstWindowMs | All trades within this window (ms) |
| Profitable | — | Total PnL > 0 |
| Sharpe filter | minSharpe | Minimum Sharpe ratio (optional) |
| Score filter | minScore | Minimum score threshold |
| Not penalized | lossPenaltySecs | Not in demotion penalty period |
| Concurrent limit | maxConcurrentPromotions | Max promoted variants per symbol |
Entry Distance Deduplication
Variants with the same entryDistance enter at the same price level regardless of other parameters. Only one variant per entry distance is promoted per symbol. Among candidates at the same entry distance, the engine picks the highest take-profit that completed the same number of trades as the lowest-TP variant — maximizing upside while proving the same entry edge.
Demotion
Promoted variants track consecutive losses. After maxLosses consecutive losing trades, the variant is demoted and enters a penalty period (lossPenaltySecs). The entry distance slot is freed so a different variant at the same level can re-qualify.
Staleness Detection
Track whether the live parameters are still performing:
"staleness": {
"windowCount": 3,
"scoreThreshold": 0.0,
"declinePct": 50.0,
"maxAgeSecs": 0
}| Key | Description |
|---|---|
windowCount | Number of evaluation windows to track |
scoreThreshold | Minimum live score before considering stale |
declinePct | Score decline percentage that triggers staleness |
maxAgeSecs | Force stale after N seconds (0 = disabled) |
Edge Decay
Detect when the best shadow variant's edge is declining:
"edgeDecay": {
"scoreThreshold": 0.0,
"consecutiveWindows": 3,
"action": "notify"
}If the top variant's score drops below scoreThreshold for consecutiveWindows consecutive evaluation windows, the configured action triggers. Actions: notify (Telegram alert), stop (stop strategy).
Shadow Reports
The shadow engine logs a report every reportIntervalSecs showing:
[shadow/burst100/AIOTUSDT] report: 19477/19477 variants (active/total),
43 eligible, 47 total trades (+35) — ram=12.5MB — top 10:
#1: chaseSensitivity=0.1_eD=0.4_SL=0.6_TP=0.3 — trades=9 pnl=+1.39% score=0.82
#2: ...Fields:
- active/total: variants still running vs total generated
- eligible: variants meeting
minTradesthreshold - total trades (+delta): cumulative trades since last report
- ram: memory usage for this symbol's shadow engine
- top 10: best variants ranked by score
Telegram Notifications
Burst promotions send a compact Telegram message:
#burst100_AIOTUSDT Promoted (trades: 3, PnL: +0.78%, score: 0.78,
sharpe: 25.99): chaseSensitivity=0.1_entryDistance=0.6_stopLoss=1_takeProfit=0.3Resource Usage
| Pairs | Variants/pair | RAM/pair | Total RAM | CPU overhead |
|---|---|---|---|---|
| 20 | 19,477 | 12.5 MB | ~250 MB | negligible |
| 100 | 19,477 | 12.5 MB | ~1.25 GB | ~2 GB RSS |
| 200 | 19,477 | 12.5 MB | ~2.5 GB | ~3 GB RSS |
The batch engine processes all variants in a single pass per event. CPU time scales linearly with variant count but the per-variant cost (~0.002us) is negligible.
Full Configuration Reference
"shadow": {
"enabled": true,
"shadowOnly": false,
"paramName": { "min": 0.1, "max": 3.0, "step": 0.1 },
"constraints": [
{ "left": "param1", "op": "<", "right": "param2" }
],
"pruneWhenIdle": ["takeProfit"],
"evaluationWindowSecs": 600,
"minTrades": 1,
"reportIntervalSecs": 30,
"promotion": {
"mode": "burst",
"burstTrades": 3,
"burstWindowMs": 300000,
"maxLosses": 3,
"lossPenaltySecs": 600,
"maxConcurrentPromotions": 3,
"minSharpe": 0.3,
"minScore": 0.5,
"minMargin": 0.2,
"maxDrawdownPct": 10.0,
"requireNoPosition": true,
"cooldownSecs": 300,
"trackLiveAsVariant": true,
"maxPromotionsPerWindow": 99,
"minTradesForPromotion": 3
},
"staleness": {
"windowCount": 3,
"scoreThreshold": 0.0,
"declinePct": 50.0,
"maxAgeSecs": 0
},
"edgeDecay": {
"scoreThreshold": 0.0,
"consecutiveWindows": 3,
"action": "notify"
}
}