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
@@ -3,7 +3,8 @@ from datetime import datetime, date
3
3
 
4
4
  import pandas as pd
5
5
 
6
- from investing_algorithm_framework.domain import PortfolioSnapshot, Trade
6
+ from investing_algorithm_framework.domain import PortfolioSnapshot, Trade, \
7
+ OperationalException
7
8
 
8
9
 
9
10
  def get_monthly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float, datetime]]:
@@ -51,7 +52,7 @@ def get_yearly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float,
51
52
  snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
52
53
 
53
54
  Returns:
54
- List[Tuple[float, datetime]]: A list of tuples containing the yearly return
55
+ List[Tuple[float, date]]: A list of tuples containing the yearly return
55
56
  and the corresponding year.
56
57
  """
57
58
 
@@ -62,6 +63,10 @@ def get_yearly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float,
62
63
  df = df.sort_values('created_at').drop_duplicates('created_at')\
63
64
  .set_index('created_at')
64
65
 
66
+ # Remove timezone information if present to avoid warning
67
+ if df.index.tz is not None:
68
+ df.index = df.index.tz_localize(None)
69
+
65
70
  # Resample to yearly frequency using last value of the year
66
71
  yearly_df = df.resample('YE').last().dropna()
67
72
  yearly_df['return'] = yearly_df['total_value'].pct_change()
@@ -75,127 +80,6 @@ def get_yearly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float,
75
80
  return yearly_returns
76
81
 
77
82
 
78
- def get_average_loss(trades: List[Trade]) -> Tuple[float, float]:
79
- """
80
- Calculate the average loss from a list of trades
81
-
82
- The average loss is calculated as the mean of all negative returns.
83
-
84
- Args:
85
- trades (List[Trade]): List of trades.
86
-
87
- Returns:
88
- Tuple[float, float]: The average loss
89
- percentage of the average loss
90
- """
91
-
92
- losses = [t.net_gain for t in trades if t.net_gain < 0]
93
- cost = sum(t.cost for t in trades if t.net_gain < 0)
94
-
95
- if not losses:
96
- return 0.0, 0.0
97
-
98
- average_loss = sum(losses) / len(losses)
99
- percentage = (average_loss / cost) * 100 if cost > 0 else 0.0
100
- return average_loss, percentage
101
-
102
-
103
- def get_average_gain(trades: List[Trade]) -> Tuple[float, float]:
104
- """
105
- Calculate the average gain from a list of trades.
106
-
107
- The average gain is calculated as the mean of all positive returns.
108
-
109
- Args:
110
- trades (List[Trade]): List of trades.
111
-
112
- Returns:
113
- Tuple[float, float]: The average gain
114
- percentage of the average loss
115
- """
116
-
117
- gains = [t.net_gain for t in trades if t.net_gain > 0]
118
- cost = sum(t.cost for t in trades if t.net_gain > 0)
119
-
120
- if not gains:
121
- return 0.0, 0.0
122
-
123
- average_gain = sum(gains) / len(gains)
124
- percentage = (average_gain / cost) * 100 if cost > 0 else 0.0
125
- return average_gain, percentage
126
-
127
-
128
- def get_best_trade(trades: List[Trade]) -> Trade:
129
- """
130
- Get the trade with the highest net gain.
131
-
132
- Args:
133
- trades (List[Trade]): List of trades.
134
-
135
- Returns:
136
- Trade: The trade with the highest net gain.
137
- """
138
-
139
- if not trades:
140
- return None
141
-
142
- return max(trades, key=lambda t: t.net_gain)
143
-
144
- def get_best_trade_date(trades: List[Trade]) -> Tuple[float, datetime]:
145
- """
146
- Get the date of the trade with the highest net gain.
147
-
148
- Args:
149
- trades (List[Trade]): List of trades.
150
-
151
- Returns:
152
- Tuple[float, datetime]: The highest net gain and the corresponding trade date.
153
- """
154
-
155
- best_trade = get_best_trade(trades)
156
-
157
- if best_trade is None:
158
- return 0.0, None
159
-
160
- return best_trade.closed_at
161
-
162
-
163
- def get_worst_trade_date(trades: List[Trade]) -> Tuple[float, datetime]:
164
- """
165
- Get the date of the trade with the lowest net gain (worst trade).
166
-
167
- Args:
168
- trades (List[Trade]): List of trades.
169
-
170
- Returns:
171
- Tuple[float, datetime]: The lowest net gain and the corresponding trade date.
172
- """
173
-
174
- worst_trade = get_worst_trade(trades)
175
-
176
- if worst_trade is None:
177
- return 0.0, None
178
-
179
- return worst_trade.closed_at
180
-
181
-
182
- def get_worst_trade(trades: List[Trade]) -> Trade:
183
- """
184
- Get the trade with the lowest net gain (worst trade).
185
-
186
- Args:
187
- trades (List[Trade]): List of trades.
188
-
189
- Returns:
190
- Trade: The trade with the lowest net gain.
191
- """
192
-
193
- if not trades:
194
- return None
195
-
196
- return min(trades, key=lambda t: t.net_gain)
197
-
198
-
199
83
  def get_percentage_winning_months(snapshots: List[PortfolioSnapshot]) -> float:
200
84
  """
201
85
  Calculate the percentage of winning months from portfolio snapshots.
@@ -216,7 +100,7 @@ def get_percentage_winning_months(snapshots: List[PortfolioSnapshot]) -> float:
216
100
  if not monthly_returns:
217
101
  return 0.0
218
102
 
219
- return (winning_months / len(monthly_returns)) * 100
103
+ return (winning_months / len(monthly_returns))
220
104
 
221
105
 
222
106
  def get_best_month(snapshots: List[PortfolioSnapshot]) -> Tuple[float, datetime]:
@@ -271,12 +155,14 @@ def get_best_year(
271
155
  yearly_returns = get_yearly_returns(snapshots)
272
156
 
273
157
  if not yearly_returns:
274
- return 0.0, None
158
+ return None, None
275
159
 
276
160
  return max(yearly_returns, key=lambda x: x[0])
277
161
 
278
162
 
279
- def get_worst_year(snapshots: List[PortfolioSnapshot]) -> Tuple[float, datetime]:
163
+ def get_worst_year(
164
+ snapshots: List[PortfolioSnapshot]
165
+ ) -> Tuple[float, date]:
280
166
  """
281
167
  Get the worst year in terms of return from portfolio snapshots.
282
168
 
@@ -290,7 +176,7 @@ def get_worst_year(snapshots: List[PortfolioSnapshot]) -> Tuple[float, datetime]
290
176
  yearly_returns = get_yearly_returns(snapshots)
291
177
 
292
178
  if not yearly_returns:
293
- return 0.0, None
179
+ return None, None
294
180
 
295
181
  return min(yearly_returns, key=lambda x: x[0])
296
182
 
@@ -313,7 +199,7 @@ def get_average_monthly_return(snapshots: List[PortfolioSnapshot]) -> float:
313
199
  if not monthly_returns:
314
200
  return 0.0
315
201
 
316
- return sum(r for r, _ in monthly_returns) / len(monthly_returns) * 100 # Convert to percentage
202
+ return sum(r for r, _ in monthly_returns) / len(monthly_returns)
317
203
 
318
204
  def get_average_monthly_return_winning_months(snapshots: List[PortfolioSnapshot]) -> float:
319
205
  """
@@ -335,7 +221,7 @@ def get_average_monthly_return_winning_months(snapshots: List[PortfolioSnapshot]
335
221
  if not winning_months:
336
222
  return 0.0
337
223
 
338
- return sum(winning_months) / len(winning_months) * 100 # Convert to percentage
224
+ return sum(winning_months) / len(winning_months)
339
225
 
340
226
  def get_average_monthly_return_losing_months(snapshots: List[PortfolioSnapshot]) -> float:
341
227
  """
@@ -357,7 +243,7 @@ def get_average_monthly_return_losing_months(snapshots: List[PortfolioSnapshot])
357
243
  if not losing_months:
358
244
  return 0.0
359
245
 
360
- return sum(losing_months) / len(losing_months) * 100 # Convert to percentage
246
+ return sum(losing_months) / len(losing_months)
361
247
 
362
248
 
363
249
  def get_average_yearly_return(snapshots: List[PortfolioSnapshot]) -> float:
@@ -378,10 +264,12 @@ def get_average_yearly_return(snapshots: List[PortfolioSnapshot]) -> float:
378
264
  if not yearly_returns:
379
265
  return 0.0
380
266
 
381
- return sum(r for r, _ in yearly_returns) / len(yearly_returns) * 100 # Convert to percentage
267
+ return sum(r for r, _ in yearly_returns) / len(yearly_returns)
382
268
 
383
269
 
384
- def get_total_return(snapshots: List[PortfolioSnapshot]) -> float:
270
+ def get_total_return(
271
+ snapshots: List[PortfolioSnapshot]
272
+ ) -> Tuple[float, float]:
385
273
  """
386
274
  Calculate the total return from portfolio snapshots.
387
275
 
@@ -392,19 +280,88 @@ def get_total_return(snapshots: List[PortfolioSnapshot]) -> float:
392
280
  snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
393
281
 
394
282
  Returns:
395
- float: The total return as a percentage.
283
+ Tuple[Float, Float]: First number is the absolute return and the
284
+ second number is the percentage total return
396
285
  """
397
286
 
398
- if not snapshots:
399
- return 0.0
287
+ if not snapshots or len(snapshots) < 2:
288
+ return 0.0, 0.0
400
289
 
401
290
  initial_value = snapshots[0].total_value
402
291
  final_value = snapshots[-1].total_value
403
292
 
404
293
  if initial_value == 0:
405
- return 0.0
294
+ return 0.0, 0.0
295
+
296
+ absolute_return = final_value - initial_value
297
+ percentage = (absolute_return / initial_value)
298
+ return absolute_return, percentage
299
+
300
+
301
+ def get_total_loss(
302
+ snapshots: List[PortfolioSnapshot]
303
+ ) -> Tuple[float, float]:
304
+ """
305
+ Calculate the total loss from portfolio snapshots.
306
+
307
+ The total loss is calculated as the percentage change in portfolio value
308
+ from the first snapshot to the last snapshot, only if there is a loss.
309
+
310
+ Args:
311
+ snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
312
+
313
+ Returns:
314
+ Tuple[Float, Float]: First number is the absolute loss and the
315
+ second number is the percentage total loss
316
+ """
317
+
318
+ if not snapshots or len(snapshots) < 2:
319
+ return 0.0, 0.0
320
+
321
+ initial_value = snapshots[0].total_value
322
+ final_value = snapshots[-1].total_value
323
+
324
+ if initial_value == 0:
325
+ return 0.0, 0.0
326
+
327
+ absolute_return = final_value - initial_value
328
+
329
+ if absolute_return >= 0:
330
+ return 0.0, 0.0
331
+
332
+ percentage = (absolute_return / initial_value)
333
+ return absolute_return, percentage
334
+
335
+
336
+ def get_total_growth(
337
+ snapshots: List[PortfolioSnapshot]
338
+ ) -> Tuple[float, float]:
339
+ """
340
+ Calculate the total growth from portfolio snapshots.
341
+
342
+ The total return is calculated as the percentage change in portfolio value
343
+ from the first snapshot to the last snapshot added to the initial value.
344
+
345
+ Args:
346
+ snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
347
+
348
+ Returns:
349
+ Tuple[Float, Float]: First number is the absolute return and the
350
+ second number is the percentage total return
351
+ """
352
+
353
+ if not snapshots or len(snapshots) < 2:
354
+ return 0.0, 0.0
406
355
 
407
- return ((final_value - initial_value) / initial_value) * 100
356
+ initial_value = snapshots[0].total_value
357
+ final_value = snapshots[-1].total_value
358
+
359
+ if initial_value == 0:
360
+ return 0.0, 0.0
361
+
362
+ growth = final_value - initial_value
363
+ growth_percentage = (growth / initial_value)
364
+ return growth, growth_percentage
408
365
 
409
366
 
410
367
  def get_percentage_winning_years(snapshots: List[PortfolioSnapshot]) -> float:
@@ -427,27 +384,69 @@ def get_percentage_winning_years(snapshots: List[PortfolioSnapshot]) -> float:
427
384
  if not yearly_returns:
428
385
  return 0.0
429
386
 
430
- return (winning_years / len(yearly_returns)) * 100
387
+ return winning_years / len(yearly_returns)
431
388
 
432
389
 
433
- def get_total_net_gain(snapshots: List[PortfolioSnapshot]) -> float:
390
+ def get_final_value(snapshots: List[PortfolioSnapshot]) -> float:
434
391
  """
435
- Calculate the total net gain from portfolio snapshots.
436
-
437
- The total net gain is calculated as the difference between the final
438
- portfolio value and the initial portfolio value.
392
+ Calculate the final portfolio value from portfolio snapshots.
439
393
 
440
394
  Args:
441
395
  snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
442
396
 
443
397
  Returns:
444
- float: The total net gain.
398
+ float: The final portfolio value.
445
399
  """
446
400
 
447
401
  if not snapshots:
448
402
  return 0.0
449
403
 
450
- initial_value = snapshots[0].total_value
451
- final_value = snapshots[-1].total_value
404
+ return snapshots[-1].total_value
405
+
406
+
407
+ def get_cumulative_return(snapshots: list[PortfolioSnapshot]) -> float:
408
+ """
409
+ Calculate cumulative return over the full period of snapshots.
410
+ Returns a single float (e.g., 0.25 for +25%).
411
+ """
412
+ if len(snapshots) < 2:
413
+ return 0.0
414
+
415
+ # Sort snapshots by date
416
+ snapshots = sorted(snapshots, key=lambda s: s.created_at)
417
+
418
+ start_value = snapshots[0].total_value
419
+ end_value = snapshots[-1].total_value
420
+
421
+ if start_value == 0:
422
+ return 0.0
423
+
424
+ return (end_value / start_value) - 1
425
+
426
+
427
+ def get_cumulative_return_series(
428
+ snapshots: list[PortfolioSnapshot]
429
+ ) -> List[Tuple[float, datetime]]:
430
+ """
431
+ Calculate cumulative returns from a list of PortfolioSnapshot objects.
432
+
433
+ Args:
434
+ snapshots (list[PortfolioSnapshot]): List of snapshots ordered by time.
435
+
436
+ Returns:
437
+ List[Tuple[float, datetime]]: Cumulative returns for each snapshot.
438
+ """
439
+
440
+ # Ensure snapshots are sorted by date
441
+ snapshots = sorted(snapshots, key=lambda s: s.get_created_at())
442
+
443
+ initial_value = snapshots[0].get_total_value()
444
+ if initial_value == 0:
445
+ raise ValueError("Initial portfolio value cannot be zero.")
446
+
447
+ cumulative_returns = []
448
+ for snap in snapshots:
449
+ cum_return = (snap.get_total_value() / initial_value) - 1
450
+ cumulative_returns.append((cum_return, snap.created_at))
452
451
 
453
- return final_value - initial_value
452
+ return cumulative_returns
@@ -0,0 +1,28 @@
1
+ import yfinance as yf
2
+ import logging
3
+
4
+
5
+ logger = logging.getLogger("investing_algorithm_framework")
6
+
7
+
8
+ def get_risk_free_rate_us():
9
+ """
10
+ Retrieves the US 10-year Treasury yield from Yahoo Finance.
11
+
12
+ Returns:
13
+ float or None: The latest yield as a decimal (e.g., 0.0423 for 4.23%), or None if unavailable.
14
+ """
15
+ try:
16
+ ten_year = yf.Ticker("^TNX")
17
+ hist = ten_year.history(period="5d")
18
+
19
+ if hist.empty or "Close" not in hist.columns:
20
+ logger.warning("Risk-free rate data is unavailable or malformed.")
21
+ return None
22
+
23
+ latest_yield = hist["Close"].dropna().iloc[-1] / 100
24
+ return latest_yield
25
+
26
+ except Exception as e:
27
+ logger.warning(f"Could not retrieve risk-free rate: {e}")
28
+ return None
@@ -49,16 +49,15 @@ from typing import Optional, List, Tuple
49
49
  import math
50
50
  import pandas as pd
51
51
  import numpy as np
52
- from datetime import datetime, timedelta
52
+ from datetime import datetime
53
53
 
54
54
  from investing_algorithm_framework.domain import PortfolioSnapshot
55
55
  from .mean_daily_return import get_mean_daily_return
56
- from .risk_free_rate import get_risk_free_rate_us
57
56
  from .standard_deviation import get_daily_returns_std
58
57
 
59
58
 
60
59
  def get_sharpe_ratio(
61
- snapshots: List[PortfolioSnapshot], risk_free_rate: Optional[float] = None,
60
+ snapshots: List[PortfolioSnapshot], risk_free_rate: float,
62
61
  ) -> float:
63
62
  """
64
63
  Calculate the Sharpe Ratio from a backtest report using daily or
@@ -79,8 +78,8 @@ def get_sharpe_ratio(
79
78
  mean_daily_return = get_mean_daily_return(snapshots)
80
79
  std_daily_return = get_daily_returns_std(snapshots)
81
80
 
82
- if risk_free_rate is None:
83
- risk_free_rate = get_risk_free_rate_us()
81
+ if std_daily_return == 0:
82
+ return float('nan') # Avoid division by zero
84
83
 
85
84
  # Formula: Sharpe Ratio = (Mean Daily Return × Periods Per Year - Risk-Free Rate) /
86
85
  # (Standard Deviation of Daily Returns × sqrt(Periods Per Year))
@@ -89,21 +88,18 @@ def get_sharpe_ratio(
89
88
 
90
89
 
91
90
  def get_rolling_sharpe_ratio(
92
- snapshots: List[PortfolioSnapshot], risk_free_rate: Optional[float] = None
91
+ snapshots: List[PortfolioSnapshot], risk_free_rate: float
93
92
  ) -> List[Tuple[float, datetime]]:
94
93
  """
95
94
  Calculate the rolling Sharpe Ratio over a 365-day window.
96
95
 
97
96
  Args:
98
97
  snapshots (List[PortfolioSnapshot]): Time-sorted list of snapshots.
99
- risk_free_rate (float, optional): Annualized risk-free rate (e.g., 0.03 for 3%).
98
+ risk_free_rate (float): Annualized risk-free rate (e.g., 0.03 for 3%).
100
99
 
101
100
  Returns:
102
101
  List[Tuple[float, datetime]]: List of (sharpe_ratio, snapshot_date).
103
102
  """
104
- if risk_free_rate is None:
105
- risk_free_rate = get_risk_free_rate_us()
106
-
107
103
  data = [(s.created_at, s.total_value) for s in snapshots]
108
104
  df = pd.DataFrame(data, columns=["created_at", "total_value"])
109
105
  df['created_at'] = pd.to_datetime(df['created_at'])
@@ -30,7 +30,7 @@ from .standard_deviation import get_downside_std_of_daily_returns
30
30
 
31
31
 
32
32
  def get_sortino_ratio(
33
- snapshots: List[PortfolioSnapshot], risk_free_rate: Optional[float] = None,
33
+ snapshots: List[PortfolioSnapshot], risk_free_rate: float
34
34
  ) -> float:
35
35
  """
36
36
  Calculate the Sortino Ratio for a given report.
@@ -46,9 +46,8 @@ def get_sortino_ratio(
46
46
  Args:
47
47
  snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
48
48
  from the backtest report.
49
- risk_free_rate (float, optional): Annual risk-free rate as a decimal
50
- (e.g., 0.047 for 4.7%). If not provided, defaults to the US risk-free
51
- rate.
49
+ risk_free_rate (float): Annual risk-free rate as a decimal
50
+ (e.g., 0.047 for 4.7%).
52
51
 
53
52
  Returns:
54
53
  float: The Sortino Ratio.
@@ -61,9 +60,6 @@ def get_sortino_ratio(
61
60
  mean_daily_return = get_mean_daily_return(snapshots)
62
61
  std_downside_daily_return = get_downside_std_of_daily_returns(snapshots)
63
62
 
64
- if risk_free_rate is None:
65
- risk_free_rate = get_risk_free_rate_us()
66
-
67
63
  if std_downside_daily_return == 0:
68
64
  return float('nan') # or 0.0, depending on preference
69
65