diff --git a/.flake8 b/.flake8
index 5a02cc1..9019a79 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,5 @@
[flake8]
exclude =
investing_algorithm_framework/domain/utils/backtesting.py
+ investing_algorithm_framework/infrastructure/database/sql_alchemy.py
examples
\ No newline at end of file
diff --git a/README.md b/README.md
index 34c1688..f199d6d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,21 @@
+
+
+
+
+
+ Rapidly build and deploy quantitative strategies and trading bots
+
+
+
+
+ View Docs
+ Getting Started
+
+
+---
+
[](https://github.com/coding-kitties/investing-algorithm-framework/actions/workflows/publish.yml)
[](https://github.com/coding-kitties/investing-algorithm-framework/actions/workflows/test.yml)
@@ -6,10 +24,6 @@
[](https://github.com/SeaQL/sea-orm/stargazers/) If you like what we do, consider starring, sharing and contributing!
-# [Investing Algorithm Framework](https://github.com/coding-kitties/investing-algorithm-framework)
-
-The Investing Algorithm Framework is a Python framework that enables swift and elegant development of trading bots.
-
## Sponsors
@@ -22,7 +36,7 @@ The Investing Algorithm Framework is a Python framework that enables swift and e
## Features and planned features:
-- [x] **Based on Python 3.9+**: Windows, macOS and Linux.
+- [x] **Based on Python 3.10+**: Windows, macOS and Linux.
- [x] **Documentation**: [Documentation](https://investing-algorithm-framework.com)
- [x] **Persistence of portfolios, orders, positions and trades**: Persistence is achieved through sqlite.
- [x] **Limit orders**: Create limit orders for buying and selling.
@@ -33,7 +47,7 @@ The Investing Algorithm Framework is a Python framework that enables swift and e
- [x] **Live trading**: Live trading.
- [x] **Backtesting and performance analysis reports** [example](./examples/backtest_example)
- [x] **Backtesting multiple algorithms with different backtest date ranges** [example](./examples/backtests_example)
-- [x] **Backtest comparison and experiments**: Compare multiple backtests and run experiments.
+- [x] **Backtesting and results evaluation**: Compare multiple backtests and run experiments. Save and load backtests. Save strategies as part of the backtest. [docs](https://investing-algorithm-framework.com/Getting%20Started/backtesting)
- [x] **Order execution**: Currently support for a wide range of crypto exchanges through [ccxt](https://github.com/ccxt/ccxt) (Support for traditional asset brokers is planned).
- [x] **Web API**: Rest API for interacting with your deployed trading bot
- [x] **PyIndicators**: Works natively with [PyIndicators](https://github.com/coding-kitties/PyIndicators) for technical analysis on your Pandas and Polars dataframes.
@@ -45,6 +59,38 @@ The Investing Algorithm Framework is a Python framework that enables swift and e
- [ ] **AWS Lambda support (Planned)**: Stateless running for cloud function deployments in AWS.
- [ ] **Azure App services support (Planned)**: deployments in Azure app services with Web UI.
+## Quickstart
+
+1. First install the framework using `pip`. The Investing Algorithm Framework is hosted on [PyPi](https://pypi.org/project/Blankly/).
+
+```bash
+$ pip install investing-algorithm-framework
+```
+
+2. Next, just run:
+
+```bash
+$ investing-algorithm-framewor init
+```
+
+or if you want the web version:
+
+```bash
+$ investing-algorithm-framework init --web
+```
+> You can always change the app to the web version by changing the `app.py` file.
+
+The command will create the file `app.py` and an example script called `strategy.py`.
+
+From there, you start building your trading bot in the `strategy.py`.
+
+More information can be found on our [docs](https://docs.blankly.finance)
+
+> Make sure you leave the `app.py` file as is, as it is the entry point for the framework.
+> You can change the `bot.py` file to your liking and add other files to the working directory.
+> The framework will automatically pick up the files in the working directory.
+```
+
## Example implementation
The following algorithm connects to binance and buys BTC every 2 hours.
@@ -262,14 +308,6 @@ app.add_portfolio_configuration(
We are continuously working on improving the performance of the framework. If
you have any suggestions, please let us know.
-## How to install
-
-You can download the framework with pypi.
-
-```bash
-pip install investing-algorithm-framework
-```
-
## Installation for local development
The framework is built with poetry. To install the framework for local development, you can run the following commands:
diff --git a/examples/app.py b/examples/app.py
deleted file mode 100644
index ee2d1cb..0000000
--- a/examples/app.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from dotenv import load_dotenv
-
-from investing_algorithm_framework import create_app, PortfolioConfiguration, \
- TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \
- CCXTTickerMarketDataSource, MarketCredential, AzureBlobStorageStateHandler
-
-load_dotenv()
-
-# Define market data sources
-# OHLCV data for candles
-bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
- identifier="BTC-ohlcv",
- market="BITVAVO",
- symbol="BTC/EUR",
- time_frame="2h",
- window_size=200
-)
-# Ticker data for orders, trades and positions
-bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource(
- identifier="BTC-ticker",
- market="BITVAVO",
- symbol="BTC/EUR",
-)
-app = create_app(state_handler=AzureBlobStorageStateHandler())
-app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h)
-algorithm = Algorithm()
-app.add_market_credential(MarketCredential(market="bitvavo"))
-app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="bitvavo",
- trading_symbol="EUR",
- initial_balance=20
- )
-)
-app.add_algorithm(algorithm)
-
-@algorithm.strategy(
- # Run every two hours
- time_unit=TimeUnit.HOUR,
- interval=2,
- # Specify market data sources that need to be passed to the strategy
- market_data_sources=[bitvavo_btc_eur_ticker, "BTC-ohlcv"]
-)
-def perform_strategy(algorithm: Algorithm, market_data: dict):
- # By default, ohlcv data is passed as polars df in the form of
- # {"": } https://pola.rs/,
- # call to_pandas() to convert to pandas
- polars_df = market_data["BTC-ohlcv"]
- print(f"I have access to {len(polars_df)} candles of ohlcv data")
diff --git a/examples/backtest_example/run_backtest.py b/examples/backtest_example/run_backtest.py
index 254b99b..4bd4ff1 100644
--- a/examples/backtest_example/run_backtest.py
+++ b/examples/backtest_example/run_backtest.py
@@ -2,14 +2,12 @@
from datetime import datetime
import logging.config
from datetime import datetime, timedelta
+from investing_algorithm_framework import (
+ CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, PortfolioConfiguration, create_app, pretty_print_backtest, BacktestDateRange, TimeUnit, TradingStrategy, OrderSide, DEFAULT_LOGGING_CONFIG, Context
+)
+from pyindicators import ema, is_crossover, is_above, is_below, is_crossunder
-from investing_algorithm_framework import CCXTOHLCVMarketDataSource, \
- CCXTTickerMarketDataSource, PortfolioConfiguration, \
- create_app, pretty_print_backtest, BacktestDateRange, TimeUnit, \
- TradingStrategy, OrderSide, DEFAULT_LOGGING_CONFIG, Context
-
-import tulipy as ti
logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
@@ -21,22 +19,22 @@
The strategy will also check if the fast moving average is above the trend
moving average. If it is not above the trend moving average it will not buy.
-It uses tulipy indicators to calculate the metrics. You need to
+It uses pyindicators to calculate the metrics. You need to
install this library in your environment to run this strategy.
You can find instructions on how to install tulipy here:
-https://tulipindicators.org/ or go directly to the pypi page:
-https://pypi.org/project/tulipy/
+https://github.com/coding-kitties/PyIndicators or go directly
+to the pypi page: https://pypi.org/project/PyIndicators/
"""
bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
- identifier="BTC/EUR-ohlcv",
+ identifier="BTC/EUR-ohlcv-2h",
market="BINANCE",
symbol="BTC/EUR",
time_frame="2h",
window_size=200
)
bitvavo_dot_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
- identifier="DOT/EUR-ohlcv",
+ identifier="DOT/EUR-ohlch-2h",
market="BINANCE",
symbol="DOT/EUR",
time_frame="2h",
@@ -55,68 +53,73 @@
backtest_time_frame="2h",
)
-
-def is_below_trend(fast_series, slow_series):
- return fast_series[-1] < slow_series[-1]
-
-
-def is_above_trend(fast_series, slow_series):
- return fast_series[-1] > slow_series[-1]
-
-
-def is_crossover(fast, slow):
- """
- Expect df to have columns: Date, ma_, ma_.
- With the given date time it will check if the ma_ is a
- crossover with the ma_
- """
- return fast[-2] <= slow[-2] and fast[-1] > slow[-1]
-
-
-def is_crossunder(fast, slow):
+class CrossOverStrategy(TradingStrategy):
"""
- Expect df to have columns: Date, ma_, ma_.
- With the given date time it will check if the ma_ is a
- crossover with the ma_
+ A simple trading strategy that uses EMA crossovers to generate buy and
+ sell signals. The strategy uses a 50-period EMA and a 100-period EMA
+ to detect golden and death crosses. It also uses a 200-period EMA to
+ determine the overall trend direction. The strategy trades BTC/EUR
+ on a 2-hour timeframe. The strategy is designed to be used with the
+ Investing Algorithm Framework and uses the PyIndicators library
+ to calculate the EMAs and crossover signals.
+
+ The strategy uses a trailing stop loss and take profit to manage
+ risk. The stop loss is set to 5% below the entry price and the
+ take profit is set to 10% above the entry price. The stop loss and
+ take profit are both trailing, meaning that they will move up
+ with the price when the price goes up.
"""
- return fast[-2] >= slow[-2] and fast[-1] < slow[-1]
-
-
-class CrossOverStrategy(TradingStrategy):
time_unit = TimeUnit.HOUR
interval = 2
- market_data_sources = [
- bitvavo_dot_eur_ticker,
- bitvavo_btc_eur_ticker,
- bitvavo_dot_eur_ohlcv_2h,
- bitvavo_btc_eur_ohlcv_2h
- ]
- symbols = ["BTC/EUR", "DOT/EUR"]
- fast = 21
- slow = 75
- trend = 150
- stop_loss_percentage = 7
+ symbol_pairs = ["BTC/EUR"]
+ market_data_sources = [bitvavo_btc_eur_ohlcv_2h, bitvavo_btc_eur_ticker]
+ fast = 50
+ slow = 100
+ trend = 200
+ stop_loss_percentage = 2
+ stop_loss_sell_size = 50
+ take_profit_percentage = 8
+ take_profit_sell_size = 50
def apply_strategy(self, context: Context, market_data):
- for symbol in self.symbols:
- target_symbol = symbol.split('/')[0]
+ for pair in self.symbol_pairs:
+ symbol = pair.split('/')[0]
- if context.has_open_orders(target_symbol):
+ # Don't trade if there are open orders for the symbol
+ # This is important to avoid placing new orders while there are
+ # existing orders that are not yet filled
+ if context.has_open_orders(symbol):
continue
- df = market_data[f"{symbol}-ohlcv"]
- ticker_data = market_data[f"{symbol}-ticker"]
- fast = ti.sma(df['Close'].to_numpy(), self.fast)
- slow = ti.sma(df['Close'].to_numpy(), self.slow)
- trend = ti.sma(df['Close'].to_numpy(), self.trend)
- price = ticker_data["bid"]
-
- if not context.has_position(target_symbol) \
- and is_crossover(fast, slow) \
- and is_above_trend(fast, trend):
+ ohlvc_data = market_data[f"{pair}-ohlcv-2h"]
+ # ticker_data = market_data[f"{symbol}-ticker"]
+ # Add fast, slow, and trend EMAs to the data
+ ohlvc_data = ema(
+ ohlvc_data,
+ source_column="Close",
+ period=self.fast,
+ result_column=f"ema_{self.fast}"
+ )
+ ohlvc_data = ema(
+ ohlvc_data,
+ source_column="Close",
+ period=self.slow,
+ result_column=f"ema_{self.slow}"
+ )
+ ohlvc_data = ema(
+ ohlvc_data,
+ source_column="Close",
+ period=self.trend,
+ result_column=f"ema_{self.trend}"
+ )
+
+ price = ohlvc_data["Close"][-1]
+
+ if not context.has_position(symbol) \
+ and self._is_buy_signal(ohlvc_data):
order = context.create_limit_order(
- target_symbol=target_symbol,
+ target_symbol=symbol,
order_side=OrderSide.BUY,
price=price,
percentage_of_portfolio=25,
@@ -126,31 +129,49 @@ def apply_strategy(self, context: Context, market_data):
context.add_stop_loss(
trade=trade,
trade_risk_type="trailing",
- percentage=5,
- sell_percentage=50
+ percentage=self.stop_loss_percentage,
+ sell_percentage=self.stop_loss_sell_size
)
context.add_take_profit(
trade=trade,
- percentage=5,
+ percentage=self.take_profit_percentage,
trade_risk_type="trailing",
- sell_percentage=50
- )
- context.add_take_profit(
- trade=trade,
- percentage=10,
- trade_risk_type="trailing",
- sell_percentage=20
+ sell_percentage=self.take_profit_sell_size
)
- if context.has_position(target_symbol) \
- and is_below_trend(fast, slow):
+ if context.has_position(symbol) \
+ and self._is_sell_signal(ohlvc_data):
open_trades = context.get_open_trades(
- target_symbol=target_symbol
+ target_symbol=symbol
)
for trade in open_trades:
context.close_trade(trade)
+ def _is_sell_signal(self, data):
+ return is_crossunder(
+ data,
+ first_column=f"ema_{self.fast}",
+ second_column=f"ema_{self.slow}",
+ number_of_data_points=2
+ ) and is_below(
+ data,
+ first_column=f"ema_{self.fast}",
+ second_column=f"ema_{self.trend}",
+ )
+
+ def _is_buy_signal(self, data):
+ return is_crossover(
+ data=data,
+ first_column=f"ema_{self.fast}",
+ second_column=f"ema_{self.slow}",
+ number_of_data_points=2
+ ) and is_above(
+ data=data,
+ first_column=f"ema_{self.fast}",
+ second_column=f"ema_{self.trend}",
+ )
+
app = create_app(name="GoldenCrossStrategy")
app.add_strategy(CrossOverStrategy)
@@ -162,9 +183,7 @@ def apply_strategy(self, context: Context, market_data):
# Add a portfolio configuration of 400 euro initial balance
app.add_portfolio_configuration(
PortfolioConfiguration(
- market="BINANCE",
- trading_symbol="EUR",
- initial_balance=400,
+ market="BINANCE", trading_symbol="EUR", initial_balance=400,
)
)
@@ -176,7 +195,9 @@ def apply_strategy(self, context: Context, market_data):
end_date=end_date
)
start_time = time.time()
- backtest_report = app.run_backtest(backtest_date_range=date_range)
+ backtest_report = app.run_backtest(
+ backtest_date_range=date_range, save_in_memory_strategies=True
+ )
pretty_print_backtest(backtest_report)
end_time = time.time()
print(f"Execution Time: {end_time - start_time:.6f} seconds")
diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py
index aa2c0fa..bf5c8c0 100644
--- a/investing_algorithm_framework/__init__.py
+++ b/investing_algorithm_framework/__init__.py
@@ -11,7 +11,8 @@
RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \
load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \
DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG, \
- BacktestReport, TradeStatus, MarketDataType, TradeRiskType
+ BacktestReport, TradeStatus, MarketDataType, TradeRiskType, \
+ APPLICATION_DIRECTORY
from investing_algorithm_framework.infrastructure import \
CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \
CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \
@@ -73,5 +74,6 @@
"TradeStatus",
"MarketDataType",
"TradeRiskType",
- "Context"
+ "Context",
+ "APPLICATION_DIRECTORY"
]
diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py
index 10fc80d..75a7719 100644
--- a/investing_algorithm_framework/app/algorithm.py
+++ b/investing_algorithm_framework/app/algorithm.py
@@ -22,7 +22,7 @@ class is responsible for managing the strategies and executing
them in the correct order.
Args:
- name (str): The name of the algorithm
+ name (str): (Optional) The name of the algorithm
description (str): The description of the algorithm
context (dict): The context of the algorithm, for backtest
references
diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py
index 61250f4..a7b0d31 100644
--- a/investing_algorithm_framework/app/app.py
+++ b/investing_algorithm_framework/app/app.py
@@ -17,9 +17,9 @@
DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \
SQLALCHEMY_DATABASE_URI, OperationalException, StateHandler, \
BACKTESTING_START_DATE, BACKTESTING_END_DATE, BacktestReport, \
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, APP_MODE, MarketCredential, \
- AppMode, BacktestDateRange, DATABASE_DIRECTORY_NAME, \
- BACKTESTING_INITIAL_AMOUNT, MarketDataSource
+ APP_MODE, MarketCredential, AppMode, BacktestDateRange, \
+ DATABASE_DIRECTORY_NAME, BACKTESTING_INITIAL_AMOUNT, \
+ MarketDataSource, APPLICATION_DIRECTORY
from investing_algorithm_framework.infrastructure import setup_sqlalchemy, \
create_all_tables
from investing_algorithm_framework.services import OrderBacktestService, \
@@ -56,7 +56,7 @@ def __init__(self, state_handler=None, name=None):
self._on_after_initialize_hooks = []
self._state_handler = state_handler
self._name = name
- self._algorithm = Algorithm()
+ self._algorithm = Algorithm(name=self.name)
@property
def algorithm(self) -> Algorithm:
@@ -634,9 +634,11 @@ def run_backtest(
self,
backtest_date_range: BacktestDateRange,
initial_amount=None,
- pending_order_check_interval=None,
output_directory=None,
- algorithm: Algorithm = None
+ algorithm: Algorithm = None,
+ save_strategy=False,
+ save_in_memory_strategies: bool = False,
+ strategy_directory: str = None
) -> BacktestReport:
"""
Run a backtest for an algorithm. This method should be called when
@@ -650,10 +652,13 @@ def run_backtest(
portfolio will start with.
algorithm: The algorithm to run a backtest for (instance of
Algorithm)
- pending_order_check_interval: str - pending_order_check_interval:
- The interval at which to check pending orders (e.g. 1h, 1d, 1w)
output_directory: str - The directory to
write the backtest report to
+ save_strategy: bool - Whether to save the strategy
+ save_strategies_directory: bool - Whether to save the
+ strategies directory
+ strategies_directory_name: str - The name of the directory
+ that contains the strategies
Returns:
Instance of BacktestReport
@@ -664,6 +669,32 @@ def run_backtest(
if self.algorithm is None:
raise OperationalException("No algorithm registered")
+ if save_strategy:
+ # Check if the strategies directory exists
+ if not save_in_memory_strategies:
+
+ if strategy_directory is None:
+ strategy_directory = os.path.join(
+ self.config[APPLICATION_DIRECTORY], "strategies"
+ )
+ else:
+ strategy_directory = os.path.join(
+ self.config[APPLICATION_DIRECTORY], strategy_directory
+ )
+
+ if not os.path.isdir(strategy_directory):
+ raise OperationalException(
+ "The backtest run is enabled with the "
+ "`include_strategy` flag but the strategies"
+ " directory: "
+ f"{strategy_directory} does not exist. "
+ "Please create the strategies directory or set "
+ "include_strategy to False. If you want to save the "
+ "strategies in memory, set save_in_memory_strategies "
+ "to True. This can be helpfull when running your "
+ "strategies in a notebook environment."
+ )
+
# Add backtest configuration to the config
self.set_config_with_dict({
ENVIRONMENT: Environment.BACKTEST.value,
@@ -671,9 +702,6 @@ def run_backtest(
BACKTESTING_END_DATE: backtest_date_range.end_date,
DATABASE_NAME: "backtest-database.sqlite3",
DATABASE_DIRECTORY_NAME: "backtest_databases",
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL: (
- pending_order_check_interval
- ),
BACKTESTING_INITIAL_AMOUNT: initial_amount
})
@@ -697,15 +725,14 @@ def run_backtest(
initial_amount=initial_amount,
backtest_date_range=backtest_date_range
)
- config = self.container.configuration_service().get_config()
- if output_directory is None:
- output_directory = os.path.join(
- config[RESOURCE_DIRECTORY], "backtest_reports"
- )
-
- backtest_service.write_report_to_json(
- report=report, output_directory=output_directory
+ backtest_service.save_report(
+ report=report,
+ algorithm=self.algorithm,
+ output_directory=output_directory,
+ save_strategy=save_strategy,
+ save_in_memory_strategies=save_in_memory_strategies,
+ strategy_directory=strategy_directory
)
return report
@@ -713,10 +740,10 @@ def run_backtests(
self,
algorithms,
initial_amount=None,
- date_ranges: List[BacktestDateRange] = None,
- pending_order_check_interval=None,
+ backtest_date_ranges: List[BacktestDateRange] = None,
output_directory=None,
- checkpoint=False
+ checkpoint=False,
+ save_strategy=False,
) -> List[BacktestReport]:
"""
Run a backtest for a set algorithm. This method should be called when
@@ -724,11 +751,9 @@ def run_backtests(
Args:
Algorithms: List[Algorithm] - The algorithms to run backtests for
- date_ranges: List[BacktestDateRange] - The date ranges to run the
- backtests for
+ backtest_date_ranges: List[BacktestDateRange] - The date ranges
+ to run the backtests for
initial_amount: The initial amount to start the backtest with.
- pending_order_check_interval: str - The interval at which to check
- pending orders
output_directory: str - The directory to write the backtest
report to.
checkpoint: bool - Whether to checkpoint the backtest,
@@ -738,6 +763,11 @@ def run_backtests(
when running backtests for a large number of algorithms
and date ranges where some of the backtests may fail
and you want to re-run only the failed backtests.
+ save_strategy: bool - Whether to save the strategy as part
+ of the backtest report. You can only save in-memory strategies
+ when running multiple backtests. This is because we can't
+ differentiate between which folders belong to a specific
+ strategy.
Returns
List of BacktestReport intances
@@ -745,8 +775,7 @@ def run_backtests(
logger.info("Initializing backtests")
reports = []
- for date_range in date_ranges:
- date_range: BacktestDateRange = date_range
+ for date_range in backtest_date_ranges:
print(
f"{COLOR_YELLOW}Running backtests for date "
f"range:{COLOR_RESET} {COLOR_GREEN}{date_range.name} "
@@ -783,10 +812,7 @@ def run_backtests(
BACKTESTING_START_DATE: date_range.start_date,
BACKTESTING_END_DATE: date_range.end_date,
DATABASE_NAME: "backtest-database.sqlite3",
- DATABASE_DIRECTORY_NAME: "backtest_databases",
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL: (
- pending_order_check_interval
- )
+ DATABASE_DIRECTORY_NAME: "backtest_databases"
})
self.initialize_config()
@@ -818,13 +844,12 @@ def run_backtests(
if date_range.name is not None:
report.date_range_name = date_range.name
- if output_directory is None:
- output_directory = os.path.join(
- self.config[RESOURCE_DIRECTORY], "backtest_reports"
- )
-
- backtest_service.write_report_to_json(
- report=report, output_directory=output_directory
+ backtest_service.save_report(
+ report=report,
+ algorithm=algorithm,
+ output_directory=output_directory,
+ save_strategy=save_strategy,
+ save_in_memory_strategies=True,
)
reports.append(report)
diff --git a/investing_algorithm_framework/cli/templates/app-web.py.template b/investing_algorithm_framework/cli/templates/app-web.py.template
new file mode 100644
index 0000000..77e1bec
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/app-web.py.template
@@ -0,0 +1,17 @@
+import logging.config
+from dotenv import load_dotenv
+
+from investing_algorithm_framework import create_app, \
+ DEFAULT_LOGGING_CONFIG, Algorithm
+from strategies.strategy import MyTradingStrategy
+
+load_dotenv()
+logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
+
+app = create_app(web=True)
+algorithm = Algorithm(name="MyTradingBot")
+algorithm.add_strategy(MyTradingStrategy)
+app.add_algorithm(algorithm)
+
+if __name__ == "__main__":
+ app.run()
diff --git a/investing_algorithm_framework/cli/templates/app.py.template b/investing_algorithm_framework/cli/templates/app.py.template
index f5f5cde..c328765 100644
--- a/investing_algorithm_framework/cli/templates/app.py.template
+++ b/investing_algorithm_framework/cli/templates/app.py.template
@@ -21,4 +21,4 @@ algorithm.add_strategy(MyTradingStrategy)
app.add_algorithm(algorithm)
if __name__ == "__main__":
- app.run()
\ No newline at end of file
+ app.run()
diff --git a/investing_algorithm_framework/cli/templates/market_data_providers.py.template b/investing_algorithm_framework/cli/templates/market_data_providers.py.template
new file mode 100644
index 0000000..53c487e
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/market_data_providers.py.template
@@ -0,0 +1,9 @@
+from investing_algorithm_framework import CCXTOHLCVMarketDataSource
+
+btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
+ identifier="BTC/EUR-ohlcv",
+ market="BINANCE",
+ symbol="BTC/EUR",
+ time_frame="2h",
+ window_size=200
+)
diff --git a/investing_algorithm_framework/cli/templates/requirements.txt.template b/investing_algorithm_framework/cli/templates/requirements.txt.template
index fc79b15..d054f28 100644
--- a/investing_algorithm_framework/cli/templates/requirements.txt.template
+++ b/investing_algorithm_framework/cli/templates/requirements.txt.template
@@ -1,2 +1,2 @@
-investing-algorithm-framework>=6.2.0
+investing-algorithm-framework>=6.2.1
pyindicators>=0.5.4
diff --git a/investing_algorithm_framework/cli/templates/run_backtest.py.template b/investing_algorithm_framework/cli/templates/run_backtest.py.template
index 7b21d28..4b58b07 100644
--- a/investing_algorithm_framework/cli/templates/run_backtest.py.template
+++ b/investing_algorithm_framework/cli/templates/run_backtest.py.template
@@ -10,4 +10,4 @@ if __name__ == "__main__":
end_date=datetime(2023, 12, 31),
)
report = app.run_backtest(backtest_date_range=backtest_date_range)
- pretty_print_backtest(report)
\ No newline at end of file
+ pretty_print_backtest(report)
diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py
index ddf1cb1..ef6fb49 100644
--- a/investing_algorithm_framework/create_app.py
+++ b/investing_algorithm_framework/create_app.py
@@ -1,9 +1,11 @@
+import os
import logging
+import inspect
from dotenv import load_dotenv
from .app import App
from .dependency_container import setup_dependency_container
-from .domain import AppMode
+from .domain import AppMode, APPLICATION_DIRECTORY
logger = logging.getLogger("investing_algorithm_framework")
@@ -44,6 +46,10 @@ def create_app(
if web:
app.set_config("APP_MODE", AppMode.WEB.value)
- logger.info("Investing algoritm framework app created")
+ # Add the application directory to the config
+ caller_frame = inspect.stack()[1]
+ caller_path = os.path.abspath(caller_frame.filename)
+ app.set_config(APPLICATION_DIRECTORY, caller_path)
+ logger.info("Investing algoritm framework app created")
return app
diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py
index d7a90b6..bf98e24 100644
--- a/investing_algorithm_framework/domain/__init__.py
+++ b/investing_algorithm_framework/domain/__init__.py
@@ -7,8 +7,8 @@
BACKTEST_DATA_DIRECTORY_NAME, TICKER_DATA_TYPE, OHLCV_DATA_TYPE, \
CURRENT_UTC_DATETIME, BACKTESTING_END_DATE, SYMBOLS, \
CCXT_DATETIME_FORMAT_WITH_TIMEZONE, RESERVED_BALANCES, \
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, APP_MODE, \
- DATABASE_DIRECTORY_NAME, BACKTESTING_INITIAL_AMOUNT
+ APP_MODE, DATABASE_DIRECTORY_NAME, BACKTESTING_INITIAL_AMOUNT, \
+ APPLICATION_DIRECTORY
from .data_structures import PeekableQueue
from .decimal_parsing import parse_decimal_to_string, parse_string_to_decimal
from .exceptions import OperationalException, ApiException, \
@@ -100,7 +100,6 @@
"MarketService",
"PeekableQueue",
"BACKTESTING_END_DATE",
- "BACKTESTING_PENDING_ORDER_CHECK_INTERVAL",
"PositionSnapshot",
"MarketCredentialService",
"TradeStatus",
@@ -128,5 +127,6 @@
"TradeRiskType",
"TradeTakeProfit",
"TradeStopLoss",
- "StateHandler"
+ "StateHandler",
+ "APPLICATION_DIRECTORY"
]
diff --git a/investing_algorithm_framework/domain/constants.py b/investing_algorithm_framework/domain/constants.py
index f880a7b..2ee2c70 100644
--- a/investing_algorithm_framework/domain/constants.py
+++ b/investing_algorithm_framework/domain/constants.py
@@ -13,6 +13,7 @@
DEFAULT_DATABASE_NAME = "database"
SYMBOLS = "SYMBOLS"
+APPLICATION_DIRECTORY = "APP_DIR"
RESOURCE_DIRECTORY = "RESOURCE_DIRECTORY"
BACKTEST_DATA_DIRECTORY_NAME = "BACKTEST_DATA_DIRECTORY_NAME"
LOG_LEVEL = 'LOG_LEVEL'
@@ -69,8 +70,6 @@
BACKTESTING_INDEX_DATETIME = "BACKTESTING_INDEX_DATETIME"
BACKTESTING_START_DATE = "BACKTESTING_START_DATE"
BACKTESTING_END_DATE = "BACKTESTING_END_DATE"
-BACKTESTING_PENDING_ORDER_CHECK_INTERVAL \
- = "BACKTESTING_PENDING_ORDER_CHECK_INTERVAL"
BACKTESTING_INITIAL_AMOUNT = "BACKTESTING_INITIAL_AMOUNT"
TICKER_DATA_TYPE = "TICKER"
OHLCV_DATA_TYPE = "OHLCV"
diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py
index f237096..5a42ba0 100644
--- a/investing_algorithm_framework/services/backtesting/backtest_service.py
+++ b/investing_algorithm_framework/services/backtesting/backtest_service.py
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta
import re
import os
+import inspect
import json
import pandas as pd
from dateutil import parser
@@ -10,7 +11,8 @@
from investing_algorithm_framework.domain import BacktestReport, \
BACKTESTING_INDEX_DATETIME, TimeUnit, BacktestPosition, \
TradingDataType, OrderStatus, OperationalException, MarketDataSource, \
- OrderSide, SYMBOLS, BacktestDateRange, DATETIME_FORMAT_BACKTESTING
+ OrderSide, SYMBOLS, BacktestDateRange, DATETIME_FORMAT_BACKTESTING, \
+ RESOURCE_DIRECTORY
from investing_algorithm_framework.services.market_data_source_service import \
MarketDataSourceService
@@ -21,6 +23,11 @@
r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$"
)
+BACKTEST_REPORT_DIRECTORY_PATTERN = (
+ r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
+ r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_"
+ r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}$"
+)
def validate_algorithm_name(name, illegal_chars=r"[\/:*?\"<>|]"):
@@ -683,6 +690,108 @@ def _is_backtest_report(self, path: str) -> bool:
return False
+ def save_report(
+ self,
+ report: BacktestReport,
+ algorithm,
+ output_directory: str,
+ save_strategy=False,
+ save_in_memory_strategies: bool = False,
+ strategy_directory: str = None
+ ) -> None:
+ """
+ Function to save the backtest report to a file. If the
+ `save_in_memory_strategies` flag is set to True, the function
+ tries to get the strategy class defintion that are loaded in
+ memory and save them to the output directory(this is usefull
+ when experimenting in notebooks). Otherwise, it copies
+ the strategy directory to the output directory.
+
+ Args:
+ report: BacktestReport - The backtest report to save
+ algorithm: Algorithm - The algorithm to save
+ output_directory: str - The directory to save the report in
+ save_in_memory_strategies: bool - Flag to save in-memory strategies
+ (default: False)
+ save_strateg
+ save_strategy_directory: bool - Flag to save strategy directory
+ (default: False)
+ strategy_directory (optional): str - Full path to
+ the strategy directory
+
+ Returns:
+ None
+ """
+
+ if output_directory is None:
+ output_directory = os.path.join(
+ self._configuration_service.config[RESOURCE_DIRECTORY],
+ "backtest_reports"
+ )
+
+ output_directory = self.create_report_directory(
+ report, output_directory, algorithm.name
+ )
+
+ if save_strategy:
+ if save_in_memory_strategies:
+ strategys = algorithm.strategies
+
+ for strategy in strategys:
+ self._save_strategy_class(strategy, output_directory)
+ else:
+ # Copy over all files in the strategy directory
+ # to the output directory
+ app_directory = self._configuration_service.config["APP_DIR"]
+
+ if strategy_directory is None:
+ strategy_directory = os.path.join(
+ app_directory, "strategies"
+ )
+
+ if not os.path.exists(strategy_directory) or \
+ not os.path.isdir(strategy_directory):
+ raise OperationalException(
+ "Default strategy directory 'strategies' does "
+ "not exist. If you have your strategies placed in "
+ "a different directory, please provide the "
+ "full path to the strategy directory as "
+ "the 'strategy_directory' argument."
+ )
+ else:
+
+ # Check if the strategy directory exists
+ if not os.path.exists(strategy_directory) or \
+ not os.path.isdir(strategy_directory):
+ raise OperationalException(
+ f"Strategy directory {strategy_directory} "
+ "does not "
+ f"exist. Please make sure the directory exists."
+ )
+
+ strategy_directory_name = os.path.basename(strategy_directory)
+ strategy_files = os.listdir(strategy_directory)
+ output_strategy_directory = os.path.join(
+ output_directory, strategy_directory_name
+ )
+
+ if not os.path.exists(output_strategy_directory):
+ os.makedirs(output_strategy_directory)
+
+ for file in strategy_files:
+ source_file = os.path.join(strategy_directory, file)
+ destination_file = os.path.join(
+ output_strategy_directory, file
+ )
+
+ if os.path.isfile(source_file):
+ # Copy the file to the output directory
+ with open(source_file, "rb") as src:
+ with open(destination_file, "wb") as dst:
+ dst.write(src.read())
+
+ self.write_report_to_json(report, output_directory)
+
def write_report_to_json(
self, report: BacktestReport, output_directory: str
) -> None:
@@ -690,21 +799,19 @@ def write_report_to_json(
Function to write a backtest report to a JSON file.
Args:
- - report: BacktestReport
+ report: BacktestReport
The backtest report to write to a file.
- - output_directory: str
+ output_directory: str
The directory to store the backtest report file.
Returns:
- - None
+ None
"""
if not os.path.exists(output_directory):
os.makedirs(output_directory)
- json_file_path = self.create_report_file_path(
- report, output_directory, extension=".json"
- )
+ json_file_path = os.path.join(output_directory, "report.json")
report_dict = report.to_dict()
# Convert dictionary to JSON
@@ -715,47 +822,132 @@ def write_report_to_json(
json_file.write(json_data)
@staticmethod
- def create_report_name(report, output_directory, extension=".json"):
- backtest_start_date = report.backtest_start_date \
- .strftime(DATETIME_FORMAT_BACKTESTING)
- backtest_end_date = report.backtest_end_date \
- .strftime(DATETIME_FORMAT_BACKTESTING)
+ def create_report_directory_name(report) -> str:
+ """
+ Function to create a directory name for a backtest report.
+ The directory name will be automatically generated based on the
+ algorithm name and creation date.
+
+ Args:
+ report: BacktestReport - The backtest report to create a
+ directory for.
+
+ Returns:
+ directory_name: str The directory name for the
+ backtest report file.
+ """
created_at = report.created_at.strftime(DATETIME_FORMAT_BACKTESTING)
- file_path = os.path.join(
- output_directory,
- f"report_{report.name}_backtest-start-date_"
- f"{backtest_start_date}_backtest-end-date_"
- f"{backtest_end_date}_created-at_{created_at}{extension}"
- )
- return file_path
+ directory_name = f"{report.name}_backtest_created-at_{created_at}"
+ return directory_name
@staticmethod
- def create_report_file_path(
- report, output_directory, extension=".json"
+ def create_report_directory(
+ report, output_directory, algorithm_name
) -> str:
"""
- Function to create a file path for a backtest report.
+ Function to create a directory for a backtest report.
+ The directory name will be automatically generated based on the
+ backtest start date, end date, and creation date.
+ The directory will be created if it does not exist.
Args:
- - report: BacktestReport
- The backtest report to create a file path for.
- - output_directory: str
- The directory to store the backtest report file.
- - extension: str (default=".json") - optional
- The file extension to use for the backtest report file.
+ report: BacktestReport - The backtest report to create a
+ directory for.
+ output_directory: str - The directory to store the backtest
+ report file.
+ algorithm_name: str - The name of the algorithm to
+ create a directory for.
+
Returns:
- - file_path: str
- The file path for the backtest report file.
+ directory_path: str The directory path for the
+ backtest report file.
"""
- backtest_start_date = report.backtest_start_date \
- .strftime(DATETIME_FORMAT_BACKTESTING)
- backtest_end_date = report.backtest_end_date \
- .strftime(DATETIME_FORMAT_BACKTESTING)
- created_at = report.created_at.strftime(DATETIME_FORMAT_BACKTESTING)
- file_path = os.path.join(
- output_directory,
- f"report_{report.name}_backtest-start-date_"
- f"{backtest_start_date}_backtest-end-date_"
- f"{backtest_end_date}_created-at_{created_at}{extension}"
- )
- return file_path
+
+ directory_name = BacktestService.create_report_directory_name(report)
+ directory_path = os.path.join(output_directory, directory_name)
+
+ if not os.path.exists(directory_path):
+ os.makedirs(directory_path)
+
+ return directory_path
+
+ def _save_strategy_class(self, strategy, output_directory):
+ """
+ Save the strategy class to a file in the specified output directory.
+
+ Args:
+ strategy: The strategy instance to save.
+ output_directory: The directory to save the
+ strategy class file.
+
+ Returns:
+ None
+ """
+ collected_imports = set()
+ class_definitions = []
+ collected_imports = []
+ class_definitions = []
+
+ cls = strategy.__class__
+ file_path = inspect.getfile(cls)
+
+ if os.path.exists(file_path):
+
+ with open(file_path, "r") as f:
+ lines = f.readlines()
+
+ class_started = False
+ class_code = []
+ current_import = []
+
+ for line in lines:
+ stripped_line = line.strip()
+
+ # Start collecting an import line
+ if stripped_line.startswith(("import ", "from ")):
+ current_import.append(line.rstrip())
+
+ # Handle single-line import directly
+ if not stripped_line.endswith(("\\", "(")):
+ collected_imports.append(" ".join(current_import))
+ current_import = []
+
+ # Continue collecting multi-line imports
+ elif current_import:
+ current_import.append(line.rstrip())
+
+ # Stop when the multi-line import finishes
+ if not stripped_line.endswith(("\\", ",")):
+ collected_imports.append(" ".join(current_import))
+ current_import = []
+
+ # Catch any unfinished import (just in case)
+ if current_import:
+ collected_imports.append(" ".join(current_import))
+
+ # Capture class definitions and functions
+ if stripped_line.startswith("class ") \
+ or stripped_line.startswith("def "):
+ class_started = True
+
+ if class_started:
+ class_code.append(line)
+
+ if class_code:
+ class_definitions.append("".join(class_code))
+
+ class_name = strategy.__class__.__name__
+ class_file_name = f"{class_name}.py"
+ filename = os.path.join(output_directory, class_file_name)
+
+ # Save everything to a single file
+ with open(filename, "w") as f:
+ # Write unique imports at the top
+ for imp in collected_imports:
+ f.write(imp)
+
+ f.write("\n\n")
+
+ # Write class and function definitions
+ for class_def in class_definitions:
+ f.write(class_def + "\n\n")
diff --git a/pyproject.toml b/pyproject.toml
index 2c05ff1..8ef3521 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ exclude = ["tests", "static", "examples", "docs"]
[tool.poetry.dependencies]
-python = ">=3.9"
+python = ">=3.10"
wrapt = ">=1.16.0"
Flask = ">=2.3.2"
Flask-Migrate = ">=2.6.0"
diff --git a/tests/app/backtesting/test_backtest_report.py b/tests/app/backtesting/test_backtest_report.py
index a96f091..0f77820 100644
--- a/tests/app/backtesting/test_backtest_report.py
+++ b/tests/app/backtesting/test_backtest_report.py
@@ -71,15 +71,16 @@ def test_report_json_creation(self):
)
report = app.run_backtest(
algorithm=algorithm,
- backtest_date_range=backtest_date_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
+ backtest_date_range=backtest_date_range,
+ save_in_memory_strategies=True
)
+ dir_name = BacktestService.create_report_directory_name(report)
+
+ path = os.path.join(self.resource_dir, "backtest_reports", dir_name)
+
# Check if the backtest report exists
- self.assertTrue(
- os.path.isfile(os.path.join(self.resource_dir, file_path))
- )
+ self.assertTrue(os.path.isdir(path))
+
def test_report_json_creation_with_multiple_strategies_with_id(self):
"""
@@ -109,12 +110,13 @@ def run_strategy(context, market_data):
end_date=datetime.utcnow()
)
report = app.run_backtest(
- algorithm=algorithm, backtest_date_range=backtest_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
+ algorithm=algorithm,
+ backtest_date_range=backtest_range,
+ save_in_memory_strategies=True
)
+ dir_name = BacktestService.create_report_directory_name(report)
+
+ path = os.path.join(self.resource_dir, "backtest_reports", dir_name)
+
# Check if the backtest report exists
- self.assertTrue(
- os.path.isfile(os.path.join(self.resource_dir, file_path))
- )
+ self.assertTrue(os.path.isdir(path))
diff --git a/tests/app/backtesting/test_run_backtest.py b/tests/app/backtesting/test_run_backtest.py
index 428be6c..6e10aab 100644
--- a/tests/app/backtesting/test_run_backtest.py
+++ b/tests/app/backtesting/test_run_backtest.py
@@ -50,7 +50,7 @@ def tearDown(self) -> None:
for name in dirs:
os.rmdir(os.path.join(root, name))
- def test_report_csv_creation(self):
+ def test_report_creation(self):
"""
Test if the backtest report is created as a CSV file
"""
@@ -72,17 +72,18 @@ def test_report_csv_creation(self):
end_date=datetime.utcnow()
)
report = app.run_backtest(
- algorithm=algorithm, backtest_date_range=backtest_date_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
+ algorithm=algorithm,
+ backtest_date_range=backtest_date_range,
+ save_in_memory_strategies=True
)
+ dir_name = BacktestService.create_report_directory_name(report)
+
+ path = os.path.join(self.resource_dir, "backtest_reports", dir_name)
+
# Check if the backtest report exists
- self.assertTrue(
- os.path.isfile(os.path.join(self.resource_dir, file_path))
- )
+ self.assertTrue(os.path.isdir(path))
- def test_report_csv_creation_without_strategy_identifier(self):
+ def test_report_creation_without_strategy_identifier(self):
"""
Test if the backtest report is created as a CSV file
when the strategy does not have an identifier
@@ -106,54 +107,17 @@ def test_report_csv_creation_without_strategy_identifier(self):
end_date=datetime.utcnow()
)
report = app.run_backtest(
- algorithm=algorithm, backtest_date_range=backtest_date_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
- )
- # Check if the backtest report exists
- self.assertTrue(os.path.isfile(file_path))
-
- def test_report_csv_creation_with_multiple_strategies(self):
- """
- Test if the backtest report is created as a CSV file
- when there are multiple strategies
- """
- app = create_app(
- config={RESOURCE_DIRECTORY: self.resource_dir}
- )
- strategy = TestStrategy()
- strategy.strategy_id = None
- algorithm = Algorithm()
- algorithm.add_strategy(strategy)
-
- @algorithm.strategy()
- def run_strategy(context, market_data):
- pass
-
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="bitvavo",
- trading_symbol="EUR",
- initial_balance=1000
- )
+ algorithm=algorithm,
+ backtest_date_range=backtest_date_range,
+ save_in_memory_strategies=True
)
+ dir_name = BacktestService.create_report_directory_name(report)
+ path = os.path.join(self.resource_dir, "backtest_reports", dir_name)
- self.assertEqual(2, len(algorithm.strategies))
- backtest_date_range = BacktestDateRange(
- start_date=datetime.utcnow() - timedelta(days=1),
- end_date=datetime.utcnow()
- )
- report = app.run_backtest(
- algorithm=algorithm, backtest_date_range=backtest_date_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
- )
# Check if the backtest report exists
- self.assertTrue(os.path.isfile(file_path))
+ self.assertTrue(os.path.isdir(path))
- def test_report_csv_creation_with_multiple_strategies_with_id(self):
+ def test_report_creation_with_multiple_strategies_with_id(self):
"""
Test if the backtest report is created as a CSV file
when there are multiple strategies with identifiers
@@ -183,10 +147,11 @@ def run_strategy(context, market_data):
)
report = app.run_backtest(
algorithm=algorithm,
- backtest_date_range=backtest_date_range
- )
- file_path = BacktestService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
+ backtest_date_range=backtest_date_range,
+ save_in_memory_strategies=True
)
+ dir_name = BacktestService.create_report_directory_name(report)
+ path = os.path.join(self.resource_dir, "backtest_reports", dir_name)
+
# Check if the backtest report exists
- self.assertTrue(os.path.isfile(file_path))
+ self.assertTrue(os.path.isdir(path))
diff --git a/tests/app/backtesting/test_run_backtests.py b/tests/app/backtesting/test_run_backtests.py
index f4a35e1..cb78c40 100644
--- a/tests/app/backtesting/test_run_backtests.py
+++ b/tests/app/backtesting/test_run_backtests.py
@@ -78,13 +78,14 @@ def test_run_backtests(self):
)
reports = app.run_backtests(
algorithms=[algorithm_one, algorithm_two, algorithm_three],
- date_ranges=[backtest_date_range]
+ backtest_date_ranges=[backtest_date_range],
)
backtest_service = app.container.backtest_service()
# Check if the backtest reports exist
for report in reports:
- file_path = backtest_service.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
+ dir_name = backtest_service.create_report_directory_name(report)
+ path = os.path.join(
+ self.resource_dir, "backtest_reports", dir_name
)
- self.assertTrue(os.path.isfile(file_path))
+ self.assertTrue(os.path.isdir(path))
diff --git a/tests/app/backtesting/test_strategy_saving.py b/tests/app/backtesting/test_strategy_saving.py
new file mode 100644
index 0000000..ae22432
--- /dev/null
+++ b/tests/app/backtesting/test_strategy_saving.py
@@ -0,0 +1,230 @@
+import os
+from datetime import datetime, timedelta
+from unittest import TestCase
+
+from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \
+ TradingStrategy, PortfolioConfiguration, TimeUnit, Algorithm, \
+ BacktestDateRange
+from investing_algorithm_framework.services import BacktestService
+
+
+class TestStrategy(TradingStrategy):
+ strategy_id = "test_strategy"
+ time_unit = TimeUnit.MINUTE
+ interval = 1
+
+ def run_strategy(self, context, market_data):
+ pass
+
+
+class Test(TestCase):
+ """
+ Collection of tests for backtest report operations where
+ the tests resolve around the saving of strategies.
+ """
+ def setUp(self) -> None:
+ self.resource_dir = os.path.abspath(
+ os.path.join(
+ os.path.join(
+ os.path.join(
+ os.path.join(
+ os.path.realpath(__file__),
+ os.pardir
+ ),
+ os.pardir
+ ),
+ os.pardir
+ ),
+ "resources"
+ )
+ )
+
+ def tearDown(self) -> None:
+ database_dir = os.path.join(
+ self.resource_dir, "databases"
+ )
+
+ 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))
+
+ def test_in_memory(self):
+ """
+ Test if the backtest report contains the in-memory strategy.
+ The strategy should be saved as a python file called
+ .py.
+ """
+ app = create_app(
+ config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ )
+ algorithm = Algorithm()
+ algorithm.add_strategy(TestStrategy())
+ app.add_algorithm(algorithm)
+ app.add_portfolio_configuration(
+ PortfolioConfiguration(
+ market="bitvavo",
+ trading_symbol="EUR",
+ initial_balance=1000
+ )
+ )
+ backtest_date_range = BacktestDateRange(
+ start_date=datetime.utcnow() - timedelta(days=1),
+ end_date=datetime.utcnow()
+ )
+ report = app.run_backtest(
+ algorithm=algorithm,
+ backtest_date_range=backtest_date_range,
+ save_strategy=True,
+ save_in_memory_strategies=True
+ )
+ report_directory = BacktestService.create_report_directory_name(report)
+ report_name = "report.json"
+ backtest_report_root_dir = os.path.join(
+ self.resource_dir, "backtest_reports"
+ )
+ backtest_report_dir = os.path.join(
+ backtest_report_root_dir, report_directory
+ )
+
+ # Check if the backtest report root directory exists
+ self.assertTrue(os.path.isdir(backtest_report_root_dir))
+
+ # Check if the backtest report directory exists
+ self.assertTrue(os.path.isdir(backtest_report_dir))
+
+ # Check if the strategy file exists
+ strategy_file_path = os.path.join(
+ backtest_report_dir, "TestStrategy.py"
+ )
+ self.assertTrue(os.path.isfile(strategy_file_path))
+
+ report_file_path = os.path.join(
+ backtest_report_dir, report_name
+ )
+ # check if the report json file exists
+ self.assertTrue(os.path.isfile(report_file_path))
+
+
+ def test_with_directory(self):
+ """
+ Test if the strategy is saved when the strategy is specified through
+ a directory attribute.
+ """
+ app = create_app(
+ config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ )
+ algorithm = Algorithm()
+ algorithm.add_strategy(TestStrategy())
+ app.add_algorithm(algorithm)
+ app.add_portfolio_configuration(
+ PortfolioConfiguration(
+ market="bitvavo",
+ trading_symbol="EUR",
+ initial_balance=1000
+ )
+ )
+ backtest_date_range = BacktestDateRange(
+ start_date=datetime.utcnow() - timedelta(days=1),
+ end_date=datetime.utcnow()
+ )
+ strategy_directory = os.path.join(
+ self.resource_dir, "strategies_for_testing"
+ )
+ report = app.run_backtest(
+ algorithm=algorithm,
+ backtest_date_range=backtest_date_range,
+ save_strategy=True,
+ strategy_directory=strategy_directory,
+ )
+ report_directory = BacktestService.create_report_directory_name(report)
+ report_name = "report.json"
+ backtest_report_root_dir = os.path.join(
+ self.resource_dir, "backtest_reports"
+ )
+ backtest_report_dir = os.path.join(
+ backtest_report_root_dir, report_directory
+ )
+
+ # Check if the backtest report root directory exists
+ self.assertTrue(os.path.isdir(backtest_report_root_dir))
+
+ # Check if the backtest report directory exists
+ self.assertTrue(os.path.isdir(backtest_report_dir))
+
+ # Check if the strategy file exists
+ strategy_one_file_path = os.path.join(
+ backtest_report_dir, "strategies_for_testing", "strategy_one.py"
+ )
+ self.assertTrue(os.path.isfile(strategy_one_file_path))
+
+ strategy_two_file_path = os.path.join(
+ backtest_report_dir, "strategies_for_testing", "strategy_two.py"
+ )
+ self.assertTrue(os.path.isfile(strategy_two_file_path))
+
+ def test_save_strategy_with_run_backtests(self):
+ """
+ Test if the in-memory strategy is saved when the run_backtests method is called with save_strategy=True.
+ """
+ app = create_app(
+ config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ )
+ algorithm_one = Algorithm()
+ algorithm_one.add_strategy(TestStrategy())
+
+ algorithm_two = Algorithm()
+ algorithm_two.add_strategy(TestStrategy())
+
+ app.add_portfolio_configuration(
+ PortfolioConfiguration(
+ market="bitvavo",
+ trading_symbol="EUR",
+ initial_balance=1000
+ )
+ )
+ backtest_date_range = BacktestDateRange(
+ start_date=datetime.utcnow() - timedelta(days=1),
+ end_date=datetime.utcnow()
+ )
+ strategy_directory = os.path.join(
+ self.resource_dir, "strategies_for_testing"
+ )
+ reports = app.run_backtests(
+ algorithms=[algorithm_one, algorithm_two],
+ backtest_date_ranges=[backtest_date_range],
+ save_strategy=True,
+ )
+
+ backtest_report_root_dir = os.path.join(
+ self.resource_dir, "backtest_reports"
+ )
+
+ # Check if the backtest report root directory exists
+ self.assertTrue(os.path.isdir(backtest_report_root_dir))
+
+ for report in reports:
+ report_directory = BacktestService\
+ .create_report_directory_name(report)
+ report_name = "report.json"
+
+ report_directory = os.path.join(
+ backtest_report_root_dir, report_directory
+ )
+
+ # Check if the backtest report directory exists
+ self.assertTrue(os.path.isdir(report_directory))
+
+ # Check if the strategy file exists
+ strategy_file_path = os.path.join(
+ report_directory, "TestStrategy.py"
+ )
+ self.assertTrue(os.path.isfile(strategy_file_path))
+
+ report_file_path = os.path.join(
+ report_directory, report_name
+ )
+ # check if the report json file exists
+ self.assertTrue(os.path.isfile(report_file_path))
diff --git a/tests/resources/strategies_for_testing/__init__.py b/tests/resources/strategies_for_testing/__init__.py
new file mode 100644
index 0000000..bc013e5
--- /dev/null
+++ b/tests/resources/strategies_for_testing/__init__.py
@@ -0,0 +1,7 @@
+from .strategy_one import StrategyOne
+from .strategy_two import StrategyTwo
+
+__all__ = [
+ "StrategyOne",
+ "StrategyTwo"
+]
diff --git a/tests/resources/strategies_for_testing/strategy_one.py b/tests/resources/strategies_for_testing/strategy_one.py
new file mode 100644
index 0000000..82fbe4e
--- /dev/null
+++ b/tests/resources/strategies_for_testing/strategy_one.py
@@ -0,0 +1,10 @@
+from investing_algorithm_framework import TradingStrategy, TimeUnit
+
+
+class StrategyOne(TradingStrategy):
+ strategy_id = "strategy_one"
+ time_unit = TimeUnit.MINUTE
+ interval = 1
+
+ def run_strategy(self, context, market_data):
+ pass
diff --git a/tests/resources/strategies_for_testing/strategy_two.py b/tests/resources/strategies_for_testing/strategy_two.py
new file mode 100644
index 0000000..be38e2c
--- /dev/null
+++ b/tests/resources/strategies_for_testing/strategy_two.py
@@ -0,0 +1,10 @@
+from investing_algorithm_framework import TradingStrategy, TimeUnit
+
+
+class StrategyTwo(TradingStrategy):
+ strategy_id = "strategy_two"
+ time_unit = TimeUnit.MINUTE
+ interval = 1
+
+ def run_strategy(self, context, market_data):
+ pass