Coverage for packages / betting / value_bets.py: 0%
25 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:37 +1200
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:37 +1200
1"""Value bet finder — identifies betting opportunities where model
2probabilities exceed market-implied probabilities.
4Uses Kelly criterion sizing and confidence filtering to surface
5actionable betting recommendations.
6"""
8from __future__ import annotations
10from decimal import Decimal
11from typing import Any
14class ValueBetFinder:
15 """Identifies value bets by comparing model vs market probabilities.
17 A "value bet" exists when the model's estimated win/place probability
18 exceeds the market-implied probability by a configurable threshold.
19 """
21 def __init__(
22 self,
23 min_edge: float = 0.05,
24 min_confidence: float = 0.3,
25 max_kelly_fraction: float = 0.25,
26 ) -> None:
27 """Initialize the value bet finder.
29 Args:
30 min_edge: Minimum edge (model_prob - market_prob) to
31 consider a value bet. Default 0.05 (5%).
32 min_confidence: Minimum model confidence (probability) to
33 consider. Filters out very low-probability bets.
34 Default 0.3.
35 max_kelly_fraction: Maximum fraction of bankroll to
36 stake per bet (Kelly cap). Default 0.25 (25%).
37 """
38 self.min_edge = min_edge
39 self.min_confidence = min_confidence
40 self.max_kelly_fraction = max_kelly_fraction
42 def find_value_bets(
43 self,
44 comparisons: list[dict[str, Any]],
45 bankroll: Decimal = Decimal("1000"),
46 ) -> list[dict[str, Any]]:
47 """Find value bets from model-vs-market comparisons.
49 Args:
50 comparisons: List of comparison dicts from
51 OddsComparisonClient.compare_to_model().
52 bankroll: Current bankroll for Kelly stake sizing.
54 Returns:
55 List of value bet dicts, sorted by edge descending, with keys:
56 - runner_name: str
57 - starter_id: int | None
58 - model_prob: float
59 - market_prob: float
60 - edge: float
61 - kelly_stake: float (fraction of bankroll)
62 - recommended_stake: Decimal (in bankroll units)
63 - confidence: str ("high", "medium", "low")
64 """
65 raise NotImplementedError
67 def kelly_criterion(
68 self,
69 model_prob: float,
70 market_prob: float,
71 ) -> float:
72 """Calculate Kelly stake fraction.
74 f* = (p * b - q) / b
75 where p = model_prob, q = 1-p, b = (1/market_prob) - 1
77 Args:
78 model_prob: Model's estimated win probability.
79 market_prob: Market-implied win probability.
81 Returns:
82 Kelly fraction (0 to max_kelly_fraction).
83 """
84 if market_prob <= 0 or market_prob >= 1:
85 return 0.0
86 if model_prob <= market_prob:
87 return 0.0
89 b = (Decimal("1") / Decimal(str(market_prob))) - Decimal("1")
90 f_star = (
91 Decimal(str(model_prob)) * b - (Decimal("1") - Decimal(str(model_prob)))
92 ) / b
93 return min(float(f_star), self.max_kelly_fraction)
95 def filter_by_confidence(
96 self,
97 bets: list[dict[str, Any]],
98 min_edge: float | None = None,
99 min_prob: float | None = None,
100 ) -> list[dict[str, Any]]:
101 """Filter value bets by minimum edge and probability thresholds.
103 Args:
104 bets: List of value bet dicts.
105 min_edge: Override default min_edge.
106 min_prob: Override default min_confidence.
108 Returns:
109 Filtered list of value bet dicts.
110 """
111 edge = min_edge if min_edge is not None else self.min_edge
112 prob = min_prob if min_prob is not None else self.min_confidence
113 return [
114 b
115 for b in bets
116 if b.get("edge", 0) >= edge and b.get("model_prob", 0) >= prob
117 ]
119 def summarize(
120 self,
121 bets: list[dict[str, Any]],
122 ) -> dict[str, Any]:
123 """Generate a summary of value bet opportunities.
125 Args:
126 bets: List of value bet dicts.
128 Returns:
129 Summary dict with count, total stake, average edge, etc.
130 """
131 if not bets:
132 return {
133 "total_bets": 0,
134 "total_stake": Decimal("0"),
135 "avg_edge": 0.0,
136 "high_confidence": 0,
137 "medium_confidence": 0,
138 "low_confidence": 0,
139 }
141 return {
142 "total_bets": len(bets),
143 "total_stake": sum(b.get("recommended_stake", Decimal("0")) for b in bets),
144 "avg_edge": sum(b.get("edge", 0) for b in bets) / len(bets),
145 "high_confidence": sum(1 for b in bets if b.get("confidence") == "high"),
146 "medium_confidence": sum(
147 1 for b in bets if b.get("confidence") == "medium"
148 ),
149 "low_confidence": sum(1 for b in bets if b.get("confidence") == "low"),
150 }