Multi-Outcome Arbitrage
When an event has N outcomes (e.g., 4 candidates in an election), the sum of all YES prices should equal $1.00. If it doesn’t, there’s an arbitrage opportunity.
How It Works
- Sum < $1.00 - fees: Buy all outcomes. One will resolve to $1.00, so you profit the difference.
- Sum > $1.00 + fees: Sell all outcomes (buy all NOs). You collect more than $1.00 upfront.
Trump: $0.30
Biden: $0.25
DeSantis: $0.20
Other: $0.15
Sum: $0.90 (< $1.00)
Edge: $0.10 - fees
Action: Buy all outcomes
Engine Methods
scan_event_arb
engine.register_event("election", ["trump", "biden", "desantis", "other"])
opp = engine.scan_event_arb("election", fee_rate=0.002)
# Returns EventArbitrageOpportunity or None
execute_event_arb
order_ids = engine.execute_event_arb(
event_id="election",
direction="buy_all", # or "sell_all"
size=50.0,
proportional=True, # Size inversely proportional to price
)
N-leg atomic execution. If any leg fails, all prior legs are canceled.
When proportional=True: cheaper outcomes get more contracts so cost is balanced across legs.
Pipeline: event_arb_scanner
scanner = hz.event_arb_scanner(
event_id="election",
min_edge=0.01,
max_size=50.0,
fee_rate=0.002,
auto_execute=True,
cooldown=10.0,
proportional=True,
)
hz.run(pipeline=[scanner], ...)
| Parameter | Type | Default | Description |
|---|
event_id | str | required | Registered event ID |
min_edge | float | 0.01 | Minimum net edge |
max_size | float | 50.0 | Maximum total size |
fee_rate | float | 0.002 | Fee rate per outcome |
auto_execute | bool | False | Auto-execute |
cooldown | float | 10.0 | Seconds between executions |
proportional | bool | True | Inverse-proportional sizing |
Stores EventArbitrageOpportunity in ctx.params["last_event_arb"].
One-Shot: event_arb_sweep
result = hz.event_arb_sweep(engine, "election", min_edge=0.01)
if result:
print(f"Direction: {result.direction}, Edge: {result.net_edge:.4f}")
print(f"Order IDs: {result.order_ids}")
Returns EventArbResult or None.
EventArbResult
| Field | Type | Description |
|---|
event_id | str | Event identifier |
direction | str | "buy_all" or "sell_all" |
price_sum | float | Sum of all outcome prices |
net_edge | float | Edge after fees |
size | float | Trade size |
order_ids | list[str] | Order IDs for each leg |
timestamp | float | Execution timestamp |
Register events with engine.register_event(event_id, market_ids) before scanning. Each market needs a corresponding feed for price data.
Logical Arb Scanners
Beyond price-sum arbitrage, Horizon detects logical relationship violations between related markets.
hz.implication_arb_scanner
For markets where A implies B (e.g., “Biden wins” implies “Democrat wins”), the price of A must be less than or equal to the price of B. Violations are arbs.
scanner = hz.implication_arb_scanner(
markets=["biden-wins", "dem-wins", "trump-wins", "gop-wins"],
implications=[
("biden-wins", "dem-wins"), # Biden wins => Democrat wins
("trump-wins", "gop-wins"), # Trump wins => GOP wins
],
min_edge=0.02,
)
hz.run(pipeline=[scanner], ...)
# Injects ctx.params["implication_arbs"] = [
# {"type": "implication", "market_a": ..., "market_b": ..., "edge": ..., "action": ...}
# ]
| Parameter | Type | Description |
|---|
markets | list[str] | Market IDs to monitor |
implications | list[tuple[str, str]] | (A, B) pairs where A implies B |
min_edge | float | Minimum price violation to flag (default 0.01) |
hz.contradiction_arb_scanner
For mutually exclusive markets (e.g., “Trump wins” and “Biden wins”), the sum of prices must be at most 1.0. If P(A) + P(B) > 1, sell both.
scanner = hz.contradiction_arb_scanner(
markets=["trump-wins", "biden-wins"],
contradictions=[
("trump-wins", "biden-wins"), # Can't both win
],
min_edge=0.02,
)
hz.run(pipeline=[scanner], ...)
# Injects ctx.params["contradiction_arbs"] = [
# {"type": "contradiction", "market_a": ..., "price_sum": ..., "edge": ..., "action": ...}
# ]
| Parameter | Type | Description |
|---|
markets | list[str] | Market IDs to monitor |
contradictions | list[tuple[str, str]] | (A, B) pairs that are mutually exclusive |
min_edge | float | Minimum sum violation above 1.0 to flag (default 0.01) |