horizon-code 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,52 @@ is_stale() returns True if no data in N seconds OR feed never connected.
22
22
  - Submitting identical orders rapidly — dedup risk check rejects them
23
23
  `;
24
24
 
25
+ const RISK_CAPITAL_GUIDE = `
26
+ ## Risk & Capital Requirements (MANDATORY)
27
+
28
+ Every strategy you generate MUST include:
29
+
30
+ ### 1. Explicit Risk Configuration
31
+ NEVER use default values. Every hz.Risk() must specify ALL fields with values appropriate to the user's context:
32
+ \`\`\`python
33
+ risk=hz.Risk(
34
+ max_position=50, # max contracts per market
35
+ max_notional=500, # max $ total exposure across all markets
36
+ max_drawdown_pct=5, # kill switch activates at this % loss
37
+ max_order_size=10, # max contracts per single order
38
+ )
39
+ \`\`\`
40
+
41
+ ### 2. Capital Context in hz.run()
42
+ Always include initial_capital context in params so the user knows the assumed capital:
43
+ \`\`\`python
44
+ params={
45
+ "initial_capital": 1000, # assumed starting capital
46
+ "spread": 0.06,
47
+ "size": 10,
48
+ }
49
+ \`\`\`
50
+
51
+ ### 3. Money Explanation (after code)
52
+ After EVERY code fence, add a brief **Capital & Risk** section explaining in plain English:
53
+ - **Capital**: How much money the strategy assumes (e.g., "$1,000 paper mode")
54
+ - **Max exposure**: Maximum $ at risk at any time (max_notional)
55
+ - **Max loss**: Worst case before kill switch (max_drawdown_pct of max_notional)
56
+ - **Position sizing**: How big each trade is (max_order_size contracts, ~$X at current prices)
57
+ - **Risk level**: Conservative / Moderate / Aggressive — based on notional-to-capital ratio
58
+
59
+ Example:
60
+ > **Capital & Risk**: This strategy assumes $1,000 in paper mode. Max exposure is $500 across all markets. The kill switch triggers at 5% drawdown ($25 loss). Each order is at most 10 contracts (~$5-7 at typical prices). Risk level: **Conservative** (50% max exposure).
61
+
62
+ ### 4. Risk Scaling Rules
63
+ - Conservative: max_notional ≤ 30% of capital, max_drawdown_pct ≤ 3%
64
+ - Moderate: max_notional ≤ 60% of capital, max_drawdown_pct ≤ 5%
65
+ - Aggressive: max_notional ≤ 90% of capital, max_drawdown_pct ≤ 10%
66
+
67
+ If the user doesn't specify risk tolerance, default to **Conservative**.
68
+ If the user doesn't specify capital, default to **$1,000 paper mode**.
69
+ `;
70
+
25
71
  const SDK_PIPELINE_SOURCE = `
26
72
  import horizon as hz
27
73
 
@@ -44,7 +90,12 @@ hz.run(
44
90
  markets=["market-slug"],
45
91
  feeds={"mid": hz.PolymarketBook("market-slug")},
46
92
  pipeline=[fair_value, quoter],
47
- risk=hz.Risk(max_position=100, max_notional=1000, max_drawdown_pct=5),
93
+ risk=hz.Risk(
94
+ max_position=100, # max 100 contracts per market
95
+ max_notional=1000, # max $1,000 total exposure
96
+ max_drawdown_pct=5, # kill switch at 5% loss ($50)
97
+ max_order_size=50, # max 50 contracts per order
98
+ ),
48
99
  mode="paper",
49
100
  params={"spread": 0.06, "size": 5}
50
101
  )
@@ -311,6 +362,82 @@ from horizon._horizon import calibration_curve, log_loss, edge_decay
311
362
  # edge_decay(edge_series, timestamps) -> half_life_seconds
312
363
  `;
313
364
 
365
+ const ADVANCED_SDK_REFERENCE = `
366
+ # ── Advanced SDK Capabilities ──
367
+
368
+ ## Volatility Suite
369
+ Six Rust-native estimators for adaptive spread sizing:
370
+ hz.volatility(lookback=20, method="yang_zhang") # Pipeline function
371
+ # Methods: "close_to_close", "parkinson", "garman_klass", "yang_zhang", "ewma", "rolling"
372
+ # Injects VolatilitySnapshot into ctx with .best property (best estimator auto-selected)
373
+ # Use: widen spreads in high-vol regimes, tighten in low-vol
374
+
375
+ ## Example — volatility-adaptive market maker:
376
+ def adaptive_quoter(ctx, fair):
377
+ if fair is None:
378
+ return []
379
+ vol = getattr(ctx, 'volatility', None)
380
+ base_spread = ctx.params.get("spread", 0.06)
381
+ if vol and vol.best > 0:
382
+ spread = base_spread * (1 + vol.best * 2) # widen on high vol
383
+ else:
384
+ spread = base_spread
385
+ return hz.quotes(fair, spread=spread, size=ctx.params.get("size", 5))
386
+
387
+ ## Execution Algorithms
388
+ For large orders that need careful execution:
389
+ from horizon import TWAP, VWAP, Iceberg
390
+
391
+ TWAP(engine, side, size, duration_secs=300, num_slices=10) # Equal time slices
392
+ VWAP(engine, side, size, duration_secs=300, volume_profile=[0.1, 0.15, ...]) # Volume-weighted
393
+ Iceberg(engine, side, total_size, show_size=5) # Hidden size
394
+
395
+ # All have: .start(), .on_tick(), .is_complete, .total_filled
396
+ # Use TWAP for uniform execution, VWAP for volume-aware, Iceberg for hiding size
397
+
398
+ ## Market Discovery
399
+ Find real markets programmatically instead of hardcoding slugs:
400
+ hz.discover_markets(exchange="polymarket", query="bitcoin", min_volume=10000, sort_by="volume")
401
+ hz.top_markets(exchange="polymarket", limit=20) # Highest-volume active markets
402
+ hz.discover_events(exchange="polymarket") # Multi-outcome events
403
+
404
+ ## Portfolio Management
405
+ For multi-strategy coordination and risk budgeting:
406
+ port = hz.Portfolio(name="my_fund", capital=10000)
407
+ port.add_position(market_id, side, size, entry_price)
408
+ port.update_price(market_id, current_price)
409
+ # Optimization: port.optimize("kelly") | "equal_weight" | "risk_parity" | "min_variance"
410
+ # Risk: port.var_95(), port.cvar_95(), port.correlation_matrix()
411
+ # Rebalance: port.needs_rebalance(threshold=0.05), port.rebalance_orders()
412
+
413
+ ## Kelly Criterion (built-in)
414
+ hz.edge(fair_prob=0.55, market_price=0.50) # Expected value
415
+ hz.kelly(prob=0.55, price=0.50) # Full Kelly fraction
416
+ hz.fractional_kelly(prob=0.55, price=0.50, fraction=0.25) # Conservative (recommended)
417
+ hz.kelly_size(prob=0.55, price=0.50, bankroll=1000, max_size=50) # Contract units
418
+ hz.kelly_sizer(fraction=0.25, bankroll=1000) # Pipeline factory
419
+ hz.kelly_sizer_with_liquidity(fraction=0.25, bankroll=1000) # Dampens for illiquid markets
420
+
421
+ ## Sentinel — Continuous Risk Monitor
422
+ hz.sentinel(portfolio, config={
423
+ "drawdown_levels": [-5, -10, -20, -30], # alert, reduce, pause, exit
424
+ "regime_lookback": 50,
425
+ "var_confidence": 0.95,
426
+ })
427
+ # Auto-reduces position size in high-drawdown regimes
428
+ # Detects: drawdown escalation, regime change, correlation spikes
429
+ # Actions: alert, reduce_positions, pause_trading, emergency_exit
430
+ hz.suggest_hedges(portfolio, budget=500) # Hedge recommendations
431
+
432
+ ## Promotion Gates (paper → live)
433
+ Before deploying live, strategies should pass:
434
+ - Sharpe ratio >= 1.0
435
+ - At least 30 trades
436
+ - Walk-forward p-value < 0.05
437
+ - Probability of Backtest Overfitting (PBO) < 50%
438
+ - Max drawdown < 10% of capital
439
+ `;
440
+
314
441
  const EXAMPLE_MOMENTUM = `
315
442
  import horizon as hz
316
443
  from collections import deque
@@ -354,9 +481,14 @@ hz.run(
354
481
  markets=["market-slug"],
355
482
  feeds={"mid": hz.PolymarketBook("market-slug")},
356
483
  pipeline=[track_prices, quoter],
357
- risk=hz.Risk(max_position=100, max_notional=1000, max_drawdown_pct=5),
484
+ risk=hz.Risk(
485
+ max_position=100, # max 100 contracts per market
486
+ max_notional=1000, # max $1,000 total exposure
487
+ max_drawdown_pct=5, # kill switch at 5% loss ($50)
488
+ max_order_size=25, # max 25 contracts per order
489
+ ),
358
490
  mode="paper",
359
- params={"lookback": 15, "threshold": 0.03, "spread": 0.04, "size": 25}
491
+ params={"initial_capital": 1000, "lookback": 15, "threshold": 0.03, "spread": 0.04, "size": 25}
360
492
  )
361
493
  `;
362
494
 
@@ -392,9 +524,14 @@ hz.run(
392
524
  markets=["market-slug"],
393
525
  feeds={"mid": hz.PolymarketBook("market-slug")},
394
526
  pipeline=[compute_inventory_skew, generate_quotes],
395
- risk=hz.Risk(max_position=100, max_notional=1000, max_drawdown_pct=5),
527
+ risk=hz.Risk(
528
+ max_position=100, # max 100 contracts per market
529
+ max_notional=1000, # max $1,000 total exposure
530
+ max_drawdown_pct=5, # kill switch at 5% loss ($50)
531
+ max_order_size=25, # max 25 contracts per order
532
+ ),
396
533
  mode="paper",
397
- params={"spread": 0.06, "size": 10, "max_position": 100}
534
+ params={"initial_capital": 1000, "spread": 0.06, "size": 10, "max_position": 100}
398
535
  )
399
536
  `;
400
537
 
@@ -455,6 +592,279 @@ print(hz.dashboard(bundle))
455
592
  print("---END_ASCII_DASHBOARD---")
456
593
  `;
457
594
 
595
+ const STRATEGY_TEMPLATES = `
596
+ # ── Strategy Templates ──
597
+ # Reference these when the user asks for a specific strategy type.
598
+ # Adapt parameters to the user's market, capital, and risk tolerance.
599
+
600
+ ## Template 1: Conservative Market Maker
601
+ # Style: Wide spreads, small size, low risk. Good for beginners.
602
+ # Capital: $500-2000 | Risk: Conservative | Expected Sharpe: 0.5-1.5
603
+ # Best for: High-volume markets with tight spreads
604
+
605
+ import horizon as hz
606
+
607
+ def fair_value(ctx):
608
+ feed = ctx.feeds.get("mid")
609
+ if not feed or feed.is_stale(30):
610
+ return None
611
+ return feed.price
612
+
613
+ def conservative_quoter(ctx, fair):
614
+ if fair is None:
615
+ return []
616
+ inv = ctx.inventory.net
617
+ max_pos = ctx.params.get("max_position", 50)
618
+ skew = inv / max_pos if max_pos > 0 else 0
619
+ spread = ctx.params.get("spread", 0.10) * (1 + abs(skew) * 0.5)
620
+ size = ctx.params.get("size", 5)
621
+ if abs(inv) > max_pos * 0.8:
622
+ size = max(1, size // 2)
623
+ return hz.quotes(fair, spread=spread, size=size)
624
+
625
+ hz.run(
626
+ name="ConservativeMarketMaker",
627
+ exchange=hz.Polymarket(),
628
+ markets=["your-market-slug"],
629
+ feeds={"mid": hz.PolymarketBook("your-market-slug")},
630
+ pipeline=[fair_value, conservative_quoter],
631
+ risk=hz.Risk(
632
+ max_position=50,
633
+ max_notional=500,
634
+ max_drawdown_pct=3,
635
+ max_order_size=5,
636
+ ),
637
+ mode="paper",
638
+ params={"initial_capital": 1000, "spread": 0.10, "size": 5, "max_position": 50}
639
+ )
640
+ # Capital & Risk: $1,000 paper. Max $500 exposure. Kill switch at 3% ($15). Conservative.
641
+
642
+ ## Template 2: Momentum Follower with Kelly Sizing
643
+ # Style: Directional, follows price trends. Uses Kelly for position sizing.
644
+ # Capital: $1000-5000 | Risk: Moderate | Expected Sharpe: 1.0-2.0
645
+ # Best for: Trending markets (elections, crypto events)
646
+
647
+ import horizon as hz
648
+ from collections import deque
649
+
650
+ _prices: dict[str, deque] = {}
651
+
652
+ def momentum_signal(ctx):
653
+ feed = ctx.feeds.get("mid")
654
+ if not feed or feed.is_stale(30):
655
+ return None
656
+ slug = ctx.market.slug
657
+ lookback = int(ctx.params.get("lookback", 20))
658
+ if slug not in _prices:
659
+ _prices[slug] = deque(maxlen=lookback)
660
+ _prices[slug].append(feed.price)
661
+ if len(_prices[slug]) < lookback:
662
+ return None
663
+ oldest = _prices[slug][0]
664
+ if oldest == 0:
665
+ return None
666
+ momentum = (feed.price - oldest) / oldest
667
+ return {"mid": feed.price, "momentum": momentum}
668
+
669
+ def kelly_quoter(ctx, signal):
670
+ if signal is None:
671
+ return []
672
+ momentum = signal["momentum"]
673
+ threshold = ctx.params.get("threshold", 0.02)
674
+ if abs(momentum) < threshold:
675
+ return []
676
+ fair = signal["mid"]
677
+ prob = 0.5 + min(0.2, abs(momentum) * 2)
678
+ fraction = ctx.params.get("kelly_fraction", 0.25)
679
+ kelly_f = max(0, (prob - (1 - prob)) * fraction)
680
+ size = max(1, int(kelly_f * ctx.params.get("bankroll", 1000) / fair))
681
+ size = min(size, ctx.params.get("max_size", 20))
682
+ spread = ctx.params.get("spread", 0.04)
683
+ return hz.quotes(fair, spread=spread, size=size)
684
+
685
+ hz.run(
686
+ name="MomentumFollower",
687
+ exchange=hz.Polymarket(),
688
+ markets=["your-market-slug"],
689
+ feeds={"mid": hz.PolymarketBook("your-market-slug")},
690
+ pipeline=[momentum_signal, kelly_quoter],
691
+ risk=hz.Risk(
692
+ max_position=100,
693
+ max_notional=1000,
694
+ max_drawdown_pct=5,
695
+ max_order_size=20,
696
+ ),
697
+ mode="paper",
698
+ params={"initial_capital": 2000, "lookback": 20, "threshold": 0.02, "spread": 0.04, "kelly_fraction": 0.25, "bankroll": 2000, "max_size": 20}
699
+ )
700
+ # Capital & Risk: $2,000 paper. Max $1,000 exposure. Kill switch at 5% ($50). Moderate.
701
+
702
+ ## Template 3: Mean Reversion
703
+ # Style: Fades extreme prices toward fair value using z-score.
704
+ # Capital: $500-2000 | Risk: Conservative-Moderate | Expected Sharpe: 0.8-1.5
705
+ # Best for: Range-bound markets, markets with known fair value
706
+
707
+ import horizon as hz
708
+ from collections import deque
709
+ import math
710
+
711
+ _history: dict[str, deque] = {}
712
+
713
+ def mean_reversion_signal(ctx):
714
+ feed = ctx.feeds.get("mid")
715
+ if not feed or feed.is_stale(30):
716
+ return None
717
+ slug = ctx.market.slug
718
+ window = int(ctx.params.get("window", 30))
719
+ if slug not in _history:
720
+ _history[slug] = deque(maxlen=window)
721
+ _history[slug].append(feed.price)
722
+ if len(_history[slug]) < window:
723
+ return None
724
+ prices = list(_history[slug])
725
+ mean = sum(prices) / len(prices)
726
+ std = math.sqrt(sum((p - mean) ** 2 for p in prices) / len(prices))
727
+ if std < 0.001:
728
+ return None
729
+ zscore = (feed.price - mean) / std
730
+ return {"mid": feed.price, "mean": mean, "zscore": zscore}
731
+
732
+ def reversion_quoter(ctx, signal):
733
+ if signal is None:
734
+ return []
735
+ z = signal["zscore"]
736
+ entry_z = ctx.params.get("entry_z", 1.5)
737
+ if abs(z) < entry_z:
738
+ return []
739
+ fair = signal["mean"]
740
+ spread = ctx.params.get("spread", 0.06)
741
+ size = ctx.params.get("size", 10)
742
+ return hz.quotes(fair, spread=spread, size=size)
743
+
744
+ hz.run(
745
+ name="MeanReversion",
746
+ exchange=hz.Polymarket(),
747
+ markets=["your-market-slug"],
748
+ feeds={"mid": hz.PolymarketBook("your-market-slug")},
749
+ pipeline=[mean_reversion_signal, reversion_quoter],
750
+ risk=hz.Risk(
751
+ max_position=80,
752
+ max_notional=800,
753
+ max_drawdown_pct=5,
754
+ max_order_size=10,
755
+ ),
756
+ mode="paper",
757
+ params={"initial_capital": 1000, "window": 30, "entry_z": 1.5, "spread": 0.06, "size": 10}
758
+ )
759
+ # Capital & Risk: $1,000 paper. Max $800 exposure. Kill switch at 5% ($40). Moderate.
760
+
761
+ ## Template 4: Cross-Market Arb (Polymarket vs Kalshi)
762
+ # Style: Captures price discrepancies between exchanges.
763
+ # Capital: $2000-10000 | Risk: Low (market-neutral) | Expected Sharpe: 1.5-3.0
764
+ # Best for: Markets listed on both Polymarket and Kalshi
765
+
766
+ import horizon as hz
767
+
768
+ def arb_detector(ctx):
769
+ poly = ctx.feeds.get("poly")
770
+ kalshi = ctx.feeds.get("kalshi")
771
+ if not poly or poly.is_stale(30) or not kalshi or kalshi.is_stale(30):
772
+ return None
773
+ edge = abs(poly.price - kalshi.price)
774
+ min_edge = ctx.params.get("min_edge", 0.03)
775
+ if edge < min_edge:
776
+ return None
777
+ if poly.price < kalshi.price:
778
+ return {"buy_exchange": "poly", "fair": poly.price, "edge": edge}
779
+ else:
780
+ return {"buy_exchange": "kalshi", "fair": kalshi.price, "edge": edge}
781
+
782
+ def arb_quoter(ctx, signal):
783
+ if signal is None:
784
+ return []
785
+ size = ctx.params.get("size", 10)
786
+ spread = ctx.params.get("spread", 0.02)
787
+ return hz.quotes(signal["fair"], spread=spread, size=size)
788
+
789
+ hz.run(
790
+ name="CrossMarketArb",
791
+ exchanges=[hz.Polymarket(), hz.Kalshi()],
792
+ markets=["your-market-slug"],
793
+ feeds={
794
+ "poly": hz.PolymarketBook("your-market-slug"),
795
+ "kalshi": hz.KalshiBook("your-kalshi-ticker"),
796
+ },
797
+ pipeline=[arb_detector, arb_quoter],
798
+ risk=hz.Risk(
799
+ max_position=100,
800
+ max_notional=2000,
801
+ max_drawdown_pct=3,
802
+ max_order_size=10,
803
+ ),
804
+ mode="paper",
805
+ params={"initial_capital": 5000, "min_edge": 0.03, "spread": 0.02, "size": 10}
806
+ )
807
+ # Capital & Risk: $5,000 paper. Max $2,000 exposure. Kill switch at 3% ($60). Low risk (market-neutral).
808
+
809
+ ## Template 5: Multi-Signal Ensemble
810
+ # Style: Combines price, spread, momentum, and flow signals.
811
+ # Capital: $1000-5000 | Risk: Moderate | Expected Sharpe: 1.0-2.5
812
+ # Best for: Liquid markets with diverse data
813
+
814
+ import horizon as hz
815
+ from collections import deque
816
+
817
+ _momentum_history: dict[str, deque] = {}
818
+
819
+ def multi_signal(ctx):
820
+ feed = ctx.feeds.get("mid")
821
+ if not feed or feed.is_stale(30):
822
+ return None
823
+ slug = ctx.market.slug
824
+ lookback = int(ctx.params.get("lookback", 15))
825
+ if slug not in _momentum_history:
826
+ _momentum_history[slug] = deque(maxlen=lookback)
827
+ _momentum_history[slug].append(feed.price)
828
+ price_signal = feed.price
829
+ spread_signal = (feed.ask - feed.bid) if feed.ask > 0 and feed.bid > 0 else 0.05
830
+ history = _momentum_history[slug]
831
+ momentum_signal = (feed.price - history[0]) / history[0] if len(history) >= lookback and history[0] > 0 else 0
832
+ inventory_signal = -ctx.inventory.net * 0.001
833
+ weights = ctx.params.get("weights", [0.4, 0.2, 0.2, 0.2])
834
+ combined = (
835
+ weights[0] * price_signal +
836
+ weights[1] * (1 - spread_signal * 10) * price_signal +
837
+ weights[2] * momentum_signal * price_signal +
838
+ weights[3] * inventory_signal
839
+ )
840
+ fair = max(0.01, min(0.99, combined))
841
+ return fair
842
+
843
+ def ensemble_quoter(ctx, fair):
844
+ if fair is None:
845
+ return []
846
+ spread = ctx.params.get("spread", 0.05)
847
+ size = ctx.params.get("size", 8)
848
+ return hz.quotes(fair, spread=spread, size=size)
849
+
850
+ hz.run(
851
+ name="MultiSignalEnsemble",
852
+ exchange=hz.Polymarket(),
853
+ markets=["your-market-slug"],
854
+ feeds={"mid": hz.PolymarketBook("your-market-slug")},
855
+ pipeline=[multi_signal, ensemble_quoter],
856
+ risk=hz.Risk(
857
+ max_position=80,
858
+ max_notional=1000,
859
+ max_drawdown_pct=5,
860
+ max_order_size=15,
861
+ ),
862
+ mode="paper",
863
+ params={"initial_capital": 2000, "lookback": 15, "spread": 0.05, "size": 8, "weights": [0.4, 0.2, 0.2, 0.2]}
864
+ )
865
+ # Capital & Risk: $2,000 paper. Max $1,000 exposure. Kill switch at 5% ($50). Moderate.
866
+ `;
867
+
458
868
  /**
459
869
  * Build the system prompt for strategy mode.
460
870
  * Code streams via ```python fences — no tool calls for code generation.
@@ -577,6 +987,8 @@ The code you generate is the EXACT code that runs on Horizon. It must:
577
987
  7. \`hz.quotes(fair, spread, size)\` returns \`list[Quote]\`
578
988
  8. \`ctx.inventory\` is InventorySnapshot (NOT dict). Use \`.positions\`, \`.net\`, \`.net_for_market(id)\`
579
989
 
990
+ ${RISK_CAPITAL_GUIDE}
991
+
580
992
  ## SDK Reference
581
993
 
582
994
  ### Pipeline Structure
@@ -588,13 +1000,21 @@ ${STATIC_MODELS_SOURCE}
588
1000
  ### Backtesting & Analytics
589
1001
  ${BACKTEST_REFERENCE}
590
1002
 
1003
+ ### Advanced Capabilities
1004
+ ${ADVANCED_SDK_REFERENCE}
1005
+
591
1006
  ${STRATEGY_GUIDE}
592
1007
 
593
1008
  ### Example: MomentumScalper
594
1009
  ${EXAMPLE_MOMENTUM}
595
1010
 
596
1011
  ### Example: InventoryAwareMarketMaker
597
- ${EXAMPLE_INVENTORY_MM}`;
1012
+ ${EXAMPLE_INVENTORY_MM}
1013
+
1014
+ ### Strategy Templates
1015
+ When a user asks for a specific strategy type (market maker, momentum, arb, etc.), reference the corresponding template below. Adapt the parameters to their market, capital, and risk tolerance.
1016
+
1017
+ ${STRATEGY_TEMPLATES}`;
598
1018
  }
599
1019
 
600
1020
  /**