Skip to content

Commit 0762184

Browse files
committed
2 parents 49077cc + 8344106 commit 0762184

218 files changed

Lines changed: 236590 additions & 4433613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
flake8 ./investing_algorithm_framework
3636
test:
3737
needs: linting
38+
timeout-minutes: 25
3839
strategy:
3940
fail-fast: true
4041
matrix:
@@ -102,6 +103,11 @@ jobs:
102103
run: |
103104
source $VENV
104105
coverage run -m unittest discover -s tests
106+
- name: Generate coverage report
107+
if: always()
108+
continue-on-error: true
109+
run: |
110+
source $VENV
105111
coverage xml
106112
#----------------------------------------------
107113
# upload coverage stats

_verify_fixes.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import warnings
2+
# Allow SQLAlchemy deprecation, but error on ours
3+
warnings.filterwarnings('ignore', category=DeprecationWarning, module='sqlalchemy')
4+
warnings.simplefilter('error', RuntimeWarning)
5+
warnings.filterwarnings('error', category=DeprecationWarning, message='.*window_size.*')
6+
7+
# Test 1: DataSource with warmup_window should NOT warn
8+
from investing_algorithm_framework.domain.models.data.data_source import DataSource
9+
ds = DataSource(warmup_window=200)
10+
print('DataSource with warmup_window: OK')
11+
12+
# Test 2: std on small series
13+
from investing_algorithm_framework.services.metrics.standard_deviation import (
14+
get_standard_deviation_downside_returns,
15+
get_standard_deviation_returns,
16+
get_daily_returns_std,
17+
get_downside_std_of_daily_returns
18+
)
19+
from datetime import datetime, timezone
20+
21+
class FakeSnapshot:
22+
def __init__(self, value, dt):
23+
self.total_value = value
24+
self.created_at = dt
25+
26+
# Only 2 snapshots -> 1 return -> std with ddof=1 would fail before fix
27+
snaps = [
28+
FakeSnapshot(100, datetime(2024, 1, 1, tzinfo=timezone.utc)),
29+
FakeSnapshot(90, datetime(2024, 1, 2, tzinfo=timezone.utc)),
30+
]
31+
r1 = get_standard_deviation_downside_returns(snaps)
32+
r2 = get_standard_deviation_returns(snaps)
33+
r3 = get_daily_returns_std(snaps)
34+
r4 = get_downside_std_of_daily_returns(snaps)
35+
print(f'std functions with 2 snapshots: OK (results: {r1}, {r2}, {r3}, {r4})')
36+
print('All checks passed!')

investing_algorithm_framework/app/app.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
BacktestOrderExecutor, CCXTOHLCVDataProvider, clear_db, \
2828
PandasOHLCVDataProvider
2929
from investing_algorithm_framework.services import OrderBacktestService, \
30-
BacktestPortfolioService, DefaultTradeOrderEvaluator, get_risk_free_rate_us
30+
BacktestPortfolioService, DefaultTradeOrderEvaluator
3131
from .app_hook import AppHook
3232
from .eventloop import EventLoopService
3333

@@ -416,7 +416,7 @@ def initialize_data_sources_backtest(
416416
self,
417417
data_sources: List[DataSource],
418418
backtest_date_range: BacktestDateRange,
419-
show_progress: bool = True,
419+
show_progress: bool = False,
420420
fill_missing_data: bool = False,
421421
):
422422
"""
@@ -632,7 +632,9 @@ def run(self, number_of_iterations: int = None):
632632
self.initialize_services()
633633
self.initialize_portfolios()
634634

635-
if AppMode.WEB.equals(self.config[APP_MODE]):
635+
if AppMode.WEB.equals(self.config[APP_MODE]) \
636+
and not (self._flask_app and self._flask_app.testing) \
637+
and number_of_iterations is None:
636638
logger.info("Running web")
637639
flask_thread = threading.Thread(
638640
name='Web App',
@@ -782,9 +784,15 @@ def _initialize_web(self):
782784
- Investing Algorithm Framework App
783785
- Algorithm
784786
"""
787+
# Preserve the testing flag if the flask app already exists
788+
was_testing = self._flask_app.testing \
789+
if self._flask_app is not None else False
785790
configuration_service = self.container.configuration_service()
786791
self._flask_app = create_flask_app(configuration_service)
787792

793+
if was_testing:
794+
self._flask_app.testing = True
795+
788796
def get_portfolio_configurations(self):
789797
portfolio_configuration_service = self.container \
790798
.portfolio_configuration_service()
@@ -827,7 +835,7 @@ def check_data_completeness(
827835
self,
828836
strategies: List[TradingStrategy],
829837
backtest_date_range: BacktestDateRange,
830-
show_progress: bool = True
838+
show_progress: bool = False
831839
) -> Tuple[bool, Dict[str, Any]]:
832840
"""
833841
Function to check the data completeness for a set of strategies
@@ -926,7 +934,7 @@ def run_vector_backtests(
926934
snapshot_interval: SnapshotInterval = SnapshotInterval.DAILY,
927935
risk_free_rate: Optional[float] = None,
928936
skip_data_sources_initialization: bool = False,
929-
show_progress: bool = True,
937+
show_progress: bool = False,
930938
market: Optional[str] = None,
931939
initial_amount: float = None,
932940
trading_symbol: Optional[str] = None,
@@ -1335,7 +1343,7 @@ def run_backtests(
13351343
metadata: Optional[Dict[str, str]] = None,
13361344
backtest_storage_directory: Optional[Union[str, Path]] = None,
13371345
use_checkpoints: bool = False,
1338-
show_progress: bool = True,
1346+
show_progress: bool = False,
13391347
continue_on_error: bool = False,
13401348
window_filter_function: Optional[Callable] = None,
13411349
final_filter_function: Optional[Callable] = None,
@@ -1502,7 +1510,7 @@ def get_backtest_data(
15021510
self,
15031511
strategy: TradingStrategy,
15041512
backtest_date_range: BacktestDateRange,
1505-
show_progress: bool = True,
1513+
show_progress: bool = False,
15061514
fill_missing_data: bool = True,
15071515
) -> Dict[str, Any]:
15081516
"""
@@ -1611,7 +1619,7 @@ def run_backtest(
16111619
metadata: Optional[Dict[str, str]] = None,
16121620
backtest_storage_directory: Optional[Union[str, Path]] = None,
16131621
use_checkpoints: bool = False,
1614-
show_progress: bool = True,
1622+
show_progress: bool = False,
16151623
market: str = None,
16161624
trading_symbol: str = None,
16171625
fill_missing_data: bool = True,
@@ -1747,7 +1755,8 @@ def run_permutation_test(
17471755
initial_amount: float = 1000.0,
17481756
market: str = None,
17491757
trading_symbol: str = None,
1750-
risk_free_rate: Optional[float] = None
1758+
risk_free_rate: Optional[float] = None,
1759+
show_progress: bool = True
17511760
) -> BacktestPermutationTest:
17521761
"""
17531762
Run a permutation test for a given strategy over a specified
@@ -1781,6 +1790,8 @@ def run_permutation_test(
17811790
portfolio configuration is provided in the strategy. If not
17821791
provided, the first trading symbol found in the portfolio
17831792
configuration will be used.
1793+
show_progress (bool): Whether to show a progress bar during
1794+
the permutation test. Defaults to True.
17841795
17851796
Raises:
17861797
OperationalException: If the risk-free rate cannot be retrieved.
@@ -1791,16 +1802,11 @@ def run_permutation_test(
17911802
"""
17921803

17931804
if risk_free_rate is None:
1794-
logger.info("No risk free rate provided, retrieving it...")
1795-
risk_free_rate = get_risk_free_rate_us()
1796-
1797-
if risk_free_rate is None:
1798-
raise OperationalException(
1799-
"Could not retrieve risk free rate for backtest metrics."
1800-
"Please provide a risk free as an argument when running "
1801-
"your backtest or make sure you have an internet "
1802-
"connection"
1803-
)
1805+
logger.info(
1806+
"No risk free rate provided, defaulting to 0.027 "
1807+
"(2.7%%). Provide risk_free_rate to override."
1808+
)
1809+
risk_free_rate = 0.027
18041810

18051811
backtest_service = self.container.backtest_service()
18061812
data_provider_service = self.container.data_provider_service()
@@ -1812,7 +1818,8 @@ def run_permutation_test(
18121818
risk_free_rate=risk_free_rate,
18131819
market=market,
18141820
trading_symbol=trading_symbol,
1815-
use_checkpoints=False
1821+
use_checkpoints=False,
1822+
show_progress=show_progress
18161823
)
18171824
backtest_metrics = backtest.get_backtest_metrics(backtest_date_range)
18181825

@@ -1848,7 +1855,8 @@ def run_permutation_test(
18481855
for _ in tqdm(
18491856
range(number_of_permutations),
18501857
desc="Running Permutation Test",
1851-
colour="green"
1858+
colour="green",
1859+
disable=not show_progress
18521860
):
18531861
permutated_datasets = []
18541862
data_provider_service.reset()
@@ -1874,7 +1882,7 @@ def run_permutation_test(
18741882
dataframe=combi[1],
18751883
symbol=data_source.symbol,
18761884
market=data_source.market,
1877-
window_size=data_source.window_size,
1885+
warmup_window=data_source.warmup_window,
18781886
time_frame=data_source.time_frame,
18791887
data_provider_identifier=data_source
18801888
.data_provider_identifier,
@@ -1896,7 +1904,8 @@ def run_permutation_test(
18961904
skip_data_sources_initialization=True,
18971905
market=market,
18981906
trading_symbol=trading_symbol,
1899-
use_checkpoints=False
1907+
use_checkpoints=False,
1908+
show_progress=show_progress
19001909
)
19011910

19021911
# Add the results of the permuted backtest to the main backtest

investing_algorithm_framework/cli/validate_backtest_checkpoints.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
def validate_and_create_checkpoints(
1717
directory_path: str,
1818
output_file: str = None,
19-
verbose: bool = False
19+
verbose: bool = False,
20+
verbose_output_file: str = None
2021
) -> Dict[str, List[str]]:
2122
"""
2223
Validate a directory for backtest checkpoints and create a checkpoint file.
@@ -28,6 +29,9 @@ def validate_and_create_checkpoints(
2829
checkpoint file. If None, will create 'checkpoints.json'
2930
in the directory_path.
3031
verbose (bool): Whether to print detailed information.
32+
verbose_output_file (str, optional): Path to a file where verbose
33+
output will be written. If None, verbose output is printed
34+
to the console.
3135
3236
Returns:
3337
Dict[str, List[str]]: Dictionary mapping date range
@@ -42,13 +46,24 @@ def validate_and_create_checkpoints(
4246
if output_file is None:
4347
output_file = os.path.join(directory_path, "checkpoints.json")
4448

49+
# Setup echo function: write to file or console
50+
verbose_file_handle = None
51+
52+
if verbose_output_file is not None:
53+
verbose_file_handle = open(verbose_output_file, 'w')
54+
55+
def echo(msg):
56+
verbose_file_handle.write(msg + "\n")
57+
else:
58+
echo = click.echo
59+
4560
checkpoints = {}
4661
valid_backtests = 0
4762
invalid_dirs = 0
4863

4964
if verbose:
50-
click.echo(f"Scanning directory: {directory_path}")
51-
click.echo("-" * 60)
65+
echo(f"Scanning directory: {directory_path}")
66+
echo("-" * 60)
5267

5368
# Scan all subdirectories in the target directory
5469
for item in os.listdir(directory_path):
@@ -68,7 +83,7 @@ def validate_and_create_checkpoints(
6883

6984
if backtest.algorithm_id is None:
7085
if verbose:
71-
click.echo(
86+
echo(
7287
f"⚠️ Warning: {item} - No "
7388
f"algorithm_id found, skipping"
7489
)
@@ -94,7 +109,7 @@ def validate_and_create_checkpoints(
94109
)
95110

96111
if verbose:
97-
click.echo(
112+
echo(
98113
f"✓ {item} - Algorithm: "
99114
f"{backtest.algorithm_id}, "
100115
f"Range: {start_date} to {end_date}"
@@ -103,14 +118,14 @@ def validate_and_create_checkpoints(
103118
valid_backtests += 1
104119
else:
105120
if verbose:
106-
click.echo(
121+
echo(
107122
f"⚠️ Warning: {item} - No backtest runs found"
108123
)
109124
invalid_dirs += 1
110125

111126
except Exception as e:
112127
if verbose:
113-
click.echo(f"✗ {item} - Not a valid backtest: {str(e)}")
128+
echo(f"✗ {item} - Not a valid backtest: {str(e)}")
114129
invalid_dirs += 1
115130
continue
116131

@@ -124,21 +139,24 @@ def validate_and_create_checkpoints(
124139

125140
# Print summary
126141
if verbose:
127-
click.echo("-" * 60)
142+
echo("-" * 60)
128143

129-
click.echo("\n✓ Checkpoint validation complete!")
130-
click.echo(f" Valid backtests found: {valid_backtests}")
131-
click.echo(f" Invalid/skipped directories: {invalid_dirs}")
132-
click.echo(f" Total date ranges: {len(checkpoints)}")
133-
click.echo(f" Checkpoint file saved to: {output_file}\n")
144+
echo("\n✓ Checkpoint validation complete!")
145+
echo(f" Valid backtests found: {valid_backtests}")
146+
echo(f" Invalid/skipped directories: {invalid_dirs}")
147+
echo(f" Total date ranges: {len(checkpoints)}")
148+
echo(f" Checkpoint file saved to: {output_file}\n")
134149

135150
if verbose and checkpoints:
136-
click.echo("Date ranges found:")
151+
echo("Date ranges found:")
137152
for date_range_key, algorithm_ids in sorted(checkpoints.items()):
138-
click.echo(
153+
echo(
139154
f" {date_range_key}: {len(algorithm_ids)} algorithm(s)"
140155
)
141156

157+
if verbose_file_handle is not None:
158+
verbose_file_handle.close()
159+
142160
return checkpoints
143161

144162

0 commit comments

Comments
 (0)