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:
- Import the vendor SDK
- Map the SDK’s order/position/fill types to Horizon’s
VenueOrder/VenuePosition/VenueFill - 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.