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
|
@@ -1,14 +1,84 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
2
|
import os
|
|
3
|
-
import
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
import webbrowser
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from
|
|
6
|
+
from dataclasses import field
|
|
7
|
+
from typing import List, Union
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
7
10
|
from IPython import get_ipython
|
|
11
|
+
from IPython.display import display, HTML
|
|
12
|
+
from jinja2 import Environment, FileSystemLoader
|
|
13
|
+
|
|
14
|
+
from investing_algorithm_framework.domain import TimeFrame, Backtest, \
|
|
15
|
+
OperationalException, BacktestDateRange
|
|
16
|
+
from .charts import get_equity_curve_with_drawdown_chart, \
|
|
17
|
+
get_rolling_sharpe_ratio_chart, get_monthly_returns_heatmap_chart, \
|
|
18
|
+
get_yearly_returns_bar_chart, get_ohlcv_data_completeness_chart
|
|
19
|
+
from .tables import create_html_time_metrics_table, \
|
|
20
|
+
create_html_trade_metrics_table, create_html_key_metrics_table, \
|
|
21
|
+
create_html_trades_table
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("investing_algorithm_framework")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_symbol_from_file_name(file_name: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Extract the symbol from the file name.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_name (str): The file name from which to extract the symbol.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: The extracted symbol.
|
|
35
|
+
"""
|
|
36
|
+
# Assuming the file name format is "symbol_timeframe.csv"
|
|
37
|
+
return file_name.split('_')[0].upper()
|
|
38
|
+
|
|
39
|
+
def get_market_from_file_name(file_name: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Extract the market from the file name.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
file_name (str): The file name from which to extract the market.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: The extracted market.
|
|
48
|
+
"""
|
|
49
|
+
# Assuming the file name format is "symbol_market_timeframe.csv"
|
|
50
|
+
parts = file_name.split('_')
|
|
51
|
+
if len(parts) < 2:
|
|
52
|
+
raise ValueError("File name does not contain a valid market.")
|
|
53
|
+
return parts[1].upper()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_time_frame_from_file_name(file_name: str) -> TimeFrame:
|
|
57
|
+
"""
|
|
58
|
+
Extract the time frame from the file name.
|
|
8
59
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
60
|
+
Args:
|
|
61
|
+
file_name (str): The file name from which to extract the time frame.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
TimeFrame: The extracted time frame.
|
|
65
|
+
"""
|
|
66
|
+
parts = file_name.split('_')
|
|
67
|
+
|
|
68
|
+
if len(parts) < 3:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"File name does not contain a valid time frame."
|
|
71
|
+
)
|
|
72
|
+
time_frame_str = parts[3]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
return TimeFrame.from_string(time_frame_str)
|
|
76
|
+
except ValueError:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Could not extract time frame from file name: {file_path}. "
|
|
79
|
+
f"Expected format 'OHLCV_<SYMBOL>_<MARKET>_<TIME_FRAME>_<START_DATE>_<END_DATE>.csv', "
|
|
80
|
+
f"got '{time_frame_str}'."
|
|
81
|
+
)
|
|
12
82
|
|
|
13
83
|
|
|
14
84
|
@dataclass
|
|
@@ -29,207 +99,251 @@ class BacktestReport:
|
|
|
29
99
|
strategy_path (str): The path to the strategy directory used in the
|
|
30
100
|
backtest.
|
|
31
101
|
"""
|
|
102
|
+
backtest: Backtest = None
|
|
32
103
|
html_report: str = None
|
|
33
104
|
html_report_path: str = None
|
|
34
|
-
metrics = None
|
|
35
|
-
results: BacktestResult = None
|
|
36
|
-
risk_free_rate: float = None
|
|
37
105
|
|
|
38
|
-
def
|
|
106
|
+
def show(
|
|
107
|
+
self,
|
|
108
|
+
backtest_date_range: BacktestDateRange,
|
|
109
|
+
browser: bool = False
|
|
110
|
+
):
|
|
39
111
|
"""
|
|
40
|
-
|
|
112
|
+
Display the HTML report in a Jupyter notebook cell.
|
|
41
113
|
"""
|
|
42
|
-
if self.metrics is None:
|
|
43
|
-
add_metrics(self, self.risk_free_rate)
|
|
44
114
|
|
|
45
|
-
if self.html_report
|
|
46
|
-
|
|
115
|
+
if not self.html_report:
|
|
116
|
+
# If the HTML report is not created, create it
|
|
117
|
+
self._create_html_report(backtest_date_range)
|
|
47
118
|
|
|
48
|
-
|
|
49
|
-
""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if self.html_report:
|
|
119
|
+
# Save the html report to a tmp location
|
|
120
|
+
path = "/tmp/backtest_report.html"
|
|
121
|
+
with open(path, "w") as html_file:
|
|
122
|
+
html_file.write(self.html_report)
|
|
53
123
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
shell = get_ipython().__class__.__name__
|
|
57
|
-
return shell == 'ZMQInteractiveShell'
|
|
58
|
-
except (NameError, ImportError):
|
|
59
|
-
return False
|
|
124
|
+
if browser:
|
|
125
|
+
webbrowser.open(f"file://{path}")
|
|
60
126
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
127
|
+
def in_jupyter_notebook():
|
|
128
|
+
try:
|
|
129
|
+
shell = get_ipython().__class__.__name__
|
|
130
|
+
return shell == 'ZMQInteractiveShell'
|
|
131
|
+
except (NameError, ImportError):
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
if in_jupyter_notebook():
|
|
135
|
+
display(HTML(self.html_report))
|
|
65
136
|
else:
|
|
66
|
-
|
|
137
|
+
webbrowser.open(f"file://{path}")
|
|
67
138
|
|
|
68
|
-
|
|
69
|
-
def open(file_path) -> "BacktestReport":
|
|
139
|
+
def _create_html_report(self, backtest_date_range: BacktestDateRange):
|
|
70
140
|
"""
|
|
71
|
-
|
|
141
|
+
Create an HTML report from the backtest metrics and results.
|
|
72
142
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
143
|
+
This method generates various charts and tables from the backtest
|
|
144
|
+
metrics and results, and renders them into an HTML template using
|
|
145
|
+
Jinja2.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
OperationalException: If no backtests are available to
|
|
149
|
+
create a report.
|
|
76
150
|
|
|
77
151
|
Returns:
|
|
78
|
-
|
|
152
|
+
None
|
|
79
153
|
"""
|
|
80
|
-
|
|
154
|
+
# Get the first backtest
|
|
155
|
+
if not self.backtest:
|
|
81
156
|
raise OperationalException(
|
|
82
|
-
"
|
|
157
|
+
"No backtest available to create a report."
|
|
83
158
|
)
|
|
84
159
|
|
|
85
|
-
if os.path.isdir(file_path):
|
|
86
|
-
report_file_path = os.path.join(file_path, "report.json")
|
|
87
|
-
results = None
|
|
88
|
-
html_report = None
|
|
89
|
-
|
|
90
|
-
# Open the results file
|
|
91
|
-
if os.path.isfile(report_file_path):
|
|
92
|
-
with open(report_file_path, 'r') as json_file:
|
|
93
|
-
data = json.load(json_file)
|
|
94
160
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
metrics = self.backtest.get_backtest_metrics(backtest_date_range)
|
|
162
|
+
run = self.backtest.get_backtest_run(backtest_date_range)
|
|
163
|
+
# Create plots
|
|
164
|
+
equity_with_drawdown_fig = get_equity_curve_with_drawdown_chart(
|
|
165
|
+
metrics.equity_curve, metrics.drawdown_series
|
|
166
|
+
)
|
|
167
|
+
equity_with_drawdown_plot_html = equity_with_drawdown_fig.to_html(
|
|
168
|
+
full_html=False, include_plotlyjs='cdn',
|
|
169
|
+
config={'responsive': True}, default_width="90%"
|
|
170
|
+
)
|
|
171
|
+
rolling_sharpe_ratio_fig = get_rolling_sharpe_ratio_chart(
|
|
172
|
+
metrics.rolling_sharpe_ratio
|
|
173
|
+
)
|
|
174
|
+
rolling_sharpe_ratio_plot_html = rolling_sharpe_ratio_fig.to_html(
|
|
175
|
+
full_html=False, include_plotlyjs='cdn',
|
|
176
|
+
config={'responsive': True}, default_width="90%"
|
|
177
|
+
)
|
|
178
|
+
monthly_returns_heatmap_fig = get_monthly_returns_heatmap_chart(
|
|
179
|
+
metrics.monthly_returns
|
|
180
|
+
)
|
|
181
|
+
monthly_returns_heatmap_html = monthly_returns_heatmap_fig.to_html(
|
|
182
|
+
full_html=False, include_plotlyjs='cdn',
|
|
183
|
+
config={'responsive': True}
|
|
184
|
+
)
|
|
185
|
+
yearly_returns_histogram_fig = get_yearly_returns_bar_chart(
|
|
186
|
+
metrics.yearly_returns
|
|
187
|
+
)
|
|
188
|
+
yearly_returns_histogram_html = yearly_returns_histogram_fig.to_html(
|
|
189
|
+
full_html=False, include_plotlyjs='cdn',
|
|
190
|
+
config={'responsive': True}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Create OHLCV data completeness charts
|
|
194
|
+
data_files = []
|
|
195
|
+
ohlcv_data_completeness_charts_html = ""
|
|
196
|
+
|
|
197
|
+
for file in data_files:
|
|
198
|
+
try:
|
|
199
|
+
if file.endswith('.csv'):
|
|
200
|
+
df = pd.read_csv(file, parse_dates=['Datetime'])
|
|
201
|
+
file_name = os.path.basename(file)
|
|
202
|
+
symbol = get_symbol_from_file_name(file_name)
|
|
203
|
+
market = get_market_from_file_name(file_name)
|
|
204
|
+
time_frame = get_time_frame_from_file_name(file_name)
|
|
205
|
+
title = f"OHLCV Data Completeness for {market} - {symbol} - {time_frame.value}"
|
|
206
|
+
ohlcv_data_completeness_chart_html = \
|
|
207
|
+
get_ohlcv_data_completeness_chart(
|
|
208
|
+
df,
|
|
209
|
+
timeframe=time_frame.value,
|
|
210
|
+
windowsize=200,
|
|
211
|
+
title=title
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
ohlcv_data_completeness_charts_html += (
|
|
215
|
+
'<div class="ohlcv-data-completeness-chart">'
|
|
216
|
+
f'{ohlcv_data_completeness_chart_html}'
|
|
217
|
+
'</div>'
|
|
218
|
+
)
|
|
103
219
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.warning(
|
|
222
|
+
"Error creating OHLCV data completeness " +
|
|
223
|
+
f"chart for {file}: {e}"
|
|
107
224
|
)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Create HTML tables
|
|
228
|
+
key_metrics_table_html = create_html_key_metrics_table(
|
|
229
|
+
metrics, run
|
|
230
|
+
)
|
|
231
|
+
trades_metrics_table_html = create_html_trade_metrics_table(
|
|
232
|
+
metrics, run
|
|
233
|
+
)
|
|
234
|
+
time_metrics_table_html = create_html_time_metrics_table(
|
|
235
|
+
metrics, run
|
|
236
|
+
)
|
|
237
|
+
trades_table_html = create_html_trades_table(run)
|
|
238
|
+
|
|
239
|
+
# Jinja2 environment setup
|
|
240
|
+
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
|
241
|
+
env = Environment(loader=FileSystemLoader(template_dir))
|
|
242
|
+
template = env.get_template('report_template.html.j2')
|
|
243
|
+
|
|
244
|
+
# Render template with variables
|
|
245
|
+
html_rendered = template.render(
|
|
246
|
+
report=run,
|
|
247
|
+
equity_with_drawdown_plot_html=equity_with_drawdown_plot_html,
|
|
248
|
+
rolling_sharpe_ratio_plot_html=rolling_sharpe_ratio_plot_html,
|
|
249
|
+
monthly_returns_heatmap_html=monthly_returns_heatmap_html,
|
|
250
|
+
yearly_returns_histogram_html=yearly_returns_histogram_html,
|
|
251
|
+
key_metrics_table_html=key_metrics_table_html,
|
|
252
|
+
trades_metrics_table_html=trades_metrics_table_html,
|
|
253
|
+
time_metrics_table_html=time_metrics_table_html,
|
|
254
|
+
trades_table_html=trades_table_html,
|
|
255
|
+
data_completeness_charts_html=ohlcv_data_completeness_charts_html
|
|
256
|
+
)
|
|
257
|
+
self.html_report = html_rendered
|
|
108
258
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
results=results,
|
|
112
|
-
html_report_path=html_file_path
|
|
113
|
-
)
|
|
114
|
-
else:
|
|
115
|
-
return BacktestReport.open(file_path)
|
|
116
|
-
|
|
117
|
-
def save(
|
|
118
|
-
self,
|
|
119
|
-
path,
|
|
120
|
-
save_strategy=True,
|
|
121
|
-
algorithm=None,
|
|
122
|
-
strategy_directory_path=None
|
|
123
|
-
):
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _load_backtest(backtest_path: str) -> Backtest:
|
|
124
261
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
JSON, and the HTML report will be saved as an HTML file.
|
|
262
|
+
Load the backtest from a give path
|
|
263
|
+
and return an instance of Backtest.
|
|
128
264
|
|
|
129
265
|
Args:
|
|
130
|
-
|
|
131
|
-
save_strategy (bool): If True, the strategy code will be copied
|
|
132
|
-
to the report directory. Defaults to True.
|
|
133
|
-
algorithm (Algorithm, optional): The algorithm used for the
|
|
134
|
-
backtest. If provided, the strategy code will be copied to
|
|
135
|
-
the report directory.
|
|
136
|
-
strategy_directory_path (str, optional): The path to the
|
|
137
|
-
strategy directory. If provided, the strategy code will be
|
|
138
|
-
copied to the report directory.
|
|
266
|
+
backtest_path (str): The path to the backtest directory.
|
|
139
267
|
|
|
140
268
|
Returns:
|
|
141
|
-
|
|
269
|
+
Backtest: An instance of Backtest loaded from the
|
|
270
|
+
backtest directory.
|
|
142
271
|
"""
|
|
272
|
+
return Backtest.open(backtest_path)
|
|
143
273
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
print(f"save_strategy: {save_strategy}")
|
|
149
|
-
if save_strategy:
|
|
150
|
-
# If strategy_directory_path is set, copy the strategy code to
|
|
151
|
-
# the report directory
|
|
152
|
-
if strategy_directory_path is not None:
|
|
153
|
-
print("Saving strategy code from directory")
|
|
154
|
-
# check if the strategy directory exists
|
|
155
|
-
if not os.path.exists(strategy_directory_path) and \
|
|
156
|
-
not os.path.isdir(strategy_directory_path):
|
|
157
|
-
raise OperationalException(
|
|
158
|
-
"Strategy directory does not exist"
|
|
159
|
-
)
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _is_backtest(backtest_path: Union[str, Path]) -> bool:
|
|
276
|
+
"""
|
|
277
|
+
Check if the given path is a valid backtest report directory.
|
|
160
278
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
strategy_directory_path
|
|
164
|
-
)
|
|
165
|
-
output_strategy_directory = os.path.join(
|
|
166
|
-
path, strategy_directory_name
|
|
167
|
-
)
|
|
168
|
-
if not os.path.exists(output_strategy_directory):
|
|
169
|
-
os.makedirs(output_strategy_directory)
|
|
170
|
-
|
|
171
|
-
strategy_files = os.listdir(strategy_directory_path)
|
|
172
|
-
for file in strategy_files:
|
|
173
|
-
source_file = os.path.join(strategy_directory_path, file)
|
|
174
|
-
destination_file = os.path.join(
|
|
175
|
-
output_strategy_directory, file
|
|
176
|
-
)
|
|
279
|
+
Args:
|
|
280
|
+
backtest_path (Union[str, Path]): The path to check.
|
|
177
281
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
strategy_directory_path = os.path.dirname(mod.__file__)
|
|
189
|
-
strategy_directory_name = os.path.basename(
|
|
190
|
-
strategy_directory_path
|
|
191
|
-
)
|
|
192
|
-
strategy_files = os.listdir(strategy_directory_path)
|
|
193
|
-
output_strategy_directory = os.path.join(
|
|
194
|
-
path, strategy_directory_name
|
|
195
|
-
)
|
|
282
|
+
Returns:
|
|
283
|
+
bool: True if the path is a valid backtest report directory,
|
|
284
|
+
False otherwise.
|
|
285
|
+
"""
|
|
286
|
+
return (
|
|
287
|
+
os.path.exists(backtest_path) and
|
|
288
|
+
os.path.isdir(backtest_path) and
|
|
289
|
+
os.path.isfile(os.path.join(backtest_path, "results.json"))
|
|
290
|
+
and os.path.isfile(os.path.join(backtest_path, "metrics.json"))
|
|
291
|
+
)
|
|
196
292
|
|
|
197
|
-
|
|
198
|
-
|
|
293
|
+
@staticmethod
|
|
294
|
+
def open(
|
|
295
|
+
backtests: List[Backtest] = [],
|
|
296
|
+
directory_path=None
|
|
297
|
+
) -> "BacktestReport":
|
|
298
|
+
"""
|
|
299
|
+
Open the backtest report from a file.
|
|
199
300
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
301
|
+
Args:
|
|
302
|
+
backtests (List[Backtest], optional): A list of Backtest instances
|
|
303
|
+
to include in the report. If provided, it will use these
|
|
304
|
+
backtests as part of the report.
|
|
305
|
+
directory_path (str, optional): The directory path from
|
|
306
|
+
which the reports will be loaded.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
BacktestReport: An instance of BacktestReport loaded from the file.
|
|
205
310
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
311
|
+
Raises:
|
|
312
|
+
OperationalException: If the backtest path is not a valid backtest
|
|
313
|
+
report directory.
|
|
314
|
+
"""
|
|
315
|
+
loaded_backtests = []
|
|
211
316
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
317
|
+
if directory_path is not None:
|
|
318
|
+
# Check if the directory is a valid backtest report directory
|
|
319
|
+
if BacktestReport._is_backtest(directory_path):
|
|
320
|
+
backtests.append(
|
|
321
|
+
Backtest.open(directory_path)
|
|
216
322
|
)
|
|
323
|
+
else:
|
|
324
|
+
# Loop over all subdirectories
|
|
325
|
+
for root, dirs, _ in os.walk(directory_path):
|
|
326
|
+
for dir_name in dirs:
|
|
327
|
+
subdir_path = os.path.join(root, dir_name)
|
|
328
|
+
if BacktestReport._is_backtest(subdir_path):
|
|
329
|
+
loaded_backtests.append(
|
|
330
|
+
Backtest.open(directory_path=subdir_path)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if len(backtests) > 0:
|
|
334
|
+
|
|
335
|
+
for backtest in backtests:
|
|
336
|
+
if not isinstance(backtest, Backtest):
|
|
337
|
+
raise OperationalException(
|
|
338
|
+
"The provided backtest is not a valid Backtest instance."
|
|
339
|
+
)
|
|
217
340
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
results_file_path = os.path.join(path, "report.json")
|
|
221
|
-
with open(results_file_path, 'w') as json_file:
|
|
222
|
-
json.dump(self.results.to_dict(), json_file, indent=4)
|
|
223
|
-
|
|
224
|
-
# Save the HTML report
|
|
225
|
-
if self.html_report is not None:
|
|
226
|
-
html_file_path = os.path.join(path, "report.html")
|
|
227
|
-
with open(html_file_path, 'w') as html_file:
|
|
228
|
-
html_file.write(self.html_report)
|
|
229
|
-
self.html_report_path = html_file_path
|
|
341
|
+
# Add the backtests to the backtests list
|
|
342
|
+
loaded_backtests.extend(backtests)
|
|
230
343
|
|
|
231
|
-
if
|
|
232
|
-
|
|
344
|
+
if len(loaded_backtests) == 0:
|
|
345
|
+
raise OperationalException(
|
|
346
|
+
f"The directory {directory_path} is not a valid backtest report."
|
|
347
|
+
)
|
|
233
348
|
|
|
234
|
-
|
|
235
|
-
json.dump(data, json_file, indent=4)
|
|
349
|
+
return BacktestReport(backtest=loaded_backtests)
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
from .equity_curve_drawdown import get_equity_curve_with_drawdown_chart
|
|
2
|
-
from .
|
|
2
|
+
from .equity_curve import get_equity_curve_chart
|
|
3
|
+
from .rolling_sharp_ratio import get_rolling_sharpe_ratio_chart
|
|
3
4
|
from .monthly_returns_heatmap import get_monthly_returns_heatmap_chart
|
|
4
5
|
from .yearly_returns_barchart import get_yearly_returns_bar_chart
|
|
6
|
+
from .ohlcv_data_completeness import get_ohlcv_data_completeness_chart
|
|
7
|
+
from .entry_exist_signals import get_entry_and_exit_signals
|
|
8
|
+
from .line_chart import create_line_scatter
|
|
5
9
|
|
|
6
10
|
__all__ = [
|
|
7
11
|
"get_equity_curve_with_drawdown_chart",
|
|
8
|
-
"
|
|
12
|
+
"get_rolling_sharpe_ratio_chart",
|
|
9
13
|
"get_monthly_returns_heatmap_chart",
|
|
10
14
|
"get_yearly_returns_bar_chart",
|
|
15
|
+
"get_ohlcv_data_completeness_chart",
|
|
16
|
+
"get_entry_and_exit_signals",
|
|
17
|
+
"create_line_scatter",
|
|
18
|
+
"get_equity_curve_chart"
|
|
11
19
|
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_entry_and_exit_signals(
|
|
6
|
+
entry_signals: pd.Series,
|
|
7
|
+
exit_signals: pd.Series,
|
|
8
|
+
price_data: pd.DataFrame
|
|
9
|
+
):
|
|
10
|
+
"""
|
|
11
|
+
Plots the price chart with entry and exit signals.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
entry_signals (pd.Series): Series containing buy
|
|
15
|
+
signals with datetime index. Entry signals should
|
|
16
|
+
be boolean values.
|
|
17
|
+
exit_signals (pd.Series): Series containing exit signals
|
|
18
|
+
with datetime index. Exit signals should be boolean values.
|
|
19
|
+
price_data (pd.DataFrame): DataFrame containing price
|
|
20
|
+
data with datetime index and 'Close'
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
go.Figure: Plotly figure with price chart and signals.
|
|
24
|
+
"""
|
|
25
|
+
# Create the base candlestick or line chart
|
|
26
|
+
fig = go.Figure()
|
|
27
|
+
|
|
28
|
+
fig.add_trace(go.Scatter(
|
|
29
|
+
x=price_data.index,
|
|
30
|
+
y=price_data["Close"],
|
|
31
|
+
mode="lines",
|
|
32
|
+
name="Close Price",
|
|
33
|
+
line=dict(color="blue")
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
# Entry points
|
|
37
|
+
entry_points = price_data[entry_signals]
|
|
38
|
+
fig.add_trace(go.Scatter(
|
|
39
|
+
x=entry_points.index,
|
|
40
|
+
y=entry_points["Close"],
|
|
41
|
+
mode="markers",
|
|
42
|
+
name="Entry",
|
|
43
|
+
marker=dict(symbol="triangle-up", color="green", size=10)
|
|
44
|
+
))
|
|
45
|
+
|
|
46
|
+
# Exit points
|
|
47
|
+
exit_points = price_data[exit_signals]
|
|
48
|
+
fig.add_trace(go.Scatter(
|
|
49
|
+
x=exit_points.index,
|
|
50
|
+
y=exit_points["Close"],
|
|
51
|
+
mode="markers",
|
|
52
|
+
name="Exit",
|
|
53
|
+
marker=dict(symbol="triangle-down", color="red", size=10)
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
# Layout settings
|
|
57
|
+
fig.update_layout(
|
|
58
|
+
title="Entry and Exit Signals",
|
|
59
|
+
xaxis_title="Date",
|
|
60
|
+
yaxis_title="Price",
|
|
61
|
+
legend=dict(x=0, y=1),
|
|
62
|
+
height=600,
|
|
63
|
+
template="plotly_white"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return fig
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from plotly import graph_objects as go
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_equity_curve_chart(equity_curve_series):
|
|
6
|
+
equity_curve_df = pd.DataFrame(
|
|
7
|
+
equity_curve_series, columns=["value", "datetime"]
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Normalize equity to start at 1
|
|
11
|
+
equity_curve_df["value"] = (
|
|
12
|
+
equity_curve_df["value"] / equity_curve_df["value"].iloc[0]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
fig = go.Figure()
|
|
16
|
+
|
|
17
|
+
# Draw equity curve
|
|
18
|
+
fig.add_trace(
|
|
19
|
+
go.Scatter(
|
|
20
|
+
x=equity_curve_df["datetime"],
|
|
21
|
+
y=equity_curve_df["value"],
|
|
22
|
+
mode="lines",
|
|
23
|
+
line=dict(color="rgba(0, 128, 0, 0.8)", width=1),
|
|
24
|
+
name="Equity Curve",
|
|
25
|
+
hovertemplate="<b>Equity</b><br>%{x}<br>Value: %{y:.2f}<extra></extra>"
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
fig.update_layout(
|
|
30
|
+
xaxis=dict(title=None),
|
|
31
|
+
yaxis=dict(title="Cumulative Equity (log)", type="log"),
|
|
32
|
+
title="Equity Curve with Drawdown",
|
|
33
|
+
hovermode="x unified",
|
|
34
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return fig
|