PromotionManager
Paper → shadow → live promotion gates with automatic demotion
PromotionManager is v1’s strategy lifecycle module. Strategies move through stages: paper → shadow → live-small → live-scaled. Each transition is gated on backtest quality, live tracking error, and drawdown performance. Demotion is automatic when live performance degrades.
The promotion pipeline
Stages
paper Running in paper mode. No real money. Collect performance data to validate the strategy.
shadow Real feeds, real venues, but orders are **logged only**, not submitted. Catches "would my order have filled at the price I assumed" issues.
live-small Real money, small size (e.g., 1% of portfolio cap). First actual touching of capital.
live-scaled Full-size live trading after stable live-small performance.
Promotion gates
Each transition has its own gate: a set of conditions that must be met before the strategy advances.
API
python
from horizon.fund._promotion import PromotionManager, PromotionStage, PromotionGate
mgr = PromotionManager(
db_path="promotions.db",
)
# Register a strategy in paper
strat_id = mgr.register(
strategy_name="bollinger_mr_w20",
initial_stage=PromotionStage.Paper,
)
# Record metrics periodically
mgr.record_metrics(
strategy_id=strat_id,
stage=PromotionStage.Paper,
metrics={
"sharpe": 1.2,
"max_drawdown": 0.08,
"n_trades": 150,
},
)
# Check if promotion gate is met
if mgr.should_promote(strat_id):
mgr.promote(strat_id)
print(f"Promoted to {mgr.current_stage(strat_id).value}")
# Check for demotion conditions
if mgr.should_demote(strat_id):
mgr.demote(strat_id, reason="drawdown breach")
# Get full state
snapshot = mgr.snapshot(strat_id)
print(f"Stage: {snapshot.stage}")
print(f"Days in stage: {snapshot.days_in_stage}")
print(f"Promotion history: {snapshot.history}")
Usage with Horizon
Development workflow
python
from horizon.fund._promotion import PromotionManager, PromotionStage
mgr = PromotionManager()
# 1. Register the strategy
strat_id = mgr.register("my_strategy", initial_stage=PromotionStage.Paper)
# 2. Run paper backtests periodically
for _ in range(30): # 30 days of paper
result = hz.run(mode="paper", strategies=[MyStrategy()], ...)
mgr.record_metrics(strat_id, PromotionStage.Paper, {
"sharpe": result.sharpe,
"max_drawdown": result.max_drawdown,
"n_trades": result.n_trades,
})
# 3. Check promotion
if mgr.should_promote(strat_id):
mgr.promote(strat_id) # now at shadow
print("Advanced to shadow")
Wiring into hz.run()
You can register a PromotionGate as a pre-run check:
python
if mgr.current_stage(strat_id) == PromotionStage.Live_Scaled:
budget = 100_000
elif mgr.current_stage(strat_id) == PromotionStage.Live_Small:
budget = 1_000 # 1% of full cap
elif mgr.current_stage(strat_id) == PromotionStage.Shadow:
mode = "shadow"
budget = 100_000
else:
mode = "paper"
budget = 100_000
hz.run(mode=mode, venues={"alpaca": Alpaca(budget_usd=budget, ...)}, ...)
CLI integration (planned)
a future release will add CLI commands:
bash
horizon strategy promote my_strategy --to shadow
horizon strategy promote my_strategy --to live --cap 0.01
horizon strategy demote my_strategy --to paper --reason "investigate"
horizon strategy status
The promotion state persists to SQLite, so multiple processes can read consistent state.
When to use
New strategy rollouts Every new idea should start in paper and climb. No skip-to-live. The gates protect you from your own overconfidence.
A/B testing at scale Run many variants in paper simultaneously; only the ones that pass the gates get promoted.
Drift detection Live-small stage catches regime changes before they affect full-size capital.
Pitfalls
Source
python/horizon/fund/_promotion.py. ~400 lines, SQLite-backed.