""" Sharpe ratio from the paper portfolio's daily PnL curve, with a minimum-sample gate. The input series is the closing total_pnl of each observed UTC day (Database.get_daily_pnl_closes). Daily returns are PnL deltas normalized by the paper bankroll: r_t = (pnl_t − pnl_{t−1}) / bankroll Sharpe = mean(r) / sample_std(r) × √365, annualized — prediction markets resolve every calendar day, so 365 is used instead of 252 trading days. Risk-free rate is taken as 0. Gate: with a tiny sample (e.g. 1 resolved trade over a flat curve plus one +299 jump) any Sharpe value is statistically meaningless — artificially huge or tiny depending on where the jump lands. So no numeric Sharpe is exposed until BOTH minimums are met: days observed >= MIN_DAYS_OBSERVED (30) resolved trades >= MIN_RESOLVED_TRADES (10) Below either minimum the value is None with status "insufficient_sample". A perfectly flat curve (zero variance) also yields None ("zero_variance"): Sharpe is undefined there, not infinite. """ from statistics import mean, stdev from typing import Optional MIN_DAYS_OBSERVED = 30 MIN_RESOLVED_TRADES = 10 ANNUALIZATION_DAYS = 365 SHARPE_OK = "ok" SHARPE_INSUFFICIENT = "insufficient_sample" SHARPE_ZERO_VARIANCE = "zero_variance" def daily_returns(daily_pnl_closes: list[float], bankroll: float) -> list[float]: """Bankroll-normalized day-over-day returns from a daily PnL-close series.""" return [ (curr - prev) / bankroll for prev, curr in zip(daily_pnl_closes, daily_pnl_closes[1:]) ] def compute_sharpe(daily_pnl_closes: list[float], bankroll: float) -> Optional[float]: """Annualized Sharpe of the daily PnL curve, or None if undefined. None when there are fewer than 2 returns (need 3+ daily closes) or the return series has zero variance. No sample-size gate here — see sharpe_with_gate() for the exposed value. """ returns = daily_returns(daily_pnl_closes, bankroll) if len(returns) < 2: return None sd = stdev(returns) if sd == 0: return None return mean(returns) / sd * ANNUALIZATION_DAYS ** 0.5 def sharpe_with_gate( daily_pnl_closes: list[float], bankroll: float, resolved_count: int, ) -> tuple[Optional[float], str]: """Return (sharpe, status) applying the minimum-sample gate. status: "ok" — sharpe is a meaningful float "insufficient_sample" — sample below minimums, sharpe is None "zero_variance" — sample OK but flat curve, sharpe is None """ days_observed = len(daily_pnl_closes) if days_observed < MIN_DAYS_OBSERVED or resolved_count < MIN_RESOLVED_TRADES: return None, SHARPE_INSUFFICIENT sharpe = compute_sharpe(daily_pnl_closes, bankroll) if sharpe is None: return None, SHARPE_ZERO_VARIANCE return sharpe, SHARPE_OK