From 5601aa90997756294bb1016ea776fdead759a709 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sun, 2 Mar 2025 19:56:57 +0100 Subject: [PATCH 1/6] Update readme and add class inspection --- README.md | 40 ++++++++-- examples/backtest_example/run_backtest.py | 7 +- .../app/algorithm.py | 2 +- investing_algorithm_framework/app/app.py | 22 ++++-- .../services/backtesting/backtest_service.py | 79 +++++++++++++++++++ 5 files changed, 132 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3aa1a15..3fe01ef 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,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 +294,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/backtest_example/run_backtest.py b/examples/backtest_example/run_backtest.py index 254b99b..7ff5a72 100644 --- a/examples/backtest_example/run_backtest.py +++ b/examples/backtest_example/run_backtest.py @@ -4,10 +4,9 @@ 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 investing_algorithm_framework import ( + CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, PortfolioConfiguration, create_app, pretty_print_backtest, BacktestDateRange, TimeUnit, TradingStrategy, OrderSide, DEFAULT_LOGGING_CONFIG, Context +) import tulipy as ti 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 b6dc5aa..9706969 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -55,7 +55,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: @@ -630,6 +630,8 @@ def run_backtest( output_directory=None, algorithm: Algorithm = None ) -> BacktestReport: + print(self.name) + print(self.algorithm.name) """ Run a backtest for an algorithm. This method should be called when running a backtest. @@ -696,8 +698,13 @@ def run_backtest( config[RESOURCE_DIRECTORY], "backtest_reports" ) - backtest_service.write_report_to_json( - report=report, output_directory=output_directory + # 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 ) return report @@ -815,9 +822,14 @@ def run_backtests( 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 ) + # backtest_service.write_report_to_json( + # report=report, output_directory=output_directory + # ) reports.append(report) return reports diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py index f237096..d5e3982 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 @@ -683,6 +684,84 @@ def _is_backtest_report(self, path: str) -> bool: return False + def save_report( + self, + report: BacktestReport, + algorithm, + output_directory: str + ) -> None: + output_directory = os.path.join(output_directory, algorithm.name) + collected_imports = set() + class_definitions = [] + self.write_report_to_json(report, output_directory) + + collected_imports = [] + class_definitions = [] + + for strategy in algorithm.strategies: + 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)) + + filename = os.path.join( + output_directory, + f"algorithm.py" + ) + + # 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") + def write_report_to_json( self, report: BacktestReport, output_directory: str ) -> None: From 4403ac0755eedb745851ed8b83e723d0bc8d7531 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 10 Mar 2025 21:58:48 +0100 Subject: [PATCH 2/6] Add init app command --- investing_algorithm_framework/cli/cli.py | 30 ++++ .../cli/intialize_app.py | 147 ++++++++++++++++++ .../cli/templates/app-web.py.template | 17 ++ .../cli/templates/app.py.template | 17 ++ .../market_data_providers.py.template | 9 ++ .../cli/templates/requirements.txt.template | 2 + .../cli/templates/run_backtest.py.template | 13 ++ .../cli/templates/strategy.py.template | 90 +++++++++++ pyproject.toml | 2 +- 9 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 investing_algorithm_framework/cli/cli.py create mode 100644 investing_algorithm_framework/cli/intialize_app.py create mode 100644 investing_algorithm_framework/cli/templates/app-web.py.template create mode 100644 investing_algorithm_framework/cli/templates/app.py.template create mode 100644 investing_algorithm_framework/cli/templates/market_data_providers.py.template create mode 100644 investing_algorithm_framework/cli/templates/requirements.txt.template create mode 100644 investing_algorithm_framework/cli/templates/run_backtest.py.template create mode 100644 investing_algorithm_framework/cli/templates/strategy.py.template diff --git a/investing_algorithm_framework/cli/cli.py b/investing_algorithm_framework/cli/cli.py new file mode 100644 index 0000000..55e4738 --- /dev/null +++ b/investing_algorithm_framework/cli/cli.py @@ -0,0 +1,30 @@ +import click +from .intialize_app import command \ + as initialize_app_command + + +@click.group() +def cli(): + """CLI for Investing Algorithm Framework""" + pass + +@click.command() +@click.option('--web', is_flag=True, help="Initialize with web UI support") +@click.option( + '--path', default=None, help="Path to directory to initialize the app in" +) +def init(web, path): + """ + Command-line tool for creating an app skeleton. + + Args: + web (bool): Flag to create an app skeleton with web UI support. + path (str): Path to directory to initialize the app in + + Returns: + None + """ + initialize_app_command(path=path, web=web) + +# Add the init command to the CLI group +cli.add_command(init) diff --git a/investing_algorithm_framework/cli/intialize_app.py b/investing_algorithm_framework/cli/intialize_app.py new file mode 100644 index 0000000..6fda09b --- /dev/null +++ b/investing_algorithm_framework/cli/intialize_app.py @@ -0,0 +1,147 @@ +import os + + +def create_directory(directory_path): + """ + Creates a new directory. + + Args: + directory_path (str): The path to the directory to create. + + Returns: + None + """ + + if not os.path.exists(directory_path): + os.makedirs(directory_path) + + +def create_file(file_path): + """ + Creates a new file. + + Args: + file_path (str): The path to the file to create. + + Returns: + None + """ + + if not os.path.exists(file_path): + with open(file_path, "w") as file: + file.write("") + + +def create_file_from_template(template_path, output_path): + """ + Creates a new file by replacing placeholders in a template file. + + Args: + template_path (str): The path to the template file. + output_path (str): The path to the output file. + replacements (dict): A dictionary of placeholder keys and + their replacements. + + Returns: + None + """ + + # Check if output path already exists + if not os.path.exists(output_path): + with open(template_path, "r") as file: + template = file.read() + + with open(output_path, "w") as file: + file.write(template) + + + + +def command(path = None, web = False): + """ + Command-line tool for creating an azure function enabled app skeleton. + + Args: + add_app_template (bool): Flag to create an app skeleton. + add_requirements_template (bool): Flag to create a + requirements template. + + Returns: + None + """ + """ + Function to create an azure function app skeleton. + + Args: + create_app_skeleton (bool): Flag to create an app skeleton. + + Returns: + None + """ + + if path == None: + path = os.getcwd() + else: + # check if directory exists + if not os.path.exists(path) or not os.path.isdir(path): + print(f"Directory {path} does not exist.") + return + + # Get the path of this script (command.py) + current_script_path = os.path.abspath(__file__) + + # Construct the path to the template file + template_app_file_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "app.py.template" + ) + requirements_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "requirements.txt.template" + ) + strategy_template_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "strategy.py.template" + ) + run_backtest_template_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "run_backtest.py.template" + ) + market_data_providers_template_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "market_data_providers.py.template" + ) + + create_file(os.path.join(path, "__init__.py")) + create_file_from_template( + template_app_file_path, + os.path.join(path, "app.py") + ) + create_file_from_template( + requirements_path, + os.path.join(path, "requirements.txt") + ) + create_file_from_template( + run_backtest_template_path, + os.path.join(path, "run_backtest.py") + ) + # Create the main directory + create_directory(os.path.join(path, "strategies")) + strategies_path = os.path.join(path, "strategies") + create_file(os.path.join(strategies_path, "__init__.py")) + create_file_from_template( + strategy_template_path, + os.path.join(strategies_path, "strategy.py") + ) + create_file_from_template( + market_data_providers_template_path, + os.path.join(path, "market_data_providers.py") + ) + print( + "App initialized successfully. " + ) 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 new file mode 100644 index 0000000..84ad3ec --- /dev/null +++ b/investing_algorithm_framework/cli/templates/app.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() +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/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 new file mode 100644 index 0000000..ae07b43 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/requirements.txt.template @@ -0,0 +1,2 @@ +investing-algorithm-framework +pyindicators \ No newline at end of file diff --git a/investing_algorithm_framework/cli/templates/run_backtest.py.template b/investing_algorithm_framework/cli/templates/run_backtest.py.template new file mode 100644 index 0000000..4b58b07 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/run_backtest.py.template @@ -0,0 +1,13 @@ +from datetime import datetime +from investing_algorithm_framework import BacktestDateRange, \ + pretty_print_backtest + +from app import app + +if __name__ == "__main__": + backtest_date_range = BacktestDateRange( + start_date=datetime(2023, 1, 1), + end_date=datetime(2023, 12, 31), + ) + report = app.run_backtest(backtest_date_range=backtest_date_range) + pretty_print_backtest(report) diff --git a/investing_algorithm_framework/cli/templates/strategy.py.template b/investing_algorithm_framework/cli/templates/strategy.py.template new file mode 100644 index 0000000..acffba5 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/strategy.py.template @@ -0,0 +1,90 @@ +from investing_algorithm_framework import TimeUnit, TradingStrategy, Context, \ + OrderSide +from .market_data_providers import btc_eur_ohlcv_2h +from pyindicators import ema, is_crossover, is_crossunder + + +class MyTradingStrategy(TradingStrategy): + time_unit = TimeUnit.HOUR + interval = 2 + symbol_pairs = ["BTC/EUR"] + market_data_sources = [btc_eur_ohlcv_2h] + + def apply_strategy(self, context: Context, market_data): + + for pair in self.symbol_pairs: + # Get the market data for the current symbol pair + market_data_indentifier = f"{pair}-ohlcv-2h" + data = market_data[market_data_indentifier] + symbol = pair.split('/')[0] + + # Calculate the EMA with a period of 200 + data = ema( + data, + period=200, + source_column="close", + result_column="ema_200" + ) + + # Calculate the EMA with a period of 50 + data = ema( + data, + period=50, + source_column="close", + result_column="ema_50" + ) + + if not context.has_position(symbol): + + if self._is_buy_signal(data): + price = data.iloc[-1]["close"] + order = context.create_limit_order( + target_symbol=symbol, + order_side=OrderSide.BUY, + price=price, + percentage_of_portfolio=25, + precision=4, + ) + trade = context.get_trade(order_id=order.id) + context.add_stop_loss( + trade=trade, + trade_risk_type="trailing", + percentage=5, + sell_percentage=50 + ) + context.add_take_profit( + trade=trade, + percentage=5, + trade_risk_type="trailing", + sell_percentage=50 + ) + context.add_take_profit( + trade=trade, + percentage=10, + trade_risk_type="trailing", + sell_percentage=20 + ) + + elif self._is_sell_signal(data): + open_trades = context.get_open_trades( + target_symbol=symbol + ) + + for trade in open_trades: + context.close_trade(trade) + + def _is_sell_signal(data): + return is_crossunder( + data, + first_column="ema_50", + second_column="ema_200", + number_of_data_points=1 + ) + + def _is_buy_signal(data): + return is_crossover( + data, + first_column="ema_50", + second_column="ema_200", + number_of_data_points=1 + ) diff --git a/pyproject.toml b/pyproject.toml index a753635..2598ac0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ tqdm = "^4.66.1" tabulate = "^0.9.0" polars = { version = "^0.20.10", extras = ["numpy", "pandas"] } jupyter = "^1.0.0" -numpy = "^2.1.3" scipy = "^1.14.1" tulipy = "^0.4.0" azure-storage-blob = "^12.24.0" @@ -48,5 +47,6 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] +investing-algorithm-framework = "investing_algorithm_framework.cli.cli:cli" deploy_to_azure_function = "investing_algorithm_framework.cli.deploy_to_azure_function:cli" create_azure_function_app_skeleton = "investing_algorithm_framework.cli.create_azure_function_app_skeleton:cli" From 36653ffe31fc89447065f15a25c386f795c1d510 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 10 Mar 2025 22:05:52 +0100 Subject: [PATCH 3/6] Add app web support --- .../cli/intialize_app.py | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/investing_algorithm_framework/cli/intialize_app.py b/investing_algorithm_framework/cli/intialize_app.py index 6fda09b..7fbb458 100644 --- a/investing_algorithm_framework/cli/intialize_app.py +++ b/investing_algorithm_framework/cli/intialize_app.py @@ -58,17 +58,6 @@ def create_file_from_template(template_path, output_path): def command(path = None, web = False): - """ - Command-line tool for creating an azure function enabled app skeleton. - - Args: - add_app_template (bool): Flag to create an app skeleton. - add_requirements_template (bool): Flag to create a - requirements template. - - Returns: - None - """ """ Function to create an azure function app skeleton. @@ -90,12 +79,20 @@ def command(path = None, web = False): # Get the path of this script (command.py) current_script_path = os.path.abspath(__file__) - # Construct the path to the template file - template_app_file_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "app.py.template" - ) + if web: + # Construct the path to the template file + template_app_file_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "app-web.py.template" + ) + else: + # Construct the path to the template file + template_app_file_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "app.py.template" + ) requirements_path = os.path.join( os.path.dirname(current_script_path), "templates", From 7fd447ec57203877aac6c1795aae12db2480f045 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Wed, 12 Mar 2025 10:09:18 +0100 Subject: [PATCH 4/6] Add jupyter notebook check --- investing_algorithm_framework/app/app.py | 2 - .../services/backtesting/backtest_service.py | 68 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 9706969..8ff8d97 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -630,8 +630,6 @@ def run_backtest( output_directory=None, algorithm: Algorithm = None ) -> BacktestReport: - print(self.name) - print(self.algorithm.name) """ Run a backtest for an algorithm. This method should be called when running a backtest. diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py index d5e3982..a464eb2 100644 --- a/investing_algorithm_framework/services/backtesting/backtest_service.py +++ b/investing_algorithm_framework/services/backtesting/backtest_service.py @@ -22,6 +22,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"[\/:*?\"<>|]"): @@ -690,7 +695,27 @@ def save_report( algorithm, output_directory: str ) -> None: - output_directory = os.path.join(output_directory, algorithm.name) + output_directory = self.create_report_directory( + report, output_directory, algorithm.name + ) + + + if self.is_running_in_notebook(): + strategys = algorithm.strategies + + for strategy in strategys: + self.save_strategy(strategy, output_directory) + else: + # Copy over all files in the strategy directory + # to the output directory + strategy_directory = os.path.dirname( + inspect.getfile(algorithm.__class__) + ) + strategy_files = os.listdir(strategy_directory) + + + + collected_imports = set() class_definitions = [] self.write_report_to_json(report, output_directory) @@ -838,3 +863,44 @@ def create_report_file_path( f"{backtest_end_date}_created-at_{created_at}{extension}" ) return file_path + + @staticmethod + def create_report_directory( + report, output_directory, algorithm_name + ) -> str: + """ + Function to create a directory for a backtest report. + + Args: + 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: + 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) + directory_path = os.path.join( + output_directory, + f"{algorithm_name}_backtest-start-date_" + f"{backtest_start_date}_backtest-end-date_" + f"{backtest_end_date}_created-at_{created_at}" + ) + return directory_path + + def is_running_in_notebook(): + try: + # Jupyter-specific modules + from IPython import get_ipython + return get_ipython() is not None + except ImportError: + return False From bd9309cbe26121620637db408b84865a261e843f Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sun, 13 Apr 2025 23:21:31 +0200 Subject: [PATCH 5/6] Fix flake8 warnings --- .../cli/intialize_app.py | 144 ------------------ .../services/backtesting/backtest_service.py | 12 +- 2 files changed, 7 insertions(+), 149 deletions(-) delete mode 100644 investing_algorithm_framework/cli/intialize_app.py diff --git a/investing_algorithm_framework/cli/intialize_app.py b/investing_algorithm_framework/cli/intialize_app.py deleted file mode 100644 index 7fbb458..0000000 --- a/investing_algorithm_framework/cli/intialize_app.py +++ /dev/null @@ -1,144 +0,0 @@ -import os - - -def create_directory(directory_path): - """ - Creates a new directory. - - Args: - directory_path (str): The path to the directory to create. - - Returns: - None - """ - - if not os.path.exists(directory_path): - os.makedirs(directory_path) - - -def create_file(file_path): - """ - Creates a new file. - - Args: - file_path (str): The path to the file to create. - - Returns: - None - """ - - if not os.path.exists(file_path): - with open(file_path, "w") as file: - file.write("") - - -def create_file_from_template(template_path, output_path): - """ - Creates a new file by replacing placeholders in a template file. - - Args: - template_path (str): The path to the template file. - output_path (str): The path to the output file. - replacements (dict): A dictionary of placeholder keys and - their replacements. - - Returns: - None - """ - - # Check if output path already exists - if not os.path.exists(output_path): - with open(template_path, "r") as file: - template = file.read() - - with open(output_path, "w") as file: - file.write(template) - - - - -def command(path = None, web = False): - """ - Function to create an azure function app skeleton. - - Args: - create_app_skeleton (bool): Flag to create an app skeleton. - - Returns: - None - """ - - if path == None: - path = os.getcwd() - else: - # check if directory exists - if not os.path.exists(path) or not os.path.isdir(path): - print(f"Directory {path} does not exist.") - return - - # Get the path of this script (command.py) - current_script_path = os.path.abspath(__file__) - - if web: - # Construct the path to the template file - template_app_file_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "app-web.py.template" - ) - else: - # Construct the path to the template file - template_app_file_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "app.py.template" - ) - requirements_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "requirements.txt.template" - ) - strategy_template_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "strategy.py.template" - ) - run_backtest_template_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "run_backtest.py.template" - ) - market_data_providers_template_path = os.path.join( - os.path.dirname(current_script_path), - "templates", - "market_data_providers.py.template" - ) - - create_file(os.path.join(path, "__init__.py")) - create_file_from_template( - template_app_file_path, - os.path.join(path, "app.py") - ) - create_file_from_template( - requirements_path, - os.path.join(path, "requirements.txt") - ) - create_file_from_template( - run_backtest_template_path, - os.path.join(path, "run_backtest.py") - ) - # Create the main directory - create_directory(os.path.join(path, "strategies")) - strategies_path = os.path.join(path, "strategies") - create_file(os.path.join(strategies_path, "__init__.py")) - create_file_from_template( - strategy_template_path, - os.path.join(strategies_path, "strategy.py") - ) - create_file_from_template( - market_data_providers_template_path, - os.path.join(path, "market_data_providers.py") - ) - print( - "App initialized successfully. " - ) diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py index d09f4da..5a42ba0 100644 --- a/investing_algorithm_framework/services/backtesting/backtest_service.py +++ b/investing_algorithm_framework/services/backtesting/backtest_service.py @@ -703,8 +703,9 @@ def save_report( 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. + 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 @@ -749,7 +750,7 @@ def save_report( ) if not os.path.exists(strategy_directory) or \ - not os.path.isdir(strategy_directory): + not os.path.isdir(strategy_directory): raise OperationalException( "Default strategy directory 'strategies' does " "not exist. If you have your strategies placed in " @@ -761,7 +762,7 @@ def save_report( # Check if the strategy directory exists if not os.path.exists(strategy_directory) or \ - not os.path.isdir(strategy_directory): + not os.path.isdir(strategy_directory): raise OperationalException( f"Strategy directory {strategy_directory} " "does not " @@ -925,7 +926,8 @@ def _save_strategy_class(self, strategy, output_directory): collected_imports.append(" ".join(current_import)) # Capture class definitions and functions - if stripped_line.startswith("class ") or stripped_line.startswith("def "): + if stripped_line.startswith("class ") \ + or stripped_line.startswith("def "): class_started = True if class_started: From 25558c67aca15b744f1f604e5831bf225c2f8af2 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sun, 13 Apr 2025 23:28:53 +0200 Subject: [PATCH 6/6] Add exclusion for flake8 --- .flake8 | 1 + 1 file changed, 1 insertion(+) 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