Horizon has full test coverage across both Rust and Python layers.
Running Tests
Rust tests
Python tests
All tests
Test Coverage
Layer Tests Description Rust 98 tests Core engine, risk pipeline, orders, positions, exchanges, feeds, persistence, contingent orders Python 395 tests Full SDK: engine, pipeline, exchanges, feeds, risk, backtest, algos, metrics, advanced orders
Test Structure
tests/
├── test_engine.py # Engine creation, orders, fills, P&L
├── test_exchange_routing.py # Exchange backend selection, cancel logic
├── test_feeds.py # Feed snapshot management
├── test_live_exchange.py # Polymarket/Kalshi construction, EIP-712, env config
├── test_multi_exchange.py # Multi-exchange engine, netting, routing
├── test_paper.py # Paper exchange matching logic
├── test_pipeline.py # Pipeline composition, context building
├── test_production_fixes.py # Production fixes regression tests
├── test_risk.py # Risk pipeline checks (kill switch, limits, dedup)
├── test_advanced_orders.py # Amendment, stop-loss, take-profit, bracket, OCO
├── test_algos.py # TWAP, VWAP, Iceberg execution algorithms
├── test_backtest.py # Backtesting engine, data normalization, analytics
└── test_metrics.py # Prometheus metrics, counters, gauges, histograms, server
Rust Tests
Rust tests (98 tests) cover the core engine, risk pipeline, order management, position tracking, exchange backends, feed system, persistence layer, contingent orders, and order amendment. Tests are located in #[cfg(test)] mod tests blocks within each source file.
Python Tests
Python tests (395 tests) cover the full SDK: engine creation, pipeline composition, exchange configuration, feed management, multi-exchange routing, risk behavior, backtesting, execution algorithms, advanced orders, and Prometheus metrics.
Writing Strategy Tests
Use the Engine directly for unit testing your strategy logic:
from horizon import Engine, RiskConfig, OrderRequest, Side, OrderSide, Quote, Fill
def test_my_strategy_logic ():
config = RiskConfig( max_position_per_market = 1000 )
engine = Engine( risk_config = config)
# Submit an order
order_id = engine.submit_order(OrderRequest(
market_id = "test" ,
side = Side.Yes,
order_side = OrderSide.Buy,
size = 10.0 ,
price = 0.55 ,
))
# Tick to fill
fills = engine.tick( "test" , 0.55 )
assert fills == 1
# Check position
positions = engine.positions()
assert len (positions) == 1
assert positions[ 0 ].size == 10.0
Testing with fills
Since Position has no Python constructor, build positions by processing fills:
def test_position_tracking ():
engine = Engine()
# Inject a fill directly
engine.process_fill(Fill(
fill_id = "f1" ,
order_id = "o1" ,
market_id = "test" ,
side = Side.Yes,
order_side = OrderSide.Buy,
price = 0.50 ,
size = 10.0 ,
))
positions = engine.positions()
assert len (positions) == 1
assert positions[ 0 ].avg_entry_price == 0.50
Testing risk limits
def test_position_limit ():
config = RiskConfig( max_position_per_market = 10.0 )
engine = Engine( risk_config = config)
# Fill up to the limit
engine.process_fill(Fill(
fill_id = "f1" , order_id = "o1" , market_id = "test" ,
side = Side.Yes, order_side = OrderSide.Buy,
price = 0.50 , size = 10.0 ,
))
# This should be rejected by the position limit
import pytest
with pytest.raises( Exception ):
engine.submit_order(OrderRequest(
market_id = "test" ,
side = Side.Yes,
order_side = OrderSide.Buy,
size = 5.0 ,
price = 0.55 ,
))
Testing pipeline functions
Test pipeline functions in isolation with a mock context:
from horizon.context import Context, FeedData, InventorySnapshot
def test_fair_value ():
ctx = Context(
feeds = { "default" : FeedData( price = 0.55 , bid = 0.54 , ask = 0.56 )},
)
result = fair_value(ctx)
assert 0 < result < 1 # Valid probability
def test_quoter ():
ctx = Context(
feeds = { "default" : FeedData( price = 0.55 )},
inventory = InventorySnapshot( positions = []),
)
quotes = quoter(ctx, 0.60 )
assert len (quotes) > 0
assert quotes[ 0 ].bid < quotes[ 0 ].ask
Testing with persistence
def test_crash_recovery ():
import tempfile, os
with tempfile.NamedTemporaryFile( suffix = ".db" , delete = False ) as f:
db_path = f.name
try :
# First run: build some state
engine = Engine( db_path = db_path)
engine.process_fill(Fill(
fill_id = "f1" , order_id = "o1" , market_id = "test" ,
side = Side.Yes, order_side = OrderSide.Buy,
price = 0.50 , size = 10.0 ,
))
engine.snapshot_positions()
del engine
# Second run: recover state
engine2 = Engine( db_path = db_path)
recovered = engine2.recover_state()
assert recovered == 1
positions = engine2.positions()
assert len (positions) == 1
finally :
os.unlink(db_path)
Test Tips
Use Engine(risk_config=RiskConfig(max_position_per_market=10000)) in tests to avoid hitting risk limits unintentionally.
Paper exchange is the default. No credentials needed for tests
engine.tick(market_id, mid_price) triggers paper matching
engine.process_fill(fill) injects fills without going through the exchange
Position snapshots require engine.positions() which returns from the live tracker, not the DB
The fills vector is capped at 1000 entries in the Engine