investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of investing-algorithm-framework might be problematic. Click here for more details.

Files changed (192) hide show
  1. investing_algorithm_framework/__init__.py +147 -44
  2. investing_algorithm_framework/app/__init__.py +23 -6
  3. investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
  4. investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
  5. investing_algorithm_framework/app/analysis/__init__.py +15 -0
  6. investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
  7. investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
  8. investing_algorithm_framework/app/analysis/permutation.py +116 -0
  9. investing_algorithm_framework/app/analysis/ranking.py +297 -0
  10. investing_algorithm_framework/app/app.py +1322 -707
  11. investing_algorithm_framework/app/context.py +196 -88
  12. investing_algorithm_framework/app/eventloop.py +590 -0
  13. investing_algorithm_framework/app/reporting/__init__.py +16 -5
  14. investing_algorithm_framework/app/reporting/ascii.py +57 -202
  15. investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
  16. investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
  17. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  18. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  19. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
  20. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  21. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  22. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
  23. investing_algorithm_framework/app/reporting/generate.py +100 -114
  24. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
  25. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
  26. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
  27. investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
  28. investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
  29. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
  30. investing_algorithm_framework/app/strategy.py +315 -175
  31. investing_algorithm_framework/app/task.py +5 -3
  32. investing_algorithm_framework/cli/cli.py +30 -12
  33. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
  34. investing_algorithm_framework/cli/initialize_app.py +20 -1
  35. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
  36. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  37. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  38. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
  39. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
  40. investing_algorithm_framework/create_app.py +3 -5
  41. investing_algorithm_framework/dependency_container.py +25 -39
  42. investing_algorithm_framework/domain/__init__.py +45 -38
  43. investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
  44. investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
  45. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
  46. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
  47. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
  48. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  49. investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
  50. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  51. investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
  52. investing_algorithm_framework/domain/config.py +27 -0
  53. investing_algorithm_framework/domain/constants.py +6 -34
  54. investing_algorithm_framework/domain/data_provider.py +200 -56
  55. investing_algorithm_framework/domain/exceptions.py +34 -1
  56. investing_algorithm_framework/domain/models/__init__.py +10 -19
  57. investing_algorithm_framework/domain/models/base_model.py +0 -6
  58. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  59. investing_algorithm_framework/domain/models/data/data_source.py +214 -0
  60. investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
  61. investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
  62. investing_algorithm_framework/domain/models/order/order.py +34 -13
  63. investing_algorithm_framework/domain/models/order/order_status.py +1 -1
  64. investing_algorithm_framework/domain/models/order/order_type.py +1 -1
  65. investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
  66. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
  67. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
  68. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  69. investing_algorithm_framework/domain/models/position/position.py +9 -0
  70. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  71. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  72. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  73. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  74. investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
  75. investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
  76. investing_algorithm_framework/domain/models/time_frame.py +7 -0
  77. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  78. investing_algorithm_framework/domain/models/time_unit.py +63 -1
  79. investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
  80. investing_algorithm_framework/domain/models/trade/trade.py +56 -32
  81. investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
  82. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
  83. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
  84. investing_algorithm_framework/domain/order_executor.py +19 -0
  85. investing_algorithm_framework/domain/portfolio_provider.py +20 -1
  86. investing_algorithm_framework/domain/services/__init__.py +0 -13
  87. investing_algorithm_framework/domain/strategy.py +1 -29
  88. investing_algorithm_framework/domain/utils/__init__.py +5 -1
  89. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  90. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  91. investing_algorithm_framework/domain/utils/polars.py +17 -14
  92. investing_algorithm_framework/download_data.py +40 -10
  93. investing_algorithm_framework/infrastructure/__init__.py +13 -25
  94. investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
  95. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
  96. investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
  97. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  98. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  99. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
  100. investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
  101. investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
  102. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
  103. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
  104. investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
  105. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  106. investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
  107. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
  108. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
  109. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
  110. investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
  111. investing_algorithm_framework/services/__init__.py +105 -8
  112. investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
  113. investing_algorithm_framework/services/configuration_service.py +14 -4
  114. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  115. investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
  116. investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
  117. investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
  118. investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
  119. investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
  120. investing_algorithm_framework/services/metrics/generate.py +358 -0
  121. investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
  122. investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
  123. investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
  124. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  125. investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
  126. investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
  127. investing_algorithm_framework/services/metrics/trades.py +500 -0
  128. investing_algorithm_framework/services/metrics/volatility.py +97 -0
  129. investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
  130. investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
  131. investing_algorithm_framework/services/order_service/order_service.py +9 -71
  132. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
  133. investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
  134. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
  135. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
  136. investing_algorithm_framework/services/repository_service.py +5 -2
  137. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  138. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
  139. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  140. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  141. investing_algorithm_framework/services/trade_service/__init__.py +7 -1
  142. investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
  143. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  144. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  145. investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
  146. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
  147. investing_algorithm_framework/app/reporting/evaluation.py +0 -243
  148. investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
  149. investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
  150. investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
  151. investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
  152. investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
  153. investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
  154. investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
  155. investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
  156. investing_algorithm_framework/domain/models/data_source.py +0 -21
  157. investing_algorithm_framework/domain/models/date_range.py +0 -64
  158. investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
  159. investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
  160. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  161. investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
  162. investing_algorithm_framework/domain/services/market_service.py +0 -153
  163. investing_algorithm_framework/domain/services/observable.py +0 -51
  164. investing_algorithm_framework/domain/services/observer.py +0 -19
  165. investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
  166. investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
  167. investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
  168. investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
  169. investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
  170. investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
  171. investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
  172. investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
  173. investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
  174. investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
  175. investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
  176. investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
  177. investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
  178. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
  179. investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
  180. /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
  181. /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
  182. /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
  183. /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
  184. /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
  185. /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
  186. /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
  187. /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
  188. /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
  189. /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
  190. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
  191. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
  192. {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,500 @@
1
+ from typing import List, Tuple
2
+
3
+ from investing_algorithm_framework.domain import Trade, TradeStatus, \
4
+ OperationalException, BacktestRun
5
+
6
+
7
+ def get_positive_trades(
8
+ trades: List[Trade]
9
+ ) -> Tuple[int, float]:
10
+ """
11
+ Calculate the number and percentage of positive trades.
12
+
13
+ Args:
14
+ trades (List[Trade]): List of Trade objects.
15
+
16
+ Returns:
17
+ Tuple[int, float]: A tuple containing the number of positive trades
18
+ and the percentage of positive trades.
19
+ """
20
+ if trades is None or len(trades) == 0:
21
+ raise OperationalException(
22
+ "Trades list is empty or None, cannot compute positive trades."
23
+ )
24
+
25
+ closed_trades = [
26
+ trade for trade in trades if TradeStatus.CLOSED.equals(trade.status)
27
+ ]
28
+
29
+ positive_trades = [
30
+ trade for trade in closed_trades if trade.net_gain_absolute > 0
31
+ ]
32
+ number_of_positive_trades = len(positive_trades)
33
+ percentage_positive_trades = (
34
+ (number_of_positive_trades / len(closed_trades)) * 100.0
35
+ if len(closed_trades) > 0 else 0.0
36
+ )
37
+ return number_of_positive_trades, percentage_positive_trades
38
+
39
+
40
+ def get_negative_trades(
41
+ trades: List[Trade]
42
+ ) -> Tuple[int, float]:
43
+ """
44
+ Calculate the number and percentage of negative trades.
45
+
46
+ Args:
47
+ trades (List[Trade]): List of Trade objects.
48
+
49
+ Returns:
50
+ Tuple[int, float]: A tuple containing the number of negative trades
51
+ and the percentage of negative trades.
52
+ """
53
+ if trades is None or len(trades) == 0:
54
+ raise OperationalException(
55
+ "Trades list is empty or None, cannot compute negative trades."
56
+ )
57
+
58
+ closed_trades = [
59
+ trade for trade in trades if TradeStatus.CLOSED.equals(trade.status)
60
+ ]
61
+
62
+ negative_trades = [
63
+ trade for trade in closed_trades if trade.net_gain_absolute < 0
64
+ ]
65
+ number_of_negative_trades = len(negative_trades)
66
+ percentage_negative_trades = (
67
+ (number_of_negative_trades / len(closed_trades)) * 100.0
68
+ if len(closed_trades) > 0 else 0.0
69
+ )
70
+ return number_of_negative_trades, percentage_negative_trades
71
+
72
+
73
+ def get_number_of_trades(
74
+ trades: List[Trade]
75
+ ) -> int:
76
+ """
77
+ Calculate the total number of trades.
78
+
79
+ Args:
80
+ trades (List[Trade]): List of Trade objects.
81
+
82
+ Returns:
83
+ int: The total number of trades.
84
+ """
85
+ if trades is None or len(trades) == 0:
86
+ raise OperationalException(
87
+ "Trades list is None, cannot compute number of trades."
88
+ )
89
+ return len(trades)
90
+
91
+
92
+ def get_number_of_open_trades(
93
+ trades: List[Trade]
94
+ ) -> int:
95
+ """
96
+ Calculate the number of open trades.
97
+
98
+ Args:
99
+ trades (List[Trade]): List of Trade objects.
100
+
101
+ Returns:
102
+ int: The number of open trades.
103
+ """
104
+ if trades is None or len(trades) == 0:
105
+ raise OperationalException(
106
+ "Trades list is None, cannot compute number of open trades."
107
+ )
108
+
109
+ open_trades = [
110
+ trade for trade in trades if TradeStatus.OPEN.equals(trade.status)
111
+ ]
112
+ return len(open_trades)
113
+
114
+
115
+ def get_number_of_closed_trades(
116
+ trades: List[Trade]
117
+ ) -> int:
118
+ """
119
+ Calculate the number of closed trades.
120
+
121
+ Args:
122
+ trades (List[Trade]): List of Trade objects.
123
+
124
+ Returns:
125
+ int: The number of closed trades.
126
+ """
127
+ if trades is None or len(trades) == 0:
128
+ raise OperationalException(
129
+ "Trades list is None, cannot compute number of closed trades."
130
+ )
131
+
132
+ closed_trades = [
133
+ trade for trade in trades if TradeStatus.CLOSED.equals(trade.status)
134
+ ]
135
+ return len(closed_trades)
136
+
137
+
138
+ def get_average_trade_duration(
139
+ trades: List[Trade]
140
+ ) -> float:
141
+ """
142
+ Calculate the average duration of closed trades in hours.
143
+
144
+ Args:
145
+ trades (List[Trade]): List of Trade objects.
146
+
147
+ Returns:
148
+ float: The average trade duration in hours.
149
+ """
150
+ if trades is None or len(trades) == 0:
151
+ raise OperationalException(
152
+ "Trades list is None, cannot compute average trade duration."
153
+ )
154
+
155
+ total_duration = 0.0
156
+
157
+ for trade in trades:
158
+ if TradeStatus.CLOSED.equals(trade.status):
159
+ total_duration += (trade.closed_at - trade.opened_at)\
160
+ .total_seconds() / 3600
161
+
162
+ number_of_trades = get_number_of_closed_trades(trades)
163
+ return total_duration / number_of_trades if number_of_trades > 0 else 0.0
164
+
165
+
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
+
201
+ def get_average_trade_size(
202
+ trades: List[Trade]
203
+ ) -> float:
204
+ """
205
+ Calculate the average trade size based on the amount
206
+ and open price of each trade.
207
+
208
+ Args:
209
+ trades (List[Trade]): List of Trade objects.
210
+
211
+ Returns:
212
+ float: The average trade size.
213
+ """
214
+
215
+ if trades is None or len(trades) == 0:
216
+ raise OperationalException(
217
+ "Trades list is None, cannot compute average trade size."
218
+ )
219
+
220
+ total_trade_size = 0.0
221
+
222
+ for trade in trades:
223
+ total_trade_size += trade.amount * trade.open_price
224
+
225
+ number_of_trades = get_number_of_trades(trades)
226
+ return total_trade_size / number_of_trades if number_of_trades > 0 else 0.0
227
+
228
+
229
+ def get_average_trade_return(trades: List[Trade]) -> Tuple[float, float]:
230
+ """
231
+ Calculate the average return (absolute PnL) and
232
+ average return percentage (per trade) of closed trades.
233
+ """
234
+ if not trades or len(trades) == 0:
235
+ raise OperationalException(
236
+ "Trades list is empty, cannot compute average return."
237
+ )
238
+
239
+ closed_trades = [t for t in trades if TradeStatus.CLOSED.equals(t.status)]
240
+
241
+ if not closed_trades:
242
+ return 0.0, 0.0
243
+
244
+ total_return = sum(t.net_gain_absolute for t in closed_trades)
245
+ average_return = total_return / len(closed_trades)
246
+
247
+ percentage_returns = [
248
+ (t.net_gain_absolute / t.cost) for t in closed_trades if t.cost > 0
249
+ ]
250
+ average_return_percentage = (
251
+ sum(percentage_returns) / len(percentage_returns)
252
+ if percentage_returns else 0.0
253
+ )
254
+
255
+ return average_return, average_return_percentage
256
+
257
+
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_absolute for t in trades)
278
+ average_return = total_return / len(trades)
279
+
280
+ percentage_returns = [
281
+ (t.net_gain_absolute / t.cost) 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
+
291
+ def get_average_trade_gain(trades: List[Trade]) -> Tuple[float, float]:
292
+ """
293
+ Calculate the average gain from a list of trades.
294
+
295
+ The average gain is calculated as the mean of all positive returns.
296
+
297
+ Args:
298
+ trades (List[Trade]): List of trades.
299
+
300
+ Returns:
301
+ Tuple[float, float]: The average gain and average gain percentage
302
+ """
303
+ if trades is None or len(trades) == 0:
304
+ raise OperationalException(
305
+ "Trades list is empty or None, cannot calculate average gain."
306
+ )
307
+
308
+ gains = [t.net_gain_absolute for t in trades if t.net_gain_absolute > 0]
309
+
310
+ if not gains:
311
+ return 0.0, 0.0
312
+
313
+ average_gain = sum(gains) / len(gains)
314
+
315
+ # Updated percentage calculation to match other functions
316
+ percentage_returns = [
317
+ (t.net_gain_absolute / t.cost) for t in trades
318
+ if t.net_gain_absolute > 0 and t.cost > 0
319
+ ]
320
+ average_gain_percentage = (
321
+ sum(percentage_returns) / len(percentage_returns)
322
+ if percentage_returns else 0.0
323
+ )
324
+
325
+ return average_gain, average_gain_percentage
326
+
327
+
328
+ def get_current_average_trade_gain(trades: List[Trade]) -> Tuple[float, float]:
329
+ """
330
+ Calculate the average gain from a list of trades,
331
+ including both closed and open trades.
332
+
333
+ The average gain is calculated as the mean of all positive returns.
334
+
335
+ Args:
336
+ trades (List[Trade]): List of trades.
337
+ Returns:
338
+ Tuple[float, float]: The average gain and average gain percentage
339
+ """
340
+ if trades is None or len(trades) == 0:
341
+ raise OperationalException(
342
+ "Trades list is empty or None, cannot calculate average gain."
343
+ )
344
+
345
+ gains = [t.net_gain_absolute for t in trades if t.net_gain_absolute > 0]
346
+
347
+ if not gains:
348
+ return 0.0, 0.0
349
+
350
+ average_gain = sum(gains) / len(gains)
351
+
352
+ # Updated percentage calculation to match other functions
353
+ percentage_returns = [
354
+ (t.net_gain_absolute / t.cost) for t in trades
355
+ if t.net_gain_absolute > 0 and t.cost > 0
356
+ ]
357
+ average_gain_percentage = (
358
+ sum(percentage_returns) / len(percentage_returns)
359
+ if percentage_returns else 0.0
360
+ )
361
+
362
+ return average_gain, average_gain_percentage
363
+
364
+
365
+ def get_average_trade_loss(trades: List[Trade]) -> Tuple[float, float]:
366
+ """
367
+ Calculate the average loss from a list of trades.
368
+
369
+ The average loss is calculated as the mean of all negative returns.
370
+
371
+ Args:
372
+ trades (List[Trade]): List of trades.
373
+ Returns:
374
+ Tuple[float, float]: The average loss
375
+ percentage of the average loss
376
+ """
377
+ if trades is None or len(trades) == 0:
378
+ raise OperationalException(
379
+ "Trades list is empty or None, cannot calculate average loss."
380
+ )
381
+
382
+ closed_trades = [t for t in trades if TradeStatus.CLOSED.equals(t.status)]
383
+ losing_trades = [t for t in closed_trades if t.net_gain < 0]
384
+
385
+ if not losing_trades or len(losing_trades) == 0:
386
+ return 0.0, 0.0
387
+
388
+ losses = [t.net_gain_absolute for t in losing_trades]
389
+ average_loss = sum(losses) / len(losses)
390
+ percentage_returns = [
391
+ (t.net_gain_absolute / t.cost) for t in losing_trades if t.cost > 0
392
+ ]
393
+ average_return_percentage = (
394
+ sum(percentage_returns) / len(percentage_returns)
395
+ if percentage_returns else 0.0
396
+ )
397
+ return average_loss, average_return_percentage
398
+
399
+
400
+ def get_current_average_trade_loss(
401
+ trades: List[Trade]
402
+ ) -> Tuple[float, float]:
403
+ """
404
+ Calculate the average loss from a list of trades,
405
+ including both closed and open trades.
406
+
407
+ The average loss is calculated as the mean of all negative returns.
408
+
409
+ Args:
410
+ trades (List[Trade]): List of trades.
411
+
412
+ Returns:
413
+ Tuple[float, float]: The average loss
414
+ percentage of the average loss
415
+ """
416
+ if trades is None or len(trades) == 0:
417
+ raise OperationalException(
418
+ "Trades list is empty or None, cannot calculate average loss."
419
+ )
420
+
421
+ losing_trades = [t for t in trades if t.net_gain_absolute < 0]
422
+
423
+ if not losing_trades or len(losing_trades) == 0:
424
+ return 0.0, 0.0
425
+
426
+ losses = [t.net_gain_absolute for t in losing_trades]
427
+ average_loss = sum(losses) / len(losses)
428
+ percentage_returns = [
429
+ (t.net_gain_absolute / t.cost) for t in losing_trades if t.cost > 0
430
+ ]
431
+ average_return_percentage = (
432
+ sum(percentage_returns) / len(percentage_returns)
433
+ if percentage_returns else 0.0
434
+ )
435
+ return average_loss, average_return_percentage
436
+
437
+
438
+ def get_median_trade_return(trades: List[Trade]) -> Tuple[float, float]:
439
+ """
440
+ Calculate the median return from a list of trades.
441
+
442
+ The median return is calculated as the median of all returns.
443
+
444
+ Args:
445
+ trades (List[Trade]): List of trades.
446
+
447
+ Returns:
448
+ Tuple[float, float]: The median return
449
+ percentage of the median return
450
+ """
451
+
452
+ if not trades:
453
+ return 0.0, 0.0
454
+
455
+ sorted_returns = sorted(t.net_gain_absolute for t in trades)
456
+ n = len(sorted_returns)
457
+ mid = n // 2
458
+
459
+ if n % 2 == 0:
460
+ median_return = (sorted_returns[mid - 1] + sorted_returns[mid]) / 2
461
+ else:
462
+ median_return = sorted_returns[mid]
463
+
464
+ cost = sum(t.cost for t in trades)
465
+ percentage = (median_return / cost) if cost > 0 else 0.0
466
+ return median_return, percentage
467
+
468
+
469
+ def get_best_trade(trades: List[Trade]) -> Trade:
470
+ """
471
+ Get the trade with the highest net gain.
472
+
473
+ Args:
474
+ trades (List[Trade]): List of trades.
475
+
476
+ Returns:
477
+ Trade: The trade with the highest net gain.
478
+ """
479
+
480
+ if not trades:
481
+ return None
482
+
483
+ return max(trades, key=lambda t: t.net_gain_absolute)
484
+
485
+
486
+ def get_worst_trade(trades: List[Trade]) -> Trade:
487
+ """
488
+ Get the trade with the lowest net gain (worst trade).
489
+
490
+ Args:
491
+ trades (List[Trade]): List of trades.
492
+
493
+ Returns:
494
+ Trade: The trade with the lowest net gain.
495
+ """
496
+
497
+ if not trades:
498
+ return None
499
+
500
+ return min(trades, key=lambda t: t.net_gain)
@@ -0,0 +1,97 @@
1
+ """
2
+ Volatility is a statistical measure of the dispersion of returns for a
3
+ given portfolio. In finance, it is commonly used as a proxy for risk.
4
+ This function calculates the standard deviation of daily log returns and
5
+ annualizes it, giving an estimate of how much the portfolio's value
6
+ fluctuates on a yearly basis.
7
+
8
+ | **Annual Volatility** | **Risk Level (Standalone)** | **Context Matters: Sharpe Ratio Impact** | **Comments** |
9
+ | --------------------- | --------------------------- | ---------------------------------------- | ----------- |
10
+ | **< 5%** | Very Low Risk | Sharpe > 2.0 = Excellent<br>Sharpe < 0.5 = Poor | Low volatility is great unless returns are negative |
11
+ | **5% – 10%** | Low Risk | Sharpe > 1.0 = Good<br>Sharpe < 0.3 = Mediocre | Typical for conservative portfolios |
12
+ | **10% – 15%** | Moderate Risk | Sharpe > 0.8 = Good<br>Sharpe < 0.2 = Risky | S&P 500 benchmark; quality matters |
13
+ | **15% – 25%** | High Risk | Sharpe > 0.6 = Acceptable<br>Sharpe < 0.0 = Avoid | **Example: 30% CAGR + 23% vol = Sharpe ~1.3 = Excellent** |
14
+ | **> 25%** | Very High Risk | Sharpe > 0.4 = Maybe acceptable<br>Sharpe < 0.0 = Dangerous | Only viable with strong positive returns |
15
+
16
+
17
+ Key takeaway: Don't interpret volatility in isolation. Always calculate
18
+ and compare the Sharpe Ratio to assess true strategy quality.
19
+ Your 30% CAGR with 23% volatility is exceptional because the return far outweighs the risk taken.
20
+
21
+ """
22
+
23
+ from typing import List
24
+
25
+ import pandas as pd
26
+ import numpy as np
27
+
28
+ from investing_algorithm_framework.domain import PortfolioSnapshot
29
+
30
+
31
+ def get_annual_volatility(
32
+ snapshots: List[PortfolioSnapshot],
33
+ trading_days_per_year=365
34
+ ) -> float:
35
+ """
36
+ Calculate the annualized volatility of portfolio net values.
37
+
38
+ !Important Note:
39
+
40
+ Volatility measures variability, not direction. For example:
41
+
42
+ A standard deviation of 0.238 (23.8%) means returns swing
43
+ wildly around their average, but it doesn't tell you if that average
44
+ is positive or negative.
45
+
46
+ Two scenarios with the same 23.8% volatility:
47
+ Mean return = +15% per year, Std = 23.8%
48
+ 16% chance of losing >8.8% (15% - 23.8%)
49
+ 16% chance of gaining >38.8% (15% + 23.8%)
50
+ This is excellent — high growth with swings
51
+
52
+ Mean return = -5% per year, Std = 23.8%
53
+ 16% chance of losing >28.8% (-5% - 23.8%)
54
+ 16% chance of gaining >18.8% (-5% + 23.8%)
55
+ This is terrible — losing money with high risk
56
+
57
+ To assess if "always good returns with high std" is perfect, you need
58
+ to consider risk-adjusted metrics like the Sharpe Ratio:
59
+ Sharpe Ratio = (Mean Return - Risk-Free Rate) / Volatility
60
+ Higher is better; tells you return per unit of risk taken
61
+
62
+ Args:
63
+ snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
64
+ from the backtest report.
65
+ trading_days_per_year (int): Number of trading days in a year.
66
+
67
+ Returns:
68
+ Float: Annualized volatility as a float
69
+ """
70
+
71
+ if len(snapshots) < 2:
72
+ return 0.0
73
+
74
+ # Build DataFrame from snapshots
75
+ records = [
76
+ (snapshot.total_value, snapshot.created_at) for snapshot in snapshots
77
+ ]
78
+ df = pd.DataFrame(records, columns=['total_value', 'created_at'])
79
+ df['created_at'] = pd.to_datetime(df['created_at'])
80
+ df = df.set_index('created_at').sort_index().drop_duplicates()
81
+
82
+ # Resample to daily frequency, taking the last value of each day
83
+ df_daily = df.resample('D').last()
84
+ df_daily = df_daily.dropna()
85
+
86
+ if len(df_daily) < 2:
87
+ return 0.0
88
+
89
+ # Calculate log returns on daily data
90
+ df_daily['log_return'] = np.log(df_daily['total_value'] / df_daily['total_value'].shift(1))
91
+ df_daily = df_daily.dropna()
92
+
93
+ # Calculate daily volatility (standard deviation of daily returns)
94
+ daily_volatility = df_daily['log_return'].std()
95
+
96
+ # Annualize using trading days per year
97
+ return daily_volatility * np.sqrt(trading_days_per_year)
@@ -42,7 +42,7 @@ def get_win_rate(trades: List[Trade]) -> float:
42
42
  The percentage of trades that are profitable.
43
43
 
44
44
  Formula:
45
- Win Rate = (Number of Profitable Trades / Total Number of Trades) * 100
45
+ Win Rate = Number of Profitable Trades / Total Number of Trades
46
46
 
47
47
  Example: If 60 out of 100 trades are profitable, the win rate is 60%.
48
48
 
@@ -50,7 +50,7 @@ def get_win_rate(trades: List[Trade]) -> float:
50
50
  trades (List[Trade]): List of trades from the backtest report.
51
51
 
52
52
  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).
54
54
  """
55
55
  trades = [
56
56
  trade for trade in trades if TradeStatus.CLOSED.equals(trade.status)
@@ -61,7 +61,34 @@ def get_win_rate(trades: List[Trade]) -> float:
61
61
  if total_trades == 0:
62
62
  return 0.0
63
63
 
64
- return (positive_trades / total_trades) * 100.0
64
+ return positive_trades / total_trades
65
+
66
+ def get_current_win_rate(trades: List[Trade]) -> float:
67
+ """
68
+ Calculate the current win rate of the portfolio based on a list
69
+ of recent trades.
70
+
71
+ Current Win Rate is defined as the percentage of the most recent trades
72
+ that were profitable. This metric also includes trades that are still open.
73
+
74
+ Formula:
75
+ Current Win Rate = Number of Profitable Recent Trades
76
+ / Total Number of Recent Trades
77
+
78
+ Args:
79
+ trades (List[Trade]): List of recent trades.
80
+
81
+ Returns:
82
+ float: The current win rate as a percentage (e.g., 0.75 for
83
+ 75% win rate).
84
+ """
85
+ if not trades:
86
+ return 0.0
87
+
88
+ positive_trades = sum(1 for trade in trades if trade.net_gain_absolute > 0)
89
+ total_trades = len(trades)
90
+
91
+ return positive_trades / total_trades
65
92
 
66
93
 
67
94
  def get_win_loss_ratio(trades: List[Trade]) -> float:
@@ -108,3 +135,43 @@ def get_win_loss_ratio(trades: List[Trade]) -> float:
108
135
  return float('inf')
109
136
 
110
137
  return avg_win / avg_loss
138
+
139
+
140
+ def get_current_win_loss_ratio(trades: List[Trade]) -> float:
141
+ """
142
+ Calculate the current win/loss ratio of the portfolio based on a list
143
+ of recent trades.
144
+
145
+ Current Win/Loss Ratio is defined as the average profit of winning
146
+ recent trades divided by the average loss of losing recent trades.
147
+ This metric also includes trades that are still open.
148
+
149
+ Formula:
150
+ Current Win/Loss Ratio = Average Profit of Winning Recent Trades
151
+ / Average Loss of Losing Recent Trades
152
+ Args:
153
+ trades (List[Trade]): List of recent trades.
154
+
155
+ Returns:
156
+ float: The current win/loss ratio.
157
+ """
158
+ if not trades:
159
+ return 0.0
160
+
161
+ # Separate winning and losing trades
162
+ winning_trades = [t for t in trades if t.net_gain_absolute > 0]
163
+ losing_trades = [t for t in trades if t.net_gain_absolute < 0]
164
+
165
+ if not winning_trades or not losing_trades:
166
+ return 0.0
167
+
168
+ # Compute averages
169
+ avg_win = sum(t.net_gain_absolute for t in winning_trades) / len(winning_trades)
170
+ avg_loss = abs(
171
+ sum(t.net_gain_absolute for t in losing_trades) / len(losing_trades))
172
+
173
+ # Avoid division by zero
174
+ if avg_loss == 0:
175
+ return float('inf')
176
+
177
+ return avg_win / avg_loss