Skip to content

Commit b89901f

Browse files
committed
Change weights calculation
1 parent 7816f25 commit b89901f

File tree

2 files changed

+152
-32
lines changed

2 files changed

+152
-32
lines changed
Lines changed: 149 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,178 @@
1-
from investing_algorithm_framework.domain import Backtest
1+
default_weights = {
2+
# Profitability
3+
"total_net_gain": 3.0,
4+
"total_net_loss": 0.0,
5+
"total_return": 0.0,
6+
"avg_return_per_trade": 0.0,
27

3-
defaults_ranking_weights = {
4-
"total_net_gain": 2.0,
8+
# Risk-adjusted returns
59
"sharpe_ratio": 1.0,
610
"sortino_ratio": 1.0,
7-
"win_rate": 1.0,
811
"profit_factor": 1.0,
9-
"max_drawdown": -1.0, # negative weight to penalize high drawdown
10-
"max_drawdown_duration": -0.5, # penalize long drawdown periods
11-
"number_of_trades": 0.5,
12+
13+
# Risk
14+
"max_drawdown": -2.0,
15+
"max_drawdown_duration": -0.5,
16+
17+
# Trading activity
18+
"number_of_trades": 2.0,
19+
"win_rate": 3.0,
20+
21+
# Exposure
1222
"exposure_factor": 0.5,
23+
"exposure_ratio": 0.0,
24+
"exposure_time": 0.0,
1325
}
1426

1527

16-
def compute_score(metrics: dict, weights: dict) -> float:
28+
def normalize(value, min_val, max_val):
29+
"""
30+
Normalize a value to a range [0, 1].
31+
32+
Args:
33+
value (float): The value to normalize.
34+
min_val (float): The minimum value of the range.
35+
max_val (float): The maximum value of the range.
36+
37+
Returns:
38+
float: The normalized value.
39+
"""
40+
if max_val == min_val:
41+
return 0
42+
return (value - min_val) / (max_val - min_val)
43+
44+
45+
def compute_score(metrics, weights, ranges):
46+
"""
47+
Compute a weighted score for the given metrics.
48+
49+
Args:
50+
metrics: The metrics to evaluate.
51+
weights: The weights to apply to each metric.
52+
ranges: The min/max ranges for each metric.
53+
54+
Returns:
55+
float: The computed score.
56+
"""
1757
score = 0
1858
for key, weight in weights.items():
1959

20-
# Metrics are attributes to the backtest
2160
if not hasattr(metrics, key):
2261
continue
23-
24-
# Get the value of the metric
2562
value = getattr(metrics, key)
2663

27-
try:
28-
score += weight * value
29-
except TypeError:
30-
continue # skip if value is not a number
64+
if key in ranges:
65+
value = normalize(value, ranges[key][0], ranges[key][1])
66+
score += weight * value
3167
return score
3268

3369

34-
def rank_results(
35-
backtests: list[Backtest], weights=defaults_ranking_weights
36-
) -> list[Backtest]:
70+
def create_weights(
71+
focus: str = "balanced",
72+
gain: float = 3.0,
73+
win_rate: float = 3.0,
74+
trades: float = 2.0,
75+
custom_weights: dict | None = None,
76+
) -> dict:
77+
"""
78+
Utility to generate weights dicts for ranking backtests.
79+
80+
This function does not assign weights to every possible performance
81+
metric. Instead, it focuses on a curated subset of commonly relevant
82+
ones (profitability, win rate, trade frequency, and risk-adjusted returns).
83+
The rationale is to avoid overfitting ranking logic to noisy or redundant
84+
statistics (e.g., monthly return breakdowns, best/worst trade), while
85+
keeping the weighting system simple and interpretable.
86+
Users who need fine-grained control can pass `custom_weights` to fully
87+
override defaults.
88+
89+
Args:
90+
focus (str): One of [
91+
"balanced", "profit", "frequency", "risk_adjusted"
92+
].
93+
gain (float): Weight for total_net_gain (default only).
94+
win_rate (float): Weight for win_rate (default only).
95+
trades (float): Weight for number_of_trades (default only).
96+
custom_weights (dict): Full override for weights (all metrics).
97+
If provided, it takes precedence over presets.
98+
99+
Returns:
100+
dict: A dictionary of weights for ranking backtests.
37101
"""
38-
Rank backtests based on their metrics and the provided weights.
39102

40-
The default weights are defined in `defaults_ranking_weights`.
41-
Please note that the weights should be adjusted based on the
42-
specific analysis needs. You can modify the `weights` parameter
43-
to include or exclude metrics as needed and reuse
44-
the `defaults_ranking_weights` as a starting point.
103+
# default / balanced
104+
base = {
105+
"total_net_gain": gain,
106+
"win_rate": win_rate,
107+
"number_of_trades": trades,
108+
"sharpe_ratio": 1.0,
109+
"sortino_ratio": 1.0,
110+
"profit_factor": 1.0,
111+
"max_drawdown": -2.0,
112+
"max_drawdown_duration": -0.5,
113+
"total_net_loss": 0.0,
114+
"total_return": 0.0,
115+
"avg_return_per_trade": 0.0,
116+
"exposure_factor": 0.5,
117+
"exposure_ratio": 0.0,
118+
"exposure_time": 0.0,
119+
}
45120

121+
# apply presets
122+
if focus == "profit":
123+
base.update({
124+
"total_net_gain": 5.0,
125+
"win_rate": 2.0,
126+
"number_of_trades": 1.0,
127+
})
128+
elif focus == "frequency":
129+
base.update({
130+
"number_of_trades": 4.0,
131+
"win_rate": 2.0,
132+
"total_net_gain": 2.0,
133+
})
134+
elif focus == "risk_adjusted":
135+
base.update({
136+
"sharpe_ratio": 3.0,
137+
"sortino_ratio": 3.0,
138+
"max_drawdown": -3.0,
139+
})
140+
141+
# if full custom dict is given → override everything
142+
if custom_weights is not None:
143+
base = {**base, **custom_weights}
144+
145+
return base
146+
147+
148+
def rank_results(backtests, focus=None, weights=None):
149+
"""
150+
Rank backtest results based on specified focus and weights.
46151
Args:
47-
backtests (list[Backtest]): List of Backtest objects to rank.
48-
weights (dict): Weights for each metric to compute the score.
152+
backtests (list): List of backtest results to rank.
153+
focus (str, optional): Focus for ranking. If None,
154+
uses default weights. Options: "balanced", "profit",
155+
"frequency", "risk_adjusted".
156+
weights (dict, optional): Custom weights for ranking metrics.
157+
If None, uses default weights based on focus.
49158
50159
Returns:
51-
list[Backtest]: List of Backtest objects sorted by
52-
their computed score.
160+
list: Sorted list of backtests based on computed scores.
53161
"""
162+
163+
if weights is None:
164+
weights = create_weights(focus=focus)
165+
166+
# First compute metric ranges for normalization
167+
ranges = {}
168+
for key in weights:
169+
values = [getattr(bt.backtest_metrics, key, None) for bt in backtests]
170+
values = [v for v in values if isinstance(v, (int, float))]
171+
if values:
172+
ranges[key] = (min(values), max(values))
173+
54174
return sorted(
55175
backtests,
56-
key=lambda bt: compute_score(bt.backtest_metrics, weights),
176+
key=lambda bt: compute_score(bt.backtest_metrics, weights, ranges),
57177
reverse=True
58178
)

investing_algorithm_framework/services/metrics/win_rate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ def get_win_rate(trades: List[Trade]) -> float:
4242
The percentage of trades that are profitable.
4343
4444
Formula:
45-
Win Rate = (Number of Profitable Trades / Total Number of Trades) * 100
45+
Win Rate = Number of Profitable Trades / Total Number of Trades
4646
4747
Example: If 60 out of 100 trades are profitable, the win rate is 60%.
4848
4949
Args:
5050
trades (List[Trade]): List of trades from the backtest report.
5151
5252
Returns:
53-
float: The win rate as a percentage (e.g., 75.0 for 75% win rate).
53+
float: The win rate as a percentage (e.g., o.75 for 75% win rate).
5454
"""
5555
trades = [
5656
trade for trade in trades if TradeStatus.CLOSED.equals(trade.status)
@@ -61,7 +61,7 @@ def get_win_rate(trades: List[Trade]) -> float:
6161
if total_trades == 0:
6262
return 0.0
6363

64-
return (positive_trades / total_trades) * 100.0
64+
return positive_trades / total_trades
6565

6666

6767
def get_win_loss_ratio(trades: List[Trade]) -> float:

0 commit comments

Comments
 (0)