diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1cb29e00..c35c9b4e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,6 +9,7 @@ on:
paths:
- 'investing_algorithm_framework/**'
- 'tests/**'
+ - '.github/workflows/test.yml'
jobs:
linting:
@@ -35,11 +36,11 @@ jobs:
flake8 ./investing_algorithm_framework
test:
needs: linting
- timeout-minutes: 25
+ timeout-minutes: 40
strategy:
fail-fast: true
matrix:
- os: [ "ubuntu-latest", "macos-latest" ]
+ os: [ "ubuntu-latest", "macos-latest", "windows-latest" ]
python-version: [ "3.10", "3.11" ]
defaults:
run:
@@ -101,14 +102,12 @@ jobs:
#----------------------------------------------
- name: Run tests
run: |
- source $VENV
- coverage run -m unittest discover -s tests
+ poetry run coverage run -m unittest discover -s tests
- name: Generate coverage report
if: always()
continue-on-error: true
run: |
- source $VENV
- coverage xml
+ poetry run coverage xml
#----------------------------------------------
# upload coverage stats
#----------------------------------------------
diff --git a/README.md b/README.md
index ce18e485..dac3bfc9 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
- Create trading strategies. Compare them side by side. Pick the best one. 🚀
+ Create trading strategies. Compare them side by side. Pick the best one and Deploy 🚀
@@ -61,7 +61,7 @@
Most quant frameworks stop at "here's your backtest result." You get a number, maybe a chart, and then you're on your own figuring out which strategy is actually better.
-This framework is built around the full loop: **create strategies → backtest them → compare them in a single report → deploy the winner.** It generates a self-contained HTML dashboard that lets you rank, filter, and visually compare every strategy you've tested — all in one view, no notebooks required.
+This framework is built around the full loop: **create strategies → vector backtest for signals analysis → compare them in a single report → event backtest the most promising strategies → deploy the winner.** It generates a self-contained HTML dashboard that lets you rank, filter, and visually compare every strategy you've tested — all in one view, no notebooks required.
@@ -69,6 +69,10 @@ This framework is built around the full loop: **create strategies → backtest t
- 📊 **30+ Metrics** — CAGR, Sharpe, Sortino, Calmar, VaR, CVaR, Max DD, Recovery & more
+- ⚡ **Vector Backtesting for Signal Analysis** — Quickly test your strategy logic on historical data to see how signals would have behaved before committing to full event-driven backtests
+- 🏃 **Event-Driven Backtesting** — Once promising strategies are identified via vector backtests, run full event-driven backtests to simulate realistic execution and portfolio management
+- 🔀 **Permutation Testing / Monte Carlo Simulations** — Assess the statistical robustness of your strategies by running them across randomized market scenarios to see how often your results could occur by chance
+- 🚀 **Deployment** — Once the best strategy is identified through backtesting and comparison, deploy it to production locally or in the cloud (AWS Lambda / Azure Functions) to start live trading
- ⚔️ **Multi-Strategy Comparison** — Rank, filter & compare strategies in a single interactive report
- 🪟 **Multi-Window Robustness** — Test across different time periods with window coverage analysis
- 📈 **Equity & Drawdown Charts** — Overlay equity curves, rolling Sharpe, drawdown & return distributions
diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py
index b779acf4..e8b9749f 100644
--- a/investing_algorithm_framework/app/app.py
+++ b/investing_algorithm_framework/app/app.py
@@ -368,6 +368,29 @@ def initialize_storage(self, remove_database_if_exists: bool = False):
logger.info(
f"Removing existing database at {database_path}"
)
+
+ # Dispose the existing engine to release file locks
+ # (required on Windows where locks are mandatory)
+ from investing_algorithm_framework.infrastructure.database \
+ import Session
+ from sqlalchemy.orm import close_all_sessions
+ close_all_sessions()
+ bind = Session.kw.get("bind")
+
+ if bind is not None:
+
+ try:
+ conn = bind.connect()
+ conn.invalidate()
+ conn.close()
+ except Exception:
+ pass
+
+ bind.dispose()
+
+ import gc
+ gc.collect()
+
os.remove(database_path)
# Create the sqlalchemy database uri
diff --git a/investing_algorithm_framework/cli/validate_backtest_checkpoints.py b/investing_algorithm_framework/cli/validate_backtest_checkpoints.py
index 3ff75ddc..5e591929 100644
--- a/investing_algorithm_framework/cli/validate_backtest_checkpoints.py
+++ b/investing_algorithm_framework/cli/validate_backtest_checkpoints.py
@@ -50,7 +50,7 @@ def validate_and_create_checkpoints(
verbose_file_handle = None
if verbose_output_file is not None:
- verbose_file_handle = open(verbose_output_file, 'w')
+ verbose_file_handle = open(verbose_output_file, 'w', encoding='utf-8')
def echo(msg):
verbose_file_handle.write(msg + "\n")
diff --git a/investing_algorithm_framework/infrastructure/database/__init__.py b/investing_algorithm_framework/infrastructure/database/__init__.py
index 7e6a29b0..dd8552d1 100644
--- a/investing_algorithm_framework/infrastructure/database/__init__.py
+++ b/investing_algorithm_framework/infrastructure/database/__init__.py
@@ -1,5 +1,5 @@
from .sql_alchemy import Session, setup_sqlalchemy, SQLBaseModel, \
- create_all_tables, clear_db, SqliteDecimal
+ create_all_tables, clear_db, teardown_sqlalchemy, SqliteDecimal
__all__ = [
"Session",
@@ -7,5 +7,6 @@
"SQLBaseModel",
"create_all_tables",
"clear_db",
+ "teardown_sqlalchemy",
"SqliteDecimal"
]
diff --git a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
index ade72c2c..e1bfbd8d 100644
--- a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
+++ b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
@@ -3,7 +3,7 @@
from sqlalchemy import create_engine, StaticPool, String
from sqlalchemy import inspect
-from sqlalchemy.orm import DeclarativeBase, sessionmaker
+from sqlalchemy.orm import DeclarativeBase, sessionmaker, close_all_sessions
from sqlalchemy import TypeDecorator
from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI, \
@@ -83,6 +83,34 @@ def create_all_tables():
SQLBaseModel.metadata.create_all(bind=Session().bind)
+def teardown_sqlalchemy():
+ """
+ Dispose the engine and close all sessions to release file locks.
+ This is essential on Windows where file locks are mandatory and
+ prevent deletion of SQLite database files while connections are open.
+ """
+ close_all_sessions()
+ bind = Session.kw.get("bind")
+
+ if bind is not None:
+
+ # StaticPool._close_connection() is a no-op, so
+ # engine.dispose() alone won't close the underlying DBAPI
+ # connection. Use invalidate() which bypasses the pool's
+ # _close_connection and calls dialect.do_close() directly,
+ # ensuring the sqlite3 file lock is released on Windows.
+ try:
+ conn = bind.connect()
+ conn.invalidate()
+ conn.close()
+ except Exception:
+ pass
+
+ bind.dispose()
+
+ Session.configure(bind=None)
+
+
from sqlalchemy import event
from sqlalchemy.orm import mapper
from datetime import timezone
@@ -98,6 +126,7 @@ def clear_db(db_uri):
Returns:
None
"""
+ engine = None
# Drop all tables before deleting file
try:
engine = create_engine(db_uri)
@@ -107,12 +136,9 @@ def clear_db(db_uri):
SQLBaseModel.metadata.drop_all(bind=engine)
except Exception as e:
logger.error(f"Error dropping tables: {e}")
-
- # # Clear mappers (if using classical mappings)
- # try:
- # clear_mappers()
- # except Exception:
- # pass # ignore if not needed
+ finally:
+ if engine is not None:
+ engine.dispose()
@event.listens_for(mapper, "load")
diff --git a/tests/app/algorithm/test_run_strategy.py b/tests/app/algorithm/test_run_strategy.py
index 694d3aa6..37c82951 100644
--- a/tests/app/algorithm/test_run_strategy.py
+++ b/tests/app/algorithm/test_run_strategy.py
@@ -8,6 +8,8 @@
from investing_algorithm_framework import create_app, TradingStrategy, \
TimeUnit, PortfolioConfiguration, RESOURCE_DIRECTORY, Algorithm, \
MarketCredential
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
from tests.resources import random_string, OrderExecutorTest, \
PortfolioProviderTest
@@ -73,6 +75,7 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
+ teardown_sqlalchemy()
database_dir = os.path.join(self.resource_dir, "databases")
if os.path.exists(database_dir):
shutil.rmtree(database_dir, ignore_errors=True)
diff --git a/tests/app/algorithm/test_trade_price_update.py b/tests/app/algorithm/test_trade_price_update.py
index 5cbebddc..c3f03a8e 100644
--- a/tests/app/algorithm/test_trade_price_update.py
+++ b/tests/app/algorithm/test_trade_price_update.py
@@ -8,6 +8,8 @@
TimeUnit, PortfolioConfiguration, RESOURCE_DIRECTORY, \
MarketCredential, DataSource, INDEX_DATETIME, DataType, \
CSVOHLCVDataProvider, BacktestDateRange
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
from tests.resources import random_string, \
PortfolioProviderTest, OrderExecutorTest
@@ -55,6 +57,7 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
+ teardown_sqlalchemy()
database_dir = os.path.join(self.resource_dir, "databases")
if os.path.exists(database_dir):
shutil.rmtree(database_dir, ignore_errors=True)
diff --git a/tests/app/backtesting/test_run_backtest.py b/tests/app/backtesting/test_run_backtest.py
index 584c25f9..80741e16 100644
--- a/tests/app/backtesting/test_run_backtest.py
+++ b/tests/app/backtesting/test_run_backtest.py
@@ -8,6 +8,8 @@
from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \
TradingStrategy, PortfolioConfiguration, TimeUnit, Algorithm, \
BacktestDateRange
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
class TestStrategy(TradingStrategy):
@@ -47,12 +49,13 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
+ teardown_sqlalchemy()
if os.path.exists(self.backtest_databases_directory):
- shutil.rmtree(self.backtest_databases_directory)
+ shutil.rmtree(self.backtest_databases_directory, ignore_errors=True)
if os.path.exists(self.backtest_report_directory):
- shutil.rmtree(self.backtest_report_directory)
+ shutil.rmtree(self.backtest_report_directory, ignore_errors=True)
def test_report_creation(self):
app = create_app(
diff --git a/tests/app/test_backtesting.py b/tests/app/test_backtesting.py
index 3e7beda0..a9241151 100644
--- a/tests/app/test_backtesting.py
+++ b/tests/app/test_backtesting.py
@@ -18,6 +18,8 @@
from investing_algorithm_framework import TradingStrategy, Algorithm, \
create_app, RESOURCE_DIRECTORY, PortfolioConfiguration, \
BacktestDateRange, TimeUnit
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI
@@ -70,13 +72,14 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
+ teardown_sqlalchemy()
for path in (
os.path.join(self.resource_dir, "databases"),
self.backtest_databases_dir,
self.backtest_reports_dir,
):
if os.path.exists(path):
- shutil.rmtree(path)
+ shutil.rmtree(path, ignore_errors=True)
# ---------------------------------------------------------------------------
@@ -253,4 +256,3 @@ def test_run_backtests(self):
risk_free_rate=0.027
)
self.assertEqual(3, len(reports))
-
diff --git a/tests/app/test_eventloop.py b/tests/app/test_eventloop.py
index eaec81ee..5082d1f3 100644
--- a/tests/app/test_eventloop.py
+++ b/tests/app/test_eventloop.py
@@ -240,7 +240,7 @@ def tearDown(self) -> None:
)
if os.path.exists(databases_directory):
- shutil.rmtree(databases_directory)
+ shutil.rmtree(databases_directory, ignore_errors=True)
if os.path.exists(backtest_databases_directory):
- shutil.rmtree(backtest_databases_directory)
+ shutil.rmtree(backtest_databases_directory, ignore_errors=True)
diff --git a/tests/app/test_run.py b/tests/app/test_run.py
index bf248f01..1c3ecff1 100644
--- a/tests/app/test_run.py
+++ b/tests/app/test_run.py
@@ -15,6 +15,8 @@
from investing_algorithm_framework import create_app, TradingStrategy, \
TimeUnit, PortfolioConfiguration, RESOURCE_DIRECTORY, \
Algorithm, MarketCredential
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
from tests.resources import random_string, OrderExecutorTest, \
PortfolioProviderTest
@@ -105,6 +107,7 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
+ teardown_sqlalchemy()
for subdir in ("databases", "backtest_databases"):
path = os.path.join(self.resource_dir, subdir)
if os.path.exists(path):
diff --git a/tests/app/test_start.py b/tests/app/test_start.py
index fd2fb242..37573616 100644
--- a/tests/app/test_start.py
+++ b/tests/app/test_start.py
@@ -7,6 +7,8 @@
from investing_algorithm_framework import create_app, TradingStrategy, \
TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \
MarketCredential
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
from tests.resources import OrderExecutorTest, PortfolioProviderTest
@@ -106,6 +108,7 @@ def setUp(self) -> None:
)
def tearDown(self):
+ teardown_sqlalchemy()
for subdir in ("databases", "backtest_databases"):
path = os.path.join(self.resource_dir, subdir)
if os.path.exists(path):
diff --git a/tests/app/web/controllers/order_controller/test_list_orders.py b/tests/app/web/controllers/order_controller/test_list_orders.py
index 41138556..bac0afd2 100644
--- a/tests/app/web/controllers/order_controller/test_list_orders.py
+++ b/tests/app/web/controllers/order_controller/test_list_orders.py
@@ -1,5 +1,6 @@
import json
import os
+import shutil
from investing_algorithm_framework import PortfolioConfiguration, \
MarketCredential
@@ -33,13 +34,7 @@ def tearDown(self) -> None:
)
if os.path.exists(database_dir):
- for root, dirs, files in os.walk(database_dir, topdown=False):
- for name in files:
- os.remove(os.path.join(root, name))
- for name in dirs:
- os.rmdir(os.path.join(root, name))
-
- os.rmdir(database_dir)
+ shutil.rmtree(database_dir, ignore_errors=True)
def test_list_portfolios(self):
self.iaf_app.add_strategy(StrategyOne)
diff --git a/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py b/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
index 003e367b..039f86f0 100644
--- a/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
+++ b/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
@@ -1,5 +1,6 @@
import json
import os
+import shutil
from investing_algorithm_framework import MarketCredential, \
PortfolioConfiguration, DataSource, INDEX_DATETIME
@@ -33,13 +34,7 @@ def tearDown(self) -> None:
)
if os.path.exists(database_dir):
- for root, dirs, files in os.walk(database_dir, topdown=False):
- for name in files:
- os.remove(os.path.join(root, name))
- for name in dirs:
- os.rmdir(os.path.join(root, name))
-
- os.rmdir(database_dir)
+ shutil.rmtree(database_dir, ignore_errors=True)
def test_list_portfolios(self):
strategy = StrategyOne()
diff --git a/tests/app/web/controllers/position_controller/test_list_positions.py b/tests/app/web/controllers/position_controller/test_list_positions.py
index ea1120c9..00a04a3c 100644
--- a/tests/app/web/controllers/position_controller/test_list_positions.py
+++ b/tests/app/web/controllers/position_controller/test_list_positions.py
@@ -1,5 +1,6 @@
import json
import os
+import shutil
from investing_algorithm_framework import PortfolioConfiguration, \
MarketCredential
@@ -33,13 +34,7 @@ def tearDown(self) -> None:
)
if os.path.exists(database_dir):
- for root, dirs, files in os.walk(database_dir, topdown=False):
- for name in files:
- os.remove(os.path.join(root, name))
- for name in dirs:
- os.rmdir(os.path.join(root, name))
-
- os.rmdir(database_dir)
+ shutil.rmtree(database_dir, ignore_errors=True)
def test_list_portfolios(self):
self.iaf_app.add_strategy(StrategyOne)
diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json
index 9ab210ec..fdb062a0 100644
--- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json
+++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json
@@ -1,3 +1,3 @@
{
- "algorithm_id": "TestStrategy"
+ "algorithm_id": "BacktestTestStrategy"
}
\ No newline at end of file
diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json
index 841ff1f7..7686a8c8 100644
--- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json
+++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json
@@ -1 +1 @@
-{"backtest_start_date": "2023-12-01 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-02 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-01T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-23 19:17:03", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []}
\ No newline at end of file
+{"backtest_start_date": "2023-12-01 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-02 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-01T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-26 14:39:23", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": [], "recorded_values": {}}
\ No newline at end of file
diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json
index 462b897c..d2baec0c 100644
--- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json
+++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json
@@ -1 +1 @@
-{"backtest_start_date": "2023-12-02 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-03 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-03T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-23 23:18:14", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []}
\ No newline at end of file
+{"backtest_start_date": "2023-12-02 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-03 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-03T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-26 14:38:58", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": [], "recorded_values": {}}
\ No newline at end of file
diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py
index 912b1d26..d25afb1b 100644
--- a/tests/resources/test_base.py
+++ b/tests/resources/test_base.py
@@ -11,8 +11,8 @@
MarketCredential
from investing_algorithm_framework.domain import RESOURCE_DIRECTORY, \
ENVIRONMENT, Environment, BACKTEST_DATA_DIRECTORY_NAME
-from investing_algorithm_framework.infrastructure.database import Session
-from sqlalchemy.orm import close_all_sessions
+from investing_algorithm_framework.infrastructure.database import \
+ Session, teardown_sqlalchemy
from tests.resources.stubs import OrderExecutorTest, PortfolioProviderTest
logger = logging.getLogger(__name__)
@@ -83,7 +83,7 @@ def setUp(self) -> None:
if self.initialize:
self.app.initialize_config()
- self.app.initialize_storage()
+ self.app.initialize_storage(remove_database_if_exists=True)
self.app.initialize_services()
self.app.initialize_portfolios()
@@ -121,8 +121,8 @@ def _remove_database_dir(resource_dir):
shutil.rmtree(database_dir, ignore_errors=True)
def _cleanup_database(self):
- """Close SQLAlchemy sessions and remove database files."""
- close_all_sessions()
+ """Close SQLAlchemy sessions, dispose engine, and remove db files."""
+ teardown_sqlalchemy()
self._remove_database_dir(self.resource_directory)
def remove_database(self):
@@ -215,7 +215,7 @@ def create_app(self):
if self.initialize:
self.iaf_app.initialize_config()
- self.iaf_app.initialize_storage()
+ self.iaf_app.initialize_storage(remove_database_if_exists=True)
self.iaf_app.initialize_services()
self.iaf_app.initialize_portfolios()
@@ -246,7 +246,7 @@ def create_app(self):
return self.iaf_app._flask_app
def tearDown(self) -> None:
- close_all_sessions()
+ teardown_sqlalchemy()
database_dir = os.path.join(self.resource_directory, "databases")
if os.path.exists(database_dir):
shutil.rmtree(database_dir, ignore_errors=True)
diff --git a/tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2021-12-15 08:00:00_2023-12-31 00:00:00.csv b/tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2021-12-15-08-00_2023-12-31-00-00.csv
similarity index 100%
rename from tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2021-12-15 08:00:00_2023-12-31 00:00:00.csv
rename to tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2021-12-15-08-00_2023-12-31-00-00.csv
diff --git a/tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2023-08-11 16:00:00_2023-12-02 00:00:00.csv b/tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2023-08-11-16-00_2023-12-02-00-00.csv
similarity index 100%
rename from tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2023-08-11 16:00:00_2023-12-02 00:00:00.csv
rename to tests/resources/test_data/ohlcv/OHLCV_DOT-EUR_BITVAVO_2h_2023-08-11-16-00_2023-12-02-00-00.csv
diff --git a/tests/scenarios/vectorized_backtests/test_metadata_preservation.py b/tests/scenarios/vectorized_backtests/test_metadata_preservation.py
index 377a1a78..1f2e9576 100644
--- a/tests/scenarios/vectorized_backtests/test_metadata_preservation.py
+++ b/tests/scenarios/vectorized_backtests/test_metadata_preservation.py
@@ -8,6 +8,9 @@
from typing import Dict, Any
from unittest import TestCase
+from investing_algorithm_framework.infrastructure.database import \
+ teardown_sqlalchemy
+
import pandas as pd
from pyindicators import ema, rsi, crossover, crossunder
@@ -424,5 +427,6 @@ def test_metadata_preserved_multiple_strategies(self):
def tearDown(self):
# Clean up storage directory after tests
+ teardown_sqlalchemy()
if os.path.exists(self.backtest_storage_dir):
- shutil.rmtree(self.backtest_storage_dir)
+ shutil.rmtree(self.backtest_storage_dir, ignore_errors=True)
diff --git a/tests/scenarios/vectorized_backtests/test_run_vector_backtests.py b/tests/scenarios/vectorized_backtests/test_run_vector_backtests.py
index dfec0503..6ea7b4c2 100644
--- a/tests/scenarios/vectorized_backtests/test_run_vector_backtests.py
+++ b/tests/scenarios/vectorized_backtests/test_run_vector_backtests.py
@@ -274,7 +274,7 @@ def test_run(self):
# Clean up any existing storage directory
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
strategies = []
for param_set in param_variations:
diff --git a/tests/scenarios/vectorized_backtests/test_use_backtest_storage_directory.py b/tests/scenarios/vectorized_backtests/test_use_backtest_storage_directory.py
index aa45ed10..18c3724c 100644
--- a/tests/scenarios/vectorized_backtests/test_use_backtest_storage_directory.py
+++ b/tests/scenarios/vectorized_backtests/test_use_backtest_storage_directory.py
@@ -280,7 +280,7 @@ def test_run_with_backtest_storage_directory(self):
# Clean up any existing storage directory
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
param_set = {
"rsi_time_frame": "2h",
@@ -427,7 +427,7 @@ def test_run_backtests_with_storage_directory(self):
# Clean up any existing storage directory
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
start_time = time.time()
@@ -475,7 +475,7 @@ def tearDown(self):
resource_directory, "backtest_reports_for_testing", "temp_storage"
)
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
def test_preexisting_backtests_not_included_in_new_run(self):
"""
@@ -519,7 +519,7 @@ def test_preexisting_backtests_not_included_in_new_run(self):
# Clean up any existing storage directory
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
# ===== FIRST RUN: Run backtests for first set of strategies =====
app1 = create_app(name="FirstRun", config=config)
@@ -693,7 +693,7 @@ def test_preexisting_backtests_not_included_in_new_run(self):
# Clean up
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
def test_preexisting_backtests_not_included_with_final_filter(self):
"""
@@ -735,7 +735,7 @@ def test_preexisting_backtests_not_included_with_final_filter(self):
)
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
# ===== FIRST RUN: Create pre-existing backtests =====
app1 = create_app(name="FirstRun", config=config)
@@ -896,4 +896,4 @@ def tracking_final_filter(backtests):
# Clean up
if os.path.exists(backtest_storage_dir):
- shutil.rmtree(backtest_storage_dir)
+ shutil.rmtree(backtest_storage_dir, ignore_errors=True)
diff --git a/tests/scenarios/vectorized_backtests/test_with_window_filter_function.py b/tests/scenarios/vectorized_backtests/test_with_window_filter_function.py
index 6e5fb432..22594174 100644
--- a/tests/scenarios/vectorized_backtests/test_with_window_filter_function.py
+++ b/tests/scenarios/vectorized_backtests/test_with_window_filter_function.py
@@ -627,4 +627,4 @@ def tearDown(self) -> None:
)
if os.path.exists(temp_storage_dir):
import shutil
- shutil.rmtree(temp_storage_dir)
+ shutil.rmtree(temp_storage_dir, ignore_errors=True)