Markets, edge and sizing¶
The market model, the edge a quote offers, what a trade costs, and how much to stake. No domain logic: every plugin builds on these.
MarketSpec
¶
Bases: BaseModel
Domain-agnostic descriptor for a single Polymarket market.
Carries the minimum information needed by the core framework —
market identity, outcome structure, and timing.
Domain-specific fields (e.g. city, weather station) live in metadata.
metadata
class-attribute
instance-attribute
¶
metadata: dict[str, Any] = Field(default_factory=dict)
resolve_winning_bin
¶
resolve_winning_bin(realized_value: float) -> str | None
Return the label of the first bin whose range contains realized_value.
Returns None if no bin matches (gap in coverage or uncovered value). For categorical markets, resolve externally by label — do not call this.
OutcomeBin
¶
Bases: BaseModel
A single outcome bin for a prediction market.
Supports: - Unbounded bins (binary YES/NO) - Half-open numeric ranges (temperature, price) - Categorical labels (driver name, country)
For categorical outcomes, leave lower/upper as None and match by label externally.
contains
¶
contains(value: float) -> bool
Return True if value falls inside this bin's numeric range.
Position
dataclass
¶
Position(
outcome_label: str, price: float, size: float, side: str
)
An open binary outcome token position.
Attributes:
| Name | Type | Description |
|---|---|---|
outcome_label |
str
|
The outcome bin label this position is on (e.g. "YES", "30°C", "Verstappen"). |
price |
float
|
Entry price per share (0.0–1.0), of the token actually held: the label's price for "buy", the NO token's price for "buy_no", and the label's (YES) price for "sell". |
size |
float
|
Number of shares held (flat_stake / entry_price). |
side |
str
|
"buy" (long the label's YES token), "buy_no" (long its NO token, which
pays 1 when the label does NOT win) or "sell" (short the YES token; the
same payoff as "buy_no" only when |
settle_position
¶
settle_position(
position: Position,
winning_label: str,
fee_paid: float = 0.0,
) -> float
Compute realized PnL when market resolves.
Formula (mirrors Polymarket binary settlement): payout = 1.0 if position.outcome_label == winning_label else 0.0 buy: pnl = (payout - price) * size - fee_paid buy_no: pnl = ((1 - payout) - price) * size - fee_paid sell: pnl = (price - payout) * size - fee_paid
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The open position to settle. |
required |
winning_label
|
str
|
The resolved outcome label. |
required |
fee_paid
|
float
|
Total fee already paid at entry (not charged again here). |
0.0
|
Returns:
| Type | Description |
|---|---|
float
|
Signed PnL in USDC (positive = profit, negative = loss). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
compute_edge
¶
compute_edge(
fair_probability: float,
executable_price: float,
fee_estimate: float = 0.0,
slippage_estimate: float = 0.0,
) -> float
Compute after-cost probability edge for a binary outcome bet.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fair_probability
|
float
|
Our model's estimated probability for this outcome (0–1). |
required |
executable_price
|
float
|
Market price we actually pay per share (0–1). |
required |
fee_estimate
|
float
|
Expected taker fee on this trade. |
0.0
|
slippage_estimate
|
float
|
Expected price impact / slippage. |
0.0
|
Returns:
| Type | Description |
|---|---|
float
|
Edge value. Positive = expected profit. Negative = expected loss. |
quote_edges
¶
quote_edges(
model_prob: float,
yes_price: float,
costs: CostModel,
stake: float,
directions: tuple[str, ...] = ("yes", "no"),
) -> QuoteEdges
After-fee edge of buying YES and of buying NO on one outcome label.
This is the one place the edge is computed: the backtest's trade policy and a live
EdgeSignal (EdgeSignal.from_quote) both call it, so a champion is promoted on
the same decision rule that trades.
yes_edge = p - fill(yes_price) - fee_rate
no_edge = (1 - p) - fill(1 - yes_price) - fee_rate
fill is the cost model's executable price (slippage and depth included) and
fee_rate is the taker fee per unit of stake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_prob
|
float
|
The model's probability that the label occurs, whichever side is
bought (the NO token is worth |
required |
yes_price
|
float
|
The label's YES price at decision time. |
required |
costs
|
CostModel
|
The cost model of the backtest or the broker. |
required |
stake
|
float
|
Notional risked, which the cost model's depth term depends on. |
required |
directions
|
tuple[str, ...]
|
The sides the strategy may take; |
('yes', 'no')
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
QuoteEdges
dataclass
¶
QuoteEdges(
yes_fill: float,
no_fill: float,
yes_edge: float,
no_edge: float,
best_edge: float,
direction: str,
)
The edge of each side of one outcome label, and the better of the two.
An edge is -inf for a side that cannot be bought (see quote_edges).
direction is "yes" or "no"; a tie goes to YES.
TradePolicy
dataclass
¶
TradePolicy(
selection: Literal[
"top_prob", "best_edge"
] = "best_edge",
directions: tuple[str, ...] = ("yes", "no"),
min_edge: float | None = DEFAULT_MIN_EDGE,
)
Which (bin, side) a strategy trades in a market, and when.
Attributes:
| Name | Type | Description |
|---|---|---|
selection |
Literal['top_prob', 'best_edge']
|
|
directions |
tuple[str, ...]
|
The sides that may be bought, a subset of |
min_edge |
float | None
|
The smallest after-fee edge worth trading under |
selection
class-attribute
instance-attribute
¶
selection: Literal['top_prob', 'best_edge'] = 'best_edge'
top_bin_yes
classmethod
¶
top_bin_yes() -> TradePolicy
The historical backtest: YES on the most probable bin of every market.
live
classmethod
¶
live(
min_edge: float = DEFAULT_MIN_EDGE,
directions: tuple[str, ...] = ("yes", "no"),
) -> TradePolicy
What record-trades trades: the best-edge (bin, side), if it clears min_edge.
segment_allowed
¶
segment_allowed(
segment: str, allowed: Collection[str] | None
) -> bool
Is segment allowed to trade?
None means no filtering. An empty collection means none: a champion that allows
nothing could never have recorded a trade. "all" on its own allows every segment;
beside other names it is just a segment called "all".
estimate_fee
¶
estimate_fee(
flat_stake: float, taker_bps: float = DEFAULT_TAKER_BPS
) -> float
Estimate taker fee for a trade sized at flat_stake USDC.
Polymarket charges taker_bps / 10_000 on the notional stake (not per share).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flat_stake
|
float
|
Notional USDC being risked (e.g. 1.0 USDC). |
required |
taker_bps
|
float
|
Taker fee in basis points (default 30 = 0.30%). |
DEFAULT_TAKER_BPS
|
Returns:
| Type | Description |
|---|---|
float
|
Fee amount in USDC. |
CostModel
dataclass
¶
CostModel(
taker_bps: float = DEFAULT_TAKER_BPS,
slippage_bps: float = 0.0,
slippage_fixed: float = 0.0,
depth_bps_per_unit: float = 0.0,
depth_reference_stake: float = 1.0,
)
Execution costs applied to a backtest fill.
Attributes:
| Name | Type | Description |
|---|---|---|
taker_bps |
float
|
Taker fee in basis points of notional stake. Defaults to Polymarket's 30bps so the default model matches historical results. |
slippage_bps |
float
|
Price-proportional slippage in basis points. Models the half-spread you cross on entry. |
slippage_fixed |
float
|
Absolute slippage added to the fill price, in probability units (0.01 = one cent of probability). |
depth_bps_per_unit |
float
|
Extra slippage in basis points for each multiple of
|
depth_reference_stake |
float
|
Stake size assumed to fill without walking the book. Must be positive. |
frictionless
classmethod
¶
frictionless() -> CostModel
A model with no costs at all — useful as a backtest upper bound.
depth_penalty_bps
¶
depth_penalty_bps(stake: float) -> float
Extra slippage in bps from walking the book at this size.
fill_price
¶
fill_price(price: float, stake: float) -> float
Price actually paid per share when buying stake notional at price.
Always >= price, and never above 1.0: costs must not manufacture a
position that pays more than certainty.
to_dict
¶
to_dict() -> dict[str, float]
Flat mapping for the experiment tracker; round-trips via CostModel(**d).
flat_stake_size
¶
flat_stake_size(
flat_stake: float, entry_price: float
) -> float
Compute share count for a flat-stake bet.
Buys flat_stake / entry_price shares so the maximum loss is flat_stake (when the trade goes to 0) and the gross profit at win is flat_stake * (1 - entry_price) / entry_price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flat_stake
|
float
|
Fixed USDC amount to risk per trade. |
required |
entry_price
|
float
|
Price per share at execution (0-1). |
required |
Returns:
| Type | Description |
|---|---|
float
|
Number of shares to buy. |
kelly_fraction
¶
kelly_fraction(
win_prob: float,
entry_price: float,
fraction: float = 0.25,
) -> float
Compute the (fractional) Kelly fraction of bankroll to wager.
Uses the binary-outcome Kelly formula
f* = (b * p - q) / b
where b = net_odds = (1 - entry_price) / entry_price, p = win_prob, q = 1 - win_prob.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
win_prob
|
float
|
Model-estimated probability the bet wins (0-1). |
required |
entry_price
|
float
|
Cost per share (0-1), i.e. market-implied probability. |
required |
fraction
|
float
|
Fraction of full-Kelly to use (default 0.25 = quarter-Kelly). |
0.25
|
Returns:
| Type | Description |
|---|---|
float
|
Fraction of bankroll to wager (0-1, floored at 0). |
kelly_stake_size
¶
kelly_stake_size(
win_prob: float,
entry_price: float,
bankroll: float,
fraction: float = 0.25,
max_exposure: float = 0.05,
) -> float
Compute Kelly-sized USDC stake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
win_prob
|
float
|
Model probability the bet wins. |
required |
entry_price
|
float
|
Cost per share (0-1). |
required |
bankroll
|
float
|
Total USDC bankroll available. |
required |
fraction
|
float
|
Fractional Kelly multiplier (default 0.25). |
0.25
|
max_exposure
|
float
|
Max fraction of bankroll per trade (default 5%). |
0.05
|
Returns:
| Type | Description |
|---|---|
float
|
USDC stake amount (>= 0). |