Skip to content

metrics.economics

Turns a pass probability plus your own prices into an expected value per attempt, and the pass rate at which it turns positive.

economics

Expected value of attempting a Combine: is the subscription worth paying?

A pass probability alone does not tell you whether to attempt an evaluation. 60% is excellent at $49/month and terrible if a pass is worth $200 to you. This module turns a :class:~topstep_backtest.metrics.montecarlo.MonteCarloResult plus the prices YOU actually pay into an expected value.

Every price is a required input. Nothing is baked in. Combine fees, reset pricing and payout terms change, vary by promotion, and are exactly the class of number this project refuses to hard-code uncalibrated (docs/topstep-rules.md §9). Passing your own figures is not a chore here — it is the only way the answer means anything.

The honest headline is breakeven_pass_value, not ev. ev requires you to state what a funded account is worth to you, and that single assumption dominates the result — it depends on your funded-phase performance, the payout schedule, and the scaling plan, none of which this package models (Express Funded is parked; see docs/ROADMAP.md). breakeven_pass_value inverts the question into one you can answer without guessing: how much would a pass have to be worth for this attempt to be worth making? If that number is uncomfortable, no EV estimate will rescue it.

EvalEconomics

Bases: Struct

What one evaluation attempt costs YOU. No defaults on the prices.

monthly_fee instance-attribute

monthly_fee: Decimal

The Combine subscription, per month. Billed for as long as the attempt runs, which is why a slow edge is expensive even when it eventually passes.

pass_value instance-attribute

pass_value: Decimal

What reaching a funded account is worth to you, in dollars.

There is no defensible default and this package will not invent one — it depends on your funded-phase results and the payout schedule, neither of which is modelled here. If you cannot state it, read EvalEV.breakeven_pass_value instead and ignore ev entirely.

reset_fee class-attribute instance-attribute

reset_fee: Decimal | None = None

Cost to reset a blown account and continue within the same subscription. None means you re-subscribe from scratch instead, and a failed attempt costs a fresh monthly_fee.

trading_days_per_month class-attribute instance-attribute

trading_days_per_month: int = BILLING_MONTH_DAYS

Sessions billed per monthly period — the SAME constant the Monte Carlo defaults its horizon to, so "one attempt" means one fee cycle on both sides of an EV calculation. Override it if your data says otherwise.

EvalEV

Bases: Struct

Expected value of one attempt, and the number that needs no assumption.

pass_probability instance-attribute

pass_probability: Decimal

expected_months instance-attribute

expected_months: Decimal

Billed months per attempt: days-to-pass for a winning path, the full horizon for a losing one, divided by trading_days_per_month and rounded UP — a subscription is not prorated by the day.

expected_cost instance-attribute

expected_cost: Decimal

Subscription plus any reset fee, weighted by outcome.

expected_gross instance-attribute

expected_gross: Decimal

pass_probability x pass_value.

ev instance-attribute

ev: Decimal

expected_gross - expected_cost. Only as meaningful as pass_value, which you supplied and this package cannot check.

breakeven_pass_value instance-attribute

breakeven_pass_value: Decimal | None

The pass_value at which ev becomes zero — how much a pass must be worth to justify the attempt.

Requires no assumption about payouts and is therefore the figure to lead with. None when the pass probability is zero: no finite value justifies an attempt that cannot succeed, and reporting a number there would be arithmetic dressed up as advice.

breakeven_pass_probability instance-attribute

breakeven_pass_probability: Decimal | None

The pass probability at which ev becomes zero, holding pass_value fixed. Compare it against the Monte-Carlo figure to see how much headroom the attempt has. None when pass_value is zero.

evaluate_ev

evaluate_ev(mc: MonteCarloResult, *, economics: EvalEconomics) -> EvalEV

Combine a bootstrapped outcome distribution with your own prices.

Uses mc.pass_probability and mc.median_days_to_pass (median, not mean: days-to-pass is right-skewed, and the mean is dragged by the paths that scrape in at the horizon). A failing attempt is billed for the full horizon, because you pay until you quit or blow up.

Every arithmetic step stays in Decimal; nothing here is estimated by simulation, so nothing here needs floats.

Source code in src/topstep_backtest/metrics/economics.py
def evaluate_ev(mc: MonteCarloResult, *, economics: EvalEconomics) -> EvalEV:
    """Combine a bootstrapped outcome distribution with your own prices.

    Uses ``mc.pass_probability`` and ``mc.median_days_to_pass`` (median, not
    mean: days-to-pass is right-skewed, and the mean is dragged by the paths
    that scrape in at the horizon). A failing attempt is billed for the full
    horizon, because you pay until you quit or blow up.

    Every arithmetic step stays in ``Decimal``; nothing here is estimated by
    simulation, so nothing here needs floats.
    """
    p = mc.pass_probability
    per_month = Decimal(economics.trading_days_per_month)

    def months(days: int) -> Decimal:
        # Subscriptions are not prorated: a 22-day attempt bills two months.
        whole, rem = divmod(Decimal(days), per_month)
        return whole + (1 if rem > _ZERO else 0)

    win_months = months(mc.median_days_to_pass) if mc.median_days_to_pass else _ZERO
    lose_months = months(mc.horizon_days)
    expected_months = p * win_months + (1 - p) * lose_months

    reset = economics.reset_fee if economics.reset_fee is not None else _ZERO
    expected_cost = expected_months * economics.monthly_fee + (1 - p) * reset
    expected_gross = p * economics.pass_value

    breakeven_value = expected_cost / p if p > _ZERO else None
    breakeven_p = (
        # cost is itself a function of p, so solve rather than divide:
        #   p*V - [p*Wm + (1-p)*Lm]*F - (1-p)*R = 0
        (lose_months * economics.monthly_fee + reset)
        / (economics.pass_value + (lose_months - win_months) * economics.monthly_fee + reset)
        if economics.pass_value > _ZERO
        else None
    )

    return EvalEV(
        pass_probability=p,
        expected_months=expected_months,
        expected_cost=expected_cost,
        expected_gross=expected_gross,
        ev=expected_gross - expected_cost,
        breakeven_pass_value=breakeven_value,
        breakeven_pass_probability=breakeven_p,
    )