Skip to content

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 orders

The 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:

json
{
  "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.

json
"shadow": {
  "enabled": true,
  "shadowOnly": true,
  ...
}

Constraints

Filter out invalid parameter combinations before they consume resources:

json
"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:

json
"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.

json
"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:

CriteriaConfig KeyDescription
Trade countburstTradesCompleted N round-trips
Time windowburstWindowMsAll trades within this window (ms)
ProfitableTotal PnL > 0
Sharpe filterminSharpeMinimum Sharpe ratio (optional)
Score filterminScoreMinimum score threshold
Not penalizedlossPenaltySecsNot in demotion penalty period
Concurrent limitmaxConcurrentPromotionsMax 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:

json
"staleness": {
  "windowCount": 3,
  "scoreThreshold": 0.0,
  "declinePct": 50.0,
  "maxAgeSecs": 0
}
KeyDescription
windowCountNumber of evaluation windows to track
scoreThresholdMinimum live score before considering stale
declinePctScore decline percentage that triggers staleness
maxAgeSecsForce stale after N seconds (0 = disabled)

Edge Decay

Detect when the best shadow variant's edge is declining:

json
"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 minTrades threshold
  • 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.3

Resource Usage

PairsVariants/pairRAM/pairTotal RAMCPU overhead
2019,47712.5 MB~250 MBnegligible
10019,47712.5 MB~1.25 GB~2 GB RSS
20019,47712.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

json
"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"
  }
}

tradectl — Automate Crypto Trading