investing-algorithm-framework 1.5__py3-none-any.whl → 7.25.6__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.
Files changed (276) hide show
  1. investing_algorithm_framework/__init__.py +192 -16
  2. investing_algorithm_framework/analysis/__init__.py +16 -0
  3. investing_algorithm_framework/analysis/backtest_data_ranges.py +202 -0
  4. investing_algorithm_framework/analysis/data.py +170 -0
  5. investing_algorithm_framework/analysis/markdown.py +91 -0
  6. investing_algorithm_framework/analysis/ranking.py +298 -0
  7. investing_algorithm_framework/app/__init__.py +29 -4
  8. investing_algorithm_framework/app/algorithm/__init__.py +7 -0
  9. investing_algorithm_framework/app/algorithm/algorithm.py +193 -0
  10. investing_algorithm_framework/app/algorithm/algorithm_factory.py +118 -0
  11. investing_algorithm_framework/app/app.py +2220 -379
  12. investing_algorithm_framework/app/app_hook.py +28 -0
  13. investing_algorithm_framework/app/context.py +1724 -0
  14. investing_algorithm_framework/app/eventloop.py +620 -0
  15. investing_algorithm_framework/app/reporting/__init__.py +27 -0
  16. investing_algorithm_framework/app/reporting/ascii.py +921 -0
  17. investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
  18. investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
  19. investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
  20. investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
  21. investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
  22. investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
  23. investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
  24. investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
  25. investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
  26. investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
  27. investing_algorithm_framework/app/reporting/generate.py +185 -0
  28. investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
  29. investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
  30. investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
  31. investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
  32. investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
  33. investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
  34. investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
  35. investing_algorithm_framework/app/stateless/action_handlers/__init__.py +6 -3
  36. investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +1 -1
  37. investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +2 -1
  38. investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +14 -7
  39. investing_algorithm_framework/app/strategy.py +867 -60
  40. investing_algorithm_framework/app/task.py +5 -3
  41. investing_algorithm_framework/app/web/__init__.py +2 -1
  42. investing_algorithm_framework/app/web/controllers/__init__.py +2 -2
  43. investing_algorithm_framework/app/web/controllers/orders.py +3 -2
  44. investing_algorithm_framework/app/web/controllers/positions.py +2 -2
  45. investing_algorithm_framework/app/web/create_app.py +4 -2
  46. investing_algorithm_framework/app/web/schemas/position.py +1 -0
  47. investing_algorithm_framework/cli/__init__.py +0 -0
  48. investing_algorithm_framework/cli/cli.py +231 -0
  49. investing_algorithm_framework/cli/deploy_to_aws_lambda.py +501 -0
  50. investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
  51. investing_algorithm_framework/cli/initialize_app.py +603 -0
  52. investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
  53. investing_algorithm_framework/cli/templates/app.py.template +18 -0
  54. investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
  55. investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
  56. investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
  57. investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
  58. investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
  59. investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
  60. investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
  61. investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
  62. investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
  63. investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
  64. investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
  65. investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
  66. investing_algorithm_framework/cli/templates/env.example.template +2 -0
  67. investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
  68. investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
  69. investing_algorithm_framework/cli/templates/readme.md.template +135 -0
  70. investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
  71. investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
  72. investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
  73. investing_algorithm_framework/cli/validate_backtest_checkpoints.py +197 -0
  74. investing_algorithm_framework/create_app.py +40 -7
  75. investing_algorithm_framework/dependency_container.py +100 -47
  76. investing_algorithm_framework/domain/__init__.py +97 -30
  77. investing_algorithm_framework/domain/algorithm_id.py +69 -0
  78. investing_algorithm_framework/domain/backtesting/__init__.py +25 -0
  79. investing_algorithm_framework/domain/backtesting/backtest.py +548 -0
  80. investing_algorithm_framework/domain/backtesting/backtest_date_range.py +113 -0
  81. investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +241 -0
  82. investing_algorithm_framework/domain/backtesting/backtest_metrics.py +470 -0
  83. investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
  84. investing_algorithm_framework/domain/backtesting/backtest_run.py +663 -0
  85. investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
  86. investing_algorithm_framework/domain/backtesting/backtest_utils.py +198 -0
  87. investing_algorithm_framework/domain/backtesting/combine_backtests.py +392 -0
  88. investing_algorithm_framework/domain/config.py +59 -136
  89. investing_algorithm_framework/domain/constants.py +18 -37
  90. investing_algorithm_framework/domain/data_provider.py +334 -0
  91. investing_algorithm_framework/domain/data_structures.py +42 -0
  92. investing_algorithm_framework/domain/exceptions.py +51 -1
  93. investing_algorithm_framework/domain/models/__init__.py +26 -19
  94. investing_algorithm_framework/domain/models/app_mode.py +34 -0
  95. investing_algorithm_framework/domain/models/data/__init__.py +7 -0
  96. investing_algorithm_framework/domain/models/data/data_source.py +222 -0
  97. investing_algorithm_framework/domain/models/data/data_type.py +46 -0
  98. investing_algorithm_framework/domain/models/event.py +35 -0
  99. investing_algorithm_framework/domain/models/market/__init__.py +5 -0
  100. investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
  101. investing_algorithm_framework/domain/models/order/__init__.py +3 -4
  102. investing_algorithm_framework/domain/models/order/order.py +198 -65
  103. investing_algorithm_framework/domain/models/order/order_status.py +2 -2
  104. investing_algorithm_framework/domain/models/order/order_type.py +1 -3
  105. investing_algorithm_framework/domain/models/portfolio/__init__.py +6 -2
  106. investing_algorithm_framework/domain/models/portfolio/portfolio.py +98 -3
  107. investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +37 -43
  108. investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +108 -11
  109. investing_algorithm_framework/domain/models/position/__init__.py +2 -1
  110. investing_algorithm_framework/domain/models/position/position.py +20 -0
  111. investing_algorithm_framework/domain/models/position/position_size.py +41 -0
  112. investing_algorithm_framework/domain/models/position/position_snapshot.py +0 -2
  113. investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
  114. investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
  115. investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
  116. investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
  117. investing_algorithm_framework/domain/models/strategy_profile.py +19 -141
  118. investing_algorithm_framework/domain/models/time_frame.py +94 -98
  119. investing_algorithm_framework/domain/models/time_interval.py +33 -0
  120. investing_algorithm_framework/domain/models/time_unit.py +66 -2
  121. investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
  122. investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
  123. investing_algorithm_framework/domain/models/trade/__init__.py +11 -0
  124. investing_algorithm_framework/domain/models/trade/trade.py +389 -0
  125. investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
  126. investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +332 -0
  127. investing_algorithm_framework/domain/models/trade/trade_take_profit.py +365 -0
  128. investing_algorithm_framework/domain/order_executor.py +112 -0
  129. investing_algorithm_framework/domain/portfolio_provider.py +118 -0
  130. investing_algorithm_framework/domain/services/__init__.py +11 -0
  131. investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
  132. investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
  133. investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
  134. investing_algorithm_framework/domain/services/rounding_service.py +27 -0
  135. investing_algorithm_framework/domain/services/state_handler.py +38 -0
  136. investing_algorithm_framework/domain/strategy.py +1 -29
  137. investing_algorithm_framework/domain/utils/__init__.py +15 -5
  138. investing_algorithm_framework/domain/utils/csv.py +22 -0
  139. investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
  140. investing_algorithm_framework/domain/utils/dates.py +57 -0
  141. investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
  142. investing_algorithm_framework/domain/utils/polars.py +53 -0
  143. investing_algorithm_framework/domain/utils/random.py +29 -0
  144. investing_algorithm_framework/download_data.py +244 -0
  145. investing_algorithm_framework/infrastructure/__init__.py +37 -11
  146. investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
  147. investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1152 -0
  148. investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
  149. investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
  150. investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
  151. investing_algorithm_framework/infrastructure/database/sql_alchemy.py +86 -12
  152. investing_algorithm_framework/infrastructure/models/__init__.py +7 -3
  153. investing_algorithm_framework/infrastructure/models/order/__init__.py +2 -2
  154. investing_algorithm_framework/infrastructure/models/order/order.py +53 -53
  155. investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
  156. investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
  157. investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +1 -1
  158. investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +8 -2
  159. investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py → sql_portfolio.py} +17 -6
  160. investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +3 -1
  161. investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
  162. investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
  163. investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +59 -0
  164. investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +55 -0
  165. investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
  166. investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
  167. investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
  168. investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
  169. investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
  170. investing_algorithm_framework/infrastructure/repositories/__init__.py +10 -4
  171. investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
  172. investing_algorithm_framework/infrastructure/repositories/order_repository.py +16 -5
  173. investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +2 -2
  174. investing_algorithm_framework/infrastructure/repositories/position_repository.py +11 -0
  175. investing_algorithm_framework/infrastructure/repositories/repository.py +84 -30
  176. investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
  177. investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +29 -0
  178. investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +29 -0
  179. investing_algorithm_framework/infrastructure/services/__init__.py +9 -4
  180. investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
  181. investing_algorithm_framework/infrastructure/services/aws/state_handler.py +193 -0
  182. investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
  183. investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
  184. investing_algorithm_framework/infrastructure/services/backtesting/__init__.py +9 -0
  185. investing_algorithm_framework/infrastructure/services/backtesting/backtest_service.py +2596 -0
  186. investing_algorithm_framework/infrastructure/services/backtesting/event_backtest_service.py +285 -0
  187. investing_algorithm_framework/infrastructure/services/backtesting/vector_backtest_service.py +468 -0
  188. investing_algorithm_framework/services/__init__.py +123 -15
  189. investing_algorithm_framework/services/configuration_service.py +77 -11
  190. investing_algorithm_framework/services/data_providers/__init__.py +5 -0
  191. investing_algorithm_framework/services/data_providers/data_provider_service.py +1058 -0
  192. investing_algorithm_framework/services/market_credential_service.py +40 -0
  193. investing_algorithm_framework/services/metrics/__init__.py +119 -0
  194. investing_algorithm_framework/services/metrics/alpha.py +0 -0
  195. investing_algorithm_framework/services/metrics/beta.py +0 -0
  196. investing_algorithm_framework/services/metrics/cagr.py +60 -0
  197. investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
  198. investing_algorithm_framework/services/metrics/drawdown.py +218 -0
  199. investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
  200. investing_algorithm_framework/services/metrics/exposure.py +210 -0
  201. investing_algorithm_framework/services/metrics/generate.py +358 -0
  202. investing_algorithm_framework/services/metrics/mean_daily_return.py +84 -0
  203. investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
  204. investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
  205. investing_algorithm_framework/services/metrics/recovery.py +113 -0
  206. investing_algorithm_framework/services/metrics/returns.py +452 -0
  207. investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
  208. investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
  209. investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
  210. investing_algorithm_framework/services/metrics/standard_deviation.py +156 -0
  211. investing_algorithm_framework/services/metrics/trades.py +473 -0
  212. investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
  213. investing_algorithm_framework/services/metrics/ulcer.py +0 -0
  214. investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
  215. investing_algorithm_framework/services/metrics/volatility.py +118 -0
  216. investing_algorithm_framework/services/metrics/win_rate.py +177 -0
  217. investing_algorithm_framework/services/order_service/__init__.py +9 -0
  218. investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
  219. investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
  220. investing_algorithm_framework/services/order_service/order_service.py +826 -0
  221. investing_algorithm_framework/services/portfolios/__init__.py +16 -0
  222. investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
  223. investing_algorithm_framework/services/{portfolio_configuration_service.py → portfolios/portfolio_configuration_service.py} +27 -12
  224. investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
  225. investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
  226. investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
  227. investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
  228. investing_algorithm_framework/services/positions/__init__.py +7 -0
  229. investing_algorithm_framework/services/positions/position_service.py +210 -0
  230. investing_algorithm_framework/services/repository_service.py +8 -2
  231. investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
  232. investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +117 -0
  233. investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
  234. investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
  235. investing_algorithm_framework/services/trade_service/__init__.py +9 -0
  236. investing_algorithm_framework/services/trade_service/trade_service.py +1099 -0
  237. investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
  238. investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
  239. investing_algorithm_framework-7.25.6.dist-info/METADATA +535 -0
  240. investing_algorithm_framework-7.25.6.dist-info/RECORD +268 -0
  241. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/WHEEL +1 -2
  242. investing_algorithm_framework-7.25.6.dist-info/entry_points.txt +3 -0
  243. investing_algorithm_framework/app/algorithm.py +0 -630
  244. investing_algorithm_framework/domain/models/backtest_profile.py +0 -414
  245. investing_algorithm_framework/domain/models/market_data/__init__.py +0 -11
  246. investing_algorithm_framework/domain/models/market_data/asset_price.py +0 -50
  247. investing_algorithm_framework/domain/models/market_data/ohlcv.py +0 -105
  248. investing_algorithm_framework/domain/models/market_data/order_book.py +0 -63
  249. investing_algorithm_framework/domain/models/market_data/ticker.py +0 -92
  250. investing_algorithm_framework/domain/models/order/order_fee.py +0 -45
  251. investing_algorithm_framework/domain/models/trade.py +0 -78
  252. investing_algorithm_framework/domain/models/trading_data_types.py +0 -47
  253. investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
  254. investing_algorithm_framework/domain/singleton.py +0 -9
  255. investing_algorithm_framework/domain/utils/backtesting.py +0 -82
  256. investing_algorithm_framework/infrastructure/models/order/order_fee.py +0 -21
  257. investing_algorithm_framework/infrastructure/repositories/order_fee_repository.py +0 -15
  258. investing_algorithm_framework/infrastructure/services/market_backtest_service.py +0 -360
  259. investing_algorithm_framework/infrastructure/services/market_service.py +0 -410
  260. investing_algorithm_framework/infrastructure/services/performance_service.py +0 -192
  261. investing_algorithm_framework/services/backtest_service.py +0 -268
  262. investing_algorithm_framework/services/market_data_service.py +0 -77
  263. investing_algorithm_framework/services/order_backtest_service.py +0 -122
  264. investing_algorithm_framework/services/order_service.py +0 -752
  265. investing_algorithm_framework/services/portfolio_service.py +0 -164
  266. investing_algorithm_framework/services/portfolio_snapshot_service.py +0 -68
  267. investing_algorithm_framework/services/position_cost_service.py +0 -5
  268. investing_algorithm_framework/services/position_service.py +0 -63
  269. investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -225
  270. investing_algorithm_framework-1.5.dist-info/AUTHORS.md +0 -8
  271. investing_algorithm_framework-1.5.dist-info/METADATA +0 -230
  272. investing_algorithm_framework-1.5.dist-info/RECORD +0 -119
  273. investing_algorithm_framework-1.5.dist-info/top_level.txt +0 -1
  274. /investing_algorithm_framework/{infrastructure/services/performance_backtest_service.py → app/reporting/tables/stop_loss_table.py} +0 -0
  275. /investing_algorithm_framework/services/{position_snapshot_service.py → positions/position_snapshot_service.py} +0 -0
  276. {investing_algorithm_framework-1.5.dist-info → investing_algorithm_framework-7.25.6.dist-info}/LICENSE +0 -0
@@ -0,0 +1,210 @@
1
+ """
2
+ High exposure (>1) means you’re deploying capital aggressively, possibly with many simultaneous positions.
3
+
4
+ Exposure around 1 means capital is nearly fully invested most of the time, but not overlapping.
5
+
6
+ Low exposure (<1) means capital is mostly idle or only partially invested.
7
+ """
8
+
9
+ from datetime import datetime, timedelta
10
+ from typing import List
11
+
12
+ from investing_algorithm_framework.domain import Trade
13
+
14
+
15
+ def get_exposure_ratio(
16
+ trades: List["Trade"], start_date: datetime, end_date: datetime
17
+ ) -> float:
18
+ """
19
+ Calculates the exposure ratio (time in market) as the fraction of the total
20
+ backtest duration where at least one position was open.
21
+
22
+ Unlike cumulative exposure, overlapping trades are not double-counted.
23
+ The result is always between 0 and 1.
24
+
25
+ Args:
26
+ trades (List[Trade]): List of trades executed during the backtest.
27
+ start_date (datetime): The start date of the backtest.
28
+ end_date (datetime): The end date of the backtest.
29
+
30
+ Returns:
31
+ A float between 0 and 1 representing the exposure ratio.
32
+ """
33
+ if not trades:
34
+ return 0.0
35
+
36
+ # Collect trade intervals
37
+ intervals = []
38
+ for trade in trades:
39
+ entry = max(trade.opened_at, start_date)
40
+ exit = min(trade.closed_at or end_date, end_date)
41
+ if exit > entry:
42
+ intervals.append((entry, exit))
43
+
44
+ if not intervals:
45
+ return 0.0
46
+
47
+ # Sort intervals by start time
48
+ intervals.sort(key=lambda x: x[0])
49
+
50
+ # Merge overlapping intervals
51
+ merged = []
52
+ current_start, current_end = intervals[0]
53
+ for start, end in intervals[1:]:
54
+ if start <= current_end: # overlap
55
+ current_end = max(current_end, end)
56
+ else:
57
+ merged.append((current_start, current_end))
58
+ current_start, current_end = start, end
59
+ merged.append((current_start, current_end))
60
+
61
+ # Total time with at least one open trade
62
+ total_exposed_time = sum((end - start for start, end in merged), timedelta(0))
63
+
64
+ backtest_duration = end_date - start_date
65
+ if backtest_duration.total_seconds() == 0:
66
+ return 0.0
67
+
68
+ return total_exposed_time.total_seconds() \
69
+ / backtest_duration.total_seconds()
70
+
71
+
72
+ def get_cumulative_exposure(
73
+ trades: List[Trade], start_date: datetime, end_date: datetime
74
+ ) -> float:
75
+ """
76
+ Calculates the exposure time as a fraction of the total backtest duration
77
+ that the strategy had capital deployed (i.e., at least one open position).
78
+
79
+ This value can be greater than 1 if the strategy had overlapping trades.
80
+ For example, if the strategy had two trades open at the same time,
81
+ the exposure factor would be 2.0, indicating that capital was deployed
82
+ for twice the duration of the backtest period.
83
+
84
+ Args:
85
+ trades (List[Trade]): List of trades executed during the backtest.
86
+ start_date (datetime): The start date.
87
+ end_date (datetime): The end date.
88
+
89
+ Returns:
90
+ A float representing the exposure factor, which is the fraction of time
91
+ the strategy had capital deployed during the backtest period.
92
+
93
+ """
94
+ if not trades:
95
+ return 0.0
96
+
97
+ total_trade_duration = timedelta(0)
98
+ for trade in trades:
99
+ entry = trade.opened_at
100
+ exit = trade.closed_at or end_date # open trades counted up to end
101
+
102
+ if exit > entry:
103
+ total_trade_duration += exit - entry
104
+
105
+ backtest_duration = end_date - start_date
106
+
107
+ if backtest_duration.total_seconds() == 0:
108
+ return 0.0
109
+
110
+ return (total_trade_duration.total_seconds()
111
+ / backtest_duration.total_seconds())
112
+
113
+
114
+ def get_average_trade_duration(trades: List[Trade]):
115
+ """
116
+ Calculates the average duration of trades in the backtest report.
117
+
118
+ Args:
119
+ trades (List[Trade]): List of trades executed during the backtest.
120
+
121
+ Returns:
122
+ A float representing the average trade duration in hours.
123
+ """
124
+ if not trades:
125
+ return 0.0
126
+
127
+ total_duration = 0
128
+
129
+ for trade in trades:
130
+ trade_duration = trade.duration
131
+
132
+ if trade_duration is not None:
133
+ total_duration += trade_duration
134
+
135
+ average_trade_duration = total_duration / len(trades)
136
+ return average_trade_duration
137
+
138
+
139
+ def get_trade_frequency(
140
+ trades: List[Trade], start_date: datetime, end_date: datetime
141
+ ) -> float:
142
+ """
143
+ Calculates the trade frequency as the number of trades per day
144
+ during the backtest period.
145
+
146
+ Args:
147
+ trades (List[Trade]): List of trades executed during the backtest.
148
+ start_date (datetime): The start date of the backtest.
149
+ end_date (datetime): The end date of the backtest.
150
+
151
+ Returns:
152
+ A float representing the average number of trades per day.
153
+ """
154
+
155
+ if not trades:
156
+ return 0.0
157
+
158
+ total_days = (end_date - start_date).days + 1
159
+ if total_days <= 0:
160
+ return 0.0
161
+
162
+ return len(trades) / total_days
163
+
164
+
165
+ def get_trades_per_day(
166
+ trades: List[Trade], start_date: datetime, end_date: datetime
167
+ ) -> float:
168
+ """
169
+ Calculates the average number of trades per day during the backtest period.
170
+
171
+ Args:
172
+ trades (List[Trade]): List of trades executed during the backtest.
173
+ start_date (datetime): The start date of the backtest.
174
+ end_date (datetime): The end date of the backtest.
175
+
176
+ Returns:
177
+ A float representing the average number of trades per day.
178
+ """
179
+ if not trades:
180
+ return 0.0
181
+
182
+ total_days = (end_date - start_date).days + 1
183
+ if total_days <= 0:
184
+ return 0.0
185
+
186
+ return len(trades) / total_days
187
+
188
+
189
+ def get_trades_per_year(
190
+ trades: List[Trade], start_date: datetime, end_date: datetime
191
+ ) -> float:
192
+ """
193
+ Calculates the average number of trades per year during the backtest period.
194
+
195
+ Args:
196
+ trades (List[Trade]): List of trades executed during the backtest.
197
+ start_date (datetime): The start date of the backtest.
198
+ end_date (datetime): The end date of the backtest.
199
+
200
+ Returns:
201
+ A float representing the average number of trades per year.
202
+ """
203
+ if not trades:
204
+ return 0.0
205
+
206
+ total_years = (end_date - start_date).days / 365.25
207
+ if total_years <= 0:
208
+ return 0.0
209
+
210
+ return len(trades) / total_years
@@ -0,0 +1,358 @@
1
+ from typing import List
2
+ from logging import getLogger
3
+
4
+ from investing_algorithm_framework.domain import BacktestMetrics, \
5
+ BacktestRun, OperationalException, Backtest, BacktestDateRange
6
+ from .cagr import get_cagr
7
+ from .calmar_ratio import get_calmar_ratio
8
+ from .drawdown import get_drawdown_series, get_max_drawdown, \
9
+ get_max_daily_drawdown, get_max_drawdown_absolute, \
10
+ get_max_drawdown_duration
11
+ from .equity_curve import get_equity_curve
12
+ from .exposure import get_exposure_ratio, get_cumulative_exposure, \
13
+ get_trades_per_year, get_trades_per_day
14
+ from .profit_factor import get_profit_factor, get_gross_loss, get_gross_profit
15
+ from .returns import get_monthly_returns, get_yearly_returns, \
16
+ get_worst_year, get_best_year, get_best_month, get_worst_month, \
17
+ get_percentage_winning_months, get_percentage_winning_years, \
18
+ get_average_monthly_return, get_average_monthly_return_winning_months, \
19
+ get_average_monthly_return_losing_months, get_cumulative_return, \
20
+ get_cumulative_return_series
21
+ from .returns import get_total_return, get_final_value, get_total_loss, \
22
+ get_total_growth
23
+ from .sharpe_ratio import get_sharpe_ratio, get_rolling_sharpe_ratio
24
+ from .sortino_ratio import get_sortino_ratio
25
+ from .volatility import get_annual_volatility
26
+ from .win_rate import get_win_rate, get_win_loss_ratio, get_current_win_rate, \
27
+ get_current_win_loss_ratio
28
+ from .trades import get_average_trade_duration, get_average_trade_size, \
29
+ get_number_of_trades, get_positive_trades, get_number_of_closed_trades, \
30
+ get_negative_trades, get_average_trade_return, get_number_of_open_trades, \
31
+ get_worst_trade, get_best_trade, get_average_trade_gain, \
32
+ get_average_trade_loss, get_median_trade_return, \
33
+ get_current_average_trade_gain, get_current_average_trade_return, \
34
+ get_current_average_trade_duration, get_current_average_trade_loss
35
+
36
+ logger = getLogger("investing_algorithm_framework")
37
+
38
+ def create_backtest_metrics_for_backtest(
39
+ backtest: Backtest,
40
+ risk_free_rate: float, metrics: List[str] = None,
41
+ backtest_date_range: BacktestDateRange = None
42
+ ) -> Backtest:
43
+
44
+ """
45
+ Create BacktestMetrics for a Backtest object.
46
+
47
+ Args:
48
+ backtest (Backtest): The Backtest object containing
49
+ backtest runs.
50
+ risk_free_rate (float): The risk-free rate used in certain
51
+ metric calculations.
52
+ metrics (List[str], optional): List of metric names to compute.
53
+ If None, a default set of metrics will be computed.
54
+ backtest_date_range (BacktestDateRange, optional): The date range
55
+ for the backtest. If None, all backtest metrics will be computed
56
+ for each backtest run.
57
+
58
+ Returns:
59
+ Backtest: The Backtest object with computed metrics for each run.
60
+ """
61
+ if backtest_date_range is not None:
62
+ backtest_runs = [
63
+ backtest.get_backtest_run(backtest_date_range)
64
+ ]
65
+ else:
66
+ backtest_runs = backtest.get_all_backtest_runs()
67
+
68
+ for backtest_run in backtest_runs:
69
+ # If a date range is provided, check if the backtest run falls
70
+ # within the range
71
+ backtest_metrics = create_backtest_metrics(
72
+ backtest_run, risk_free_rate, metrics
73
+ )
74
+ backtest_run.backtest_metrics = backtest_metrics
75
+
76
+ backtest.backtest_runs = backtest_runs
77
+ return backtest
78
+
79
+
80
+ def create_backtest_metrics(
81
+ backtest_run: BacktestRun, risk_free_rate: float, metrics: List[str] = None
82
+ ) -> BacktestMetrics:
83
+ """
84
+ Create a BacktestMetrics instance and optionally save it to a file.
85
+
86
+ Args:
87
+ backtest_run (BacktestRun): The BacktestRun object containing
88
+ portfolio snapshots and trades.
89
+ risk_free_rate (float): The risk-free rate used in certain
90
+ metric calculations.
91
+ metrics (List[str], optional): List of metric names to compute.
92
+ If None, a default set of metrics will be computed.
93
+
94
+ Returns:
95
+ BacktestMetrics: The computed backtest metrics.
96
+ """
97
+
98
+ if metrics is None:
99
+ metrics = [
100
+ "backtest_start_date",
101
+ "backtest_end_date",
102
+ "equity_curve",
103
+ "final_value",
104
+ "total_growth",
105
+ "total_growth_percentage",
106
+ "total_net_gain",
107
+ "total_net_gain_percentage",
108
+ "total_loss",
109
+ "total_loss_percentage",
110
+ "cumulative_return",
111
+ "cumulative_return_series",
112
+ "cagr",
113
+ "sharpe_ratio",
114
+ "rolling_sharpe_ratio",
115
+ "sortino_ratio",
116
+ "calmar_ratio",
117
+ "profit_factor",
118
+ "annual_volatility",
119
+ "monthly_returns",
120
+ "yearly_returns",
121
+ "drawdown_series",
122
+ "max_drawdown",
123
+ "max_drawdown_absolute",
124
+ "max_daily_drawdown",
125
+ "max_drawdown_duration",
126
+ "trades_per_year",
127
+ "trade_per_day",
128
+ "exposure_ratio",
129
+ "cumulative_exposure",
130
+ "best_trade",
131
+ "worst_trade",
132
+ "number_of_positive_trades",
133
+ "percentage_positive_trades",
134
+ "number_of_negative_trades",
135
+ "percentage_negative_trades",
136
+ "average_trade_duration",
137
+ "average_trade_size",
138
+ "average_trade_loss",
139
+ "average_trade_loss_percentage",
140
+ "average_trade_gain",
141
+ "average_trade_gain_percentage",
142
+ "average_trade_return",
143
+ "average_trade_return_percentage",
144
+ "median_trade_return",
145
+ "number_of_trades",
146
+ "number_of_trades_closed",
147
+ "number_of_trades_opened",
148
+ "number_of_trades_open_at_end",
149
+ "win_rate",
150
+ "current_win_rate",
151
+ "win_loss_ratio",
152
+ "current_win_loss_ratio",
153
+ "percentage_winning_months",
154
+ "percentage_winning_years",
155
+ "average_monthly_return",
156
+ "average_monthly_return_losing_months",
157
+ "average_monthly_return_winning_months",
158
+ "best_month",
159
+ "best_year",
160
+ "worst_month",
161
+ "worst_year",
162
+ "total_number_of_days",
163
+ "current_average_trade_gain",
164
+ "current_average_trade_return",
165
+ "current_average_trade_duration",
166
+ "current_average_trade_loss",
167
+ ]
168
+
169
+ backtest_metrics = BacktestMetrics(
170
+ backtest_start_date=backtest_run.backtest_start_date,
171
+ backtest_end_date=backtest_run.backtest_end_date,
172
+ )
173
+
174
+ def safe_set(metric_name, func, *args, index=None):
175
+ if metric_name in metrics:
176
+ try:
177
+ value = func(*args)
178
+ if index is not None and isinstance(value, (list, tuple)):
179
+ setattr(backtest_metrics, metric_name, value[index])
180
+ else:
181
+ setattr(backtest_metrics, metric_name, value)
182
+ except OperationalException as e:
183
+ logger.warning(f"{metric_name} failed: {e}")
184
+
185
+ # Grouped metrics needing special handling
186
+ if "total_net_gain" in metrics or "total_net_gain_percentage" in metrics:
187
+ try:
188
+ total_return = get_total_return(backtest_run.portfolio_snapshots)
189
+ if "total_net_gain" in metrics:
190
+ backtest_metrics.total_net_gain = total_return[0]
191
+ if "total_net_gain_percentage" in metrics:
192
+ backtest_metrics.total_net_gain_percentage = total_return[1]
193
+ except OperationalException as e:
194
+ logger.warning(f"total_return failed: {e}")
195
+
196
+ if "total_growth" in metrics or "total_growth_percentage" in metrics:
197
+ try:
198
+ total_growth = get_total_growth(backtest_run.portfolio_snapshots)
199
+ if "total_growth" in metrics:
200
+ backtest_metrics.total_growth = total_growth[0]
201
+ if "total_growth_percentage" in metrics:
202
+ backtest_metrics.total_growth_percentage = total_growth[1]
203
+ except OperationalException as e:
204
+ logger.warning(f"total_growth failed: {e}")
205
+
206
+ if "total_loss" in metrics or "total_loss_percentage" in metrics:
207
+ try:
208
+ total_loss = get_total_loss(backtest_run.portfolio_snapshots)
209
+ if "total_loss" in metrics:
210
+ backtest_metrics.total_loss = total_loss[0]
211
+ if "total_loss_percentage" in metrics:
212
+ backtest_metrics.total_loss_percentage = total_loss[1]
213
+ except OperationalException as e:
214
+ logger.warning(f"total_loss failed: {e}")
215
+
216
+ if ("average_trade_return" in metrics
217
+ or "average_trade_return_percentage" in metrics):
218
+ try:
219
+ avg_return = get_average_trade_return(backtest_run.trades)
220
+ if "average_trade_return" in metrics:
221
+ backtest_metrics.average_trade_return = avg_return[0]
222
+ if "average_trade_return_percentage" in metrics:
223
+ backtest_metrics.average_trade_return_percentage = \
224
+ avg_return[1]
225
+ except OperationalException as e:
226
+ logger.warning(f"average_trade_return failed: {e}")
227
+
228
+ if ("average_trade_gain" in metrics
229
+ or "average_trade_gain_percentage" in metrics):
230
+ try:
231
+ avg_gain = get_average_trade_gain(backtest_run.trades)
232
+ if "average_trade_gain" in metrics:
233
+ backtest_metrics.average_trade_gain = avg_gain[0]
234
+ if "average_trade_gain_percentage" in metrics:
235
+ backtest_metrics.average_trade_gain_percentage = avg_gain[1]
236
+ except OperationalException as e:
237
+ logger.warning(f"average_trade_gain failed: {e}")
238
+
239
+ if ("average_trade_loss" in metrics
240
+ or "average_trade_loss_percentage" in metrics):
241
+ try:
242
+ avg_loss = get_average_trade_loss(backtest_run.trades)
243
+ if "average_trade_loss" in metrics:
244
+ backtest_metrics.average_trade_loss = avg_loss[0]
245
+ if "average_trade_loss_percentage" in metrics:
246
+ backtest_metrics.average_trade_loss_percentage = avg_loss[1]
247
+ except OperationalException as e:
248
+ logger.warning(f"average_trade_loss failed: {e}")
249
+
250
+ if ("current_average_trade_gain" in metrics
251
+ or "get_current_average_trade_gain_percentage" in metrics):
252
+ try:
253
+ current_avg_gain = get_current_average_trade_gain(
254
+ backtest_run.trades
255
+ )
256
+
257
+ if "current_average_trade_gain" in metrics:
258
+ backtest_metrics.current_average_trade_gain = \
259
+ current_avg_gain[0]
260
+
261
+ if "current_average_trade_gain_percentage" in metrics:
262
+ backtest_metrics.current_average_trade_gain_percentage = \
263
+ current_avg_gain[1]
264
+ except OperationalException as e:
265
+ logger.warning(f"current_average_trade_gain failed: {e}")
266
+
267
+ if ("current_average_trade_return" in metrics
268
+ or "current_average_trade_return_percentage" in metrics):
269
+ try:
270
+ current_avg_return = get_current_average_trade_return(
271
+ backtest_run.trades
272
+ )
273
+
274
+ if "current_average_trade_return" in metrics:
275
+ backtest_metrics.current_average_trade_return = \
276
+ current_avg_return[0]
277
+ if "current_average_trade_return_percentage" in metrics:
278
+ backtest_metrics.current_average_trade_return_percentage =\
279
+ current_avg_return[1]
280
+ except OperationalException as e:
281
+ logger.warning(f"current_average_trade_return failed: {e}")
282
+
283
+ if "current_average_trade_duration" in metrics:
284
+ try:
285
+ current_avg_duration = get_current_average_trade_duration(
286
+ backtest_run.trades, backtest_run
287
+ )
288
+ backtest_metrics.current_average_trade_duration = \
289
+ current_avg_duration
290
+ except OperationalException as e:
291
+ logger.warning(f"current_average_trade_duration failed: {e}")
292
+
293
+ if ("current_average_trade_loss" in metrics
294
+ or "current_average_trade_loss_percentage" in metrics):
295
+ try:
296
+ current_avg_loss = get_current_average_trade_loss(
297
+ backtest_run.trades
298
+ )
299
+ if "current_average_trade_loss" in metrics:
300
+ backtest_metrics.current_average_trade_loss = \
301
+ current_avg_loss[0]
302
+ if "current_average_trade_loss_percentage" in metrics:
303
+ backtest_metrics.current_average_trade_loss_percentage = \
304
+ current_avg_loss[1]
305
+ except OperationalException as e:
306
+ logger.warning(f"current_average_trade_loss failed: {e}")
307
+
308
+ safe_set("number_of_positive_trades", get_positive_trades, backtest_run.trades)
309
+ safe_set("percentage_positive_trades", get_positive_trades, backtest_run.trades, index=1)
310
+ safe_set("number_of_negative_trades", get_negative_trades, backtest_run.trades)
311
+ safe_set("percentage_negative_trades", get_negative_trades, backtest_run.trades, index=1)
312
+ safe_set("median_trade_return", get_median_trade_return, backtest_run.trades, index=0)
313
+ safe_set("median_trade_return_percentage", get_median_trade_return, backtest_run.trades, index=1)
314
+ safe_set("number_of_trades", get_number_of_trades, backtest_run.trades)
315
+ safe_set("number_of_trades_closed", get_number_of_closed_trades, backtest_run.trades)
316
+ safe_set("number_of_trades_opened", get_number_of_open_trades, backtest_run.trades)
317
+ safe_set("average_trade_duration", get_average_trade_duration, backtest_run.trades)
318
+ safe_set("average_trade_size", get_average_trade_size, backtest_run.trades)
319
+ safe_set("equity_curve", get_equity_curve, backtest_run.portfolio_snapshots)
320
+ safe_set("final_value", get_final_value, backtest_run.portfolio_snapshots)
321
+ safe_set("cagr", get_cagr, backtest_run.portfolio_snapshots)
322
+ safe_set("sharpe_ratio", get_sharpe_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
323
+ safe_set("rolling_sharpe_ratio", get_rolling_sharpe_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
324
+ safe_set("sortino_ratio", get_sortino_ratio, backtest_run.portfolio_snapshots, risk_free_rate)
325
+ safe_set("profit_factor", get_profit_factor, backtest_run.trades)
326
+ safe_set("calmar_ratio", get_calmar_ratio, backtest_run.portfolio_snapshots)
327
+ safe_set("annual_volatility", get_annual_volatility, backtest_run.portfolio_snapshots)
328
+ safe_set("monthly_returns", get_monthly_returns, backtest_run.portfolio_snapshots)
329
+ safe_set("yearly_returns", get_yearly_returns, backtest_run.portfolio_snapshots)
330
+ safe_set("drawdown_series", get_drawdown_series, backtest_run.portfolio_snapshots)
331
+ safe_set("max_drawdown", get_max_drawdown, backtest_run.portfolio_snapshots)
332
+ safe_set("max_drawdown_absolute", get_max_drawdown_absolute, backtest_run.portfolio_snapshots)
333
+ safe_set("max_daily_drawdown", get_max_daily_drawdown, backtest_run.portfolio_snapshots)
334
+ safe_set("max_drawdown_duration", get_max_drawdown_duration, backtest_run.portfolio_snapshots)
335
+ safe_set("trades_per_year", get_trades_per_year, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
336
+ safe_set("trades_per_day", get_trades_per_day, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
337
+ safe_set("exposure_ratio", get_exposure_ratio, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
338
+ safe_set("cumulative_exposure", get_cumulative_exposure, backtest_run.trades, backtest_run.backtest_start_date, backtest_run.backtest_end_date)
339
+ safe_set("best_trade", get_best_trade, backtest_run.trades)
340
+ safe_set("worst_trade", get_worst_trade, backtest_run.trades)
341
+ safe_set("win_rate", get_win_rate, backtest_run.trades)
342
+ safe_set("current_win_rate", get_current_win_rate, backtest_run.trades)
343
+ safe_set("win_loss_ratio", get_win_loss_ratio, backtest_run.trades)
344
+ safe_set("current_win_loss_ratio", get_current_win_loss_ratio, backtest_run.trades)
345
+ safe_set("percentage_winning_months", get_percentage_winning_months, backtest_run.portfolio_snapshots)
346
+ safe_set("percentage_winning_years", get_percentage_winning_years, backtest_run.portfolio_snapshots)
347
+ safe_set("average_monthly_return", get_average_monthly_return, backtest_run.portfolio_snapshots)
348
+ safe_set("average_monthly_return_winning_months", get_average_monthly_return_winning_months, backtest_run.portfolio_snapshots)
349
+ safe_set("average_monthly_return_losing_months", get_average_monthly_return_losing_months, backtest_run.portfolio_snapshots)
350
+ safe_set("best_month", get_best_month, backtest_run.portfolio_snapshots)
351
+ safe_set("best_year", get_best_year, backtest_run.portfolio_snapshots)
352
+ safe_set("worst_month", get_worst_month, backtest_run.portfolio_snapshots)
353
+ safe_set("worst_year", get_worst_year, backtest_run.portfolio_snapshots)
354
+ safe_set("gross_loss", get_gross_loss, backtest_run.trades)
355
+ safe_set("gross_profit", get_gross_profit, backtest_run.trades)
356
+ safe_set("cumulative_return_series", get_cumulative_return_series, backtest_run.portfolio_snapshots)
357
+ safe_set("cumulative_return", get_cumulative_return, backtest_run.portfolio_snapshots)
358
+ return backtest_metrics
@@ -0,0 +1,84 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ from .cagr import get_cagr
5
+
6
+
7
+ def get_mean_daily_return(snapshots):
8
+ """
9
+ Calculate the mean daily return from the total value of the snapshots.
10
+
11
+ This function computes the mean daily return based on the list of
12
+ snapshots in the report. If the snapshots have a granularity of less
13
+ than a day, the function will resample to daily frequency and compute
14
+ average daily returns.
15
+
16
+ If there is less data then for a year, it will use cagr to
17
+ calculate the mean daily return.
18
+
19
+ Args:
20
+ snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
21
+
22
+ Returns:
23
+ float: The mean daily return.
24
+ """
25
+
26
+ if len(snapshots) < 2:
27
+ return 0.0 # Not enough data
28
+
29
+ # Create DataFrame from snapshots
30
+ data = [(s.created_at, s.total_value) for s in snapshots]
31
+ df = pd.DataFrame(data, columns=["created_at", "total_value"])
32
+ df['created_at'] = pd.to_datetime(df['created_at'])
33
+ df = df.sort_values('created_at').drop_duplicates('created_at')\
34
+ .set_index('created_at')
35
+
36
+ start_date = df.iloc[0].name
37
+ end_date = df.iloc[-1].name
38
+
39
+ # Check if the period is less than a year
40
+ if (end_date - start_date).days < 365:
41
+ # Use CAGR to calculate mean daily return
42
+ cagr = get_cagr(snapshots)
43
+ if cagr == 0.0:
44
+ return 0.0
45
+
46
+ return (1 + cagr) ** (1 / 365) - 1
47
+
48
+ # Resample to daily frequency using last value of the day
49
+ daily_df = df.resample('1D').last().dropna()
50
+
51
+ # Calculate daily returns
52
+ daily_df['return'] = daily_df['total_value'].pct_change()
53
+ daily_df = daily_df.dropna()
54
+
55
+ if daily_df.empty:
56
+ return 0.0
57
+
58
+ mean_return = daily_df['return'].mean()
59
+
60
+ if np.isnan(mean_return):
61
+ return 0.0
62
+
63
+ return mean_return
64
+
65
+
66
+ def get_mean_yearly_return(report, periods_per_year=365):
67
+ """
68
+ Calculate the mean yearly return from a backtest report by
69
+ annualizing the mean daily return.
70
+
71
+ Args:
72
+ report (BacktestReport): The report containing the snapshots.
73
+ periods_per_year (int): Number of periods in a year (e.g., 365 for daily data).
74
+
75
+ Returns:
76
+ float: The mean yearly return (annualized).
77
+ """
78
+ mean_daily_return = get_mean_daily_return(report)
79
+
80
+ if mean_daily_return == 0.0:
81
+ return 0.0
82
+
83
+ return (1 + mean_daily_return) ** periods_per_year - 1
84
+