Files
my-polymarket-copytrader/src/services/kelly.py
T

70 lines
2.4 KiB
Python

"""Kelly position sizer with Favorite-Longshot bias correction.
Kelly formula (half-Kelly by default for safety):
p = estimated win probability (from signal strength)
b = payoff ratio: (1 - price)/price for BUY, price/(1-price) for SELL
q = 1 - p
f* = (b * p - q) / b
use = f* * kelly_fraction (default 0.5 → half-Kelly)
Favorite-Longshot bias correction (research-driven):
factor = (1 - 2 * |price - 0.5|)^beta
Edge at extreme prices (<0.10 or >0.90) is reduced.
"""
import math
from src.config import get_settings
class KellySizer:
"""Position sizing per the research framework."""
def __init__(self):
self.settings = get_settings()
def win_probability(self, strength: float) -> float:
"""Map signal strength [0,1] → win probability.
strength=0.5 → p=0.55 (baseline)
strength=1.0 → p=0.85 (strong consensus)
strength=0.0 → p=0.50 (coin flip)
"""
strength = max(0.0, min(1.0, strength))
return 0.50 + 0.35 * strength
def payoff_ratio(self, price: float, side: str) -> float:
"""How much we win vs how much we risk."""
p = max(0.01, min(0.99, price))
if side == "BUY":
return (1.0 - p) / p # win=(1-p), risk=p
# SELL: assume we already hold the position at avg price p, hedge at current
return p / (1.0 - p)
def favorite_longshot_correction(self, price: float, beta: float = 1.5) -> float:
"""Smooth penalty for extreme prices. 1.0 at price=0.5, ~0 at extremes."""
return (1.0 - 2.0 * abs(price - 0.5)) ** beta
def fraction(
self,
signal_strength: float,
price: float,
side: str,
beta: float = 1.5,
) -> float:
"""Compute Kelly fraction (capped 0..1) for a single signal."""
p = self.win_probability(signal_strength)
q = 1.0 - p
b = self.payoff_ratio(price, side)
f_star = max(0.0, (b * p - q) / b)
f_star *= self.favorite_longshot_correction(price, beta)
f_star *= self.settings.kelly_fraction
return min(f_star, self.settings.max_position_pct)
def position_usd(self, fraction: float, capital: float) -> float:
"""Translate fraction → dollar size, capped by per-trade position limit."""
size = fraction * capital
max_size = capital * self.settings.max_position_pct
return min(size, max_size)