PromotionManager

Paper → shadow → live promotion gates with automatic demotion

PromotionManager is v1’s strategy lifecycle module. Strategies move through stages: papershadowlive-smalllive-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.

Next