Venues

Venue protocol, Paper venue, imperative API

Venue protocol

Every venue (paper, live, custom) implements this interface:

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Venue(Protocol):
    venue_name: str
    supported_classes: list[AssetClass]
    budget_usd: float

    def connect(self) -> None
    def close(self) -> None
    def is_connected(self) -> bool

    def submit(self, action: OrderAction) -> VenueOrder
    def cancel(self, order_id: str) -> bool
    def cancel_all(self, market_id: str | None = None) -> int
    def amend(self, order_id: str, new_quantity: float | None = None,
              new_price: float | None = None) -> VenueOrder

    def positions(self) -> list[VenuePosition]
    def open_orders(self, market_id: str | None = None) -> list[VenueOrder]
    def balance(self) -> VenueCapital
    def drain_fills(self) -> list[VenueFill]

    def list_markets(self, filter: Any = None) -> list[Any]
    def orderbook(self, market_id: str) -> Any
    def market_info(self, market_id: str) -> Any

VenueOrder / VenueFill / VenuePosition / VenueCapital

python
@dataclass(frozen=True)
class VenueOrder:
    id: str
    market_id: str
    side: str                # "buy" | "sell"
    quantity: float
    filled_quantity: float
    price: float | None
    order_type: str
    status: str              # "new" | "partial" | "filled" | "canceled" | "rejected"
    submitted_at: datetime | None
    venue_name: str
    metadata: dict[str, Any]

    @property
    def remaining: float        # max(0, quantity - filled_quantity)
    @property
    def is_open: bool           # status in ("new", "partial")

@dataclass(frozen=True)
class VenueFill:
    order_id: str
    market_id: str
    side: str
    quantity: float
    price: float
    fee: float = 0.0
    timestamp: datetime | None = None
    venue_name: str = ""
    multiplier: float = 1.0

@dataclass(frozen=True)
class VenuePosition:
    market_id: str
    quantity: float
    avg_cost: float
    unrealized_pnl: float
    realized_pnl: float
    notional_usd: float
    capital_used_usd: float
    multiplier: float = 1.0
    venue_name: str = ""
    metadata: dict[str, Any]

@dataclass(frozen=True)
class VenueCapital:
    venue_name: str
    total_equity_usd: float
    cash_usd: float
    buying_power_usd: float
    used_buying_power_usd: float
    free_buying_power_usd: float
    maintenance_margin_usd: float = 0.0
    maintenance_ratio: float = 0.0
    currency: str = "USD"

Paper venue

In-memory paper exchange with tick-based matching.

python
from horizon.venues import Paper

paper = Paper(
    initial_cash_usd=100_000.0,
    supported_classes=None,        # defaults to all asset classes
    fee_bps=0.0,
    slippage_bps=0.0,
    bar_fill_mode="cross",         # "cross" | "touch"
)
paper.connect()

Tick matching

python
paper.tick("AAPL", 99.0, timestamp=datetime(2024, 1, 1))
  • Updates _last_prices["AAPL"] = 99.0
  • For every resting limit order on AAPL, if the price crosses, fill at the limit price
  • Returns a list of new VenueFill events

Market orders

Market orders fill immediately at the last known price plus slippage:

python
paper.tick("AAPL", 100.0)   # establish last price
order = paper.submit(
    OrderAction.place(market_id="AAPL", side="buy", quantity=10, order_type="market")
)
# fills immediately at 100 + slippage

If no tick has been seen yet, market orders are rejected cleanly (status="rejected").

Queries

python
paper.positions()        # list[VenuePosition]
paper.open_orders()      # list[VenueOrder]
paper.balance()          # VenueCapital
paper.drain_fills()      # list[VenueFill]. drains the pending queue
paper.cash()             # float. convenience
paper.realized_pnl()     # float. convenience

Tests

16 tests in tests/test_paper_venue.py covering limit matching on cross, market fills, slippage, cancels, amends, cash/position tracking, fee application, and fill draining.

Imperative API (level 1)

Use hz.connect() to get an ImperativeClient wrapped around any venue:

python
import horizon as hz

with hz.connect("paper", initial_cash_usd=100_000) as ex:
    ex.buy("AAPL", qty=10, limit=180.0)
    ex.sell("MSFT", qty=5, market=True)
    print(ex.positions())
    print(ex.balance())

ImperativeClient methods

python
# Place orders
ex.place_order(market_id, side, quantity, order_type="limit", price=None, ...)
ex.buy(market_id, qty, limit=None, market=False, **kwargs)
ex.sell(market_id, qty, limit=None, market=False, **kwargs)
ex.short(market_id, qty, limit=None, **kwargs)
ex.cover(market_id, qty, limit=None, **kwargs)

# Asset-class helpers
ex.buy_option(underlying, strike, expiry, right, qty, limit=None)
ex.buy_prediction(market, qty, limit, side="yes")
ex.buy_perp(market_id, qty, leverage=1.0, limit=None, reduce_only=False)

# Modify / cancel
ex.amend(order_id, new_quantity=None, new_price=None)
ex.cancel(order_id)
ex.cancel_all(market_id=None)
ex.flatten(market_id)          # close all positions in a market
ex.flatten_all()                # close everything

# Queries
ex.positions()      # list[VenuePosition]
ex.open_orders()    # list[VenueOrder]
ex.balance()        # VenueCapital
ex.fills()          # drain pending fills
ex.realized_pnl()   # float

Context-manager support is built in:

python
with hz.connect("paper", initial_cash_usd=100_000) as ex:
    ex.buy("AAPL", 10, limit=180)
# ex.close() called automatically

Writing a custom venue

python
from horizon.asset_classes import AssetClass
from horizon.venues.base import Venue, VenueCapital

class MyBroker:
    venue_name = "my_broker"
    supported_classes = [AssetClass.Equity]

    def __init__(self, api_key_env: str, budget_usd: float):
        self._api_key = os.environ[api_key_env]
        self.budget_usd = budget_usd

    def connect(self): ...
    def close(self): ...
    def is_connected(self): ...
    def submit(self, action): ...
    def cancel(self, order_id): ...
    # ... etc

Drop into hz.run(venues={"mine": MyBroker(...)}, ...).

Next