Skip to content

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.

market_id instance-attribute

market_id: str

slug instance-attribute

slug: str

question instance-attribute

question: str

outcome_bins instance-attribute

outcome_bins: list[OutcomeBin]

close_time instance-attribute

close_time: str

market_family instance-attribute

market_family: str

tags class-attribute instance-attribute

tags: list[str] = Field(default_factory=list)

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.

label instance-attribute

label: str

lower class-attribute instance-attribute

lower: float | None = None

upper class-attribute instance-attribute

upper: float | None = None

lower_inclusive class-attribute instance-attribute

lower_inclusive: bool = True

upper_inclusive class-attribute instance-attribute

upper_inclusive: bool = True

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 price is the YES price).

outcome_label instance-attribute

outcome_label: str

price instance-attribute

price: float

size instance-attribute

size: float

side instance-attribute

side: str

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 position.side is not "buy", "buy_no" or "sell". An unrecognised side used to fall through to the short-YES formula.

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 1 - model_prob).

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; best_edge and direction pick among these. Both edges are always reported.

('yes', 'no')

Raises:

Type Description
ValueError

model_prob or yes_price is not a number in [0, 1], or directions is empty or names a side other than yes/no.

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.

yes_fill instance-attribute

yes_fill: float

no_fill instance-attribute

no_fill: float

yes_edge instance-attribute

yes_edge: float

no_edge instance-attribute

no_edge: float

best_edge instance-attribute

best_edge: float

direction instance-attribute

direction: str

DEFAULT_MIN_EDGE module-attribute

DEFAULT_MIN_EDGE = 0.2

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']

"top_prob" (YES on the most probable bin) or "best_edge" (the best after-fee edge over bins and the allowed sides).

directions tuple[str, ...]

The sides that may be bought, a subset of ("yes", "no").

min_edge float | None

The smallest after-fee edge worth trading under best_edge; None under top_prob, which has no threshold.

selection class-attribute instance-attribute

selection: Literal['top_prob', 'best_edge'] = 'best_edge'

directions class-attribute instance-attribute

directions: tuple[str, ...] = ('yes', 'no')

min_edge class-attribute instance-attribute

min_edge: float | None = DEFAULT_MIN_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.

to_dict

to_dict() -> dict[str, Any]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> TradePolicy

ALL_SEGMENTS module-attribute

ALL_SEGMENTS = 'all'

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 above that reference. 0 disables the term.

depth_reference_stake float

Stake size assumed to fill without walking the book. Must be positive.

taker_bps class-attribute instance-attribute

taker_bps: float = DEFAULT_TAKER_BPS

slippage_bps class-attribute instance-attribute

slippage_bps: float = 0.0

slippage_fixed class-attribute instance-attribute

slippage_fixed: float = 0.0

depth_bps_per_unit class-attribute instance-attribute

depth_bps_per_unit: float = 0.0

depth_reference_stake class-attribute instance-attribute

depth_reference_stake: float = 1.0

frictionless classmethod

frictionless() -> CostModel

A model with no costs at all — useful as a backtest upper bound.

fee

fee(stake: float) -> float

Taker fee charged on stake notional.

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).