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.
- investing_algorithm_framework/__init__.py +147 -44
- investing_algorithm_framework/app/__init__.py +23 -6
- investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
- 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 +1322 -707
- investing_algorithm_framework/app/context.py +196 -88
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +16 -5
- investing_algorithm_framework/app/reporting/ascii.py +57 -202
- investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
- investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
- 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 +11 -26
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
- investing_algorithm_framework/app/reporting/generate.py +100 -114
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
- investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
- investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
- investing_algorithm_framework/app/strategy.py +315 -175
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/cli/cli.py +30 -12
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
- investing_algorithm_framework/cli/initialize_app.py +20 -1
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
- 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_requirements.txt.template +2 -2
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
- investing_algorithm_framework/create_app.py +3 -5
- investing_algorithm_framework/dependency_container.py +25 -39
- investing_algorithm_framework/domain/__init__.py +45 -38
- 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 +605 -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 +27 -0
- investing_algorithm_framework/domain/constants.py +6 -34
- investing_algorithm_framework/domain/data_provider.py +200 -56
- investing_algorithm_framework/domain/exceptions.py +34 -1
- investing_algorithm_framework/domain/models/__init__.py +10 -19
- investing_algorithm_framework/domain/models/base_model.py +0 -6
- 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/{market_data_type.py → data/data_type.py} +7 -7
- investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +34 -13
- investing_algorithm_framework/domain/models/order/order_status.py +1 -1
- investing_algorithm_framework/domain/models/order/order_type.py +1 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +9 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +7 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +63 -1
- investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
- investing_algorithm_framework/domain/models/trade/trade.py +56 -32
- investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
- investing_algorithm_framework/domain/order_executor.py +19 -0
- investing_algorithm_framework/domain/portfolio_provider.py +20 -1
- investing_algorithm_framework/domain/services/__init__.py +0 -13
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +5 -1
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +17 -14
- investing_algorithm_framework/download_data.py +40 -10
- investing_algorithm_framework/infrastructure/__init__.py +13 -25
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
- investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
- investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
- investing_algorithm_framework/services/__init__.py +105 -8
- investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
- investing_algorithm_framework/services/configuration_service.py +14 -4
- 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/{app/reporting → services}/metrics/__init__.py +48 -17
- investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
- investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
- investing_algorithm_framework/services/order_service/order_service.py +9 -71
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
- investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
- investing_algorithm_framework/services/repository_service.py +5 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
- investing_algorithm_framework/app/reporting/evaluation.py +0 -243
- investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
- investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
- investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
- investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
- investing_algorithm_framework/domain/models/data_source.py +0 -21
- investing_algorithm_framework/domain/models/date_range.py +0 -64
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/services/observable.py +0 -51
- investing_algorithm_framework/domain/services/observer.py +0 -19
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
- investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
- investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
- investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
- /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
- {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,
|
|
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))
|
|
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
|
|
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(
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
267
|
+
return sum(r for r, _ in yearly_returns) / len(yearly_returns)
|
|
382
268
|
|
|
383
269
|
|
|
384
|
-
def get_total_return(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
387
|
+
return winning_years / len(yearly_returns)
|
|
431
388
|
|
|
432
389
|
|
|
433
|
-
def
|
|
390
|
+
def get_final_value(snapshots: List[PortfolioSnapshot]) -> float:
|
|
434
391
|
"""
|
|
435
|
-
Calculate the
|
|
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
|
|
398
|
+
float: The final portfolio value.
|
|
445
399
|
"""
|
|
446
400
|
|
|
447
401
|
if not snapshots:
|
|
448
402
|
return 0.0
|
|
449
403
|
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
83
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
50
|
-
(e.g., 0.047 for 4.7%).
|
|
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
|
|