---
title: "Testing"
description: "Test structure, running the test suite, and writing strategy tests."
icon: "flask-vial"
---

Horizon has full test coverage across both Rust and Python layers.

## Running Tests

<CodeGroup>
```bash Rust tests
cargo test
```

```bash Python tests
pytest tests/ -v
```

```bash All tests
cargo test && pytest tests/ -v
```
</CodeGroup>

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

```python
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:

```python
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

```python
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:

```python
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

```python
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

<Tip>
  Use `Engine(risk_config=RiskConfig(max_position_per_market=10000))` in tests to avoid hitting risk limits unintentionally.
</Tip>

- 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
