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

1"""Winrate and expectancy metrics. 

2 

3Calculates win rate, expectancy, and related statistics. 

4""" 

5 

6from __future__ import annotations 

7 

8import numpy as np 

9 

10from monte_neo.utils.logger import get_logger 

11 

12logger = get_logger(__name__) 

13 

14 

15class WinrateMetric: 

16 """Winrate and expectancy calculator.""" 

17 

18 def calculate(self, pnls: list[float] | np.ndarray) -> float: 

19 """Calculate win rate. 

20 

21 Win Rate = Winning Trades / Total Trades 

22 

23 Args: 

24 pnls: List of P&L values. 

25 

26 Returns: 

27 Win rate as decimal (e.g., 0.60 = 60%). 

28 """ 

29 if not len(pnls): 

30 return 0.0 

31 

32 pnls = np.array(pnls) 

33 winners = np.sum(pnls > 0) 

34 

35 return float(winners / len(pnls)) 

36 

37 def expectancy(self, pnls: list[float] | np.ndarray) -> float: 

38 """Calculate expectancy (expected value per trade). 

39 

40 Expectancy = (Winrate × Avg Win) - ((1 - Winrate) × Avg Loss) 

41 

42 Positive expectancy indicates profitable system. 

43 

44 Args: 

45 pnls: List of P&L values. 

46 

47 Returns: 

48 Expected value per trade. 

49 """ 

50 if not len(pnls): 

51 return 0.0 

52 

53 pnls = np.array(pnls) 

54 

55 winrate = self.calculate(pnls) 

56 avg_win = self.avg_win(pnls) 

57 avg_loss = self.avg_loss(pnls) 

58 

59 expectancy = (winrate * avg_win) - ((1 - winrate) * avg_loss) 

60 

61 return float(expectancy) 

62 

63 def avg_win(self, pnls: list[float] | np.ndarray) -> float: 

64 """Calculate average winning trade. 

65 

66 Args: 

67 pnls: List of P&L values. 

68 

69 Returns: 

70 Average win value. 

71 """ 

72 if not len(pnls): 

73 return 0.0 

74 

75 pnls = np.array(pnls) 

76 winners = pnls[pnls > 0] 

77 

78 if len(winners) == 0: 

79 return 0.0 

80 

81 return float(np.mean(winners)) 

82 

83 def avg_loss(self, pnls: list[float] | np.ndarray) -> float: 

84 """Calculate average losing trade (as positive value). 

85 

86 Args: 

87 pnls: List of P&L values. 

88 

89 Returns: 

90 Average loss value (positive). 

91 """ 

92 if not len(pnls): 

93 return 0.0 

94 

95 pnls = np.array(pnls) 

96 losers = pnls[pnls < 0] 

97 

98 if len(losers) == 0: 

99 return 0.0 

100 

101 return float(abs(np.mean(losers))) 

102 

103 def win_loss_ratio(self, pnls: list[float] | np.ndarray) -> float: 

104 """Calculate average win / average loss ratio. 

105 

106 Also known as reward-to-risk ratio. 

107 

108 Args: 

109 pnls: List of P&L values. 

110 

111 Returns: 

112 Win/loss ratio. 

113 """ 

114 avg_win = self.avg_win(pnls) 

115 avg_loss = self.avg_loss(pnls) 

116 

117 if avg_loss == 0: 

118 return float("inf") if avg_win > 0 else 0.0 

119 

120 return float(avg_win / avg_loss) 

121 

122 def required_winrate(self, reward_risk_ratio: float) -> float: 

123 """Calculate required winrate for breakeven. 

124 

125 Args: 

126 reward_risk_ratio: Reward-to-risk ratio. 

127 

128 Returns: 

129 Breakeven win rate. 

130 """ 

131 if reward_risk_ratio <= 0: 

132 return 1.0 

133 

134 return 1 / (1 + reward_risk_ratio) 

135 

136 def edge_ratio(self, pnls: list[float] | np.ndarray) -> float: 

137 """Calculate edge ratio. 

138 

139 Edge = Actual Winrate - Required Winrate 

140 

141 Args: 

142 pnls: List of P&L values. 

143 

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) 

150 

151 return float(winrate - required) 

152 

153 def get_trade_distribution( 

154 self, 

155 pnls: list[float] | np.ndarray, 

156 ) -> dict: 

157 """Get trade P&L distribution statistics. 

158 

159 Args: 

160 pnls: List of P&L values. 

161 

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 } 

172 

173 pnls = np.array(pnls) 

174 

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 } 

186 

187 def _skewness(self, data: np.ndarray) -> float: 

188 """Calculate skewness of distribution. 

189 

190 Args: 

191 data: Data array. 

192 

193 Returns: 

194 Skewness value. 

195 """ 

196 if len(data) < 3: 

197 return 0.0 

198 

199 mean = np.mean(data) 

200 std = np.std(data) 

201 

202 if std == 0: 

203 return 0.0 

204 

205 return float(np.mean(((data - mean) / std) ** 3))