investing-algorithm-framework 7.19.14__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.
- investing_algorithm_framework/__init__.py +197 -0
- investing_algorithm_framework/app/__init__.py +47 -0
- investing_algorithm_framework/app/algorithm/__init__.py +7 -0
- investing_algorithm_framework/app/algorithm/algorithm.py +239 -0
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +114 -0
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +2204 -0
- investing_algorithm_framework/app/app_hook.py +28 -0
- investing_algorithm_framework/app/context.py +1667 -0
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +27 -0
- investing_algorithm_framework/app/reporting/ascii.py +921 -0
- investing_algorithm_framework/app/reporting/backtest_report.py +349 -0
- investing_algorithm_framework/app/reporting/charts/__init__.py +19 -0
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +74 -0
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/monthly_returns_heatmap.py +70 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +79 -0
- investing_algorithm_framework/app/reporting/charts/yearly_returns_barchart.py +55 -0
- investing_algorithm_framework/app/reporting/generate.py +185 -0
- investing_algorithm_framework/app/reporting/tables/__init__.py +11 -0
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +217 -0
- investing_algorithm_framework/app/reporting/tables/stop_loss_table.py +0 -0
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +80 -0
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +147 -0
- investing_algorithm_framework/app/reporting/tables/trades_table.py +75 -0
- investing_algorithm_framework/app/reporting/tables/utils.py +29 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +154 -0
- investing_algorithm_framework/app/stateless/__init__.py +35 -0
- investing_algorithm_framework/app/stateless/action_handlers/__init__.py +84 -0
- investing_algorithm_framework/app/stateless/action_handlers/action_handler_strategy.py +8 -0
- investing_algorithm_framework/app/stateless/action_handlers/check_online_handler.py +15 -0
- investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py +40 -0
- investing_algorithm_framework/app/stateless/exception_handler.py +40 -0
- investing_algorithm_framework/app/strategy.py +675 -0
- investing_algorithm_framework/app/task.py +41 -0
- investing_algorithm_framework/app/web/__init__.py +5 -0
- investing_algorithm_framework/app/web/controllers/__init__.py +13 -0
- investing_algorithm_framework/app/web/controllers/orders.py +20 -0
- investing_algorithm_framework/app/web/controllers/portfolio.py +20 -0
- investing_algorithm_framework/app/web/controllers/positions.py +18 -0
- investing_algorithm_framework/app/web/create_app.py +20 -0
- investing_algorithm_framework/app/web/error_handler.py +59 -0
- investing_algorithm_framework/app/web/responses.py +20 -0
- investing_algorithm_framework/app/web/run_strategies.py +4 -0
- investing_algorithm_framework/app/web/schemas/__init__.py +12 -0
- investing_algorithm_framework/app/web/schemas/order.py +12 -0
- investing_algorithm_framework/app/web/schemas/portfolio.py +22 -0
- investing_algorithm_framework/app/web/schemas/position.py +15 -0
- investing_algorithm_framework/app/web/setup_cors.py +6 -0
- investing_algorithm_framework/cli/__init__.py +0 -0
- investing_algorithm_framework/cli/cli.py +207 -0
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +499 -0
- investing_algorithm_framework/cli/deploy_to_azure_function.py +718 -0
- investing_algorithm_framework/cli/initialize_app.py +603 -0
- investing_algorithm_framework/cli/templates/.gitignore.template +178 -0
- investing_algorithm_framework/cli/templates/app.py.template +18 -0
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +48 -0
- investing_algorithm_framework/cli/templates/app_azure_function.py.template +14 -0
- investing_algorithm_framework/cli/templates/app_web.py.template +18 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_readme.md.template +110 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +65 -0
- investing_algorithm_framework/cli/templates/azure_function_host.json.template +15 -0
- investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template +8 -0
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +3 -0
- investing_algorithm_framework/cli/templates/data_providers.py.template +17 -0
- investing_algorithm_framework/cli/templates/env.example.template +2 -0
- investing_algorithm_framework/cli/templates/env_azure_function.example.template +4 -0
- investing_algorithm_framework/cli/templates/market_data_providers.py.template +9 -0
- investing_algorithm_framework/cli/templates/readme.md.template +135 -0
- investing_algorithm_framework/cli/templates/requirements.txt.template +2 -0
- investing_algorithm_framework/cli/templates/run_backtest.py.template +20 -0
- investing_algorithm_framework/cli/templates/strategy.py.template +124 -0
- investing_algorithm_framework/create_app.py +54 -0
- investing_algorithm_framework/dependency_container.py +155 -0
- investing_algorithm_framework/domain/__init__.py +148 -0
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +435 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +111 -0
- investing_algorithm_framework/domain/constants.py +83 -0
- investing_algorithm_framework/domain/data_provider.py +334 -0
- investing_algorithm_framework/domain/data_structures.py +42 -0
- investing_algorithm_framework/domain/decimal_parsing.py +40 -0
- investing_algorithm_framework/domain/exceptions.py +112 -0
- investing_algorithm_framework/domain/models/__init__.py +43 -0
- investing_algorithm_framework/domain/models/app_mode.py +34 -0
- investing_algorithm_framework/domain/models/base_model.py +25 -0
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/data/data_type.py +46 -0
- investing_algorithm_framework/domain/models/event.py +35 -0
- investing_algorithm_framework/domain/models/market/__init__.py +5 -0
- investing_algorithm_framework/domain/models/market/market_credential.py +88 -0
- investing_algorithm_framework/domain/models/order/__init__.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +384 -0
- investing_algorithm_framework/domain/models/order/order_side.py +36 -0
- investing_algorithm_framework/domain/models/order/order_status.py +37 -0
- investing_algorithm_framework/domain/models/order/order_type.py +30 -0
- investing_algorithm_framework/domain/models/portfolio/__init__.py +9 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +169 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +93 -0
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +208 -0
- investing_algorithm_framework/domain/models/position/__init__.py +4 -0
- investing_algorithm_framework/domain/models/position/position.py +68 -0
- investing_algorithm_framework/domain/models/position/position_snapshot.py +47 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +45 -0
- investing_algorithm_framework/domain/models/strategy_profile.py +33 -0
- investing_algorithm_framework/domain/models/time_frame.py +153 -0
- investing_algorithm_framework/domain/models/time_interval.py +124 -0
- investing_algorithm_framework/domain/models/time_unit.py +149 -0
- investing_algorithm_framework/domain/models/tracing/__init__.py +0 -0
- investing_algorithm_framework/domain/models/tracing/trace.py +23 -0
- investing_algorithm_framework/domain/models/trade/__init__.py +13 -0
- investing_algorithm_framework/domain/models/trade/trade.py +388 -0
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +34 -0
- investing_algorithm_framework/domain/models/trade/trade_status.py +40 -0
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +267 -0
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +303 -0
- investing_algorithm_framework/domain/order_executor.py +112 -0
- investing_algorithm_framework/domain/portfolio_provider.py +118 -0
- investing_algorithm_framework/domain/positions/__init__.py +4 -0
- investing_algorithm_framework/domain/positions/position_size.py +41 -0
- investing_algorithm_framework/domain/services/__init__.py +11 -0
- investing_algorithm_framework/domain/services/market_credential_service.py +37 -0
- investing_algorithm_framework/domain/services/portfolios/__init__.py +5 -0
- investing_algorithm_framework/domain/services/portfolios/portfolio_sync_service.py +9 -0
- investing_algorithm_framework/domain/services/rounding_service.py +27 -0
- investing_algorithm_framework/domain/services/state_handler.py +38 -0
- investing_algorithm_framework/domain/stateless_actions.py +7 -0
- investing_algorithm_framework/domain/strategy.py +44 -0
- investing_algorithm_framework/domain/utils/__init__.py +27 -0
- investing_algorithm_framework/domain/utils/csv.py +104 -0
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/dates.py +57 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +53 -0
- investing_algorithm_framework/domain/utils/random.py +41 -0
- investing_algorithm_framework/domain/utils/signatures.py +17 -0
- investing_algorithm_framework/domain/utils/stoppable_thread.py +26 -0
- investing_algorithm_framework/domain/utils/synchronized.py +12 -0
- investing_algorithm_framework/download_data.py +108 -0
- investing_algorithm_framework/infrastructure/__init__.py +50 -0
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +36 -0
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +1143 -0
- investing_algorithm_framework/infrastructure/data_providers/csv.py +568 -0
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +10 -0
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +120 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +16 -0
- investing_algorithm_framework/infrastructure/models/decimal_parser.py +14 -0
- investing_algorithm_framework/infrastructure/models/model_extension.py +6 -0
- investing_algorithm_framework/infrastructure/models/order/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/order/order.py +124 -0
- investing_algorithm_framework/infrastructure/models/order/order_metadata.py +44 -0
- investing_algorithm_framework/infrastructure/models/order_trade_association.py +10 -0
- investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/portfolio/portfolio_snapshot.py +37 -0
- investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py +114 -0
- investing_algorithm_framework/infrastructure/models/position/__init__.py +4 -0
- investing_algorithm_framework/infrastructure/models/position/position.py +63 -0
- investing_algorithm_framework/infrastructure/models/position/position_snapshot.py +23 -0
- investing_algorithm_framework/infrastructure/models/trades/__init__.py +9 -0
- investing_algorithm_framework/infrastructure/models/trades/trade.py +130 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +40 -0
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +41 -0
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py +200 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/__init__.py +19 -0
- investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py +199 -0
- investing_algorithm_framework/infrastructure/repositories/__init__.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py +17 -0
- investing_algorithm_framework/infrastructure/repositories/order_repository.py +96 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_repository.py +30 -0
- investing_algorithm_framework/infrastructure/repositories/portfolio_snapshot_repository.py +56 -0
- investing_algorithm_framework/infrastructure/repositories/position_repository.py +66 -0
- investing_algorithm_framework/infrastructure/repositories/position_snapshot_repository.py +21 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +299 -0
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +71 -0
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +23 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +23 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +7 -0
- investing_algorithm_framework/infrastructure/services/aws/__init__.py +6 -0
- investing_algorithm_framework/infrastructure/services/aws/state_handler.py +113 -0
- investing_algorithm_framework/infrastructure/services/azure/__init__.py +5 -0
- investing_algorithm_framework/infrastructure/services/azure/state_handler.py +158 -0
- investing_algorithm_framework/services/__init__.py +132 -0
- investing_algorithm_framework/services/backtesting/__init__.py +5 -0
- investing_algorithm_framework/services/backtesting/backtest_service.py +651 -0
- investing_algorithm_framework/services/configuration_service.py +96 -0
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/services/market_credential_service.py +40 -0
- investing_algorithm_framework/services/metrics/__init__.py +114 -0
- investing_algorithm_framework/services/metrics/alpha.py +0 -0
- investing_algorithm_framework/services/metrics/beta.py +0 -0
- investing_algorithm_framework/services/metrics/cagr.py +60 -0
- investing_algorithm_framework/services/metrics/calmar_ratio.py +40 -0
- investing_algorithm_framework/services/metrics/drawdown.py +181 -0
- investing_algorithm_framework/services/metrics/equity_curve.py +24 -0
- investing_algorithm_framework/services/metrics/exposure.py +210 -0
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/services/metrics/mean_daily_return.py +83 -0
- investing_algorithm_framework/services/metrics/price_efficiency.py +57 -0
- investing_algorithm_framework/services/metrics/profit_factor.py +165 -0
- investing_algorithm_framework/services/metrics/recovery.py +113 -0
- investing_algorithm_framework/services/metrics/returns.py +452 -0
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/services/metrics/sharpe_ratio.py +137 -0
- investing_algorithm_framework/services/metrics/sortino_ratio.py +74 -0
- investing_algorithm_framework/services/metrics/standard_deviation.py +157 -0
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/treynor_ratio.py +0 -0
- investing_algorithm_framework/services/metrics/ulcer.py +0 -0
- investing_algorithm_framework/services/metrics/value_at_risk.py +0 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/services/metrics/win_rate.py +177 -0
- investing_algorithm_framework/services/order_service/__init__.py +9 -0
- investing_algorithm_framework/services/order_service/order_backtest_service.py +178 -0
- investing_algorithm_framework/services/order_service/order_executor_lookup.py +110 -0
- investing_algorithm_framework/services/order_service/order_service.py +826 -0
- investing_algorithm_framework/services/portfolios/__init__.py +16 -0
- investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +54 -0
- investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +75 -0
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +106 -0
- investing_algorithm_framework/services/portfolios/portfolio_service.py +188 -0
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +136 -0
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +182 -0
- investing_algorithm_framework/services/positions/__init__.py +7 -0
- investing_algorithm_framework/services/positions/position_service.py +210 -0
- investing_algorithm_framework/services/positions/position_snapshot_service.py +18 -0
- investing_algorithm_framework/services/repository_service.py +40 -0
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +132 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +66 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +41 -0
- investing_algorithm_framework/services/trade_service/__init__.py +3 -0
- investing_algorithm_framework/services/trade_service/trade_service.py +1083 -0
- investing_algorithm_framework-7.19.14.dist-info/LICENSE +201 -0
- investing_algorithm_framework-7.19.14.dist-info/METADATA +459 -0
- investing_algorithm_framework-7.19.14.dist-info/RECORD +260 -0
- investing_algorithm_framework-7.19.14.dist-info/WHEEL +4 -0
- investing_algorithm_framework-7.19.14.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
🔍 What is Recovery Factor:
|
|
3
|
+
Recovery Factor =
|
|
4
|
+
🔹 Total Net Profit (how much you made overall)
|
|
5
|
+
➗
|
|
6
|
+
🔹 Max Drawdown (worst capital drop at any point)
|
|
7
|
+
|
|
8
|
+
This metric answers:
|
|
9
|
+
|
|
10
|
+
💡 "How much return did I get per unit of worst-case risk?"
|
|
11
|
+
|
|
12
|
+
🎯 Why this matters:
|
|
13
|
+
Even if a strategy has many small drawdowns, only the worst one is used.
|
|
14
|
+
|
|
15
|
+
A strategy that earns €100k total, but once dropped €50k from peak, has a
|
|
16
|
+
recovery factor of 2.0 — suggesting that it made 2x the worst pain it endured.
|
|
17
|
+
|
|
18
|
+
✅ Pros:
|
|
19
|
+
Simple and interpretable.
|
|
20
|
+
|
|
21
|
+
Useful when comparing strategies with very different drawdown profiles.
|
|
22
|
+
|
|
23
|
+
⚠️ Limitations:
|
|
24
|
+
It ignores the frequency or duration of drawdowns.
|
|
25
|
+
|
|
26
|
+
It can be misleading if net profit is inflated due to one lucky trend or
|
|
27
|
+
max drawdown happened early and was never tested again.
|
|
28
|
+
|
|
29
|
+
| **Recovery Factor** | **Interpretation** | **Comments** |
|
|
30
|
+
| ------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
|
|
31
|
+
| **< 1.0** | ❌ *Poor* – Risk outweighs reward | Losing more in drawdowns than you're making overall |
|
|
32
|
+
| **1.0 – 1.5** | ⚠️ *Weak* – Barely recovering from drawdowns | Net profit is only slightly higher than worst drawdown |
|
|
33
|
+
| **1.5 – 2.0** | 🤔 *Moderate* – Acceptable, but not robust | Strategy is viable but may lack strong risk-adjusted performance |
|
|
34
|
+
| **2.0 – 3.0** | ✅ *Good* – Solid recovery from worst drawdowns | Indicates decent resilience and profitability |
|
|
35
|
+
| **3.0 – 5.0** | 🚀 *Very Good* – Strong risk-adjusted performance | Shows good ability to recover and generate consistent profit |
|
|
36
|
+
| **> 5.0** | 🏆 *Excellent* – Exceptional recovery vs. risk taken | Often seen in highly optimized or low-volatility strategies |
|
|
37
|
+
| **∞ (Infinity)** | 💡 *No drawdown* – Unrealistic or incomplete data | Can happen if drawdown is zero — investigate carefully |
|
|
38
|
+
"""
|
|
39
|
+
from typing import List
|
|
40
|
+
|
|
41
|
+
from investing_algorithm_framework.domain import PortfolioSnapshot
|
|
42
|
+
from .drawdown import get_max_drawdown_absolute
|
|
43
|
+
from .returns import get_total_return
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_recovery_factor(snapshots: List[PortfolioSnapshot]) -> float:
|
|
47
|
+
"""
|
|
48
|
+
Calculate the recovery factor of a backtest report.
|
|
49
|
+
|
|
50
|
+
The recovery factor is defined as:
|
|
51
|
+
Recovery Factor = Total Net Profit / Maximum Drawdown
|
|
52
|
+
|
|
53
|
+
This metric indicates how efficiently a trading strategy recovers
|
|
54
|
+
from drawdowns. A higher recovery factor implies better
|
|
55
|
+
risk-adjusted performance.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
|
|
59
|
+
from the backtest report.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
float: The recovery factor. Returns 0.0 if max drawdown is
|
|
63
|
+
zero or undefined.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if not snapshots:
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
max_drawdown_absolute = get_max_drawdown_absolute(snapshots)
|
|
70
|
+
net_profit, _ = get_total_return(snapshots)
|
|
71
|
+
|
|
72
|
+
if max_drawdown_absolute == 0:
|
|
73
|
+
return float('inf') if net_profit > 0 else 0.0
|
|
74
|
+
|
|
75
|
+
return net_profit / max_drawdown_absolute
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_recovery_time(snapshots: List[PortfolioSnapshot]) -> float:
|
|
79
|
+
"""
|
|
80
|
+
Calculate the recovery time of a backtest report.
|
|
81
|
+
|
|
82
|
+
The recovery time is defined as the number of days it takes to recover
|
|
83
|
+
from the maximum drawdown.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots
|
|
87
|
+
from the backtest report.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
float: The recovery time in days. Returns 0.0 if no drawdown
|
|
91
|
+
occurred.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
if not snapshots:
|
|
95
|
+
return 0.0
|
|
96
|
+
|
|
97
|
+
max_drawdown_absolute = get_max_drawdown_absolute(snapshots)
|
|
98
|
+
|
|
99
|
+
if max_drawdown_absolute == 0:
|
|
100
|
+
return 0.0
|
|
101
|
+
|
|
102
|
+
# Find the first snapshot after the maximum drawdown
|
|
103
|
+
first_snapshot_after_drawdown = next(
|
|
104
|
+
(s for s in snapshots if s.net_size >= max_drawdown_absolute), None)
|
|
105
|
+
|
|
106
|
+
if not first_snapshot_after_drawdown:
|
|
107
|
+
return 0.0
|
|
108
|
+
|
|
109
|
+
# Calculate recovery time in days
|
|
110
|
+
recovery_time = (first_snapshot_after_drawdown.created_at -
|
|
111
|
+
snapshots[0].created_at).days
|
|
112
|
+
|
|
113
|
+
return recovery_time if recovery_time > 0 else 1.0
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from datetime import datetime, date
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from investing_algorithm_framework.domain import PortfolioSnapshot, Trade, \
|
|
7
|
+
OperationalException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_monthly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float, datetime]]:
|
|
11
|
+
"""
|
|
12
|
+
Calculate the monthly returns from a list of portfolio snapshots.
|
|
13
|
+
|
|
14
|
+
Monthly return is calculated as the percentage change in portfolio value
|
|
15
|
+
from the end of one month to the end of the next month.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List[Tuple[float, datetime]]: A list of tuples containing the monthly return
|
|
22
|
+
and the corresponding month.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Create DataFrame from snapshots
|
|
26
|
+
data = [(s.created_at, s.total_value) for s in snapshots]
|
|
27
|
+
df = pd.DataFrame(data, columns=["created_at", "total_value"])
|
|
28
|
+
df['created_at'] = pd.to_datetime(df['created_at'])
|
|
29
|
+
df = df.sort_values('created_at').drop_duplicates('created_at')\
|
|
30
|
+
.set_index('created_at')
|
|
31
|
+
|
|
32
|
+
# Resample to monthly frequency using last value of the month
|
|
33
|
+
monthly_df = df.resample('ME').last().dropna()
|
|
34
|
+
monthly_df['return'] = monthly_df['total_value'].pct_change()
|
|
35
|
+
monthly_df = monthly_df.dropna()
|
|
36
|
+
|
|
37
|
+
# Ensure returns are Python floats, not numpy floats
|
|
38
|
+
monthly_returns = [
|
|
39
|
+
(float(row['return']), row.name) for _, row in monthly_df.iterrows()
|
|
40
|
+
]
|
|
41
|
+
return monthly_returns
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_yearly_returns(snapshots: List[PortfolioSnapshot]) -> List[Tuple[float, date]]:
|
|
45
|
+
"""
|
|
46
|
+
Calculate the yearly returns from a list of portfolio snapshots.
|
|
47
|
+
|
|
48
|
+
Yearly return is calculated as the percentage change in portfolio value
|
|
49
|
+
from the end of one year to the end of the next year.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List[Tuple[float, date]]: A list of tuples containing the yearly return
|
|
56
|
+
and the corresponding year.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Create DataFrame from snapshots
|
|
60
|
+
data = [(s.created_at, s.total_value) for s in snapshots]
|
|
61
|
+
df = pd.DataFrame(data, columns=["created_at", "total_value"])
|
|
62
|
+
df['created_at'] = pd.to_datetime(df['created_at'])
|
|
63
|
+
df = df.sort_values('created_at').drop_duplicates('created_at')\
|
|
64
|
+
.set_index('created_at')
|
|
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
|
+
|
|
70
|
+
# Resample to yearly frequency using last value of the year
|
|
71
|
+
yearly_df = df.resample('YE').last().dropna()
|
|
72
|
+
yearly_df['return'] = yearly_df['total_value'].pct_change()
|
|
73
|
+
yearly_df = yearly_df.dropna()
|
|
74
|
+
|
|
75
|
+
# Yearly returns with date objects only representing the year
|
|
76
|
+
yearly_df.index = yearly_df.index.to_period('Y').to_timestamp()
|
|
77
|
+
yearly_returns = [
|
|
78
|
+
(float(row['return']), row.name) for _, row in yearly_df.iterrows()
|
|
79
|
+
]
|
|
80
|
+
return yearly_returns
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_percentage_winning_months(snapshots: List[PortfolioSnapshot]) -> float:
|
|
84
|
+
"""
|
|
85
|
+
Calculate the percentage of winning months from portfolio snapshots.
|
|
86
|
+
|
|
87
|
+
A winning month is defined as a month where the portfolio value at the end
|
|
88
|
+
of the month is greater than at the start of the month.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
float: The percentage of winning months.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
98
|
+
winning_months = sum(1 for r, _ in monthly_returns if r > 0)
|
|
99
|
+
|
|
100
|
+
if not monthly_returns:
|
|
101
|
+
return 0.0
|
|
102
|
+
|
|
103
|
+
return (winning_months / len(monthly_returns))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_best_month(snapshots: List[PortfolioSnapshot]) -> Tuple[float, datetime]:
|
|
107
|
+
"""
|
|
108
|
+
Get the best month in terms of return from portfolio snapshots.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tuple[float, datetime]: The best monthly return and the corresponding month.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
118
|
+
|
|
119
|
+
if not monthly_returns:
|
|
120
|
+
return 0.0, None
|
|
121
|
+
|
|
122
|
+
return max(monthly_returns, key=lambda x: x[0])
|
|
123
|
+
|
|
124
|
+
def get_worst_month(snapshots: List[PortfolioSnapshot]) -> Tuple[float, datetime]:
|
|
125
|
+
"""
|
|
126
|
+
Get the worst month in terms of return from portfolio snapshots.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Tuple[float, datetime]: The worst monthly return and the corresponding month.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
136
|
+
|
|
137
|
+
if not monthly_returns:
|
|
138
|
+
return 0.0, None
|
|
139
|
+
|
|
140
|
+
return min(monthly_returns, key=lambda x: x[0])
|
|
141
|
+
|
|
142
|
+
def get_best_year(
|
|
143
|
+
snapshots: List[PortfolioSnapshot]
|
|
144
|
+
) -> Tuple[float, datetime]:
|
|
145
|
+
"""
|
|
146
|
+
Get the best year in terms of return from portfolio snapshots.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Tuple[float, datetime]: The best yearly return and the corresponding year.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
yearly_returns = get_yearly_returns(snapshots)
|
|
156
|
+
|
|
157
|
+
if not yearly_returns:
|
|
158
|
+
return None, None
|
|
159
|
+
|
|
160
|
+
return max(yearly_returns, key=lambda x: x[0])
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_worst_year(
|
|
164
|
+
snapshots: List[PortfolioSnapshot]
|
|
165
|
+
) -> Tuple[float, date]:
|
|
166
|
+
"""
|
|
167
|
+
Get the worst year in terms of return from portfolio snapshots.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Tuple[float, datetime]: The worst yearly return and the corresponding year.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
yearly_returns = get_yearly_returns(snapshots)
|
|
177
|
+
|
|
178
|
+
if not yearly_returns:
|
|
179
|
+
return None, None
|
|
180
|
+
|
|
181
|
+
return min(yearly_returns, key=lambda x: x[0])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_average_monthly_return(snapshots: List[PortfolioSnapshot]) -> float:
|
|
185
|
+
"""
|
|
186
|
+
Calculate the average monthly return from portfolio snapshots.
|
|
187
|
+
|
|
188
|
+
The average monthly return is calculated as the mean of all monthly returns.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
float: The average monthly return as a percentage.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
198
|
+
|
|
199
|
+
if not monthly_returns:
|
|
200
|
+
return 0.0
|
|
201
|
+
|
|
202
|
+
return sum(r for r, _ in monthly_returns) / len(monthly_returns)
|
|
203
|
+
|
|
204
|
+
def get_average_monthly_return_winning_months(snapshots: List[PortfolioSnapshot]) -> float:
|
|
205
|
+
"""
|
|
206
|
+
Calculate the average monthly return from winning months in portfolio snapshots.
|
|
207
|
+
|
|
208
|
+
The average monthly return is calculated as the mean of all monthly returns
|
|
209
|
+
where the return is positive.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
float: The average monthly return from winning months as a percentage.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
219
|
+
winning_months = [r for r, _ in monthly_returns if r > 0]
|
|
220
|
+
|
|
221
|
+
if not winning_months:
|
|
222
|
+
return 0.0
|
|
223
|
+
|
|
224
|
+
return sum(winning_months) / len(winning_months)
|
|
225
|
+
|
|
226
|
+
def get_average_monthly_return_losing_months(snapshots: List[PortfolioSnapshot]) -> float:
|
|
227
|
+
"""
|
|
228
|
+
Calculate the average monthly return from losing months in portfolio snapshots.
|
|
229
|
+
|
|
230
|
+
The average monthly return is calculated as the mean of all monthly returns
|
|
231
|
+
where the return is negative.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
float: The average monthly return from losing months as a percentage.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
monthly_returns = get_monthly_returns(snapshots)
|
|
241
|
+
losing_months = [r for r, _ in monthly_returns if r < 0]
|
|
242
|
+
|
|
243
|
+
if not losing_months:
|
|
244
|
+
return 0.0
|
|
245
|
+
|
|
246
|
+
return sum(losing_months) / len(losing_months)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_average_yearly_return(snapshots: List[PortfolioSnapshot]) -> float:
|
|
250
|
+
"""
|
|
251
|
+
Calculate the average yearly return from portfolio snapshots.
|
|
252
|
+
|
|
253
|
+
The average yearly return is calculated as the mean of all yearly returns.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
float: The average yearly return as a percentage.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
yearly_returns = get_yearly_returns(snapshots)
|
|
263
|
+
|
|
264
|
+
if not yearly_returns:
|
|
265
|
+
return 0.0
|
|
266
|
+
|
|
267
|
+
return sum(r for r, _ in yearly_returns) / len(yearly_returns)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_total_return(
|
|
271
|
+
snapshots: List[PortfolioSnapshot]
|
|
272
|
+
) -> Tuple[float, float]:
|
|
273
|
+
"""
|
|
274
|
+
Calculate the total return from portfolio snapshots.
|
|
275
|
+
|
|
276
|
+
The total return is calculated as the percentage change in portfolio value
|
|
277
|
+
from the first snapshot to the last snapshot.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Tuple[Float, Float]: First number is the absolute return and the
|
|
284
|
+
second number is the percentage total return
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
if not snapshots or len(snapshots) < 2:
|
|
288
|
+
return 0.0, 0.0
|
|
289
|
+
|
|
290
|
+
initial_value = snapshots[0].total_value
|
|
291
|
+
final_value = snapshots[-1].total_value
|
|
292
|
+
|
|
293
|
+
if initial_value == 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
|
|
355
|
+
|
|
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
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def get_percentage_winning_years(snapshots: List[PortfolioSnapshot]) -> float:
|
|
368
|
+
"""
|
|
369
|
+
Calculate the percentage of winning years from portfolio snapshots.
|
|
370
|
+
|
|
371
|
+
A winning year is defined as a year where the portfolio value at the end
|
|
372
|
+
of the year is greater than at the start of the year.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
float: The percentage of winning years.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
yearly_returns = get_yearly_returns(snapshots)
|
|
382
|
+
winning_years = sum(1 for r, _ in yearly_returns if r > 0)
|
|
383
|
+
|
|
384
|
+
if not yearly_returns:
|
|
385
|
+
return 0.0
|
|
386
|
+
|
|
387
|
+
return winning_years / len(yearly_returns)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_final_value(snapshots: List[PortfolioSnapshot]) -> float:
|
|
391
|
+
"""
|
|
392
|
+
Calculate the final portfolio value from portfolio snapshots.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
float: The final portfolio value.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
if not snapshots:
|
|
402
|
+
return 0.0
|
|
403
|
+
|
|
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))
|
|
451
|
+
|
|
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
|