Skip to content

Commit

Permalink
Update backtest infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
MDUYN committed Oct 15, 2023
1 parent 249fd4c commit 2c56b35
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 94 deletions.
23 changes: 12 additions & 11 deletions examples/backtesting/backtesting_with_ccxt_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,22 @@ def apply_strategy(

# No resource directory specified, so an in-memory database will be used
app = create_app({RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve()})
app.add_portfolio_configuration(
PortfolioConfiguration(
market="bitvavo",
api_key="<your_api_key>",
secret_key="<your_secret_key>",
trading_symbol="EUR"
)
)
# app.add_portfolio_configuration(
# PortfolioConfiguration(
# market="bitvavo",
# api_key="<your_api_key>",
# secret_key="<your_secret_key>",
# trading_symbol="EUR"
# )
# )
app.add_strategy(MyTradingStrategy)

if __name__ == "__main__":
report = app.backtest(
start_datetime=datetime.utcnow() - timedelta(days=1),
end_datetime=datetime.utcnow(),
source="ccxt"
start_date=datetime.utcnow() - timedelta(days=1),
end_date=datetime.utcnow(),
unallocated=1000,
trading_symbol="EUR"
)
# pretty_print_report(report)
# print(report.get_orders())
Expand Down
6 changes: 5 additions & 1 deletion investing_algorithm_framework/app/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(
order_service,
market_service,
market_data_service,
strategy_orchestrator_service
strategy_orchestrator_service,
):
self.portfolio_service = portfolio_service
self.position_service = position_service
Expand Down Expand Up @@ -400,6 +400,10 @@ def add_strategies(self, strategies):
def add_tasks(self, tasks):
self.strategy_orchestrator_service.add_tasks(tasks)

@property
def strategies(self):
return self.strategy_orchestrator_service.get_strategies()

def get_allocated(self, market=None, identifier=None) -> float:

if self.portfolio_configuration_service.count() > 1 \
Expand Down
127 changes: 97 additions & 30 deletions investing_algorithm_framework/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from investing_algorithm_framework.app.web import create_flask_app
from investing_algorithm_framework.domain import DATABASE_NAME, TimeUnit, \
DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \
SQLALCHEMY_DATABASE_URI, Config, OperationalException
SQLALCHEMY_DATABASE_URI, Config, OperationalException, \
PortfolioConfiguration
from investing_algorithm_framework.infrastructure import setup_sqlalchemy, \
create_all_tables

Expand All @@ -35,18 +36,20 @@ def __init__(self, config=None, stateless=False, web=False):
self._strategies = []
self._tasks = []

def initialize(self):
def initialize(self, backtest=False):

if self._web:
self._initialize_web()
elif self._stateless:
self._initialize_stateless()
if backtest:
self._initialize_standard(backtest=True)
else:
self._initialize_standard()
if self._web:
self._initialize_web()
elif self._stateless:
self._initialize_stateless()
else:
self._initialize_standard()

setup_sqlalchemy(self)
create_all_tables()

self.algorithm = self.container.algorithm()
self.algorithm.add_strategies(self.strategies)
self.algorithm.add_tasks(self.tasks)
Expand Down Expand Up @@ -294,32 +297,44 @@ def create_portfolios(self):

for portfolio_configuration in \
portfolio_configuration_service.get_all():
market_service.initialize(portfolio_configuration)

if portfolio_repository.exists(
{"identifier": portfolio_configuration.identifier}
):
continue
if portfolio_configuration.backtest:
creation_data = {
"unallocated": portfolio_configuration.max_unallocated,
"identifier": portfolio_configuration.identifier,
"trading_symbol": portfolio_configuration.trading_symbol,
"market": portfolio_configuration.market,
}
unallocated = portfolio_configuration.max_unallocated
else:
market_service.initialize(portfolio_configuration)

balances = market_service.get_balance()
if portfolio_configuration.trading_symbol.upper() not in balances:
raise OperationalException(
f"Trading symbol balance not available "
f"in portfolio on market {portfolio_configuration.market}"
if portfolio_repository.exists(
{"identifier": portfolio_configuration.identifier}
):
continue

balances = market_service.get_balance()

if portfolio_configuration.trading_symbol.upper() not in balances:
raise OperationalException(
f"Trading symbol balance not available "
f"in portfolio on market {portfolio_configuration.market}"
)

unallocated = float(
balances[portfolio_configuration.trading_symbol.upper()]
["free"]
)

unallocated = float(
balances[portfolio_configuration.trading_symbol.upper()]
["free"]
)
portfolio_repository.create(
{
creation_data = {
"unallocated": unallocated,
"identifier": portfolio_configuration.identifier,
"trading_symbol": portfolio_configuration.trading_symbol,
"market": portfolio_configuration.market,
}
)

portfolio_repository.create(creation_data)
portfolio = portfolio_repository.find(
{"identifier": portfolio_configuration.identifier}
)
Expand Down Expand Up @@ -355,23 +370,51 @@ def _initialize_web(self):
def _initialize_stateless(self):
self._config[SQLALCHEMY_DATABASE_URI] = "sqlite://"

def _initialize_standard(self):
def _initialize_standard(self, backtest=False):
resource_dir = self._config[RESOURCE_DIRECTORY]

if resource_dir is None:
self._config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
else:
if backtest:

if resource_dir is None:
raise OperationalException(
"Resource directory is not specified. "
"A resource directory is required for running a backtest."
)

resource_dir = self._create_resource_directory_if_not_exists()
self._config[DATABASE_DIRECTORY_PATH] = os.path.join(
resource_dir, "databases"
)
self._config[DATABASE_NAME] = "prod-database.sqlite3"
self._config[DATABASE_NAME] = "backtest-database.sqlite3"
database_path = os.path.join(
self._config[DATABASE_DIRECTORY_PATH],
self._config[DATABASE_NAME]
)

if os.path.exists(database_path):
os.remove(database_path)

self._config[SQLALCHEMY_DATABASE_URI] = \
"sqlite:///" + os.path.join(
self._config[DATABASE_DIRECTORY_PATH],
self._config[DATABASE_NAME]
)
self._create_database_if_not_exists()
else:
if resource_dir is None:
self._config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
else:
resource_dir = self._create_resource_directory_if_not_exists()
self._config[DATABASE_DIRECTORY_PATH] = os.path.join(
resource_dir, "databases"
)
self._config[DATABASE_NAME] = "prod-database.sqlite3"
self._config[SQLALCHEMY_DATABASE_URI] = \
"sqlite:///" + os.path.join(
self._config[DATABASE_DIRECTORY_PATH],
self._config[DATABASE_NAME]
)
self._create_database_if_not_exists()

def _create_resource_directory_if_not_exists(self):
if self._stateless:
Expand Down Expand Up @@ -422,3 +465,27 @@ def _create_database_if_not_exists(self):

def get_portfolio_configurations(self):
return self.algorithm.get_portfolio_configurations()

def backtest(self, start_date, end_date, unallocated=None, trading_symbol=None):

# Add custom portfolio configuration
# if trading symbol and unallocated are specified
if unallocated is not None and trading_symbol is not None:
portfolio_configuration_service = self.container \
.portfolio_configuration_service()
portfolio_configuration_service.clear()
portfolio_configuration_service.add(PortfolioConfiguration(
market="backtest",
trading_symbol=trading_symbol,
api_key=None,
secret_key=None,
track_from=None,
identifier="backtest",
max_unallocated=unallocated,
backtest=True
))
print(portfolio_configuration_service.get_all())

self.initialize(backtest=True)
backtest_service = self.container.backtest_service()
return backtest_service.backtest(self.algorithm, start_date, end_date)
9 changes: 5 additions & 4 deletions investing_algorithm_framework/dependency_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class DependencyContainer(containers.DeclarativeContainer):
StrategyOrchestratorService,
market_data_service=market_data_service
)
backtest_service = providers.Factory(
BackTestService,
market_data_service=market_data_service,
)
algorithm = providers.Factory(
Algorithm,
portfolio_configuration_service=portfolio_configuration_service,
Expand All @@ -78,7 +82,4 @@ class DependencyContainer(containers.DeclarativeContainer):
market_data_service=market_data_service,
position_cost_service=position_cost_service,
)
backtest_service = providers.Factory(
BackTestService,
market_data_service=market_data_service,
)

16 changes: 15 additions & 1 deletion investing_algorithm_framework/domain/models/backtest_profile.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class BacktestProfile:
from .base_model import BaseModel
from datetime import datetime

class BacktestProfile(BaseModel):

def __init__(self, strategy_id, interval, time_unit):
self._strategy_id = strategy_id
Expand Down Expand Up @@ -64,3 +67,14 @@ def backtest_end_date(self, value):
@backtest_index_date.setter
def backtest_index_date(self, value):
self._backtest_index_date = value

def __repr__(self):
return self.repr(
strategy_id=self._strategy_id,
start_date=self.backtest_start_date,
end_date=self.backtest_end_date,
backtest_index_date=self.backtest_index_date,
start_date_data=self.backtest_start_date_data,
time_unit=self.time_unit,
interval=self.interval
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(
track_from=None,
identifier=None,
max_unallocated=-1,
backtest=False
):
self._market = market
self._api_key = api_key
Expand All @@ -23,6 +24,7 @@ def __init__(
self._trading_symbol = trading_symbol.upper()
self._identifier = identifier
self._max_unallocated = max_unallocated
self._backtest = backtest

if self.identifier is None:
self._identifier = market.lower()
Expand All @@ -35,12 +37,12 @@ def __init__(
"Portfolio configuration requires a trading symbol"
)

if self.api_key is None:
if self.api_key is None and not self._backtest:
raise ImproperlyConfigured(
"Portfolio configuration requires an api key"
)

if self.secret_key is None:
if self.secret_key is None and not self._backtest:
raise ImproperlyConfigured(
"Portfolio configuration requires a secret key"
)
Expand Down Expand Up @@ -81,6 +83,10 @@ def max_unallocated(self):
def has_unallocated_limit(self):
return self.max_unallocated != -1

@property
def backtest(self):
return self._backtest

def __repr__(self):
return self.repr(
market=self.market,
Expand Down
Loading

0 comments on commit 2c56b35

Please sign in to comment.