Writing Custom Venues

Implement the Venue protocol to add a new broker or exchange

A custom venue is any class that implements the Venue protocol. No base class, no registration. Drop it into hz.run(venues={"mine": MyVenue(...)}) and it works.

Minimal structure

python
from horizon.asset_classes import AssetClass, Equity
from horizon.venues.base import (
    Venue,
    VenueCapital,
    VenueFill,
    VenueOrder,
    VenuePosition,
)

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

    def __init__(self, api_key_env: str, budget_usd: float):
        import os
        self._api_key = os.environ[api_key_env]
        self.budget_usd = budget_usd
        self._connected = False
        self._client = None

    def connect(self) -> None:
        self._client = MyBrokerClient(self._api_key)
        self._connected = True

    def close(self) -> None:
        self._connected = False
        self._client = None

    def is_connected(self) -> bool:
        return self._connected

    def submit(self, action) -> VenueOrder:
        resp = self._client.submit_order(
            symbol=action.market_id,
            side=action.side,
            qty=action.quantity,
            limit=action.price,
            type=action.order_type,
        )
        return VenueOrder(
            id=resp["order_id"],
            market_id=action.market_id,
            side=action.side,
            quantity=action.quantity,
            filled_quantity=0.0,
            price=action.price,
            order_type=action.order_type,
            status="new",
            venue_name=self.venue_name,
        )

    def cancel(self, order_id: str) -> bool:
        return self._client.cancel(order_id).ok

    def cancel_all(self, market_id=None) -> int:
        return self._client.cancel_all(symbol=market_id)

    def amend(self, order_id, new_quantity=None, new_price=None) -> VenueOrder:
        resp = self._client.amend_order(order_id, quantity=new_quantity, price=new_price)
        return VenueOrder(
            id=resp["order_id"],
            market_id=resp["symbol"],
            side=resp["side"],
            quantity=resp["qty"],
            filled_quantity=resp["filled_qty"],
            price=resp["price"],
            order_type=resp["type"],
            status=resp["status"],
            venue_name=self.venue_name,
        )

    def positions(self) -> list[VenuePosition]:
        raw = self._client.list_positions()
        return [
            VenuePosition(
                market_id=p["symbol"],
                quantity=float(p["qty"]),
                avg_cost=float(p["avg_cost"]),
                unrealized_pnl=float(p["unrealized_pl"]),
                realized_pnl=0.0,
                notional_usd=abs(float(p["market_value"])),
                capital_used_usd=abs(float(p["market_value"])),
                venue_name=self.venue_name,
            )
            for p in raw
        ]

    def open_orders(self, market_id=None) -> list[VenueOrder]:
        raw = self._client.list_open_orders(symbol=market_id)
        return [
            VenueOrder(
                id=o["id"],
                market_id=o["symbol"],
                side=o["side"],
                quantity=float(o["qty"]),
                filled_quantity=float(o["filled_qty"]),
                price=float(o.get("limit_price", 0)) or None,
                order_type=o["type"],
                status=o["status"],
                venue_name=self.venue_name,
            )
            for o in raw
        ]

    def balance(self) -> VenueCapital:
        acct = self._client.account()
        return VenueCapital(
            venue_name=self.venue_name,
            total_equity_usd=float(acct["equity"]),
            cash_usd=float(acct["cash"]),
            buying_power_usd=float(acct["buying_power"]),
            used_buying_power_usd=float(acct["equity"]) - float(acct["cash"]),
            free_buying_power_usd=float(acct["cash"]),
        )

    def drain_fills(self) -> list[VenueFill]:
        raw = self._client.recent_fills()
        return [
            VenueFill(
                order_id=f["order_id"],
                market_id=f["symbol"],
                side=f["side"],
                quantity=float(f["qty"]),
                price=float(f["price"]),
                fee=float(f.get("commission", 0)),
                venue_name=self.venue_name,
            )
            for f in raw
        ]

    def list_markets(self, filter=None) -> list:
        return self._client.list_assets()

    def orderbook(self, market_id):
        return self._client.orderbook(market_id)

    def market_info(self, market_id):
        return self._client.asset(market_id)

Using it

python
from my_venue import MyBroker

hz.run(
    venues={
        "mine": MyBroker(api_key_env="MY_BROKER_KEY", budget_usd=50_000),
    },
    ...
)

Or via the imperative API:

python
with hz.connect("mine", api_key_env="MY_BROKER_KEY", budget_usd=50_000) as ex:
    ex.buy("AAPL", 10, limit=180)

Testing your venue

Mock the underlying client and test the protocol implementation:

python
def test_submit_returns_venue_order():
    client = MockBrokerClient()
    venue = MyBroker(api_key_env="TEST_KEY", budget_usd=10_000)
    venue._client = client   # bypass connect for test
    venue._connected = True

    action = OrderAction.place(
        market_id="AAPL",
        side="buy",
        quantity=10,
        price=180,
        order_type="limit",
    )
    order = venue.submit(action)
    assert order.market_id == "AAPL"
    assert order.quantity == 10
    assert order.status == "new"

Wrapping an existing client library

For well-known brokers (Alpaca, IBKR, etc.), the pattern is:

  1. Import the vendor SDK
  2. Map the SDK’s order/position/fill types to Horizon’s VenueOrder / VenuePosition / VenueFill
  3. Handle connect / close / auth

For Alpaca:

python
from alpaca.trading.client import TradingClient

class AlpacaVenue:
    venue_name = "alpaca"
    supported_classes = [AssetClass.Equity, AssetClass.Option, AssetClass.Crypto]

    def connect(self):
        self._client = TradingClient(self._api_key, self._api_secret, paper=self.paper)
        self._connected = True

    def submit(self, action):
        from alpaca.trading.requests import LimitOrderRequest, MarketOrderRequest
        ...

See horizon/venues/alpaca.py for the scaffold: the binding is a future release.

Next