Skip to content

Commit bc09c1c

Browse files
committed
Add current trade metrics
1 parent ba65ee6 commit bc09c1c

File tree

7 files changed

+321
-11
lines changed

7 files changed

+321
-11
lines changed

investing_algorithm_framework/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@
4444
get_rolling_sharpe_ratio, create_backtest_metrics, get_total_growth, \
4545
get_total_loss, get_cumulative_exposure, get_median_trade_return, \
4646
get_average_trade_return, get_risk_free_rate_us, get_cumulative_return, \
47-
get_cumulative_return_series
47+
get_cumulative_return_series, get_current_average_trade_return, \
48+
get_current_average_trade_gain, get_current_average_trade_duration, \
49+
get_current_average_trade_loss, get_negative_trades, \
50+
get_positive_trades, get_number_of_trades, get_current_win_rate, \
51+
get_current_win_loss_ratio
4852

4953

5054
__all__ = [
@@ -175,5 +179,14 @@
175179
"get_total_loss",
176180
"get_total_growth",
177181
"generate_backtest_summary_metrics",
178-
"get_equity_curve_chart"
182+
"get_equity_curve_chart",
183+
"get_current_win_rate",
184+
"get_current_win_loss_ratio",
185+
"get_current_average_trade_loss",
186+
"get_current_average_trade_duration",
187+
"get_current_average_trade_gain",
188+
"get_current_average_trade_return",
189+
"get_negative_trades",
190+
"get_positive_trades",
191+
"get_number_of_trades",
179192
]

investing_algorithm_framework/domain/backtesting/backtest_metrics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ class BacktestMetrics:
179179
average_trade_gain_percentage: float = 0.0
180180
average_trade_return: float = 0.0
181181
average_trade_return_percentage: float = 0.0
182+
current_average_trade_gain: float = 0.0
183+
current_average_trade_gain_percentage: float = 0.0
184+
current_average_trade_return: float = 0.0
185+
current_average_trade_return_percentage: float = 0.0
186+
current_average_trade_duration: float = 0.0
187+
current_average_trade_loss: float = 0.0
188+
current_average_trade_loss_percentage: float = 0.0
182189
median_trade_return: float = 0.0
183190
median_trade_return_percentage: float = 0.0
184191
number_of_trades: int = 0

investing_algorithm_framework/services/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@
2626
get_average_trade_loss, get_average_monthly_return, \
2727
get_percentage_winning_months, get_average_trade_duration, \
2828
get_trade_frequency, get_win_rate, get_win_loss_ratio, \
29-
get_calmar_ratio, get_max_drawdown_absolute, \
29+
get_calmar_ratio, get_max_drawdown_absolute, get_current_win_loss_ratio, \
3030
get_max_drawdown_duration, get_max_daily_drawdown, get_trades_per_day, \
3131
get_trades_per_year, get_average_monthly_return_losing_months, \
3232
get_average_monthly_return_winning_months, get_percentage_winning_years, \
3333
get_rolling_sharpe_ratio, create_backtest_metrics, get_total_growth, \
3434
get_total_loss, get_risk_free_rate_us, get_median_trade_return, \
3535
get_average_trade_return, get_cumulative_return, \
3636
get_cumulative_return_series, get_average_trade_size, \
37-
get_positive_trades, get_negative_trades, get_number_of_trades
37+
get_positive_trades, get_negative_trades, get_number_of_trades, \
38+
get_current_win_rate, get_current_average_trade_return, \
39+
get_current_average_trade_loss, get_current_average_trade_duration, \
40+
get_current_average_trade_gain
3841

3942
__all__ = [
4043
"OrderService",
@@ -118,4 +121,11 @@
118121
"get_number_of_trades",
119122
"get_cumulative_return",
120123
"get_cumulative_return_series",
124+
"get_current_win_loss_ratio",
125+
"get_current_win_rate",
126+
"get_current_win_loss_ratio",
127+
"get_current_average_trade_loss",
128+
"get_current_average_trade_duration",
129+
"get_current_average_trade_gain",
130+
"get_current_average_trade_return",
121131
]

investing_algorithm_framework/services/metrics/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@
2424
from .exposure import get_average_trade_duration, \
2525
get_trade_frequency, get_trades_per_day, get_trades_per_year, \
2626
get_cumulative_exposure, get_exposure_ratio
27-
from .win_rate import get_win_rate, get_win_loss_ratio
27+
from .win_rate import get_win_rate, get_win_loss_ratio, get_current_win_rate, \
28+
get_current_win_loss_ratio
2829
from .calmar_ratio import get_calmar_ratio
2930
from .generate import create_backtest_metrics
3031
from .risk_free_rate import get_risk_free_rate_us
3132
from .trades import get_negative_trades, get_positive_trades, \
3233
get_number_of_trades, get_number_of_closed_trades, \
3334
get_average_trade_size, get_average_trade_return, get_best_trade, \
3435
get_worst_trade, get_average_trade_gain, get_median_trade_return, \
35-
get_average_trade_loss
36+
get_average_trade_loss, get_current_average_trade_loss, \
37+
get_current_average_trade_duration, get_current_average_trade_gain, \
38+
get_current_average_trade_return
3639

3740
__all__ = [
3841
"get_annual_volatility",
@@ -97,4 +100,10 @@
97100
"get_positive_trades",
98101
"get_cumulative_return",
99102
"get_cumulative_return_series",
103+
"get_current_win_rate",
104+
"get_current_win_loss_ratio",
105+
"get_current_average_trade_loss",
106+
"get_current_average_trade_duration",
107+
"get_current_average_trade_gain",
108+
"get_current_average_trade_return",
100109
]

investing_algorithm_framework/services/metrics/generate.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from logging import getLogger
33

44
from investing_algorithm_framework.domain import BacktestMetrics, \
5-
TradeStatus, BacktestRun, OperationalException
5+
BacktestRun, OperationalException
66
from .cagr import get_cagr
77
from .calmar_ratio import get_calmar_ratio
88
from .drawdown import get_drawdown_series, get_max_drawdown, \
@@ -28,7 +28,9 @@
2828
get_number_of_trades, get_positive_trades, get_number_of_closed_trades, \
2929
get_negative_trades, get_average_trade_return, get_number_of_open_trades, \
3030
get_worst_trade, get_best_trade, get_average_trade_gain, \
31-
get_average_trade_loss, get_median_trade_return
31+
get_average_trade_loss, get_median_trade_return, \
32+
get_current_average_trade_gain, get_current_average_trade_return, \
33+
get_current_average_trade_duration, get_current_average_trade_loss
3234

3335
logger = getLogger("investing_algorithm_framework")
3436

@@ -113,7 +115,11 @@ def create_backtest_metrics(
113115
"best_year",
114116
"worst_month",
115117
"worst_year",
116-
"total_number_of_days"
118+
"total_number_of_days",
119+
"current_average_trade_gain",
120+
"current_average_trade_return",
121+
"current_average_trade_duration",
122+
"current_average_trade_loss",
117123
]
118124

119125
backtest_metrics = BacktestMetrics(
@@ -197,6 +203,68 @@ def safe_set(metric_name, func, *args, index=None):
197203
except OperationalException as e:
198204
logger.warning(f"average_trade_loss failed: {e}")
199205

206+
if ("current_average_trade_gain" in metrics
207+
or "get_current_average_trade_gain_percentage" in metrics):
208+
try:
209+
current_avg_gain = get_current_average_trade_gain(
210+
backtest_run.trades
211+
)
212+
213+
if "current_average_trade_gain" in metrics:
214+
backtest_metrics.current_average_trade_gain = \
215+
current_avg_gain[0]
216+
217+
if "current_average_trade_gain_percentage" in metrics:
218+
backtest_metrics.current_average_trade_gain_percentage = \
219+
current_avg_gain[1]
220+
except OperationalException as e:
221+
logger.warning(f"current_average_trade_gain failed: {e}")
222+
223+
if ("current_average_trade_return" in metrics
224+
or "current_average_trade_return_percentage" in metrics):
225+
try:
226+
current_avg_return = get_current_average_trade_return(
227+
backtest_run.trades
228+
)
229+
230+
if "current_average_trade_return" in metrics:
231+
backtest_metrics.current_average_trade_return = \
232+
current_avg_return[0]
233+
if "current_average_trade_return_percentage" in metrics:
234+
backtest_metrics.current_average_trade_return_percentage =\
235+
current_avg_return[1]
236+
except OperationalException as e:
237+
logger.warning(f"current_average_trade_return failed: {e}")
238+
239+
if "current_average_trade_duration" in metrics:
240+
try:
241+
current_avg_duration = get_current_average_trade_duration(
242+
backtest_run.trades, backtest_run
243+
)
244+
backtest_metrics.current_average_trade_duration = \
245+
current_avg_duration
246+
except OperationalException as e:
247+
logger.warning(f"current_average_trade_duration failed: {e}")
248+
249+
if ("current_average_trade_loss" in metrics
250+
or "current_average_trade_loss_percentage" in metrics):
251+
try:
252+
current_avg_loss = get_current_average_trade_loss(
253+
backtest_run.trades
254+
)
255+
if "current_average_trade_loss" in metrics:
256+
backtest_metrics.current_average_trade_loss = \
257+
current_avg_loss[0]
258+
if "current_average_trade_loss_percentage" in metrics:
259+
backtest_metrics.current_average_trade_loss_percentage = \
260+
current_avg_loss[1]
261+
except OperationalException as e:
262+
logger.warning(f"current_average_trade_loss failed: {e}")
263+
264+
safe_set("number_of_positive_trades", get_positive_trades, backtest_run.trades)
265+
safe_set("percentage_positive_trades", get_positive_trades, backtest_run.trades, index=1)
266+
safe_set("number_of_negative_trades", get_negative_trades, backtest_run.trades)
267+
safe_set("percentage_negative_trades", get_negative_trades, backtest_run.trades, index=1)
200268
safe_set("median_trade_return", get_median_trade_return, backtest_run.trades, index=0)
201269
safe_set("median_trade_return_percentage", get_median_trade_return, backtest_run.trades, index=1)
202270
safe_set("number_of_trades", get_number_of_trades, backtest_run.trades)
@@ -241,5 +309,4 @@ def safe_set(metric_name, func, *args, index=None):
241309
safe_set("gross_profit", get_gross_profit, backtest_run.trades)
242310
safe_set("cumulative_return_series", get_cumulative_return_series, backtest_run.portfolio_snapshots)
243311
safe_set("cumulative_return", get_cumulative_return, backtest_run.portfolio_snapshots)
244-
245312
return backtest_metrics

investing_algorithm_framework/services/metrics/trades.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import List, Tuple
22

33
from investing_algorithm_framework.domain import Trade, TradeStatus, \
4-
OperationalException
4+
OperationalException, BacktestRun
55

66

77
def get_positive_trades(
@@ -163,6 +163,41 @@ def get_average_trade_duration(
163163
return total_duration / number_of_trades if number_of_trades > 0 else 0.0
164164

165165

166+
def get_current_average_trade_duration(
167+
trades: List[Trade], backtest_run: BacktestRun
168+
) -> float:
169+
"""
170+
Calculate the average duration of currently closed and open trades
171+
in hours.
172+
173+
Args:
174+
trades (List[Trade]): List of Trade objects.
175+
backtest_run (BacktestRun): The backtest run containing trades.
176+
177+
Returns:
178+
float: The average trade duration in hours.
179+
"""
180+
if trades is None or len(trades) == 0:
181+
raise OperationalException(
182+
"Trades list is None, cannot compute average trade duration."
183+
)
184+
185+
total_duration = 0.0
186+
187+
for trade in trades:
188+
189+
if TradeStatus.CLOSED.equals(trade.status):
190+
total_duration += (trade.closed_at - trade.opened_at)\
191+
.total_seconds() / 3600
192+
else:
193+
total_duration += (
194+
backtest_run.backtest_end_date - trade.opened_at
195+
).total_seconds() / 3600
196+
197+
number_of_trades = len(trades)
198+
return total_duration / number_of_trades if number_of_trades > 0 else 0.0
199+
200+
166201
def get_average_trade_size(
167202
trades: List[Trade]
168203
) -> float:
@@ -220,6 +255,39 @@ def get_average_trade_return(trades: List[Trade]) -> Tuple[float, float]:
220255
return average_return, average_return_percentage
221256

222257

258+
def get_current_average_trade_return(
259+
trades: List[Trade]
260+
) -> Tuple[float, float]:
261+
"""
262+
Calculate the average return (absolute PnL) and
263+
average return percentage (per trade) of closed and open trades.
264+
265+
Args:
266+
trades (List[Trade]): List of trades.
267+
268+
Returns:
269+
Tuple[float, float]: The average return
270+
percentage of the average return
271+
"""
272+
if not trades or len(trades) == 0:
273+
raise OperationalException(
274+
"Trades list is empty, cannot compute average return."
275+
)
276+
277+
total_return = sum(t.net_gain for t in trades)
278+
average_return = total_return / len(trades)
279+
280+
percentage_returns = [
281+
(t.net_gain / t.cost) * 100.0 for t in trades if t.cost > 0
282+
]
283+
average_return_percentage = (
284+
sum(percentage_returns) / len(percentage_returns)
285+
if percentage_returns else 0.0
286+
)
287+
288+
return average_return, average_return_percentage
289+
290+
223291
def get_average_trade_gain(trades: List[Trade]) -> Tuple[float, float]:
224292
"""
225293
Calculate the average gain from a list of trades.
@@ -249,6 +317,37 @@ def get_average_trade_gain(trades: List[Trade]) -> Tuple[float, float]:
249317
return average_gain, percentage
250318

251319

320+
def get_current_average_trade_gain(
321+
trades: List[Trade]
322+
) -> Tuple[float, float]:
323+
"""
324+
Calculate the average gain from a list of trades,
325+
including both closed and open trades.
326+
327+
The average gain is calculated as the mean of all positive returns.
328+
329+
Args:
330+
trades (List[Trade]): List of trades.
331+
Returns:
332+
Tuple[float, float]: The average gain
333+
percentage of the average loss
334+
"""
335+
if trades is None or len(trades) == 0:
336+
raise OperationalException(
337+
"Trades list is empty or None, cannot calculate average gain."
338+
)
339+
340+
gains = [t.net_gain for t in trades if t.net_gain > 0]
341+
cost = sum(t.cost for t in trades if t.net_gain > 0)
342+
343+
if not gains:
344+
return 0.0, 0.0
345+
346+
average_gain = sum(gains) / len(gains)
347+
percentage = (average_gain / cost) if cost > 0 else 0.0
348+
return average_gain, percentage
349+
350+
252351
def get_average_trade_loss(trades: List[Trade]) -> Tuple[float, float]:
253352
"""
254353
Calculate the average loss from a list of trades.
@@ -284,6 +383,44 @@ def get_average_trade_loss(trades: List[Trade]) -> Tuple[float, float]:
284383
return average_loss, average_return_percentage
285384

286385

386+
def get_current_average_trade_loss(
387+
trades: List[Trade]
388+
) -> Tuple[float, float]:
389+
"""
390+
Calculate the average loss from a list of trades,
391+
including both closed and open trades.
392+
393+
The average loss is calculated as the mean of all negative returns.
394+
395+
Args:
396+
trades (List[Trade]): List of trades.
397+
398+
Returns:
399+
Tuple[float, float]: The average loss
400+
percentage of the average loss
401+
"""
402+
if trades is None or len(trades) == 0:
403+
raise OperationalException(
404+
"Trades list is empty or None, cannot calculate average loss."
405+
)
406+
407+
losing_trades = [t for t in trades if t.net_gain < 0]
408+
409+
if not losing_trades or len(losing_trades) == 0:
410+
return 0.0, 0.0
411+
412+
losses = [t.net_gain for t in losing_trades]
413+
average_loss = sum(losses) / len(losses)
414+
percentage_returns = [
415+
(t.net_gain / t.cost) * 100.0 for t in losing_trades if t.cost > 0
416+
]
417+
average_return_percentage = (
418+
sum(percentage_returns) / len(percentage_returns)
419+
if percentage_returns else 0.0
420+
)
421+
return average_loss, average_return_percentage
422+
423+
287424
def get_median_trade_return(trades: List[Trade]) -> Tuple[float, float]:
288425
"""
289426
Calculate the median return from a list of trades.

0 commit comments

Comments
 (0)