Coverage for src / monte_neo / metrics / winrate.py: 66%
64 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
1"""Winrate and expectancy metrics.
3Calculates win rate, expectancy, and related statistics.
4"""
6from __future__ import annotations
8import numpy as np
10from monte_neo.utils.logger import get_logger
12logger = get_logger(__name__)
15class WinrateMetric:
16 """Winrate and expectancy calculator."""
18 def calculate(self, pnls: list[float] | np.ndarray) -> float:
19 """Calculate win rate.
21 Win Rate = Winning Trades / Total Trades
23 Args:
24 pnls: List of P&L values.
26 Returns:
27 Win rate as decimal (e.g., 0.60 = 60%).
28 """
29 if not len(pnls):
30 return 0.0
32 pnls = np.array(pnls)
33 winners = np.sum(pnls > 0)
35 return float(winners / len(pnls))
37 def expectancy(self, pnls: list[float] | np.ndarray) -> float:
38 """Calculate expectancy (expected value per trade).
40 Expectancy = (Winrate × Avg Win) - ((1 - Winrate) × Avg Loss)
42 Positive expectancy indicates profitable system.
44 Args:
45 pnls: List of P&L values.
47 Returns:
48 Expected value per trade.
49 """
50 if not len(pnls):
51 return 0.0
53 pnls = np.array(pnls)
55 winrate = self.calculate(pnls)
56 avg_win = self.avg_win(pnls)
57 avg_loss = self.avg_loss(pnls)
59 expectancy = (winrate * avg_win) - ((1 - winrate) * avg_loss)
61 return float(expectancy)
63 def avg_win(self, pnls: list[float] | np.ndarray) -> float:
64 """Calculate average winning trade.
66 Args:
67 pnls: List of P&L values.
69 Returns:
70 Average win value.
71 """
72 if not len(pnls):
73 return 0.0
75 pnls = np.array(pnls)
76 winners = pnls[pnls > 0]
78 if len(winners) == 0:
79 return 0.0
81 return float(np.mean(winners))
83 def avg_loss(self, pnls: list[float] | np.ndarray) -> float:
84 """Calculate average losing trade (as positive value).
86 Args:
87 pnls: List of P&L values.
89 Returns:
90 Average loss value (positive).
91 """
92 if not len(pnls):
93 return 0.0
95 pnls = np.array(pnls)
96 losers = pnls[pnls < 0]
98 if len(losers) == 0:
99 return 0.0
101 return float(abs(np.mean(losers)))
103 def win_loss_ratio(self, pnls: list[float] | np.ndarray) -> float:
104 """Calculate average win / average loss ratio.
106 Also known as reward-to-risk ratio.
108 Args:
109 pnls: List of P&L values.
111 Returns:
112 Win/loss ratio.
113 """
114 avg_win = self.avg_win(pnls)
115 avg_loss = self.avg_loss(pnls)
117 if avg_loss == 0:
118 return float("inf") if avg_win > 0 else 0.0
120 return float(avg_win / avg_loss)
122 def required_winrate(self, reward_risk_ratio: float) -> float:
123 """Calculate required winrate for breakeven.
125 Args:
126 reward_risk_ratio: Reward-to-risk ratio.
128 Returns:
129 Breakeven win rate.
130 """
131 if reward_risk_ratio <= 0:
132 return 1.0
134 return 1 / (1 + reward_risk_ratio)
136 def edge_ratio(self, pnls: list[float] | np.ndarray) -> float:
137 """Calculate edge ratio.
139 Edge = Actual Winrate - Required Winrate
141 Args:
142 pnls: List of P&L values.
144 Returns:
145 Edge ratio (positive = profitable edge).
146 """
147 winrate = self.calculate(pnls)
148 rr_ratio = self.win_loss_ratio(pnls)
149 required = self.required_winrate(rr_ratio)
151 return float(winrate - required)
153 def get_trade_distribution(
154 self,
155 pnls: list[float] | np.ndarray,
156 ) -> dict:
157 """Get trade P&L distribution statistics.
159 Args:
160 pnls: List of P&L values.
162 Returns:
163 Distribution statistics.
164 """
165 if not len(pnls):
166 return {
167 "count": 0,
168 "winners": 0,
169 "losers": 0,
170 "breakeven": 0,
171 }
173 pnls = np.array(pnls)
175 return {
176 "count": len(pnls),
177 "winners": int(np.sum(pnls > 0)),
178 "losers": int(np.sum(pnls < 0)),
179 "breakeven": int(np.sum(pnls == 0)),
180 "best_trade": float(np.max(pnls)),
181 "worst_trade": float(np.min(pnls)),
182 "median": float(np.median(pnls)),
183 "std": float(np.std(pnls)),
184 "skew": float(self._skewness(pnls)),
185 }
187 def _skewness(self, data: np.ndarray) -> float:
188 """Calculate skewness of distribution.
190 Args:
191 data: Data array.
193 Returns:
194 Skewness value.
195 """
196 if len(data) < 3:
197 return 0.0
199 mean = np.mean(data)
200 std = np.std(data)
202 if std == 0:
203 return 0.0
205 return float(np.mean(((data - mean) / std) ** 3))