first commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Self
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
def _uuid() -> str:
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
class Side(str, Enum):
|
||||
BUY = "BUY"
|
||||
SELL = "SELL"
|
||||
|
||||
|
||||
class OrderType(str, Enum):
|
||||
GTC = "GTC"
|
||||
FAK = "FAK"
|
||||
FOK = "FOK"
|
||||
GTD = "GTD"
|
||||
|
||||
|
||||
class BasketStatus(str, Enum):
|
||||
OPEN = "open"
|
||||
PARTIAL = "partial"
|
||||
FAILED = "failed"
|
||||
PENDING_RESOLUTION = "pending_resolution"
|
||||
REDEEMED = "redeemed"
|
||||
INVALID = "invalid"
|
||||
|
||||
|
||||
class Outcome(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
token_id: str
|
||||
name: str
|
||||
outcome_index: int
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str
|
||||
slug: str
|
||||
title: str
|
||||
is_neg_risk: bool
|
||||
end_date: datetime | None
|
||||
outcomes: tuple[Outcome, ...]
|
||||
|
||||
@property
|
||||
def n_outcomes(self) -> int:
|
||||
return len(self.outcomes)
|
||||
|
||||
|
||||
class BookLevel(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
price: Decimal
|
||||
size: Decimal
|
||||
|
||||
|
||||
class Book(BaseModel):
|
||||
"""L2 order book for a single token. Bids descending, asks ascending."""
|
||||
|
||||
token_id: str
|
||||
bids: list[BookLevel] = Field(default_factory=list)
|
||||
asks: list[BookLevel] = Field(default_factory=list)
|
||||
sequence: int = 0
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def best_bid(self) -> BookLevel | None:
|
||||
return self.bids[0] if self.bids else None
|
||||
|
||||
@property
|
||||
def best_ask(self) -> BookLevel | None:
|
||||
return self.asks[0] if self.asks else None
|
||||
|
||||
def vwap_buy(self, target_size: Decimal) -> tuple[Decimal, Decimal] | None:
|
||||
"""Walk asks to fill `target_size`. Returns (vwap_price, filled_size).
|
||||
If the book can't fill the full size, returns the partial fill."""
|
||||
if target_size <= 0 or not self.asks:
|
||||
return None
|
||||
remaining = target_size
|
||||
cost = Decimal(0)
|
||||
filled = Decimal(0)
|
||||
for level in self.asks:
|
||||
take = min(remaining, level.size)
|
||||
cost += take * level.price
|
||||
filled += take
|
||||
remaining -= take
|
||||
if remaining <= 0:
|
||||
break
|
||||
if filled <= 0:
|
||||
return None
|
||||
return (cost / filled, filled)
|
||||
|
||||
|
||||
class OpportunityLeg(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
token_id: str
|
||||
outcome_name: str
|
||||
outcome_index: int
|
||||
vwap_price: Decimal
|
||||
size: Decimal
|
||||
levels_consumed: int
|
||||
|
||||
|
||||
class Opportunity(BaseModel):
|
||||
"""A snapshot of edge math for one event at one moment."""
|
||||
|
||||
id: str = Field(default_factory=_uuid)
|
||||
detected_at: datetime
|
||||
event_id: str
|
||||
event_title: str
|
||||
legs: tuple[OpportunityLeg, ...]
|
||||
sum_vwap_asks: Decimal
|
||||
fees_per_share: Decimal
|
||||
gas_per_basket_usd: Decimal
|
||||
gross_edge_usd_per_basket: Decimal
|
||||
net_edge_usd_per_basket: Decimal
|
||||
net_edge_bps: int
|
||||
max_baskets: Decimal
|
||||
expected_profit_usd: Decimal
|
||||
|
||||
@classmethod
|
||||
def from_legs(
|
||||
cls,
|
||||
*,
|
||||
detected_at: datetime,
|
||||
event: Event,
|
||||
legs: tuple[OpportunityLeg, ...],
|
||||
fees_per_share: Decimal,
|
||||
gas_per_basket_usd: Decimal,
|
||||
max_baskets: Decimal,
|
||||
) -> Self:
|
||||
sum_asks = sum((leg.vwap_price for leg in legs), Decimal(0))
|
||||
gross_per_basket = Decimal(1) - sum_asks
|
||||
n = Decimal(event.n_outcomes)
|
||||
net_per_basket = gross_per_basket - (fees_per_share * n) - gas_per_basket_usd
|
||||
net_bps = int((net_per_basket / Decimal(1)) * Decimal(10_000)) if net_per_basket else 0
|
||||
expected = net_per_basket * max_baskets
|
||||
return cls(
|
||||
detected_at=detected_at,
|
||||
event_id=event.id,
|
||||
event_title=event.title,
|
||||
legs=legs,
|
||||
sum_vwap_asks=sum_asks,
|
||||
fees_per_share=fees_per_share,
|
||||
gas_per_basket_usd=gas_per_basket_usd,
|
||||
gross_edge_usd_per_basket=gross_per_basket,
|
||||
net_edge_usd_per_basket=net_per_basket,
|
||||
net_edge_bps=net_bps,
|
||||
max_baskets=max_baskets,
|
||||
expected_profit_usd=expected,
|
||||
)
|
||||
|
||||
|
||||
class Fill(BaseModel):
|
||||
token_id: str
|
||||
side: Side
|
||||
price: Decimal
|
||||
size: Decimal
|
||||
fee_usd: Decimal
|
||||
filled_at: datetime
|
||||
|
||||
|
||||
class Basket(BaseModel):
|
||||
id: str = Field(default_factory=_uuid)
|
||||
opportunity_id: str
|
||||
event_id: str
|
||||
is_paper: bool
|
||||
created_at: datetime
|
||||
basket_count: Decimal
|
||||
total_cost_usd: Decimal
|
||||
status: BasketStatus
|
||||
fills: list[Fill] = Field(default_factory=list)
|
||||
redeemed_at: datetime | None = None
|
||||
redeemed_payout_usd: Decimal | None = None
|
||||
realized_pnl_usd: Decimal | None = None
|
||||
|
||||
|
||||
class Resolution(BaseModel):
|
||||
event_id: str
|
||||
winning_outcome_token_id: str | None
|
||||
resolved_at: datetime
|
||||
source: str
|
||||
Reference in New Issue
Block a user