diff --git a/.gitignore b/.gitignore
index f80c3c6e..2bb2cda5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,10 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
+*__pycache__/
*.py[cod]
*$py.class
+.DS_Store
# C extensions
*.so
diff --git a/README.md b/README.md
index 36890816..4c6513e8 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
[![GitHub stars](https://img.shields.io/github/stars/coding-kitties/investing-algorithm-framework.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) If you like what we do, consider starring, sharing and contributing!
###### Sponsors
+
@@ -36,30 +37,13 @@ Features:
The following algorithm connects to binance and buys BTC every 5 seconds. It also exposes an REST API that allows you to interact with the algorithm.
```python
-import pathlib
+import logging
from investing_algorithm_framework import create_app, PortfolioConfiguration, \
- RESOURCE_DIRECTORY, TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \
- CCXTTickerMarketDataSource, MarketCredential, SYMBOLS
-
-# Define the symbols you want to trade for optimization, otherwise the
-# algorithm will check if you have orders and balances on all available
-# symbols on the market
-symbols = ["BTC/EUR"]
-
-# Define resource directory and the symbols you want to trade
-config = {
- RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve(),
- SYMBOLS: symbols
-}
-
-state_manager = AzureBlobStorageStateManager(
- account_name="",
- account_key="",
- container_name="",
- blob_name="",
-)
+ TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \
+ CCXTTickerMarketDataSource, MarketCredential, DEFAULT_LOGGING_CONFIG
+
+logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
-# Define market data sources
# OHLCV data for candles
bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
identifier="BTC-ohlcv",
@@ -74,17 +58,10 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource(
market="BITVAVO",
symbol="BTC/EUR",
)
-app = create_app(
- config=config,
- sync_portfolio=True,
- state_manager=state_manager
-)
+app = create_app()
algorithm = Algorithm()
-app.add_market_credential(MarketCredential(
- market="bitvavo",
- api_key="",
- secret_key="",
-))
+# Bitvavo market credentials are read from .env file
+app.add_market_credential(MarketCredential(market="bitvavo"))
app.add_portfolio_configuration(
PortfolioConfiguration(
market="bitvavo",
@@ -94,21 +71,18 @@ app.add_portfolio_configuration(
)
app.add_algorithm(algorithm)
+# Run every two hours and register the data sources
@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, bitvavo_btc_eur_ohlcv_2h]
)
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
+ # Access the data sources with the indentifier
polars_df = market_data["BTC-ohlcv"]
- print(f"I have access to {len(polars_df)} candles of ohlcv data")
- # Ticker data is passed as {"": }
+ # Convert the polars dataframe to a pandas dataframe
+ pandas_df = polars_df.to_pandas()
ticker_data = market_data["BTC-ticker"]
unallocated_balance = algorithm.get_unallocated()
positions = algorithm.get_positions()
@@ -257,7 +231,6 @@ app.add_portfolio_configuration(
PortfolioConfiguration(
market="",
initial_balance=400,
- track_from="01/01/2022",
trading_symbol="EUR"
)
)
diff --git a/examples/app.py b/examples/app.py
new file mode 100644
index 00000000..ee2d1cbd
--- /dev/null
+++ b/examples/app.py
@@ -0,0 +1,49 @@
+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/bitvavo_trading_bot/bitvavo.py b/examples/bitvavo_trading_bot/bitvavo.py
index 46bd11a2..a3a69852 100644
--- a/examples/bitvavo_trading_bot/bitvavo.py
+++ b/examples/bitvavo_trading_bot/bitvavo.py
@@ -1,31 +1,28 @@
-import os
-
from investing_algorithm_framework import MarketCredential, TimeUnit, \
CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \
- create_app, PortfolioConfiguration, Algorithm, SYMBOLS, RESOURCE_DIRECTORY
+ create_app, PortfolioConfiguration, Algorithm
"""
Bitvavo trading bot example with market data sources of bitvavo.
-Bitvavo does not requires you to have an API key and secret key to access
-their market data. If you just want to backtest your strategy,
+Bitvavo does not requires you to have an API key and secret key to access
+their market data. If you just want to backtest your strategy,
you don't need to add a market credential. If your running your strategy live,
-you need to add a market credential to the app, that accesses your
+you need to add a market credential to the app, that accesses your
account on bitvavo.
"""
-
# Define your market credential for bitvavo
bitvavo_market_credential = MarketCredential(
- api_key="",
- secret_key="",
market="bitvavo",
+ api_key="your_api_key",
+ secret_key="your_secret_key"
)
# Define your market data sources for coinbase
bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource(
identifier="BTC/EUR-ohlcv",
market="bitvavo",
symbol="BTC/EUR",
- timeframe="2h",
+ time_frame="2h",
window_size=200
)
bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource(
@@ -36,26 +33,20 @@
class BitvavoTradingStrategy(TradingStrategy):
- time_unit = TimeUnit.HOUR
- interval = 2
+ time_unit = TimeUnit.SECOND
+ interval = 10
market_data_sources = [bitvavo_btc_eur_ohlcv_2h, bitvavo_btc_eur_ticker]
def apply_strategy(self, algorithm, market_data):
print(market_data["BTC/EUR-ohlcv"])
print(market_data["BTC/EUR-ticker"])
-
-config = {
- SYMBOLS: ["BTC/EUR"],
- RESOURCE_DIRECTORY: os.path.join(os.path.dirname(__file__), "resources")
-}
-
# Create an algorithm and link your trading strategy to it
algorithm = Algorithm()
algorithm.add_strategy(BitvavoTradingStrategy)
# Create an app and add the market data sources and market credentials to it
-app = create_app(config=config)
+app = create_app()
app.add_market_credential(bitvavo_market_credential)
app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h)
app.add_market_data_source(bitvavo_btc_eur_ticker)
diff --git a/examples/deployments/azure_function/.funcignore b/examples/deployments/azure_function/.funcignore
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/examples/deployments/azure_function/.funcignore
@@ -0,0 +1 @@
+
diff --git a/examples/deployments/azure_function/.gitignore b/examples/deployments/azure_function/.gitignore
new file mode 100644
index 00000000..f15ac3fc
--- /dev/null
+++ b/examples/deployments/azure_function/.gitignore
@@ -0,0 +1,48 @@
+bin
+obj
+csx
+.vs
+edge
+Publish
+
+*.user
+*.suo
+*.cscfg
+*.Cache
+project.lock.json
+
+/packages
+/TestResults
+
+/tools/NuGet.exe
+/App_Data
+/secrets
+/data
+.secrets
+appsettings.json
+local.settings.json
+
+node_modules
+dist
+
+# Local python packages
+.python_packages/
+
+# Python Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Azurite artifacts
+__blobstorage__
+__queuestorage__
+__azurite_db*__.json
\ No newline at end of file
diff --git a/examples/deployments/azure_function/README.md b/examples/deployments/azure_function/README.md
new file mode 100644
index 00000000..a790b331
--- /dev/null
+++ b/examples/deployments/azure_function/README.md
@@ -0,0 +1,76 @@
+# Investing Algorithm Framework App Deployment to Azure Functions
+
+This article demonstrates how to deploy your trading bot to Azure Functions.
+We will deploy an example bot that uses the investing algorithm framework to
+azure functions. In order to do that we will do the following:
+
+1. Create a new app using the framework with the azure blob storage state handler.
+2. Use the framework provided ci tools to create a azure functions ready application.
+3. Use the framework provided ci tools to deploy the application to azure functions.
+
+## Prerequisites
+
+For this example, you need to have the following:
+
+- An Azure account with an active subscription. [Create an account](https://azure.microsoft.com/en-us/free/)
+- The Azure CLI installed. [Install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
+- The Azure Functions Core Tools installed. [Install the Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local)
+- The investing algorithm framework installed. [Install the framework]() or simply run `pip install investing_algorithm_framework`
+
+### Creating a new app
+
+First run the following command to create a new app azure functions ready app:
+
+```bash
+create_azure_function_trading_bot_app
+```
+
+This command will create a new app with the following structure:
+
+```yaml
+.
+├── function_app.py
+├── host.json
+├── local.settings.json
+└── requirements.txt
+```
+
+The function_app.py while import the app from the app.py file in the root of the project and run it as an azure function. It is therefore important that you have a file named `app.py` in the root of your project that defines the application in a variable named `app`.
+
+Additionaly, because Azure Functions are stateless, you need to use a state storage solution. In this example, we will use Azure Blob Storage state handler provided by the framework. This state handler is specifically designed to work with Azure Functions and Azure Blob Storage.
+
+The reason why we need a state storage solution is that Azure Functions are stateless. This means that each function execution is independent of the previous one. This is a problem for trading bots because they need to keep track of their state between executions (portfolios, order, positions and trades). In order to solve this problem, we need to use a state storage solution that can store the bot's databases between executions.
+
+Combining all of this, the `app.py` file should look like this:
+
+> When you are using the cli command 'deploy_trading_bot_to_azure_function' (which we will use later) you don't need to provide any connection strings. The command will take care of provisioning all
+> resourses and configuration of all required parameters for the state handler.
+
+```python
+from investing_algorithm_framework import AzureBlobStorageStateHandler, create_app
+
+app = create_app(state_handler=AzureBlobStorageStateHandler)
+
+# Write here your code where your register your portfolio configurations, strategies and data providers
+....
+```
+
+## Deployment to Azure
+
+To deploy your trading bot to Azure Functions, you need to run the following command:
+
+```bash
+deploy_trading_bot_to_azure_function
+```
+
+This command will do the following:
+
+- Create a new resource group in your Azure account.
+- Create a new storage account in the resource group.
+- Create a new blob container in the storage account.
+- Create a new function app in the resource group.
+- Deploy the trading bot to the function app.
+- Configure the function app to use the blob container as the state handler.
+- Print the URL of the function app.
+
+After running the command, you will see the URL of the function app. You can use this URL to access your trading bot. Now your trading bot is running on Azure Functions!
diff --git a/tests/app/test_tasks.py b/examples/deployments/azure_function/__init__.py
similarity index 100%
rename from tests/app/test_tasks.py
rename to examples/deployments/azure_function/__init__.py
diff --git a/examples/deployments/azure_function/function_app.py b/examples/deployments/azure_function/function_app.py
new file mode 100644
index 00000000..a4c8905d
--- /dev/null
+++ b/examples/deployments/azure_function/function_app.py
@@ -0,0 +1,87 @@
+import logging
+
+import azure.functions as func
+# from investing_algorithm_framework import StatelessAction
+# from app import app as investing_algorithm_framework_app
+
+
+import logging
+import logging.config
+
+LOGGING_CONFIG = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'default': {
+ 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'default',
+ },
+ 'file': {
+ 'class': 'logging.FileHandler',
+ 'formatter': 'default',
+ 'filename': 'app_logs.log',
+ },
+ },
+ 'loggers': { # Make sure to add a 'loggers' section
+ 'investing_algorithm_framework': { # Define your logger here
+ 'level': 'INFO', # Set the desired level
+ 'handlers': ['console', 'file'], # Use these handlers
+ 'propagate': False, # Prevent logs from propagating to the root logger (optional)
+ },
+ },
+ 'root': { # Optional: Root logger configuration
+ 'level': 'WARNING', # Root logger defaults to WARNING
+ 'handlers': ['console', 'file'],
+ },
+}
+
+logging.config.dictConfig(LOGGING_CONFIG)
+app = func.FunctionApp()
+
+# Change your interval here, e.ge. "0 */1 * * * *" for every minute
+# or "0 0 */1 * * *" for every hour or "0 */5 * * * *" for every 5 minutes
+# @func.timer_trigger(
+# schedule="0 */5 * * * *",
+# arg_name="myTimer",
+# run_on_startup=False,
+# use_monitor=False
+# )
+# def app(myTimer: func.TimerRequest) -> None:
+
+# if myTimer.past_due:
+# logging.info('The timer is past due!')
+
+# logging.info('Python timer trigger function ran at %s', myTimer.next)
+# investing_algorithm_framework_app.run(
+# payload={"ACTION": StatelessAction.RUN_STRATEGY.value}
+# )
+
+@app.route(route="test", auth_level=func.AuthLevel.ANONYMOUS)
+def test(req: func.HttpRequest) -> func.HttpResponse:
+ logging.info('Python HTTP trigger function processed a request.')
+
+ name = req.params.get('name')
+ # investing_algorithm_framework_app.run(
+ # payload={"ACTION": StatelessAction.RUN_STRATEGY.value}
+ # )
+
+ if not name:
+ try:
+ req_body = req.get_json()
+ except ValueError:
+ pass
+ else:
+ name = req_body.get('name')
+
+ if name:
+ return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
+ else:
+ return func.HttpResponse(
+ "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
+ status_code=200
+ )
diff --git a/examples/deployments/azure_function/host.json b/examples/deployments/azure_function/host.json
new file mode 100644
index 00000000..9df91361
--- /dev/null
+++ b/examples/deployments/azure_function/host.json
@@ -0,0 +1,15 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ }
+ }
+ },
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[4.*, 5.0.0)"
+ }
+}
\ No newline at end of file
diff --git a/examples/deployments/azure_function/requirements.txt b/examples/deployments/azure_function/requirements.txt
new file mode 100644
index 00000000..f41fb1bf
--- /dev/null
+++ b/examples/deployments/azure_function/requirements.txt
@@ -0,0 +1,2 @@
+investing-algorithm-framework
+azure-functions==1.21.3
diff --git a/examples/example_strategies/macd_wr/macd_wr.ipynb b/examples/example_strategies/macd_wr/macd_wr.ipynb
index 33b6f036..5158c3a2 100644
--- a/examples/example_strategies/macd_wr/macd_wr.ipynb
+++ b/examples/example_strategies/macd_wr/macd_wr.ipynb
@@ -33,166 +33,6 @@
"* Evaluate the backtest reports to determine the best configuration"
]
},
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {
- "metadata": {}
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Requirement already satisfied: investing_algorithm_framework in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (3.7.3)\n",
- "Requirement already satisfied: Flask<3.0.0,>=2.3.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.3.3)\n",
- "Requirement already satisfied: Flask-Cors<5.0.0,>=3.0.9 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.0.1)\n",
- "Requirement already satisfied: Flask-Migrate<3.0.0,>=2.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.7.0)\n",
- "Requirement already satisfied: MarkupSafe<3.0.0,>=2.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.1.5)\n",
- "Requirement already satisfied: SQLAlchemy<3.0.0,>=2.0.18 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.0.31)\n",
- "Requirement already satisfied: ccxt<5.0.0,>=4.2.48 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.3.64)\n",
- "Requirement already satisfied: dependency-injector<5.0.0,>=4.40.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.41.0)\n",
- "Requirement already satisfied: jupyter<2.0.0,>=1.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.0.0)\n",
- "Requirement already satisfied: marshmallow<4.0.0,>=3.5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (3.21.3)\n",
- "Requirement already satisfied: plotly<6.0.0,>=5.22.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (5.22.0)\n",
- "Requirement already satisfied: polars<0.21.0,>=0.20.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (0.20.31)\n",
- "Requirement already satisfied: python-dateutil<3.0.0,>=2.8.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.9.0.post0)\n",
- "Requirement already satisfied: schedule<2.0.0,>=1.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.2.2)\n",
- "Requirement already satisfied: tabulate<0.10.0,>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (0.9.0)\n",
- "Requirement already satisfied: tqdm<5.0.0,>=4.66.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.66.4)\n",
- "Requirement already satisfied: wrapt<2.0.0,>=1.16.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.16.0)\n",
- "Requirement already satisfied: setuptools>=60.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (74.1.0)\n",
- "Requirement already satisfied: certifi>=2018.1.18 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2024.8.30)\n",
- "Requirement already satisfied: requests>=2.18.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.32.3)\n",
- "Requirement already satisfied: cryptography>=2.6.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (42.0.8)\n",
- "Requirement already satisfied: typing-extensions>=4.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.12.2)\n",
- "Requirement already satisfied: aiohttp>=3.8 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.9.5)\n",
- "Requirement already satisfied: aiodns>=1.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.2.0)\n",
- "Requirement already satisfied: yarl>=1.7.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.9.4)\n",
- "Requirement already satisfied: six<=1.16.0,>=1.7.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from dependency-injector<5.0.0,>=4.40.0->investing_algorithm_framework) (1.16.0)\n",
- "Requirement already satisfied: Werkzeug>=2.3.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.0.3)\n",
- "Requirement already satisfied: Jinja2>=3.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.1.4)\n",
- "Requirement already satisfied: itsdangerous>=2.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (2.2.0)\n",
- "Requirement already satisfied: click>=8.1.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (8.1.7)\n",
- "Requirement already satisfied: blinker>=1.6.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (1.8.2)\n",
- "Requirement already satisfied: importlib-metadata>=3.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (8.4.0)\n",
- "Requirement already satisfied: Flask-SQLAlchemy>=1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (3.1.1)\n",
- "Requirement already satisfied: alembic>=0.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (1.13.2)\n",
- "Requirement already satisfied: notebook in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.2.2)\n",
- "Requirement already satisfied: qtconsole in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.5.2)\n",
- "Requirement already satisfied: jupyter-console in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.6.3)\n",
- "Requirement already satisfied: nbconvert in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.16.4)\n",
- "Requirement already satisfied: ipykernel in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.29.5)\n",
- "Requirement already satisfied: ipywidgets in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.1.3)\n",
- "Requirement already satisfied: packaging>=17.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from marshmallow<4.0.0,>=3.5.0->investing_algorithm_framework) (24.1)\n",
- "Requirement already satisfied: tenacity>=6.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from plotly<6.0.0,>=5.22.0->investing_algorithm_framework) (8.5.0)\n",
- "Requirement already satisfied: numpy>=1.16.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (1.24.4)\n",
- "Requirement already satisfied: pyarrow>=7.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (17.0.0)\n",
- "Requirement already satisfied: pandas in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2.0.3)\n",
- "Requirement already satisfied: pycares>=4.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiodns>=1.1.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.4.0)\n",
- "Requirement already satisfied: aiosignal>=1.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.3.1)\n",
- "Requirement already satisfied: attrs>=17.3.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (24.2.0)\n",
- "Requirement already satisfied: frozenlist>=1.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.4.1)\n",
- "Requirement already satisfied: multidict<7.0,>=4.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (6.0.5)\n",
- "Requirement already satisfied: async-timeout<5.0,>=4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.0.3)\n",
- "Requirement already satisfied: Mako in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from alembic>=0.7->Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (1.3.5)\n",
- "Requirement already satisfied: cffi>=1.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from cryptography>=2.6.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.17.0)\n",
- "Requirement already satisfied: zipp>=0.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from importlib-metadata>=3.6.0->Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.20.1)\n",
- "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.3.2)\n",
- "Requirement already satisfied: idna<4,>=2.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.8)\n",
- "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.2.2)\n",
- "Requirement already satisfied: appnope in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.4)\n",
- "Requirement already satisfied: comm>=0.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.2)\n",
- "Requirement already satisfied: debugpy>=1.6.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.5)\n",
- "Requirement already satisfied: ipython>=7.23.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.18.1)\n",
- "Requirement already satisfied: jupyter-client>=6.1.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.6.2)\n",
- "Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.7.2)\n",
- "Requirement already satisfied: matplotlib-inline>=0.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.7)\n",
- "Requirement already satisfied: nest-asyncio in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.6.0)\n",
- "Requirement already satisfied: psutil in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.0.0)\n",
- "Requirement already satisfied: pyzmq>=24 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (26.2.0)\n",
- "Requirement already satisfied: tornado>=6.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.4.1)\n",
- "Requirement already satisfied: traitlets>=5.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.14.3)\n",
- "Requirement already satisfied: widgetsnbextension~=4.0.11 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.0.11)\n",
- "Requirement already satisfied: jupyterlab-widgets~=3.0.11 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.11)\n",
- "Requirement already satisfied: prompt-toolkit>=3.0.30 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.47)\n",
- "Requirement already satisfied: pygments in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.18.0)\n",
- "Requirement already satisfied: beautifulsoup4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.12.3)\n",
- "Requirement already satisfied: bleach!=5.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.1.0)\n",
- "Requirement already satisfied: defusedxml in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.7.1)\n",
- "Requirement already satisfied: jupyterlab-pygments in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.3.0)\n",
- "Requirement already satisfied: mistune<4,>=2.0.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.2)\n",
- "Requirement already satisfied: nbclient>=0.5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.10.0)\n",
- "Requirement already satisfied: nbformat>=5.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.10.4)\n",
- "Requirement already satisfied: pandocfilters>=1.4.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.5.1)\n",
- "Requirement already satisfied: tinycss2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n",
- "Requirement already satisfied: jupyter-server<3,>=2.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.14.2)\n",
- "Requirement already satisfied: jupyterlab-server<3,>=2.27.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.27.3)\n",
- "Requirement already satisfied: jupyterlab<4.3,>=4.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.2.5)\n",
- "Requirement already satisfied: notebook-shim<0.3,>=0.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.4)\n",
- "Requirement already satisfied: pytz>=2020.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pandas->polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2024.1)\n",
- "Requirement already satisfied: tzdata>=2022.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pandas->polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2024.1)\n",
- "Requirement already satisfied: qtpy>=2.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from qtconsole->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.4.1)\n",
- "Requirement already satisfied: webencodings in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from bleach!=5.0.0->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.5.1)\n",
- "Requirement already satisfied: pycparser in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from cffi>=1.12->cryptography>=2.6.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.22)\n",
- "Requirement already satisfied: decorator in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.1.1)\n",
- "Requirement already satisfied: jedi>=0.16 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.19.1)\n",
- "Requirement already satisfied: stack-data in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.6.3)\n",
- "Requirement already satisfied: exceptiongroup in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.2.2)\n",
- "Requirement already satisfied: pexpect>4.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.9.0)\n",
- "Requirement already satisfied: platformdirs>=2.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-core!=5.0.*,>=4.12->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.2.2)\n",
- "Requirement already satisfied: anyio>=3.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.4.0)\n",
- "Requirement already satisfied: argon2-cffi>=21.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (23.1.0)\n",
- "Requirement already satisfied: jupyter-events>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.10.0)\n",
- "Requirement already satisfied: jupyter-server-terminals>=0.4.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.5.3)\n",
- "Requirement already satisfied: overrides>=5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.7.0)\n",
- "Requirement already satisfied: prometheus-client>=0.9 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.20.0)\n",
- "Requirement already satisfied: send2trash>=1.8.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.3)\n",
- "Requirement already satisfied: terminado>=0.8.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.18.1)\n",
- "Requirement already satisfied: websocket-client>=1.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.0)\n",
- "Requirement already satisfied: async-lru>=1.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.4)\n",
- "Requirement already satisfied: httpx>=0.25.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.27.2)\n",
- "Requirement already satisfied: jupyter-lsp>=2.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.2.5)\n",
- "Requirement already satisfied: tomli>=1.2.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.1)\n",
- "Requirement already satisfied: babel>=2.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.16.0)\n",
- "Requirement already satisfied: json5>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.9.25)\n",
- "Requirement already satisfied: jsonschema>=4.18.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.23.0)\n",
- "Requirement already satisfied: fastjsonschema>=2.15 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbformat>=5.7->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.20.0)\n",
- "Requirement already satisfied: wcwidth in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from prompt-toolkit>=3.0.30->jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.13)\n",
- "Requirement already satisfied: soupsieve>1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from beautifulsoup4->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.6)\n",
- "Requirement already satisfied: sniffio>=1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from anyio>=3.1.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.1)\n",
- "Requirement already satisfied: argon2-cffi-bindings in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (21.2.0)\n",
- "Requirement already satisfied: httpcore==1.* in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.0.5)\n",
- "Requirement already satisfied: h11<0.15,>=0.13 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from httpcore==1.*->httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.14.0)\n",
- "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jedi>=0.16->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.8.4)\n",
- "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2023.12.1)\n",
- "Requirement already satisfied: referencing>=0.28.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.35.1)\n",
- "Requirement already satisfied: rpds-py>=0.7.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.20.0)\n",
- "Requirement already satisfied: python-json-logger>=2.0.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.7)\n",
- "Requirement already satisfied: pyyaml>=5.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.0.2)\n",
- "Requirement already satisfied: rfc3339-validator in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.4)\n",
- "Requirement already satisfied: rfc3986-validator>=0.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.1)\n",
- "Requirement already satisfied: ptyprocess>=0.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pexpect>4.3->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.7.0)\n",
- "Requirement already satisfied: executing>=1.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.1.0)\n",
- "Requirement already satisfied: asttokens>=2.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.4.1)\n",
- "Requirement already satisfied: pure-eval in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.3)\n",
- "Requirement already satisfied: fqdn in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.5.1)\n",
- "Requirement already satisfied: isoduration in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (20.11.0)\n",
- "Requirement already satisfied: jsonpointer>1.13 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.0)\n",
- "Requirement already satisfied: uri-template in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n",
- "Requirement already satisfied: webcolors>=24.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (24.8.0)\n",
- "Requirement already satisfied: arrow>=0.15.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n",
- "Requirement already satisfied: types-python-dateutil>=2.8.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from arrow>=0.15.0->isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.9.0.20240821)\n",
- "\n",
- "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.3.1\u001b[0m\n",
- "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"
- ]
- }
- ],
- "source": [
- "!{sys.executable} -m pip install investing_algorithm_framework"
- ]
- },
{
"cell_type": "markdown",
"metadata": {},
@@ -227,14 +67,6 @@
" !{sys.executable} -m pip install investing_algorithm_framework\n",
"\n",
"try:\n",
- " import investing_algorithm_framework.indicators\n",
- " print(f\"investing_algorithm_framork indicators plugin is already installed.\")\n",
- "except ImportError:\n",
- " print(\"investing_algorithm_framork indicators plugin is not installed. Installing...\")\n",
- " import sys\n",
- " !{sys.executable} -m pip install investing_algorithm_framework[indicators]\n",
- "\n",
- "try:\n",
" import ipywidgets\n",
" print(f\"ipywidgets is already installed.\")\n",
"except ImportError:\n",
@@ -312,15 +144,18 @@
"outputs": [],
"source": [
"from datetime import datetime, timedelta\n",
+ "from investing_algorithm_framework import DateRange\n",
"\n",
- "start_date = datetime(year=2021, month=1, day=1)\n",
- "end_date = datetime(year=2024, month=6, day=1)\n",
- "total_date_range = (datetime(year=2022, month=1, day=1), datetime(year=2024, month=6, day=1))\n",
+ "total_date_range = DateRange(\n",
+ " start_date=datetime(year=2021, month=1, day=1), \n",
+ " end_date=datetime(year=2024, month=6, day=1), \n",
+ " name=\"Total date range\"\n",
+ ")\n",
"\n",
"# Our start date for our data is different then our start_date for our backtest range. This is because we will be using indicators such as the 200 sma, \n",
"# which need to have atleast 200 data points before the start date of our backtest range. If we don't do this,\n",
"# we can't calculate indicators such as the 200 sma for our strategy.\n",
- "start_date_data = start_date - timedelta(days=200)"
+ "start_date_data = total_date_range.start_date - timedelta(days=200)"
]
},
{
@@ -410,8 +245,7 @@
"import ipywidgets as widgets\n",
"from IPython.display import display\n",
"import plotly.graph_objects as go\n",
- "from investing_algorithm_framework.indicators import get_up_and_downtrends, get_sma\n",
- "from investing_algorithm_framework import DateRange\n",
+ "from investing_algorithm_framework.indicators import get_sma\n",
"\n",
"# Add sma 50 and sma 200\n",
"total_data_pandas_df = get_sma(total_data_pandas_df, period=50, source_column_name=\"Close\", result_column_name=\"SMA_50\")\n",
@@ -980,7 +814,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "investing-algorithm-framework-ygOLr3-a-py3.9",
"language": "python",
"name": "python3"
},
diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py
index 2fa42e83..fd8fae81 100644
--- a/investing_algorithm_framework/__init__.py
+++ b/investing_algorithm_framework/__init__.py
@@ -3,7 +3,7 @@
StatelessAction, Task
from investing_algorithm_framework.domain import ApiException, \
TradingDataType, TradingTimeFrame, OrderType, OperationalException, \
- OrderStatus, OrderSide, Config, TimeUnit, TimeInterval, Order, Portfolio, \
+ OrderStatus, OrderSide, TimeUnit, TimeInterval, Order, Portfolio, \
Position, TimeFrame, BACKTESTING_INDEX_DATETIME, MarketCredential, \
PortfolioConfiguration, RESOURCE_DIRECTORY, pretty_print_backtest, \
Trade, OHLCVMarketDataSource, OrderBookMarketDataSource, SYMBOLS, \
@@ -11,11 +11,11 @@
pretty_print_backtest_reports_evaluation, load_backtest_reports, \
RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \
load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \
- DateRange, get_backtest_report
+ DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG
from investing_algorithm_framework.infrastructure import \
CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \
CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \
- CSVTickerMarketDataSource
+ CSVTickerMarketDataSource, AzureBlobStorageStateHandler
from .create_app import create_app
from investing_algorithm_framework.indicators import get_rsi, get_peaks, \
is_uptrend, is_downtrend, is_crossover, is_crossunder, is_above, \
@@ -34,7 +34,6 @@
"OrderType",
"OrderStatus",
"OrderSide",
- "Config",
"PortfolioConfiguration",
"TimeUnit",
"TimeInterval",
@@ -88,5 +87,7 @@
"has_crossed_downward",
"get_willr",
"is_divergence",
- "get_backtest_report"
+ "get_backtest_report",
+ "AzureBlobStorageStateHandler",
+ "DEFAULT_LOGGING_CONFIG"
]
diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py
index c19396bc..8cc3ec16 100644
--- a/investing_algorithm_framework/app/algorithm.py
+++ b/investing_algorithm_framework/app/algorithm.py
@@ -23,13 +23,13 @@ class Algorithm:
class is responsible for managing the strategies and executing
them in the correct order.
- :param (optional) name: The name of the algorithm
- :param (optional) description: The description of the algorithm
- :param (optional) context: The context of the algorithm,
- for backtest references
- :param (optional) strategy: A single strategy to add to the algorithm
- :param (optional) data_sources: The list of data sources to
- add to the algorithm
+ Args:
+ name (str): The name of the algorithm
+ description (str): The description of the algorithm
+ context (dict): The context of the algorithm, for backtest
+ references
+ strategy: A single strategy to add to the algorithm
+ data_sources: The list of data sources to add to the algorithm
"""
def __init__(
self,
@@ -135,13 +135,24 @@ def initialize_services(
self._strategies
)
- def start(self, number_of_iterations=None, stateless=False):
+ def start(self, number_of_iterations: int = None):
+ """
+ Function to start the algorithm.
+ This function will start the algorithm by scheduling all
+ jobs in the strategy orchestrator service. The jobs are not
+ run immediately, but are scheduled to run in the future by the
+ app.
- if not stateless:
- self.strategy_orchestrator_service.start(
- algorithm=self,
- number_of_iterations=number_of_iterations
- )
+ Args:
+ number_of_iterations (int): (Optional) The number of iterations to run the algorithm
+
+ Returns:
+ None
+ """
+ self.strategy_orchestrator_service.start(
+ algorithm=self,
+ number_of_iterations=number_of_iterations
+ )
@property
def name(self):
@@ -229,17 +240,20 @@ def create_order(
and execute it if the execute parameter is set to True. If the
validate parameter is set to True, the order will be validated
- :param target_symbol: The symbol of the asset to trade
- :param price: The price of the asset
- :param order_type: The type of the order
- :param order_side: The side of the order
- :param amount: The amount of the asset to trade
- :param market: The market to trade the asset
- :param execute: If set to True, the order will be executed
- :param validate: If set to True, the order will be validated
- :param sync: If set to True, the created order will be synced
- with the portfolio of the algorithm.
- :return: The order created
+ Args:
+ target_symbol: The symbol of the asset to trade
+ price: The price of the asset
+ order_type: The type of the order
+ order_side: The side of the order
+ amount: The amount of the asset to trade
+ market: The market to trade the asset
+ execute: If set to True, the order will be executed
+ validate: If set to True, the order will be validated
+ sync: If set to True, the created order will be synced
+ with the portfolio of the algorithm.
+
+ Returns:
+ The order created
"""
portfolio = self.portfolio_service.find({"market": market})
order_data = {
@@ -345,9 +359,8 @@ def create_limit_order(
"Percentage of portfolio is only supported for BUY orders."
)
- percentage_of_portfolio = percentage_of_portfolio
net_size = portfolio.get_net_size()
- size = net_size * percentage_of_portfolio / 100
+ size = net_size * (percentage_of_portfolio / 100)
amount = size / price
elif percentage_of_position is not None:
diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py
index 889f4dc2..2604c883 100644
--- a/investing_algorithm_framework/app/app.py
+++ b/investing_algorithm_framework/app/app.py
@@ -1,10 +1,8 @@
import inspect
import logging
import os
-import shutil
import threading
from abc import abstractmethod
-from distutils.sysconfig import get_python_lib
from time import sleep
from typing import List, Optional
@@ -16,7 +14,7 @@
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, OperationalException, BACKTESTING_FLAG, \
+ SQLALCHEMY_DATABASE_URI, OperationalException, \
BACKTESTING_START_DATE, BACKTESTING_END_DATE, BacktestReport, \
BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, APP_MODE, MarketCredential, \
AppMode, BacktestDateRange
@@ -41,10 +39,9 @@ def on_run(self, app, algorithm: Algorithm):
class App:
- def __init__(self, stateless=False, web=False):
+ def __init__(self, state_handler=None, web=False):
self._flask_app: Optional[Flask] = None
self.container = None
- self._stateless = stateless
self._web = web
self._algorithm: Optional[Algorithm] = None
self._started = False
@@ -56,6 +53,7 @@ def __init__(self, stateless=False, web=False):
Optional[MarketCredentialService] = None
self._on_initialize_hooks = []
self._on_after_initialize_hooks = []
+ self._state_handler = state_handler
def add_algorithm(self, algorithm: Algorithm) -> None:
"""
@@ -64,9 +62,13 @@ def add_algorithm(self, algorithm: Algorithm) -> None:
"""
self._algorithm = algorithm
- def set_config(self, config: dict) -> None:
+ def set_config(self, key, value) -> None:
configuration_service = self.container.configuration_service()
- configuration_service.initialize_from_dict(config)
+ configuration_service.add_value(key, value)
+
+ def set_config_with_dict(self, dictionary) -> None:
+ configuration_service = self.container.configuration_service()
+ configuration_service.add_dict(dictionary)
def initialize_services(self) -> None:
self._configuration_service = self.container.configuration_service()
@@ -83,7 +85,64 @@ def algorithm(self) -> Algorithm:
def algorithm(self, algorithm: Algorithm) -> None:
self._algorithm = algorithm
- def initialize(self, sync=False):
+ def initialize_config(self):
+ """
+ Function to initialize the configuration for the app. This method
+ should be called before running the algorithm.
+ """
+ logger.info("Initializing configuration")
+ configuration_service = self.container.configuration_service()
+ config = configuration_service.get_config()
+
+ # Check if the resource directory is set
+ if RESOURCE_DIRECTORY not in config \
+ or config[RESOURCE_DIRECTORY] is None:
+ logger.info(
+ "Resource directory not set, setting" +
+ " to current working directory"
+ )
+ path = os.path.join(os.getcwd(), "resources")
+ configuration_service.add_value(RESOURCE_DIRECTORY, path)
+
+ config = configuration_service.get_config()
+ logger.info(f"Resource directory set to {config[RESOURCE_DIRECTORY]}")
+
+ if DATABASE_NAME not in config or config[DATABASE_NAME] is None:
+ configuration_service.add_value(
+ DATABASE_NAME, "prod-database.sqlite3"
+ )
+
+ config = configuration_service.get_config()
+
+ if DATABASE_DIRECTORY_PATH not in config \
+ or config[DATABASE_DIRECTORY_PATH] is None:
+ resource_dir = config[RESOURCE_DIRECTORY]
+ configuration_service.add_value(
+ DATABASE_DIRECTORY_PATH,
+ os.path.join(resource_dir, "databases")
+ )
+
+ config = configuration_service.get_config()
+
+ if SQLALCHEMY_DATABASE_URI not in config \
+ or config[SQLALCHEMY_DATABASE_URI] is None:
+ path = "sqlite:///" + os.path.join(
+ configuration_service.config[DATABASE_DIRECTORY_PATH],
+ configuration_service.config[DATABASE_NAME]
+ )
+ configuration_service.add_value(SQLALCHEMY_DATABASE_URI, path)
+
+ config = configuration_service.get_config()
+
+ if APP_MODE not in config or config[APP_MODE] is None:
+ if self._web:
+ configuration_service.add_value(APP_MODE, AppMode.WEB.value)
+ else:
+ configuration_service.add_value(
+ APP_MODE, AppMode.DEFAULT.value
+ )
+
+ def initialize(self):
"""
Method to initialize the app. This method should be called before
running the algorithm. It initializes the services and the algorithm
@@ -91,11 +150,11 @@ def initialize(self, sync=False):
Also, it initializes all required services for the algorithm.
- Args:
- sync (bool): Whether to sync the portfolio with the exchange
Returns:
None
"""
+ logger.info("Initializing app")
+
if self.algorithm is None:
raise OperationalException("No algorithm registered")
@@ -122,26 +181,31 @@ def initialize(self, sync=False):
trade_service=self.container.trade_service(),
)
- if APP_MODE not in self.config:
- if self._stateless:
- self.config[APP_MODE] = AppMode.STATELESS.value
- elif self._web:
- self.config[APP_MODE] = AppMode.WEB.value
- else:
- self.config[APP_MODE] = AppMode.DEFAULT.value
+ # Ensure that all resource directories exist
+ self._create_resources_if_not_exists()
+
+ # Setup the database
+ setup_sqlalchemy(self)
+ create_all_tables()
- if AppMode.WEB.from_value(self.config[APP_MODE]):
+ # Initialize all market credentials
+ market_credential_service = self.container.market_credential_service()
+ market_credential_service.initialize()
+
+ # Initialize all market data sources from registered the strategies
+ market_data_source_service = \
+ self.container.market_data_source_service()
+
+ for strategy in self.algorithm.strategies:
+
+ for market_data_source in strategy.market_data_sources:
+ market_data_source_service.add(market_data_source)
+
+ if self._web:
+ self._configuration_service.add_value(
+ APP_MODE, AppMode.WEB.value
+ )
self._initialize_web()
- setup_sqlalchemy(self)
- create_all_tables()
- elif AppMode.STATELESS.from_value(self.config[APP_MODE]):
- self._initialize_stateless()
- setup_sqlalchemy(self)
- create_all_tables()
- else:
- self._initialize_standard()
- setup_sqlalchemy(self)
- create_all_tables()
# Initialize all portfolios that are registered
portfolio_configuration_service = self.container \
@@ -165,16 +229,13 @@ def initialize(self, sync=False):
.create_portfolio_from_configuration(
portfolio_configuration
)
- # self.sync(portfolio)
- # synced_portfolios.append(portfolio)
- if sync:
- portfolios = portfolio_service.get_all()
+ portfolios = portfolio_service.get_all()
- for portfolio in portfolios:
+ for portfolio in portfolios:
- if portfolio not in synced_portfolios:
- self.sync(portfolio)
+ if portfolio not in synced_portfolios:
+ self.sync(portfolio)
def sync(self, portfolio):
"""
@@ -182,38 +243,25 @@ def sync(self, portfolio):
before running the algorithm. It syncs the portfolio with the
exchange by syncing the unallocated balance, positions, orders, and
trades.
+
+ Args:
+ portfolio (Portfolio): The portfolio to sync
+
+ Returns:
+ None
"""
+ logger.info(f"Syncing portfolio {portfolio.identifier}")
portfolio_sync_service = self.container.portfolio_sync_service()
# Sync unallocated balance
portfolio_sync_service.sync_unallocated(portfolio)
- # Sync all positions from exchange with current
- # position history
- portfolio_sync_service.sync_positions(portfolio)
-
# Sync all orders from exchange with current order history
portfolio_sync_service.sync_orders(portfolio)
# Sync all trades from exchange with current trade history
portfolio_sync_service.sync_trades(portfolio)
- def _initialize_stateless(self):
- """
- Initialize the app for stateless mode by setting the configuration
- parameters for stateless mode and overriding the services with the
- stateless services equivalents.
-
- In stateless mode, sqlalchemy is-setup with an in-memory database.
-
- Stateless has the following implications:
- db: in-memory
- web: False
- app: Run with stateless action objects
- algorithm: Run with stateless action objects
- """
- configuration_service = self.container.configuration_service()
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
def _initialize_standard(self):
"""
@@ -266,16 +314,21 @@ def _initialize_app_for_backtest(
"""
# Set all config vars for backtesting
configuration_service = self.container.configuration_service()
- configuration_service.config[BACKTESTING_FLAG] = True
- configuration_service.config[BACKTESTING_START_DATE] = \
- backtest_date_range.start_date
- configuration_service.config[BACKTESTING_END_DATE] = \
- backtest_date_range.end_date
+ configuration_service.add_value(
+ ENVIRONMENT, Environment.BACKTEST.value
+ )
+ configuration_service.add_value(
+ BACKTESTING_START_DATE, backtest_date_range.start_date
+ )
+ configuration_service.add_value(
+ BACKTESTING_END_DATE, backtest_date_range.end_date
+ )
if pending_order_check_interval is not None:
- configuration_service.config[
- BACKTESTING_PENDING_ORDER_CHECK_INTERVAL
- ] = pending_order_check_interval
+ configuration_service.add_value(
+ BACKTESTING_PENDING_ORDER_CHECK_INTERVAL,
+ pending_order_check_interval
+ )
# Create resource dir if not exits
self._create_resource_directory_if_not_exists()
@@ -296,10 +349,14 @@ def _create_backtest_database_if_not_exists(self):
resource_dir = configuration_service.config[RESOURCE_DIRECTORY]
# Create the database if not exists
- configuration_service.config[DATABASE_DIRECTORY_PATH] = \
+ configuration_service.add_value(
+ DATABASE_NAME, "backtest-database.sqlite3"
+ )
+ configuration_service.add_value(
+ DATABASE_DIRECTORY_PATH,
os.path.join(resource_dir, "databases")
- configuration_service.config[DATABASE_NAME] = \
- "backtest-database.sqlite3"
+ )
+
database_path = os.path.join(
configuration_service.config[DATABASE_DIRECTORY_PATH],
configuration_service.config[DATABASE_NAME]
@@ -308,11 +365,15 @@ def _create_backtest_database_if_not_exists(self):
if os.path.exists(database_path):
os.remove(database_path)
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = \
+ sql_alchemy_uri = \
"sqlite:///" + os.path.join(
configuration_service.config[DATABASE_DIRECTORY_PATH],
configuration_service.config[DATABASE_NAME]
)
+
+ configuration_service.add_value(
+ SQLALCHEMY_DATABASE_URI, sql_alchemy_uri
+ )
self._create_database_if_not_exists()
setup_sqlalchemy(self)
create_all_tables()
@@ -460,97 +521,100 @@ def _initialize_algorithm_for_backtest(self, algorithm):
portfolio_configuration
)
- def _initialize_management_commands(self):
-
- if not Environment.TEST.equals(self.config.get(ENVIRONMENT)):
- # Copy the template manage.py file to the resource directory of the
- # algorithm
- management_commands_template = os.path.join(
- get_python_lib(),
- "investing_algorithm_framework/templates/manage.py"
- )
- destination = os.path.join(
- self.config.get(RESOURCE_DIRECTORY), "manage.py"
- )
-
- if not os.path.exists(destination):
- shutil.copy(management_commands_template, destination)
-
def run(
self,
payload: dict = None,
number_of_iterations: int = None,
- sync=False
):
"""
Entry point to run the application. This method should be called to
- start the algorithm. The method runs the algorithm for the specified
- number of iterations and handles the payload if the app is running in
- stateless mode.
+ start the algorithm. This method can be called in three modes:
+
+ - Without any params: In this mode, the app runs until a keyboard
+ interrupt is received. This mode is useful when running the app in
+ a loop.
+ - With a payload: In this mode, the app runs only once with the
+ payload provided. This mode is useful when running the app in a
+ one-off mode, such as running the app from the command line or
+ on a schedule. Payload is a dictionary that contains the data to
+ handle for the algorithm. This data should look like this:
+ {
+ "action": "RUN_STRATEGY",
+ }
+ - With a number of iterations: In this mode, the app runs for the
+ number of iterations provided. This mode is useful when running the
+ app in a loop for a fixed number of iterations.
+
+ This function first checks if there is an algorithm registered. If not, it raises an OperationalException. Then it initializes the algorithm with the services and the configuration.
- First the app checks if there is an algorithm registered. If not, it
- raises an OperationalException. Then it initializes the algorithm
- with the services and the configuration.
-
- If the app is running in stateless mode, it handles the
- payload. If the app is running in web mode, it starts the web app in a
- separate thread.
Args:
- payload (dict): The payload to handle if the app is running in
- stateless mode
+ payload (dict): The payload to handle for the algorithm
number_of_iterations (int): The number of iterations to run the
algorithm for
- sync (bool): Whether to sync the portfolio with the exchange
Returns:
None
"""
- # Run all on_initialize hooks
- for hook in self._on_after_initialize_hooks:
- hook.on_run(self, self.algorithm)
-
- self.initialize(sync=sync)
-
- # Run all on_initialize hooks
- for hook in self._on_initialize_hooks:
- hook.on_run(self, self.algorithm)
-
- self.algorithm.start(
- number_of_iterations=number_of_iterations,
- stateless=self.stateless
- )
-
- if AppMode.STATELESS.equals(self.config[APP_MODE]):
- logger.info("Running stateless")
- action_handler = ActionHandler.of(payload)
- return action_handler.handle(
- payload=payload, algorithm=self.algorithm
- )
- elif AppMode.WEB.equals(self.config[APP_MODE]):
- logger.info("Running web")
- flask_thread = threading.Thread(
- name='Web App',
- target=self._flask_app.run,
- kwargs={"port": 8080}
- )
- flask_thread.setDaemon(True)
- flask_thread.start()
-
- number_of_iterations_since_last_orders_check = 1
- self.algorithm.check_pending_orders()
-
try:
- while self.algorithm.running:
- if number_of_iterations_since_last_orders_check == 30:
- logger.info("Checking pending orders")
- number_of_iterations_since_last_orders_check = 1
+ self.initialize_config()
+
+ # Load the state if a state handler is provided
+ if self._state_handler is not None:
+ logger.info("Detected state handler, loading state")
+ config = self.container.configuration_service().get_config()
+ self._state_handler.load(config[RESOURCE_DIRECTORY])
+
+ self.initialize()
+ logger.info("Initialization complete")
+
+ # Run all on_initialize hooks
+ for hook in self._on_initialize_hooks:
+ hook.on_run(self, self.algorithm)
+
+ configuration_service = self.container.configuration_service()
+ config = configuration_service.get_config()
+
+ # Run in payload mode if payload is provided
+ if payload is not None:
+ logger.info("Running with payload")
+ action_handler = ActionHandler.of(payload)
+ response = action_handler.handle(
+ payload=payload, algorithm=self.algorithm
+ )
+ return response
+
+ if AppMode.WEB.equals(config[APP_MODE]):
+ logger.info("Running web")
+ flask_thread = threading.Thread(
+ name='Web App',
+ target=self._flask_app.run,
+ kwargs={"port": 8080}
+ )
+ flask_thread.setDaemon(True)
+ flask_thread.start()
+
+ self.algorithm.start(number_of_iterations=number_of_iterations)
+ number_of_iterations_since_last_orders_check = 1
+ self.algorithm.check_pending_orders()
- self.algorithm.run_jobs()
- number_of_iterations_since_last_orders_check += 1
- sleep(1)
- except KeyboardInterrupt:
- exit(0)
+ try:
+ while self.algorithm.running:
+ if number_of_iterations_since_last_orders_check == 30:
+ logger.info("Checking pending orders")
+ number_of_iterations_since_last_orders_check = 1
+
+ self.algorithm.run_jobs()
+ number_of_iterations_since_last_orders_check += 1
+ sleep(1)
+ except KeyboardInterrupt:
+ exit(0)
+ finally:
+ # Upload state if state handler is provided
+ if self._state_handler is not None:
+ logger.info("Detected state handler, saving state")
+ config = self.container.configuration_service().get_config()
+ self._state_handler.save(config[RESOURCE_DIRECTORY])
@property
def started(self):
@@ -580,10 +644,6 @@ def add_portfolio_configuration(self, portfolio_configuration):
.portfolio_configuration_service()
portfolio_configuration_service.add(portfolio_configuration)
- @property
- def stateless(self):
- return self._stateless
-
@property
def web(self):
return self._web
@@ -633,39 +693,26 @@ def _initialize_web(self):
- Algorithm
"""
configuration_service = self.container.configuration_service()
- resource_dir = configuration_service.config[RESOURCE_DIRECTORY]
+ self._flask_app = create_flask_app(configuration_service)
- if resource_dir is None:
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://"
- else:
- resource_dir = self._create_resource_directory_if_not_exists()
- configuration_service.config[DATABASE_DIRECTORY_PATH] = \
- os.path.join(resource_dir, "databases")
- configuration_service.config[DATABASE_NAME] \
- = "prod-database.sqlite3"
- configuration_service.config[SQLALCHEMY_DATABASE_URI] = \
- "sqlite:///" + os.path.join(
- configuration_service.config[DATABASE_DIRECTORY_PATH],
- configuration_service.config[DATABASE_NAME]
- )
- self._create_database_if_not_exists()
-
- self._flask_app = create_flask_app(configuration_service.config)
-
- def _create_resource_directory_if_not_exists(self):
-
- if self._stateless:
- return
+ def _create_resources_if_not_exists(self):
+ """
+ Function to create the resources required by the app if they do not exist. This function will check if the resource directory exists and
+ check if the database directory exists. If they do not exist, it will create them.
+ Returns:
+ None
+ """
configuration_service = self.container.configuration_service()
- resource_dir = configuration_service.config.get(
- RESOURCE_DIRECTORY, None
- )
+ config = configuration_service.get_config()
+ resource_dir = config[RESOURCE_DIRECTORY]
+ database_dir = config[DATABASE_DIRECTORY_PATH]
if resource_dir is None:
raise OperationalException(
- "Resource directory is not specified. "
- "A resource directory is required for running a backtest."
+ "Resource directory is not specified in the config, please "
+ "specify the resource directory in the config with the key "
+ "RESOURCE_DIRECTORY"
)
if not os.path.isdir(resource_dir):
@@ -677,13 +724,16 @@ def _create_resource_directory_if_not_exists(self):
"Could not create resource directory"
)
- return resource_dir
+ if not os.path.isdir(database_dir):
+ try:
+ os.makedirs(database_dir)
+ except OSError as e:
+ logger.error(e)
+ raise OperationalException(
+ "Could not create database directory"
+ )
def _create_database_if_not_exists(self):
-
- if self._stateless:
- return
-
configuration_service = self.container.configuration_service()
database_dir = configuration_service.config \
.get(DATABASE_DIRECTORY_PATH, None)
@@ -691,7 +741,8 @@ def _create_database_if_not_exists(self):
if database_dir is None:
return
- database_name = configuration_service.config.get(DATABASE_NAME, None)
+ config = configuration_service.get_config()
+ database_name = config[DATABASE_NAME]
if database_name is None:
return
@@ -750,9 +801,9 @@ def run_backtest(
algorithm=self.algorithm
)
backtest_service = self.container.backtest_service()
- backtest_service.resource_directory = self.config.get(
- RESOURCE_DIRECTORY
- )
+ configuration_service = self.container.configuration_service()
+ config = configuration_service.get_config()
+ backtest_service.resource_directory = config[RESOURCE_DIRECTORY]
# Run the backtest with the backtest_service and collect the report
report = backtest_service.run_backtest(
@@ -763,8 +814,7 @@ def run_backtest(
if output_directory is None:
output_directory = os.path.join(
- self.config.get(RESOURCE_DIRECTORY),
- "backtest_reports"
+ config[RESOURCE_DIRECTORY], "backtest_reports"
)
backtest_report_writer_service.write_report_to_json(
@@ -847,9 +897,9 @@ def run_backtests(
self._initialize_algorithm_for_backtest(algorithm)
backtest_service = self.container.backtest_service()
- backtest_service.resource_directory = self.config.get(
+ backtest_service.resource_directory = self.config[
RESOURCE_DIRECTORY
- )
+ ]
# Run the backtest with the backtest_service
# and collect the report
@@ -866,8 +916,7 @@ def run_backtests(
if output_directory is None:
output_directory = os.path.join(
- self.config.get(RESOURCE_DIRECTORY),
- "backtest_reports"
+ self.config[RESOURCE_DIRECTORY], "backtest_reports"
)
backtest_report_writer_service.write_report_to_json(
diff --git a/investing_algorithm_framework/app/web/__init__.py b/investing_algorithm_framework/app/web/__init__.py
index c3b5dfba..b5ed007f 100644
--- a/investing_algorithm_framework/app/web/__init__.py
+++ b/investing_algorithm_framework/app/web/__init__.py
@@ -1,4 +1,5 @@
from .create_app import create_flask_app
from .run_strategies import run_strategies
+from .schemas import OrderSerializer
-__all__ = ["create_flask_app", "run_strategies"]
+__all__ = ["create_flask_app", "run_strategies", 'OrderSerializer']
diff --git a/investing_algorithm_framework/app/web/create_app.py b/investing_algorithm_framework/app/web/create_app.py
index e82badf4..9a1ec0af 100644
--- a/investing_algorithm_framework/app/web/create_app.py
+++ b/investing_algorithm_framework/app/web/create_app.py
@@ -5,10 +5,12 @@
from .error_handler import setup_error_handler
-def create_flask_app(config):
+def create_flask_app(configuration_service):
app = Flask(__name__.split('.')[0])
- for key, value in config.items():
+ flask_config = configuration_service.get_flask_config()
+
+ for key, value in flask_config.items():
app.config[key] = value
app = setup_cors(app)
diff --git a/investing_algorithm_framework/deployment/__init__.py b/investing_algorithm_framework/cli/__init__.py
similarity index 100%
rename from investing_algorithm_framework/deployment/__init__.py
rename to investing_algorithm_framework/cli/__init__.py
diff --git a/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py
new file mode 100644
index 00000000..a9868d83
--- /dev/null
+++ b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py
@@ -0,0 +1,118 @@
+import os
+import click
+
+
+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
+ """
+ with open(template_path, "r") as file:
+ template = file.read()
+
+ with open(output_path, "w") as file:
+ file.write(template)
+
+def create_azure_function_skeleton(
+ add_app_template, add_requirements_template
+):
+ """
+ Function to create an azure function app skeleton.
+
+ Args:
+ create_app_skeleton (bool): Flag to create an app skeleton.
+
+ Returns:
+ None
+ """
+
+ # Get current working directory
+ cwd = os.getcwd()
+
+ # Get the path of this script (command.py)
+ current_script_path = os.path.abspath(__file__)
+
+ # Construct the path to the template file
+ template_host_file_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_host.json.template"
+ )
+ template_settings_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_local.settings.json.template"
+ )
+ function_app_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_function_app.py.template"
+ )
+
+ if add_app_template:
+ function_app_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_framework_app.py.template"
+ )
+
+ if add_requirements_template:
+ requirements_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_requirements.txt.template"
+ )
+
+ create_file_from_template(
+ template_host_file_path,
+ os.path.join(cwd, "host.json")
+ )
+ create_file_from_template(
+ template_settings_path,
+ os.path.join(cwd, "local.settings.json")
+ )
+ create_file_from_template(
+ function_app_path,
+ os.path.join(cwd, "function_app.py")
+ )
+ create_file_from_template(
+ requirements_path,
+ os.path.join(cwd, "requirements.txt")
+ )
+ print(
+ f"Function App trading bot skeleton creation completed"
+ )
+
+
+@click.command()
+@click.option(
+ '--add-app-template',
+ is_flag=True,
+ help='Flag to create an framework app skeleton',
+ default=False
+)
+@click.option(
+ '--add-requirements-template',
+ is_flag=True,
+ help='Flag to create an framework app skeleton',
+ default=True
+)
+def cli(add_app_template, add_requirements_template):
+ """
+ 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
+ """
+ create_azure_function_skeleton(add_app_template, add_requirements_template)
diff --git a/investing_algorithm_framework/cli/deploy_to_azure_function.py b/investing_algorithm_framework/cli/deploy_to_azure_function.py
new file mode 100644
index 00000000..79bb820f
--- /dev/null
+++ b/investing_algorithm_framework/cli/deploy_to_azure_function.py
@@ -0,0 +1,651 @@
+import os
+import subprocess
+import re
+import click
+import random
+import string
+import asyncio
+
+from azure.identity import DefaultAzureCredential
+from azure.mgmt.resource import ResourceManagementClient
+from azure.mgmt.storage import StorageManagementClient
+from azure.mgmt.web import WebSiteManagementClient
+
+STORAGE_ACCOUNT_NAME_PREFIX = "iafstorageaccount"
+
+
+def generate_unique_resource_name(base_name):
+ """
+ Function to generate a unique resource name by appending a random suffix.
+
+ Args:
+ base_name (str): The base name for the resource.
+
+ Returns:
+ str: The unique resource name.
+ """
+ unique_suffix = ''.join(
+ random.choices(string.ascii_lowercase + string.digits, k=6)
+ )
+ return f"{base_name}{unique_suffix}".lower()
+
+
+def ensure_azure_functools():
+ """
+ Function to ensure that the Azure Functions Core Tools are installed.
+ If not, it will prompt the user to install it.
+ """
+
+ try:
+ result = subprocess.run(
+ ["func", "--version"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ if result.returncode != 0:
+ raise FileNotFoundError("Azure Functions Core Tools not found.")
+ except FileNotFoundError:
+ print("Azure Functions Core Tools not found. Please install it.")
+ print("You can install it using the following command:")
+ print("npm install -g azure-functions-core-tools@4 --unsafe-perm true")
+ exit(1)
+
+
+async def publish_function_app(
+ function_app_name,
+ storage_connection_string,
+ storage_container_name,
+ resource_group_name
+):
+ """
+ Function to publish the Function App using Azure Functions Core Tools.
+
+ Args:
+ function_app_name (str): Name of the Function App to publish.
+ storage_connection_string (str): Azure Storage Connection String.
+ storage_container_name (str): Azure Storage Container Name.
+ resource_group_name (str): Resource Group Name.
+
+ Returns:
+ None
+ """
+ print(f"Publishing Function App {function_app_name}")
+
+ try:
+ # Step 1: Publish the Azure Function App
+ process = await asyncio.create_subprocess_exec(
+ "func", "azure", "functionapp", "publish", function_app_name
+ )
+
+ # Wait for the subprocess to finish
+ _, stderr = await process.communicate()
+
+ # Check the return code
+ if process.returncode != 0:
+
+ if stderr is not None:
+ raise Exception(
+ f"Error publishing Function App: {stderr.decode().strip()}"
+ )
+ else:
+ raise Exception("Error publishing Function App")
+
+ print(f"Function App {function_app_name} published successfully.")
+
+ # Step 2: Add app settings
+ add_settings_process = await asyncio.create_subprocess_exec(
+ "az", "functionapp", "config", "appsettings", "set",
+ "--name", function_app_name,
+ "--settings",
+ f"AZURE_STORAGE_CONNECTION_STRING={storage_connection_string}",
+ f"AZURE_STORAGE_CONTAINER_NAME={storage_container_name}",
+ f"--resource-group", resource_group_name
+ )
+ _, stderr1 = await add_settings_process.communicate()
+
+ if add_settings_process.returncode != 0:
+
+ if stderr1 is not None:
+ raise Exception(
+ f"Error adding App settings: {stderr1.decode().strip()}"
+ )
+ else:
+ raise Exception("Error adding App settings")
+
+ print(
+ f"Added app settings to the Function App successfully"
+ )
+
+ # Step 3: Update the cors settings
+ cors_process = await asyncio.create_subprocess_exec(
+ "az", "functionapp", "cors", "add",
+ "--name", function_app_name,
+ "--allowed-origins", "*",
+ "--resource-group", resource_group_name
+ )
+
+ _, stderr1 = await add_settings_process.communicate()
+
+ if cors_process.returncode != 0:
+
+ if stderr1 is not None:
+ raise Exception(
+ f"Error adding cors settings: {stderr1.decode().strip()}"
+ )
+ else:
+ raise Exception("Error adding cors settings")
+
+ print("All app settings have been added successfully.")
+ print(f"Function App creation completed successfully.")
+ except Exception as e:
+ print(f"Error publishing Function App: {e}")
+
+
+async def create_function_app(
+ resource_group_name,
+ deployment_name,
+ storage_account_name,
+ region
+):
+ """
+ Creates an Azure Function App in a Consumption Plan and deploys a Python Function.
+
+ Args:
+ resource_group_name (str): Resource group name.
+ deployment_name (str): Name of the Function App to create.
+ storage_account_name (str): Name of the associated Storage Account.
+ region (str): Azure region (e.g., "eastus").
+
+ Returns:
+ dict: Details of the created or existing Function App.
+ """
+ # Check if the Function App already exists
+ print(f"Checking if Function App '{deployment_name}' exists...")
+
+ try:
+ # Check for the Function App
+ check_process = await asyncio.create_subprocess_exec(
+ "az",
+ "functionapp",
+ "show",
+ "--name",
+ deployment_name,
+ "--resource-group",
+ resource_group_name,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE
+ )
+ stdout, stderr = await check_process.communicate()
+
+ if check_process.returncode == 0:
+ # The Function App exists, return details
+ print(f"Function App '{deployment_name}' already exists.")
+ return stdout.decode()
+
+ # If the return code is non-zero, and the error indicates the Function App doesn't exist, proceed to create it
+ if "ResourceNotFound" in stderr.decode():
+ print(f"Function App '{deployment_name}' does not exist. Proceeding to create it...")
+ else:
+ # If the error is something else, raise it
+ print(f"Error checking for Function App: {stderr.decode()}")
+ raise Exception(stderr.decode())
+
+ # Create the Function App
+ print(f"Creating Function App '{deployment_name}'...")
+ create_process = await asyncio.create_subprocess_exec(
+ "az",
+ "functionapp",
+ "create",
+ "--resource-group",
+ resource_group_name,
+ "--consumption-plan-location",
+ region,
+ "--runtime",
+ "python",
+ "--runtime-version",
+ "3.10",
+ "--functions-version",
+ "4",
+ "--name",
+ deployment_name,
+ "--os-type",
+ "linux",
+ "--storage-account",
+ storage_account_name
+ )
+
+ # Wait for the subprocess to finish
+ _, create_stderr = await create_process.communicate()
+
+ # Check the return code for the create command
+ if create_process.returncode != 0:
+ print(f"Error creating Function App: {create_stderr.decode().strip()}")
+ raise Exception(f"Error creating Function App: {create_stderr.decode().strip()}")
+
+ print(f"Function App '{deployment_name}' created successfully.")
+ return {"status": "created"}
+
+ except Exception as e:
+ print(f"Error creating Function App: {e}")
+ raise e
+
+
+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
+ """
+ with open(template_path, "r") as file:
+ template = file.read()
+
+ with open(output_path, "w") as file:
+ file.write(template)
+
+
+def ensure_consumption_plan(
+ resource_group_name,
+ plan_name,
+ region,
+ subscription_id,
+ credential
+):
+ """
+ Ensures that an App Service Plan with the Consumption Plan exists. If not, creates it.
+
+ Args:
+ resource_group_name (str): The name of the resource group.
+ plan_name (str): The name of the App Service Plan.
+ region (str): The Azure region for the resources.
+ subscription_id (str): The Azure subscription ID.
+
+ Returns:
+ object: The App Service Plan object.
+ """
+ web_client = WebSiteManagementClient(credential, subscription_id)
+
+ try:
+ print(
+ f"Checking if App Service Plan '{plan_name}' exists in resource group '{resource_group_name}'..."
+ )
+ plan = web_client.app_service_plans.get(resource_group_name, plan_name)
+ print(f"App Service Plan '{plan_name}' already exists.")
+ except Exception: # Plan does not exist
+ print(
+ f"App Service Plan '{plan_name}' not found. Creating it as a Consumption Plan..."
+ )
+ plan = web_client.app_service_plans.begin_create_or_update(
+ resource_group_name,
+ plan_name,
+ {
+ "location": region,
+ "sku": {"name": "Y1", "tier": "Dynamic"},
+ "kind": "functionapp", # Mark this as for Function Apps
+ "properties": {}
+ },
+ ).result()
+ print(f"App Service Plan '{plan_name}' created successfully.")
+ return plan
+
+def ensure_storage_account(
+ storage_account_name,
+ resource_group_name,
+ region,
+ subscription_id,
+ credential,
+):
+ """
+ Checks if a storage account exists. If it doesn't, creates it.
+
+ If no storage account name is provided, a unique name will be generated.
+ However, before we create a new storage account, we check if there a storage account exists with the prefix 'iafstorageaccount'. If it exists, we use that storage account.
+
+ Args:
+ storage_account_name (str): The name of the storage account.
+ resource_group_name (str): The name of the resource group.
+ region (str): The Azure region for the resources.
+ subscription_id (str): The Azure subscription ID.
+ credential: Azure credentials object.
+
+ Returns:
+ StorageAccount: The created storage account object.
+ """
+ # Create Storage Management Client
+ storage_client = StorageManagementClient(credential, subscription_id)
+
+ # Check if the storage account exists
+ try:
+
+ # Check if provided storage account name has prefix 'iafstorageaccount'
+ if storage_account_name.startswith(STORAGE_ACCOUNT_NAME_PREFIX):
+ # List all storage accounts in the resource group
+ storage_accounts = storage_client\
+ .storage_accounts.list_by_resource_group(
+ resource_group_name
+ )
+
+ for account in storage_accounts:
+ if account.name.startswith(STORAGE_ACCOUNT_NAME_PREFIX):
+ storage_account_name = account.name
+ break
+
+ storage_client.storage_accounts.get_properties(
+ resource_group_name,
+ storage_account_name,
+ )
+ print(f"Storage account '{storage_account_name}' already exists.")
+ account_key = storage_client.storage_accounts.list_keys(
+ resource_group_name,
+ storage_account_name,
+ ).keys[1].value
+ connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net"
+ return connection_string, storage_account_name
+ except Exception as e: # If the storage account does not exist
+ print("Creating storage account ...")
+
+ # Create storage account
+ storage_async_operation = storage_client.storage_accounts.begin_create(
+ resource_group_name,
+ storage_account_name,
+ {
+ "location": region,
+ "sku": {"name": "Standard_LRS"},
+ "kind": "StorageV2",
+ },
+ )
+ storage_async_operation.result()
+
+ if storage_async_operation.status() == "Succeeded":
+ print(f"Storage account '{storage_account_name}' created successfully.")
+
+ account_key = storage_client.storage_accounts.list_keys(
+ resource_group_name,
+ storage_account_name,
+ ).keys[1].value
+ connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net"
+ return connection_string, storage_account_name
+
+
+def ensure_az_login(skip_check=False):
+ """
+ Ensures the user is logged into Azure using `az login`.
+ If not logged in, it will prompt the user to log in.
+
+ Raises:
+ Exception: An error occurred during the login process.
+ """
+
+ if skip_check:
+ return
+
+ result = subprocess.run(["az", "login"], check=True)
+
+ if result.returncode != 0:
+ raise Exception("An error occurred during 'az login'.")
+
+
+def get_default_subscription_id():
+ """
+ Fetches the default subscription ID using Azure CLI.
+
+ Returns:
+ str: The default subscription ID.
+ """
+ print("Fetching default subscription ID...")
+
+ # Check if an default subscription ID is set in the environment
+ if "AZURE_SUBSCRIPTION_ID" in os.environ:
+ return os.environ["AZURE_SUBSCRIPTION_ID"]
+
+ try:
+ print(
+ "If you want to use a different subscription, please provide the"
+ " subscription ID with the '--subscription_id' option or"
+ " by setting the 'AZURE_SUBSCRIPTION_ID' environment variable."
+ )
+ result = subprocess.run(
+ ["az", "account", "show", "--query", "id", "-o", "tsv"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ check=True,
+ )
+ subscription_id = result.stdout.strip()
+ print(f"Default subscription ID: {subscription_id}")
+ return subscription_id
+ except subprocess.CalledProcessError as e:
+ print("Error fetching default subscription ID. Please log in with 'az login'.")
+ raise
+
+def ensure_resource_group(
+ resource_group_name,
+ region,
+ subscription_id,
+ create_if_not_exists
+):
+ """
+ Checks if a resource group exists. If it doesn't, creates it if `create_if_not_exists` is True.
+
+ Args:
+ resource_group_name (str): The name of the resource group.
+ region (str): The Azure region for the resources.
+ subscription_id (str): The Azure subscription ID.
+ create_if_not_exists (bool): Flag to create the resource group if it does not exist.
+
+ Returns:
+ None
+ """
+ credential = DefaultAzureCredential()
+ resource_client = ResourceManagementClient(credential, subscription_id)
+
+ print(f"Checking if resource group '{resource_group_name}' exists...")
+ try:
+ resource_group = resource_client.resource_groups.get(resource_group_name)
+ print(f"Resource group '{resource_group_name}' already exists.")
+ except Exception: # If the resource group does not exist
+
+ try:
+ if create_if_not_exists:
+ print(f"Resource group '{resource_group_name}' not found. Creating it...")
+ resource_client.resource_groups.create_or_update(
+ resource_group_name,
+ {"location": region},
+ )
+ print(f"Resource group '{resource_group_name}' created successfully.")
+ else:
+ print(f"Resource group '{resource_group_name}' does not exist, and 'create_if_not_exists' is False.")
+ raise ValueError(f"Resource group '{resource_group_name}' does not exist.")
+ except Exception as e:
+ raise Exception(f"Error creating resource group: {e}")
+
+
+def create_storage_and_function(
+ resource_group_name,
+ storage_account_name,
+ container_name,
+ deployment_name,
+ region,
+ subscription_id=None,
+ create_resource_group_if_not_exists=False,
+ skip_login=False
+):
+
+ # Make sure that the deployment name only contains lowercase letters, and
+ # uppercase letters
+ regex = r"^[a-zA-Z0-9]+$"
+ if not re.match(regex, deployment_name):
+ raise ValueError(
+ "--deployment_name can only contain " +
+ "letters (uppercase and lowercase)."
+ )
+ # Get current working directory
+ cwd = os.getcwd()
+
+ # Get the path of this script (command.py)
+ current_script_path = os.path.abspath(__file__)
+
+ # Construct the path to the template file
+ template_host_file_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_host.json.template"
+ )
+ template_settings_path = os.path.join(
+ os.path.dirname(current_script_path),
+ "templates",
+ "azure_function_local.settings.json.template"
+ )
+
+ create_file_from_template(
+ template_host_file_path, os.path.join(cwd, "host.json")
+ )
+
+ create_file_from_template(
+ template_settings_path, os.path.join(cwd, "local.settings.json")
+ )
+
+ # Ensure the user is logged in
+ ensure_az_login(skip_check=skip_login)
+
+ # ensure_azure_functools()
+ ensure_azure_functools()
+
+ # Fetch default subscription ID if not provided
+ if not subscription_id:
+ subscription_id = get_default_subscription_id()
+
+ # Authenticate using DefaultAzureCredential (requires environment variables or Azure CLI login)
+ credential = DefaultAzureCredential()
+
+ # Check if the resource group exists
+ ensure_resource_group(
+ resource_group_name,
+ region,
+ subscription_id,
+ create_resource_group_if_not_exists
+ )
+
+ if storage_account_name is None:
+ storage_account_name = \
+ generate_unique_resource_name(STORAGE_ACCOUNT_NAME_PREFIX)
+
+ # Ensure storage account exists
+ storage_account_connection_string, storage_account_name = ensure_storage_account(
+ storage_account_name,
+ resource_group_name,
+ region,
+ subscription_id,
+ credential
+ )
+
+ # Create Function App
+ asyncio.run(
+ create_function_app(
+ resource_group_name=resource_group_name,
+ deployment_name=deployment_name,
+ region=region,
+ storage_account_name=storage_account_name
+ )
+ )
+
+ # Publish Function App
+ asyncio.run(
+ publish_function_app(
+ function_app_name=deployment_name,
+ storage_connection_string=storage_account_connection_string,
+ storage_container_name=container_name,
+ resource_group_name=resource_group_name
+ )
+ )
+
+ print(
+ f"Function App '{deployment_name}' deployment" +
+ "completed successfully."
+ )
+
+
+@click.command()
+@click.option(
+ '--resource_group',
+ required=True,
+ help='The name of the resource group.',
+)
+@click.option(
+ '--subscription_id',
+ required=False,
+ help='The subscription ID. If not provided, the default will be used.'
+)
+@click.option(
+ '--storage_account_name',
+ required=False,
+ help='The name of the storage account.',
+)
+@click.option(
+ '--container_name',
+ required=False,
+ help='The name of the blob container.',
+ default='iafcontainer'
+)
+@click.option(
+ '--deployment_name',
+ required=True,
+ help='The name of the deployment. This will be used as the name of the Function App.'
+)
+@click.option(
+ '--region',
+ required=True,
+ help='The Azure region for the resources.'
+)
+@click.option(
+ '--create_resource_group_if_not_exists',
+ is_flag=True,
+ help='Flag to create the resource group if it does not exist.'
+)
+@click.option(
+ '--skip_login',
+ is_flag=True,
+ help='Flag to create the resource group if it does not exist.',
+ default=False
+)
+def cli(
+ resource_group,
+ subscription_id,
+ storage_account_name,
+ container_name,
+ deployment_name,
+ region,
+ create_resource_group_if_not_exists,
+ skip_login
+):
+ """
+ Command-line tool for creating an Azure storage account, blob container, and Function App.
+
+ Args:
+ resource_group (str): The name of the resource group.
+ subscription_id (str): The Azure subscription ID.
+ storage_account_name (str): The name of the storage account.
+ container_name (str): The name of the blob container.
+ function_app (str): The name of the Azure Function App.
+ region (str): The Azure region for the resources.
+ create_resource_group_if_not_exists (bool): Flag to create the resource group if it does not exist.
+
+ Returns:
+ None
+ """
+ create_storage_and_function(
+ resource_group_name=resource_group,
+ storage_account_name=storage_account_name,
+ container_name=container_name,
+ deployment_name=deployment_name,
+ region=region,
+ subscription_id=subscription_id,
+ skip_login=skip_login,
+ create_resource_group_if_not_exists=create_resource_group_if_not_exists
+ )
diff --git a/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template
new file mode 100644
index 00000000..ee2d1cbd
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template
@@ -0,0 +1,49 @@
+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/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template
new file mode 100644
index 00000000..7291ffa1
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template
@@ -0,0 +1,90 @@
+import logging
+
+import azure.functions as func
+from investing_algorithm_framework import StatelessAction
+from app import app as investing_algorithm_framework_app
+
+
+import logging
+import logging.config
+
+LOGGING_CONFIG = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'default': {
+ 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'default',
+ },
+ 'file': {
+ 'class': 'logging.FileHandler',
+ 'formatter': 'default',
+ 'filename': 'app_logs.log',
+ },
+ },
+ 'loggers': { # Make sure to add a 'loggers' section
+ 'investing_algorithm_framework': { # Define your logger here
+ 'level': 'INFO', # Set the desired level
+ 'handlers': ['console', 'file'], # Use these handlers
+ 'propagate': False, # Prevent logs from propagating to the root logger (optional)
+ },
+ },
+ 'root': { # Optional: Root logger configuration
+ 'level': 'WARNING', # Root logger defaults to WARNING
+ 'handlers': ['console', 'file'],
+ },
+}
+
+logging.config.dictConfig(LOGGING_CONFIG)
+app: func.FunctionApp = func.FunctionApp()
+
+# Change your interval here, e.ge. "0 */1 * * * *" for every minute
+# or "0 0 */1 * * *" for every hour or "0 */5 * * * *" for every 5 minutes
+@func.timer_trigger(
+ schedule="0 */5 * * * *",
+ arg_name="myTimer",
+ run_on_startup=False,
+ use_monitor=False
+)
+@func.http_trigger(
+ route='start', methods=["GET"], auth_level=func.AuthLevel.FUNCTION
+)
+def app(myTimer: func.TimerRequest) -> None:
+
+ if myTimer.past_due:
+ logging.info('The timer is past due!')
+
+ logging.info('Python timer trigger function ran at %s', myTimer.next)
+ investing_algorithm_framework_app.run(
+ payload={"ACTION": StatelessAction.RUN_STRATEGY.value}
+ )
+
+@app.route(route="test", auth_level=func.AuthLevel.ANONYMOUS)
+def test(req: func.HttpRequest) -> func.HttpResponse:
+ logging.info('Python HTTP trigger function processed a request.')
+
+ name = req.params.get('name')
+ investing_algorithm_framework_app.run(
+ payload={"ACTION": StatelessAction.RUN_STRATEGY.value}
+ )
+
+ if not name:
+ try:
+ req_body = req.get_json()
+ except ValueError:
+ pass
+ else:
+ name = req_body.get('name')
+
+ if name:
+ return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
+ else:
+ return func.HttpResponse(
+ "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
+ status_code=200
+ )
diff --git a/investing_algorithm_framework/cli/templates/azure_function_host.json.template b/investing_algorithm_framework/cli/templates/azure_function_host.json.template
new file mode 100644
index 00000000..9df91361
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/azure_function_host.json.template
@@ -0,0 +1,15 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ }
+ }
+ },
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[4.*, 5.0.0)"
+ }
+}
\ No newline at end of file
diff --git a/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template b/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template
new file mode 100644
index 00000000..67043d71
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template
@@ -0,0 +1,8 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "python",
+ "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
+ "AzureWebJobsStorage": ""
+ }
+}
\ No newline at end of file
diff --git a/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template b/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template
new file mode 100644
index 00000000..ca3bf070
--- /dev/null
+++ b/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template
@@ -0,0 +1,2 @@
+investing-algorithm-framework
+azure-functions==1.17.0
diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py
index 57155b7f..e2de4557 100644
--- a/investing_algorithm_framework/create_app.py
+++ b/investing_algorithm_framework/create_app.py
@@ -1,4 +1,6 @@
import logging
+import os
+from dotenv import load_dotenv
from .app import App
from .dependency_container import setup_dependency_container
@@ -6,8 +8,26 @@
logger = logging.getLogger("investing_algorithm_framework")
-def create_app(config=None, stateless=False, web=False) -> App:
- app = App(web=web, stateless=stateless)
+def create_app(
+ config: dict =None,
+ web=False,
+ state_handler=None
+) -> App:
+ """
+ Factory method to create an app instance.
+
+ Args:
+ config (dict): Configuration dictionary
+ web (bool): Whether to create a web app
+ state_handler (StateHandler): State handler for the app
+
+ Returns:
+ App: App instance
+ """
+ # Load the environment variables
+ load_dotenv()
+
+ app = App(web=web, state_handler=state_handler)
app = setup_dependency_container(
app,
["investing_algorithm_framework"],
@@ -15,6 +35,9 @@ def create_app(config=None, stateless=False, web=False) -> App:
)
# After the container is setup, initialize the services
app.initialize_services()
- app.set_config(config)
+
+ if config is not None:
+ app.set_config_with_dict(config)
+
logger.info("Investing algoritm framework app created")
return app
diff --git a/investing_algorithm_framework/deployment/azure/__init__.py b/investing_algorithm_framework/deployment/azure/__init__.py
deleted file mode 100644
index 32b8d704..00000000
--- a/investing_algorithm_framework/deployment/azure/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# from .azure_functions import deploy_to_azure_functions
-
-# __all__ = ['deploy_to_azure_functions']
diff --git a/investing_algorithm_framework/deployment/azure/azure_functions.py b/investing_algorithm_framework/deployment/azure/azure_functions.py
deleted file mode 100644
index 6b12c011..00000000
--- a/investing_algorithm_framework/deployment/azure/azure_functions.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# import os
-# import json
-# from azure.identity import DefaultAzureCredential
-# from azure.mgmt.resource import ResourceManagementClient
-# from azure.mgmt.storage import StorageManagementClient
-# from azure.mgmt.web import WebSiteManagementClient
-# import shutil
-
-
-# def deploy_to_azure_functions(azure_credentials_json, azure_function_path):
-# """
-# This function deploys a Python function app to Azure Functions.
-
-# Parameters:
-# - azure_credentials_json (str): Path to the Azure credentials
-# JSON file.
-# - azure_function_path (str): Path to the Python function
-# app directory.
-
-# Returns:
-# None
-# """
-
-# # Load Azure credentials
-# with open('azure_credentials.json') as f:
-# credentials = json.load(f)
-
-# SUBSCRIPTION_ID = credentials['subscriptionId']
-# RESOURCE_GROUP_NAME = "myResourceGroup"
-# LOCATION = "eastus"
-# STORAGE_ACCOUNT_NAME = "mystorageaccount123"
-# FUNCTION_APP_NAME = "my-python-function-app"
-
-# # Authenticate using DefaultAzureCredential
-# credential = DefaultAzureCredential()
-
-# # Clients
-# resource_client = ResourceManagementClient(credential, SUBSCRIPTION_ID)
-# storage_client = StorageManagementClient(credential, SUBSCRIPTION_ID)
-# web_client = WebSiteManagementClient(credential, SUBSCRIPTION_ID)
-
-# # Create Resource Group
-# resource_client.resource_groups.create_or_update(RESOURCE_GROUP_NAME,
-# {"location": LOCATION})
-
-# # Create Storage Account
-# storage_client.storage_accounts.begin_create(
-# RESOURCE_GROUP_NAME,
-# STORAGE_ACCOUNT_NAME,
-# {
-# "sku": {"name": "Standard_LRS"},
-# "kind": "StorageV2",
-# "location": LOCATION
-# }
-# ).result()
-
-# # Create Function App (with a Consumption Plan)
-# site_config = {
-# "location": LOCATION,
-# "server_farm_id": f"/subscriptions/{SUBSCRIPTION_ID}" +
-# "/resourceGroups" +
-# "/{RESOURCE_GROUP_NAME}/providers/Microsoft.Web/" +
-# "serverfarms/{APP_SERVICE_PLAN_NAME}",
-# "reserved": True, # This is necessary for Linux-based function apps
-# "site_config": {
-# "app_settings": [
-# {
-# "name": "FUNCTIONS_WORKER_RUNTIME", "value": "python"
-# },
-# {
-# "name": "AzureWebJobsStorage",
-# "value": "DefaultEndpointsProtocol=https;" +
-# f"AccountName={STORAGE_ACCOUNT_NAME}" +
-# ";AccountKey=account_key>",
-# }
-# ]
-# },
-# "kind": "functionapp",
-# }
-
-# web_client.web_apps.begin_create_or_update(RESOURCE_GROUP_NAME,
-# FUNCTION_APP_NAME,
-# site_config).result()
-
-# # Zip Function Code
-# def zipdir(path, zipfile):
-# for root, dirs, files in os.walk(path):
-# for file in files:
-# zipfile.write(os.path.join(root, file),
-# os.path.relpath(os.path.join(root, file), path))
-
-# shutil.make_archive('myfunctionapp', 'zip', 'myfunctionapp/')
-
-# # Deploy Function Code
-# def deploy_function():
-# with open("myfunctionapp.zip", "rb") as z:
-# web_client.web_apps.begin_create_zip_deployment(
-# RESOURCE_GROUP_NAME, FUNCTION_APP_NAME, z).result()
-
-# deploy_function()
-
-# print(f"Function app '{FUNCTION_APP_NAME}' deployed to Azure.")
diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py
index 5257bff8..76fcba7d 100644
--- a/investing_algorithm_framework/domain/__init__.py
+++ b/investing_algorithm_framework/domain/__init__.py
@@ -1,4 +1,4 @@
-from .config import Config, Environment
+from .config import Environment, DEFAULT_LOGGING_CONFIG
from .constants import ITEMIZE, ITEMIZED, PER_PAGE, PAGE, ENVIRONMENT, \
DATABASE_DIRECTORY_PATH, DATABASE_NAME, DEFAULT_PER_PAGE_VALUE, \
DEFAULT_PAGE_VALUE, SQLALCHEMY_DATABASE_URI, RESOURCE_DIRECTORY, \
@@ -34,7 +34,6 @@
from .metrics import get_price_efficiency_ratio
__all__ = [
- 'Config',
"OrderStatus",
"OrderSide",
"OrderType",
@@ -119,5 +118,6 @@
"get_price_efficiency_ratio",
"convert_polars_to_pandas",
"DateRange",
- "get_backtest_report"
+ "get_backtest_report",
+ "DEFAULT_LOGGING_CONFIG"
]
diff --git a/investing_algorithm_framework/domain/config.py b/investing_algorithm_framework/domain/config.py
index 3962d367..ac2ce7d8 100644
--- a/investing_algorithm_framework/domain/config.py
+++ b/investing_algorithm_framework/domain/config.py
@@ -1,8 +1,6 @@
import logging
-import os
from enum import Enum
-from .constants import RESOURCE_DIRECTORY
from .exceptions import OperationalException
logger = logging.getLogger("investing_algorithm_framework")
@@ -15,6 +13,7 @@ class Environment(Enum):
DEV = 'DEV'
PROD = 'PROD'
TEST = 'TEST'
+ BACKTEST = 'BACKTEST'
# Static factory method to convert
# a string to environment
@@ -52,92 +51,34 @@ def equals(self, other):
return other == self.value
-class Config(dict):
- ENVIRONMENT = Environment.PROD.value
- LOG_LEVEL = 'DEBUG'
- APP_DIR = os.path.abspath(os.path.dirname(__file__))
- PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
- DEBUG_TB_INTERCEPT_REDIRECTS = False
- SQLALCHEMY_TRACK_MODIFICATIONS = False
- CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
- CORS_ORIGIN_WHITELIST = [
- 'http://0.0.0.0:4100',
- 'http://localhost:4100',
- 'http://0.0.0.0:8000',
- 'http://localhost:8000',
- 'http://0.0.0.0:4200',
- 'http://localhost:4200',
- 'http://0.0.0.0:4000',
- 'http://localhost:4000',
- ]
- RESOURCE_DIRECTORY = os.getenv(RESOURCE_DIRECTORY)
- SCHEDULER_API_ENABLED = True
- CHECK_PENDING_ORDERS = True
- SQLITE_ENABLED = True
- SQLITE_INITIALIZED = False
- BACKTEST_DATA_DIRECTORY_NAME = "backtest_data"
- SYMBOLS = None
- DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
-
- def __init__(self, resource_directory=None):
- super().__init__()
-
- if resource_directory is not None:
- self.RESOURCE_DIRECTORY = resource_directory
-
- super().__init__(vars(self.__class__))
-
- def __str__(self):
- field_strings = []
-
- for attribute_key in self:
-
- if attribute_key.isupper():
- field_strings.append(
- f'{attribute_key}='
- f'{self[attribute_key]!r}'
- )
-
- return f"<{self.__class__.__name__}({','.join(field_strings)})>"
-
- def get(self, key: str, default=None):
- """
- Mimics the dict get() functionality
- """
-
- try:
- return self[key]
- # Ignore exception
- except Exception:
- pass
-
- return default
-
- def set(self, key: str, value) -> None:
- self[key] = value
-
- @staticmethod
- def from_dict(dictionary):
- config = Config()
-
- if dictionary is not None:
- for attribute_key in dictionary:
-
- if attribute_key:
- config.set(attribute_key, dictionary[attribute_key])
- config[attribute_key] = dictionary[attribute_key]
-
- return config
-
-
-class TestConfig(Config):
- ENVIRONMENT = Environment.TEST.value
- TESTING = True
- DATABASE_CONFIG = {'DATABASE_NAME': "test"}
- LOG_LEVEL = "INFO"
-
-
-class DevConfig(Config):
- ENVIRONMENT = Environment.DEV.value
- DATABASE_CONFIG = {'DATABASE_NAME': "dev"}
- LOG_LEVEL = "INFO"
+DEFAULT_LOGGING_CONFIG = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'default': {
+ 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'default',
+ },
+ 'file': {
+ 'class': 'logging.FileHandler',
+ 'formatter': 'default',
+ 'filename': 'app_logs.log',
+ },
+ },
+ 'loggers': { # Make sure to add a 'loggers' section
+ 'investing_algorithm_framework': { # Define your logger here
+ 'level': 'INFO', # Set the desired level
+ 'handlers': ['console', 'file'], # Use these handlers
+ 'propagate': False, # Prevent logs from propagating to the root logger (optional)
+ },
+ },
+ 'root': { # Optional: Root logger configuration
+ 'level': 'WARNING', # Root logger defaults to WARNING
+ 'handlers': ['console', 'file'],
+ },
+}
diff --git a/investing_algorithm_framework/domain/models/market/market_credential.py b/investing_algorithm_framework/domain/models/market/market_credential.py
index 3ea70dfa..ebb9aa3d 100644
--- a/investing_algorithm_framework/domain/models/market/market_credential.py
+++ b/investing_algorithm_framework/domain/models/market/market_credential.py
@@ -1,9 +1,62 @@
+import os
+import logging
+
+from investing_algorithm_framework.domain import OperationalException
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
class MarketCredential:
- def __init__(self, api_key: str, secret_key: str, market: str):
+ """
+ Market credential model to store the api key and secret key for a market.
+ """
+ def __init__(
+ self, market: str, api_key: str = None, secret_key: str = None
+ ):
self._api_key = api_key
self._secret_key = secret_key
self._market = market
+ def initialize(self):
+ """
+ Internal helper to initialize the market credential.
+ """
+ logger.info(f"Initializing market credential for {self.market}")
+
+ if self.api_key is None:
+ logger.info(
+ "Reading api key from environment variable"
+ f" {self.market.upper()}_API_KEY"
+ )
+
+ # Check if environment variable is set
+ environment_variable = f"{self.market.upper()}_API_KEY"
+ self._api_key = os.getenv(environment_variable)
+
+ if self.api_key is None:
+ raise OperationalException(
+ "Market credential requires an api key, either"
+ " as an argument or as an environment variable"
+ f" named as {self._market.upper()}_API_KEY"
+ )
+
+ if self.secret_key is None:
+ logger.info(
+ "Reading secret key from environment variable"
+ f" {self.market.upper()}_SECRET_KEY"
+ )
+
+ # Check if environment variable is set
+ environment_variable = f"{self.market.upper()}_SECRET_KEY"
+ self._secret_key = os.getenv(environment_variable)
+
+ if self.secret_key is None:
+ raise OperationalException(
+ "Market credential requires a secret key, either"
+ " as an argument or as an environment variable"
+ f" named as {self._market.upper()}_SECRET_KEY"
+ )
+
def get_api_key(self):
return self.api_key
diff --git a/investing_algorithm_framework/domain/models/order/order.py b/investing_algorithm_framework/domain/models/order/order.py
index e51d99eb..e76885f8 100644
--- a/investing_algorithm_framework/domain/models/order/order.py
+++ b/investing_algorithm_framework/domain/models/order/order.py
@@ -42,7 +42,8 @@ def __init__(
position_id=None,
order_fee=None,
order_fee_currency=None,
- order_fee_rate=None
+ order_fee_rate=None,
+ id=None
):
if target_symbol is None:
raise OperationalException("Target symbol is not specified")
@@ -83,6 +84,7 @@ def __init__(
self.order_fee = order_fee
self.order_fee_currency = order_fee_currency
self.order_fee_rate = order_fee_rate
+ self.id = id
def get_id(self):
return self.id
@@ -321,6 +323,7 @@ def from_dict(data: dict):
order_fee=data.get("order_fee", None),
order_fee_currency=data.get("order_fee_currency", None),
order_fee_rate=data.get("order_fee_rate", None),
+ id=data.get("id", None)
)
@staticmethod
diff --git a/investing_algorithm_framework/domain/models/portfolio/portfolio.py b/investing_algorithm_framework/domain/models/portfolio/portfolio.py
index 2b314feb..05dc7d99 100644
--- a/investing_algorithm_framework/domain/models/portfolio/portfolio.py
+++ b/investing_algorithm_framework/domain/models/portfolio/portfolio.py
@@ -16,21 +16,25 @@ def __init__(
total_net_gain=0,
total_trade_volume=0,
created_at=None,
- updated_at=None
+ updated_at=None,
+ initialized=False,
+ initial_balance=None
):
self.identifier = identifier
self.updated_at = None
self.trading_symbol = trading_symbol.upper()
self.net_size = net_size
self.unallocated = unallocated
+ self.initial_balance = initial_balance
self.realized = realized
self.total_revenue = total_revenue
self.total_cost = total_cost
self.total_net_gain = total_net_gain
self.total_trade_volume = total_trade_volume
- self.market = market
+ self.market = market.upper()
self.created_at = created_at
self.updated_at = updated_at
+ self.initialized = initialized
def __repr__(self):
return self.repr(
@@ -42,6 +46,7 @@ def __repr__(self):
total_revenue=self.total_revenue,
total_cost=self.total_cost,
market=self.market,
+ initial_balance=self.initial_balance
)
def get_identifier(self):
@@ -80,12 +85,41 @@ def get_trading_symbol(self):
def get_market(self):
return self.market
+ def get_initial_balance(self):
+ return self.initial_balance
+
@staticmethod
def from_portfolio_configuration(portfolio_configuration):
+ """
+ Function to create a portfolio from a portfolio configuration
+
+ We assume that a portfolio that is created from a configuration
+ is always un initialized.
+
+ Args:
+ portfolio_configuration: PortfolioConfiguration
+
+ Returns:
+ Portfolio
+ """
return Portfolio(
identifier=portfolio_configuration.identifier,
trading_symbol=portfolio_configuration.trading_symbol,
unallocated=portfolio_configuration.initial_balance,
net_size=portfolio_configuration.initial_balance,
- market=portfolio_configuration.market
+ market=portfolio_configuration.market,
+ initial_balance=portfolio_configuration.initial_balance,
+ initialized=False
)
+
+ def to_dict(self):
+ return {
+ "trading_symbol": self.trading_symbol,
+ "market": self.market,
+ "unallocated": self.unallocated,
+ "identifier": self.identifier,
+ "created_at": self.created_at,
+ "updated_at": self.updated_at,
+ "initialized": self.initialized,
+ "initial_balance": self.initial_balance,
+ }
diff --git a/investing_algorithm_framework/domain/utils/backtesting.py b/investing_algorithm_framework/domain/utils/backtesting.py
index 57a379aa..7c051a7f 100644
--- a/investing_algorithm_framework/domain/utils/backtesting.py
+++ b/investing_algorithm_framework/domain/utils/backtesting.py
@@ -524,7 +524,6 @@ def get_backtest_report(
get_start_date_from_backtest_report_file(
os.path.join(root, file)
)
- print(backtest_start_date)
backtest_end_date = \
get_end_date_from_backtest_report_file(
os.path.join(root, file)
diff --git a/investing_algorithm_framework/infrastructure/__init__.py b/investing_algorithm_framework/infrastructure/__init__.py
index ed22bae3..cc6be269 100644
--- a/investing_algorithm_framework/infrastructure/__init__.py
+++ b/investing_algorithm_framework/infrastructure/__init__.py
@@ -8,7 +8,8 @@
from .repositories import SQLOrderRepository, SQLPositionRepository, \
SQLPortfolioRepository, \
SQLPortfolioSnapshotRepository, SQLPositionSnapshotRepository
-from .services import PerformanceService, CCXTMarketService
+from .services import PerformanceService, CCXTMarketService, \
+ AzureBlobStorageStateHandler
__all__ = [
"create_all_tables",
@@ -34,4 +35,5 @@
"CSVTickerMarketDataSource",
"CCXTOHLCVBacktestMarketDataSource",
"CCXTOrderBookMarketDataSource",
+ "AzureBlobStorageStateHandler"
]
diff --git a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
index 84ca4c70..2b7a8233 100644
--- a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
+++ b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py
@@ -19,18 +19,11 @@ def __init__(self, app):
raise OperationalException("SQLALCHEMY_DATABASE_URI not set")
global Session
-
- if app.config[SQLALCHEMY_DATABASE_URI] != "sqlite:///:memory:":
- engine = create_engine(
- app.config[SQLALCHEMY_DATABASE_URI],
- connect_args={'check_same_thread': False},
- poolclass=StaticPool
- )
- else:
- engine = create_engine(
- app.config[SQLALCHEMY_DATABASE_URI],
- )
-
+ engine = create_engine(
+ app.config[SQLALCHEMY_DATABASE_URI],
+ connect_args={'check_same_thread': False},
+ poolclass=StaticPool
+ )
Session.configure(bind=engine)
diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py
index 1dc0c50b..ddea17bc 100644
--- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py
+++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py
@@ -13,7 +13,7 @@
from investing_algorithm_framework.infrastructure.services import \
CCXTMarketService
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("investing_algorithm_framework")
class CCXTOHLCVBacktestMarketDataSource(
@@ -63,7 +63,6 @@ def prepare_data(
config,
backtest_start_date,
backtest_end_date,
- **kwargs
):
"""
Prepare data implementation of ccxt based ohlcv backtest market
@@ -86,6 +85,10 @@ def prepare_data(
Returns:
None
"""
+
+ if config is None:
+ config = self.config
+
# Calculating the backtest data start date
backtest_data_start_date = \
backtest_start_date - timedelta(
@@ -103,8 +106,7 @@ def prepare_data(
# Creating the backtest data directory and file
self.backtest_data_directory = os.path.join(
- config.get(RESOURCE_DIRECTORY),
- config.get(BACKTEST_DATA_DIRECTORY_NAME)
+ config[RESOURCE_DIRECTORY], config[BACKTEST_DATA_DIRECTORY_NAME]
)
if not os.path.isdir(self.backtest_data_directory):
@@ -303,6 +305,7 @@ def prepare_data(
When downloading the data it will use the ccxt library.
"""
+ config = self.config
total_minutes = TimeFrame.from_string(self.time_frame)\
.amount_of_minutes
self.backtest_data_start_date = \
@@ -311,8 +314,7 @@ def prepare_data(
# Creating the backtest data directory and file
self.backtest_data_directory = os.path.join(
- config.get(RESOURCE_DIRECTORY),
- config.get(BACKTEST_DATA_DIRECTORY_NAME)
+ config[RESOURCE_DIRECTORY], config[BACKTEST_DATA_DIRECTORY_NAME]
)
if not os.path.isdir(self.backtest_data_directory):
@@ -521,6 +523,9 @@ def get_data(self, **kwargs):
else:
storage_path = self.get_storage_path()
+ logger.info(
+ f"Getting OHLCV data for {self.symbol} from {start_date} to {end_date}"
+ )
data = None
if storage_path is not None:
diff --git a/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py b/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py
index 9338f074..c2b45922 100644
--- a/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py
+++ b/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py
@@ -1,4 +1,4 @@
-from .portfolio import SQLPortfolio
+from .sql_portfolio import SQLPortfolio
from .portfolio_snapshot import SQLPortfolioSnapshot
__all__ = ['SQLPortfolio', "SQLPortfolioSnapshot"]
diff --git a/investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py b/investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py
similarity index 89%
rename from investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py
rename to investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py
index bdc08d15..8a252b0c 100644
--- a/investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py
+++ b/investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from sqlalchemy import Column, Integer, String, DateTime, Float
+from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
@@ -23,6 +23,7 @@ class SQLPortfolio(Portfolio, SQLBaseModel, SQLAlchemyModelExtension):
total_trade_volume = Column(Float, nullable=False, default=0)
net_size = Column(Float, nullable=False, default=0)
unallocated = Column(Float, nullable=False, default=0)
+ initial_balance = Column(Float, nullable=True)
market = Column(String, nullable=False)
positions = relationship(
"SQLPosition",
@@ -32,6 +33,8 @@ class SQLPortfolio(Portfolio, SQLBaseModel, SQLAlchemyModelExtension):
)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow)
+ initialized = Column(Boolean, nullable=False, default=False)
+
__table_args__ = (
UniqueConstraint(
'trading_symbol',
@@ -53,8 +56,11 @@ def __init__(
trading_symbol,
market,
unallocated,
+ initialized,
+ initial_balance=None,
identifier=None,
created_at=None,
+ updated_at=None,
):
if identifier is None:
@@ -73,6 +79,9 @@ def __init__(
total_revenue=0,
total_cost=0,
created_at=created_at,
+ updated_at=updated_at,
+ initialized=initialized,
+ initial_balance=initial_balance,
)
def update(self, data):
diff --git a/investing_algorithm_framework/infrastructure/repositories/order_repository.py b/investing_algorithm_framework/infrastructure/repositories/order_repository.py
index 50a06ebe..0a2112e2 100644
--- a/investing_algorithm_framework/infrastructure/repositories/order_repository.py
+++ b/investing_algorithm_framework/infrastructure/repositories/order_repository.py
@@ -7,6 +7,7 @@
class SQLOrderRepository(Repository):
base_class = SQLOrder
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested order was not found"
def _apply_query_params(self, db, query, query_params):
external_id_query_param = self.get_query_param(
diff --git a/investing_algorithm_framework/infrastructure/repositories/repository.py b/investing_algorithm_framework/infrastructure/repositories/repository.py
index 16e671f0..8d1d68a1 100644
--- a/investing_algorithm_framework/infrastructure/repositories/repository.py
+++ b/investing_algorithm_framework/infrastructure/repositories/repository.py
@@ -243,3 +243,15 @@ def get_query_param(self, key, params, default=None, many=False):
return new_selection[0]
return new_selection
+
+ def save(self, object):
+
+ with Session() as db:
+ try:
+ db.add(object)
+ db.commit()
+ return self.get(object)
+ except SQLAlchemyError as e:
+ logger.error(e)
+ db.rollback()
+ raise ApiException("Error saving object")
diff --git a/investing_algorithm_framework/infrastructure/services/__init__.py b/investing_algorithm_framework/infrastructure/services/__init__.py
index 3186c9e7..3cd88f29 100644
--- a/investing_algorithm_framework/infrastructure/services/__init__.py
+++ b/investing_algorithm_framework/infrastructure/services/__init__.py
@@ -1,7 +1,9 @@
from .market_service import CCXTMarketService
from .performance_service import PerformanceService
+from .azure import AzureBlobStorageStateHandler
__all__ = [
"PerformanceService",
- "CCXTMarketService"
+ "CCXTMarketService",
+ "AzureBlobStorageStateHandler"
]
diff --git a/investing_algorithm_framework/infrastructure/services/azure/__init__.py b/investing_algorithm_framework/infrastructure/services/azure/__init__.py
new file mode 100644
index 00000000..8a8440f4
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/services/azure/__init__.py
@@ -0,0 +1,5 @@
+from .state_handler import AzureBlobStorageStateHandler
+
+__all__ = [
+ "AzureBlobStorageStateHandler"
+]
diff --git a/investing_algorithm_framework/infrastructure/services/azure/state_handler.py b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py
new file mode 100644
index 00000000..68e0a4ca
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py
@@ -0,0 +1,142 @@
+import os
+import logging
+
+from azure.storage.blob import ContainerClient
+from investing_algorithm_framework.domain import OperationalException
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
+class AzureBlobStorageStateHandler:
+
+ def __init__(
+ self, connection_string: str = None, container_name: str = None
+ ):
+ self.connection_string = connection_string
+ self.container_name = container_name
+ self._initialize()
+
+ def _initialize(self):
+ """
+ Internal helper to initialize the state handler.
+ """
+
+ if self.connection_string is None:
+
+ # Check if environment variable is set
+ self.connection_string = \
+ os.getenv("AZURE_STORAGE_CONNECTION_STRING")
+
+ if self.connection_string is None:
+ raise OperationalException(
+ "Azure Blob Storage state handler requires a connection string or an environment variable AZURE_STORAGE_CONNECTION_STRING to be set"
+ )
+
+ if self.container_name is None:
+
+ # Check if environment variable is set
+ self.container_name = os.getenv("AZURE_STORAGE_CONTAINER_NAME")
+
+ if self.container_name is None:
+ raise OperationalException(
+ "Azure Blob Storage state handler requires a container name or an environment variable AZURE_STORAGE_CONTAINER_NAME to be set"
+ )
+
+ def save(self, source_directory: str):
+ """
+ Save the state to Azure Blob Storage.
+
+ Parameters:
+ source_directory (str): Directory to save the state
+
+ Returns:
+ None
+ """
+ logger.info("Saving state to Azure Blob Storage ...")
+
+ try:
+ container_client = self._create_container_client()
+
+ # Create container if it does not exist
+ if not container_client.exists():
+ container_client.create_container()
+
+ # Walk through the directory
+ for root, _, files in os.walk(source_directory):
+ for file_name in files:
+ # Get the full path of the file
+ file_path = os.path.join(root, file_name)
+
+ # Construct the blob name (relative path in the container)
+ blob_name = os.path.relpath(file_path, source_directory)\
+ .replace("\\", "/")
+
+ # Upload the file
+ with open(file_path, "rb") as data:
+ container_client.upload_blob(name=blob_name, data=data, overwrite=True)
+
+ except Exception as ex:
+ logger.error(f"Error saving state to Azure Blob Storage: {ex}")
+ raise ex
+
+ def load(self, target_directory: str):
+ """
+ Load the state from Azure Blob Storage.
+
+ Parameters:
+ target_directory (str): Directory to load the state
+
+ Returns:
+ None
+ """
+ logger.info("Loading state from Azure Blob Storage ...")
+
+ try:
+ container_client = self._create_container_client()
+
+ # Ensure the local directory exists
+ if not os.path.exists(target_directory):
+ os.makedirs(target_directory)
+
+ # List and download blobs
+ for blob in container_client.list_blobs():
+ blob_name = blob.name
+ blob_file_path = os.path.join(target_directory, blob_name)
+
+ # Create subdirectories locally if needed
+ os.makedirs(os.path.dirname(blob_file_path), exist_ok=True)
+
+ # Download blob to file
+ with open(blob_file_path, "wb") as file:
+ blob_client = container_client.get_blob_client(blob_name)
+ file.write(blob_client.download_blob().readall())
+
+ except Exception as ex:
+ logger.error(f"Error loading state from Azure Blob Storage: {ex}")
+ raise ex
+
+ def _create_container_client(self):
+ """
+ Internal helper to create a Container clinet.
+
+ Returns:
+ ContainerClient
+ """
+
+ # Ensure the container exists
+ try:
+ container_client = ContainerClient.from_connection_string(
+ conn_str=self.connection_string,
+ container_name=self.container_name
+ )
+ container_client.create_container(timeout=10)
+ except Exception as e:
+
+ if "ContainerAlreadyExists" in str(e):
+ pass
+ else:
+ raise OperationalException(
+ f"Error occurred while creating the container: {e}"
+ )
+
+ return container_client
diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py
index 62dc528b..cf2dab2c 100644
--- a/investing_algorithm_framework/services/backtesting/backtest_service.py
+++ b/investing_algorithm_framework/services/backtesting/backtest_service.py
@@ -440,6 +440,7 @@ def _check_if_required_market_data_sources_are_registered(self):
if a ticker market data source is registered for the symbol and market.
"""
symbols = self._configuration_service.config[SYMBOLS]
+ print(symbols)
if symbols is not None:
diff --git a/investing_algorithm_framework/services/configuration_service.py b/investing_algorithm_framework/services/configuration_service.py
index 2fd82d6e..5bd74bb7 100644
--- a/investing_algorithm_framework/services/configuration_service.py
+++ b/investing_algorithm_framework/services/configuration_service.py
@@ -1,29 +1,77 @@
-from investing_algorithm_framework.domain import Config
+import os
+from investing_algorithm_framework.domain import Environment, \
+ RESOURCE_DIRECTORY
+DEFAULT_CONFIGURATION = {
+ "ENVIRONMENT": Environment.PROD.value,
+ "LOG_LEVEL": 'DEBUG',
+ "APP_DIR": os.path.abspath(os.path.dirname(__file__)),
+ "PROJECT_ROOT": os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir)),
+ "RESOURCE_DIRECTORY": os.getenv(RESOURCE_DIRECTORY),
+ "CHECK_PENDING_ORDERS": True,
+ "SQLITE_INITIALIZED": False,
+ "BACKTEST_DATA_DIRECTORY_NAME": "backtest_data",
+ "SYMBOLS": None,
+ "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",
+ "DATABASE_DIRECTORY_PATH": None
+}
+
+DEFAULT_FLASK_CONFIGURATION = {
+ "DEBUG_TB_INTERCEPT_REDIRECTS": False,
+ "SQLALCHEMY_TRACK_MODIFICATIONS": False,
+ "CACHE_TYPE": 'simple',
+ "CORS_ORIGIN_WHITELIST": [
+ 'http://0.0.0.0:4100',
+ 'http://localhost:4100',
+ 'http://0.0.0.0:8000',
+ 'http://localhost:8000',
+ 'http://0.0.0.0:4200',
+ 'http://localhost:4200',
+ 'http://0.0.0.0:4000',
+ 'http://localhost:4000',
+ ],
+ "SCHEDULER_API_ENABLED": True,
+}
class ConfigurationService:
def __init__(self):
- self._config = Config()
+ self._config = DEFAULT_CONFIGURATION
+ self._flask_config = DEFAULT_FLASK_CONFIGURATION
@property
def config(self):
- return self._config
+ # Make a copy of the config to prevent external modifications
+ copy = self._config.copy()
+ return copy
def get_config(self):
- return self._config
+ copy = self._config.copy()
+ return copy
- def set_config(self, config):
- self._config = config
+ def get_flask_config(self):
+ # Make a copy of the config to prevent external modifications
+ copy = self._flask_config.copy()
+ return copy
def add_value(self, key, value):
- self._config.set(key, value)
+ self._config[key] = value
- def get_value(self, key):
- return self._config.get(key)
+ def add_dict(self, dictionary):
+ self._config.update(dictionary)
def remove_value(self, key):
self._config.pop(key)
- def initialize_from_dict(self, data):
- self._config = Config.from_dict(data)
+ def initialize_from_dict(self, data: dict):
+ """
+ Initialize the configuration from a dictionary.
+
+ Args:
+ data (dict): The dictionary containing the configuration values.
+
+ Returns:
+ None
+ """
+
+ self._config.update(data)
diff --git a/investing_algorithm_framework/services/market_credential_service.py b/investing_algorithm_framework/services/market_credential_service.py
index 624d0fbb..014e2be0 100644
--- a/investing_algorithm_framework/services/market_credential_service.py
+++ b/investing_algorithm_framework/services/market_credential_service.py
@@ -23,3 +23,11 @@ def get(self, market) -> Union[MarketCredential, None]:
def get_all(self) -> List[MarketCredential]:
return list(self._market_credentials.values())
+
+ def initialize(self):
+ """
+ Initialize all market credentials.
+ """
+
+ for market_credential in self.get_all():
+ market_credential.initialize()
diff --git a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py
index b6adb770..174964f3 100644
--- a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py
+++ b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py
@@ -2,7 +2,7 @@
from investing_algorithm_framework.domain import MarketService, \
MarketDataSource, OHLCVMarketDataSource, TickerMarketDataSource, \
- OrderBookMarketDataSource, TimeFrame
+ OrderBookMarketDataSource, TimeFrame, OperationalException
from investing_algorithm_framework.services.market_credential_service \
import MarketCredentialService
@@ -85,12 +85,19 @@ def get_ohlcv(
)
def get_data(self, identifier):
+
for market_data_source in self._market_data_sources:
if market_data_source.get_identifier() == identifier:
return market_data_source.get_data(
market_credential_service=self._market_credential_service
)
+ if isinstance(identifier, str):
+ raise OperationalException(
+ f"Market data source with identifier {identifier} not found. "
+ "Please make sure that the market data source is registered to the app if you refer to it by identifier in your strategy."
+ )
+
def get_ticker_market_data_source(self, symbol, market=None):
if self.market_data_sources is not None:
@@ -188,6 +195,10 @@ def market_data_sources(self, market_data_sources):
def add(self, market_data_source):
+ # Check if the market data source is an instance of MarketDataSource
+ if not isinstance(market_data_source, MarketDataSource):
+ return
+
# Check if there is already a market data source with the same
# identifier
for existing_market_data_source in self._market_data_sources:
diff --git a/investing_algorithm_framework/services/order_service/order_backtest_service.py b/investing_algorithm_framework/services/order_service/order_backtest_service.py
index fc5afcd8..d836be3b 100644
--- a/investing_algorithm_framework/services/order_service/order_backtest_service.py
+++ b/investing_algorithm_framework/services/order_service/order_backtest_service.py
@@ -35,9 +35,10 @@ def __init__(
market_data_source_service
def create(self, data, execute=True, validate=True, sync=True) -> Order:
+ config = self.configuration_service.get_config()
+
# Make sure the created_at is set to the current backtest time
- data["created_at"] = self.configuration_service\
- .config[BACKTESTING_INDEX_DATETIME]
+ data["created_at"] = config[BACKTESTING_INDEX_DATETIME]
# Call super to have standard behavior
return super(OrderBacktestService, self)\
.create(data, execute, validate, sync)
@@ -106,13 +107,12 @@ def check_pending_orders(self):
f"{config[BACKTESTING_PENDING_ORDER_CHECK_INTERVAL]} "
)
+ config = self.configuration_service.get_config()
df = self._market_data_source_service.get_ohlcv(
symbol=symbol,
market=portfolio.market,
time_frame=time_frame,
- to_timestamp=self.configuration_service.config.get(
- BACKTESTING_INDEX_DATETIME
- ),
+ to_timestamp=config[BACKTESTING_INDEX_DATETIME],
from_timestamp=order.get_created_at(),
)
diff --git a/investing_algorithm_framework/services/order_service/order_service.py b/investing_algorithm_framework/services/order_service/order_service.py
index c2ab390f..7abf2bd2 100644
--- a/investing_algorithm_framework/services/order_service/order_service.py
+++ b/investing_algorithm_framework/services/order_service/order_service.py
@@ -254,9 +254,16 @@ def validate_limit_order(self, order_data, portfolio):
"symbol": portfolio.trading_symbol
}
)
- amount = unallocated_position.get_amount()
+ unallocated_amount = unallocated_position.get_amount()
- if amount < total_price:
+ if unallocated_amount is None:
+ raise OperationalException(
+ "Unallocated amount of the portfolio is None" +
+ "can't validate limit order. Please check if " +
+ "the portfolio configuration is correct"
+ )
+
+ if unallocated_amount < total_price:
raise OperationalException(
f"Order total: {total_price} "
f"{portfolio.trading_symbol}, is "
@@ -336,11 +343,10 @@ def _sync_portfolio_with_created_buy_order(self, order):
"symbol": portfolio.trading_symbol
}
)
-
- self.portfolio_repository.update(
+ portfolio = self.portfolio_repository.update(
portfolio.id, {"unallocated": portfolio.get_unallocated() - size}
)
- self.position_repository.update(
+ position = self.position_repository.update(
trading_symbol_position.id,
{
"amount": trading_symbol_position.get_amount() - size
diff --git a/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py b/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py
index f5c45475..581e99e6 100644
--- a/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py
+++ b/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py
@@ -16,5 +16,6 @@ def create_portfolio_from_configuration(
"market": portfolio_configuration.market,
"trading_symbol": portfolio_configuration.trading_symbol,
"unallocated": portfolio_configuration.initial_balance,
+ "initialized": False
}
return self.create(data)
diff --git a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py
index 91af4079..2c8e0208 100644
--- a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py
+++ b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py
@@ -6,6 +6,9 @@
class PortfolioConfigurationService:
+ """
+ Service to manage portfolio configurations. This service will manage the portfolio configurations that the user has registered in the app.
+ """
def __init__(self, portfolio_repository, position_repository):
self.portfolio_repository = portfolio_repository
diff --git a/investing_algorithm_framework/services/portfolios/portfolio_service.py b/investing_algorithm_framework/services/portfolios/portfolio_service.py
index dc729c04..bf99d45f 100644
--- a/investing_algorithm_framework/services/portfolios/portfolio_service.py
+++ b/investing_algorithm_framework/services/portfolios/portfolio_service.py
@@ -2,7 +2,8 @@
from datetime import datetime
from investing_algorithm_framework.domain import OrderSide, OrderStatus, \
- OperationalException, MarketService, MarketCredentialService, Portfolio
+ OperationalException, MarketService, MarketCredentialService, Portfolio, \
+ Environment, ENVIRONMENT
from investing_algorithm_framework.services.configuration_service import \
ConfigurationService
from investing_algorithm_framework.services.repository_service \
@@ -45,8 +46,48 @@ def find(self, query_params):
portfolio.configuration = portfolio_configuration
return portfolio
+ def get_all(self, query_params=None):
+ selection = super().get_all(query_params)
+
+ for portfolio in selection:
+ portfolio_configuration = self.portfolio_configuration_service\
+ .get(portfolio.identifier)
+ portfolio.configuration = portfolio_configuration
+
+ return selection
+
def create(self, data):
unallocated = data.get("unallocated", 0)
+ market = data.get("market")
+
+ # Check if the app is in backtest mode
+ config = self.configuration_service.get_config()
+ environment = config[ENVIRONMENT]
+
+ if not Environment.BACKTEST.equals(environment):
+ # Check if there is a market credential
+ # for the portfolio configuration
+ market_credential = self.market_credential_service.get(market)
+
+ if market_credential is None:
+ raise OperationalException(
+ f"No market credential found for market "
+ f"{market}. Cannot "
+ f"initialize portfolio configuration."
+ )
+
+ identifier = data.get("identifier")
+ # Check if the portfolio already exists
+ # If the portfolio already exists, return the portfolio after checking
+ # the unallocated balance of the portfolio on the exchange
+ if identifier is not None and self.repository.exists(
+ {"identifier": identifier}
+ ):
+ portfolio = self.repository.find(
+ {"identifier": identifier}
+ )
+ return portfolio
+
portfolio = super(PortfolioService, self).create(data)
self.position_service.create(
{
@@ -87,6 +128,12 @@ def create_portfolio_from_configuration(
provided. If the portfolio already exists, it will be returned.
If the portfolio does not exist, it will be created.
+
+ Args:
+ portfolio_configuration (PortfolioConfiguration) Portfolio configuration to create the portfolio from
+
+ Returns:
+ Portfolio: Portfolio created from the configuration
"""
logger.info("Creating portfolios")
@@ -117,4 +164,7 @@ def create_portfolio_from_configuration(
portfolio = Portfolio.from_portfolio_configuration(
portfolio_configuration
)
+ data = portfolio.to_dict()
+ self.create(data)
+
return portfolio
diff --git a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py
index ee1b4419..b84592b3 100644
--- a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py
+++ b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py
@@ -1,8 +1,8 @@
import logging
from investing_algorithm_framework.domain import OperationalException, \
- AbstractPortfolioSyncService, RESERVED_BALANCES, APP_MODE, SYMBOLS, \
- OrderSide, AppMode
+ AbstractPortfolioSyncService, RESERVED_BALANCES, SYMBOLS, \
+ OrderSide, OrderStatus
from investing_algorithm_framework.services.trade_service import TradeService
logger = logging.getLogger(__name__)
@@ -14,15 +14,15 @@ class PortfolioSyncService(AbstractPortfolioSyncService):
"""
def __init__(
- self,
- trade_service: TradeService,
- configuration_service,
- order_repository,
- position_repository,
- portfolio_repository,
- portfolio_configuration_service,
- market_credential_service,
- market_service
+ self,
+ trade_service: TradeService,
+ configuration_service,
+ order_repository,
+ position_repository,
+ portfolio_repository,
+ portfolio_configuration_service,
+ market_credential_service,
+ market_service
):
self.trade_service = trade_service
self.configuration_service = configuration_service
@@ -40,23 +40,18 @@ def sync_unallocated(self, portfolio):
available balance of the portfolio from the exchange and update the
unallocated balance of the portfolio accordingly.
- If the algorithm is running stateless it will update the unallocated
- balance of the portfolio to the available balance on the exchange.
-
- If the algorithm is running stateful, the unallocated balance of the
- portfolio will only check if the amount on the exchange is less
- than the unallocated balance of the portfolio. If the amount on the
- exchange is less than the unallocated balance of the portfolio, the
- unallocated balance of the portfolio will be updated to the amount on
- the exchange or an OperationalException will be raised if the
- throw_exception_on_insufficient_balance is set to True.
-
- If in the config the RESERVED_BALANCES key is set, the reserved amount
- will be subtracted from the unallocated amount. This is to prevent
- the algorithm from using reserved balances for trading. The reserved
- config is not used for the stateless mode, because this would mean
- that the algorithm should be aware of how much it already used for
- trading. This is not possible in stateless mode.
+ If the portfolio already exists (exists in the database), then a check is done if the exchange has the available balance of
+ the portfolio unallocated balance. If the exchange does not have
+ the available balance of the portfolio, an OperationalException will be raised.
+
+ If the portfolio does not exist, the portfolio will be created with
+ the unallocated balance of the portfolio set to the available balance on the exchange. If also a initial balance is set in the portfolio configuration, the unallocated balance will be set to the initial balance (given the balance is available on the exchange). If the initial balance is not set, the unallocated balance will be set to the available balance on the exchange.
+
+ Args:
+ portfolio: Portfolio object
+
+ Returns:
+ Portfolio object
"""
market_credential = self.market_credential_service.get(
portfolio.market
@@ -71,70 +66,71 @@ def sync_unallocated(self, portfolio):
# Get the unallocated balance of the portfolio from the exchange
balances = self.market_service.get_balance(market=portfolio.market)
- if portfolio.trading_symbol.upper() not in balances:
- unallocated = 0
- else:
- unallocated = float(balances[portfolio.trading_symbol.upper()])
-
- reserved_unallocated = 0
- config = self.configuration_service.config
- mode = config.get(APP_MODE)
-
- if not AppMode.STATELESS.equals(mode):
- if RESERVED_BALANCES in config:
- reserved = config[RESERVED_BALANCES]
-
- if portfolio.trading_symbol.upper() in reserved:
- reserved_unallocated \
- = reserved[portfolio.trading_symbol.upper()]
-
- unallocated = unallocated - reserved_unallocated
-
- if portfolio.unallocated is not None and \
- unallocated != portfolio.unallocated:
+ if not portfolio.initialized:
+ # Check if the portfolio has an initial balance set
+ if portfolio.initial_balance is not None:
+ available = float(balances[portfolio.trading_symbol.upper()])
- if unallocated < portfolio.unallocated:
+ if portfolio.initial_balance > available:
raise OperationalException(
- "There seems to be a mismatch between "
- "the portfolio configuration and the balances on"
- " the exchange. "
- f"Please make sure that the available "
- f"{portfolio.trading_symbol} "
- f"on your exchange {portfolio.market} "
- "matches the portfolio configuration amount of: "
- f"{portfolio.unallocated} "
- f"{portfolio.trading_symbol}. "
- f"You have currently {unallocated} "
- f"{portfolio.trading_symbol} available on the "
- f"exchange."
+ "The initial balance of the " +
+ "portfolio configuration " +
+ f"({portfolio.initial_balance} "
+ f"{portfolio.trading_symbol}) is more " +
+ "than the available balance on the exchange. " +
+ "Please make sure that the initial balance of " +
+ "the portfolio configuration is less " +
+ "than the available balance on the " +
+ f"exchange {available} {portfolio.trading_symbol}."
)
+ else:
+ unallocated = portfolio.initial_balance
+ else:
+ # If the portfolio does not have an initial balance set, get the available balance on the exchange
+ if portfolio.trading_symbol.upper() not in balances:
+ raise OperationalException(
+ f"There is no available balance on the exchange for "
+ f"{portfolio.trading_symbol.upper()} on market "
+ f"{portfolio.market}. Please make sure that you have "
+ f"an available balance on the exchange for "
+ f"{portfolio.trading_symbol.upper()} on market "
+ f"{portfolio.market}."
+ )
+ else:
+ unallocated = float(balances[portfolio.trading_symbol.upper()])
- # If portfolio does not exist and initial balance is set,
- # create the portfolio with the initial balance
- if unallocated > portfolio.unallocated and \
- not self.portfolio_repository.exists(
- {"identifier": portfolio.identifier}
- ):
- unallocated = portfolio.unallocated
-
- if not self.portfolio_repository.exists(
- {"identifier": portfolio.identifier}
- ):
- create_data = {
- "identifier": portfolio.get_identifier(),
- "market": portfolio.get_market().upper(),
- "trading_symbol": portfolio.get_trading_symbol(),
- "unallocated": unallocated,
- }
- portfolio = self.portfolio_repository.create(create_data)
- else:
update_data = {
"unallocated": unallocated,
+ "net_size": unallocated,
+ "initialized": True
}
portfolio = self.portfolio_repository.update(
portfolio.id, update_data
)
+ # Update also a trading symbol position
+ trading_symbol_position = self.position_repository.find(
+ {
+ "symbol": portfolio.trading_symbol,
+ "portfolio_id": portfolio.id
+ }
+ )
+ self.position_repository.update(
+ trading_symbol_position.id, {"amount": unallocated}
+ )
+
+ else:
+ # Check if the portfolio unallocated balance is available on the exchange
+ if portfolio.unallocated > 0:
+ if portfolio.trading_symbol.upper() not in balances or portfolio.unallocated > float(balances[portfolio.trading_symbol.upper()]):
+ raise OperationalException(
+ f"Out of sync: the unallocated balance"
+ " of the portfolio is more than the available"
+ " balance on the exchange. Please make sure that you" f" have at least {portfolio.unallocated}"
+ f" {portfolio.trading_symbol.upper()} available"
+ " on the exchange."
+ )
+
return portfolio
def sync_positions(self, portfolio):
@@ -198,121 +194,39 @@ def sync_positions(self, portfolio):
def sync_orders(self, portfolio):
"""
Function to sync all local orders with the orders on the exchange.
- This function will retrieve all orders from the exchange and
- update the portfolio balances and positions accordingly.
-
- First all orders are retrieved from the exchange and updated in the
- database. If the order does not exist in the database, it will be
- created and treated as a new order.
-
- When an order is closed on the exchange, the order will be updated
- in the database to closed. We will also then update the portfolio
- and position balances accordingly.
+ This method will go over all local open orders and check if they are
+ changed on the exchange. If they are, the local order will be updated to match the status on the exchange.
- If the order exists, we will check if the filled amount of the order
- has changed. If the filled amount has changed, we will update the
- order in the database and update the portfolio and position balances
+ Args:
+ portfolio: Portfolio object
- if the status of an existing order has changed, we will update the
- order in the database and update the portfolio and position balances
-
- During the syncing of the orders, new orders are not executed. They
- are only created in the database. This is to prevent the algorithm
- from executing orders that are already executed on the exchange.
+ Returns:
+ None
"""
portfolio_configuration = self.portfolio_configuration_service \
.get(portfolio.identifier)
- symbols = self._get_symbols(portfolio)
- positions = self.position_repository.get_all(
- {"portfolio_id": portfolio_configuration.identifier}
- )
- # Remove the portfolio trading symbol from the symbols
- symbols = [
- symbol for symbol in symbols if symbol != portfolio.trading_symbol
- ]
+ open_orders = self.order_repository.get_all(
+ {
+ "portfolio": portfolio.identifier,
+ "status": OrderStatus.OPEN.value
+ }
+ )
- # Check if there are orders for the available symbols
- for symbol in symbols:
- symbol = f"{symbol.upper()}"
- orders = self.market_service.get_orders(
- symbol=symbol,
- since=portfolio_configuration.track_from,
+ for order in open_orders:
+ external_orders = self.market_service.get_orders(
+ symbol=order.get_symbol(),
+ since=order.created_at,
market=portfolio.market
)
- if orders is not None and len(orders) > 0:
- # Order the list of orders by created_at
- ordered_external_order_list = sorted(
- orders, key=lambda x: x.created_at
- )
+ for external_order in external_orders:
- if portfolio_configuration.track_from is not None:
- ordered_external_order_list = [
- order for order in ordered_external_order_list
- if order.created_at >= portfolio_configuration
- .track_from
- ]
-
- for external_order in ordered_external_order_list:
-
- if self.order_repository.exists(
- {"external_id": external_order.external_id}
- ):
- logger.info("Updating existing order")
- order = self.order_repository.find(
- {"external_id": external_order.external_id}
- )
- self.order_repository.update(
- order.id, external_order.to_dict()
- )
- else:
- logger.info(
- "Creating new order, based on external order"
- )
- data = external_order.to_dict()
- data.pop("trade_closed_at", None)
- data.pop("trade_closed_price", None)
- data.pop("trade_closed_amount", None)
- position_id = None
-
- # Get position id
- for position in positions:
- if position.symbol == external_order.target_symbol:
- position_id = position.id
- break
-
- # Create the new order
- new_order_data = {
- "target_symbol": external_order.target_symbol,
- "trading_symbol": portfolio.trading_symbol,
- "amount": external_order.amount,
- "price": external_order.price,
- "order_side": external_order.order_side,
- "order_type": external_order.order_type,
- "external_id": external_order.external_id,
- "status": "open",
- "position_id": position_id,
- "created_at": external_order.created_at,
- }
- new_order = self.order_repository.create(
- new_order_data,
- )
-
- # Update the order to its current status
- # By default it should not sync the unallocated
- # balance as this has already by done.
- # Position amounts should be updated
- update_data = {
- "status": external_order.status,
- "filled": external_order.filled,
- "remaining": external_order.remaining,
- "updated_at": external_order.created_at,
- }
- self.order_repository.update(
- new_order.id, update_data
- )
+ if order.external_id == external_order.external_id:
+ self.order_repository.update(
+ order.id, external_order.to_dict()
+ )
def sync_trades(self, portfolio):
orders = self.order_repository.get_all(
diff --git a/investing_algorithm_framework/services/repository_service.py b/investing_algorithm_framework/services/repository_service.py
index fb8a1c04..42887e7c 100644
--- a/investing_algorithm_framework/services/repository_service.py
+++ b/investing_algorithm_framework/services/repository_service.py
@@ -32,3 +32,6 @@ def count(self, query_params=None):
def exists(self, query_params):
return self.repository.exists(query_params)
+
+ def save(self, object):
+ return self.repository.save(object)
diff --git a/investing_algorithm_framework/services/strategy_orchestrator_service.py b/investing_algorithm_framework/services/strategy_orchestrator_service.py
index ab14ee3f..44503f5b 100644
--- a/investing_algorithm_framework/services/strategy_orchestrator_service.py
+++ b/investing_algorithm_framework/services/strategy_orchestrator_service.py
@@ -69,11 +69,9 @@ def run_strategy(self, strategy, algorithm, sync=False):
and len(strategy.market_data_sources) > 0:
for data_id in strategy.market_data_sources:
-
if isinstance(data_id, MarketDataSource):
market_data[data_id.get_identifier()] = \
- self.market_data_source_service\
- .get_data(identifier=data_id.get_identifier())
+ data_id.get_data()
else:
market_data[data_id] = \
self.market_data_source_service \
diff --git a/poetry.lock b/poetry.lock
index 0169cd64..a06ba9a0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiodns"
@@ -341,6 +341,138 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+[[package]]
+name = "azure-common"
+version = "1.1.28"
+description = "Microsoft Azure Client Library for Python (Common)"
+optional = false
+python-versions = "*"
+files = [
+ {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"},
+ {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"},
+]
+
+[[package]]
+name = "azure-core"
+version = "1.32.0"
+description = "Microsoft Azure Core Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure_core-1.32.0-py3-none-any.whl", hash = "sha256:eac191a0efb23bfa83fddf321b27b122b4ec847befa3091fa736a5c32c50d7b4"},
+ {file = "azure_core-1.32.0.tar.gz", hash = "sha256:22b3c35d6b2dae14990f6c1be2912bf23ffe50b220e708a28ab1bb92b1c730e5"},
+]
+
+[package.dependencies]
+requests = ">=2.21.0"
+six = ">=1.11.0"
+typing-extensions = ">=4.6.0"
+
+[package.extras]
+aio = ["aiohttp (>=3.0)"]
+
+[[package]]
+name = "azure-identity"
+version = "1.19.0"
+description = "Microsoft Azure Identity Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure_identity-1.19.0-py3-none-any.whl", hash = "sha256:e3f6558c181692d7509f09de10cca527c7dce426776454fb97df512a46527e81"},
+ {file = "azure_identity-1.19.0.tar.gz", hash = "sha256:500144dc18197d7019b81501165d4fa92225f03778f17d7ca8a2a180129a9c83"},
+]
+
+[package.dependencies]
+azure-core = ">=1.31.0"
+cryptography = ">=2.5"
+msal = ">=1.30.0"
+msal-extensions = ">=1.2.0"
+typing-extensions = ">=4.0.0"
+
+[[package]]
+name = "azure-mgmt-core"
+version = "1.5.0"
+description = "Microsoft Azure Management Core Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure_mgmt_core-1.5.0-py3-none-any.whl", hash = "sha256:18aaa5a723ee8ae05bf1bfc9f6d0ffb996631c7ea3c922cc86f522973ce07b5f"},
+ {file = "azure_mgmt_core-1.5.0.tar.gz", hash = "sha256:380ae3dfa3639f4a5c246a7db7ed2d08374e88230fd0da3eb899f7c11e5c441a"},
+]
+
+[package.dependencies]
+azure-core = ">=1.31.0"
+
+[[package]]
+name = "azure-mgmt-resource"
+version = "23.2.0"
+description = "Microsoft Azure Resource Management Client Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure_mgmt_resource-23.2.0-py3-none-any.whl", hash = "sha256:7af2bca928ecd58e57ea7f7731d245f45e9d927036d82f1d30b96baa0c26b569"},
+ {file = "azure_mgmt_resource-23.2.0.tar.gz", hash = "sha256:747b750df7af23ab30e53d3f36247ab0c16de1e267d666b1a5077c39a4292529"},
+]
+
+[package.dependencies]
+azure-common = ">=1.1"
+azure-mgmt-core = ">=1.3.2"
+isodate = ">=0.6.1"
+typing-extensions = ">=4.6.0"
+
+[[package]]
+name = "azure-mgmt-storage"
+version = "21.2.1"
+description = "Microsoft Azure Storage Management Client Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure-mgmt-storage-21.2.1.tar.gz", hash = "sha256:503a7ff9c31254092b0656445f5728bfdfda2d09d46a82e97019eaa9a1ecec64"},
+ {file = "azure_mgmt_storage-21.2.1-py3-none-any.whl", hash = "sha256:f97df1fa39cde9dbacf2cd96c9cba1fc196932185e24853e276f74b18a0bd031"},
+]
+
+[package.dependencies]
+azure-common = ">=1.1"
+azure-mgmt-core = ">=1.3.2"
+isodate = ">=0.6.1"
+
+[[package]]
+name = "azure-mgmt-web"
+version = "7.3.1"
+description = "Microsoft Azure Web Apps Management Client Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure-mgmt-web-7.3.1.tar.gz", hash = "sha256:87b771436bc99a7a8df59d0ad185b96879a06dce14764a06b3fc3dafa8fcb56b"},
+ {file = "azure_mgmt_web-7.3.1-py3-none-any.whl", hash = "sha256:ccf881e3ab31c3fdbf9cbff32773d9c0006b5dcd621ea074d7ec89e51049fb72"},
+]
+
+[package.dependencies]
+azure-common = ">=1.1"
+azure-mgmt-core = ">=1.3.2"
+isodate = ">=0.6.1"
+typing-extensions = ">=4.6.0"
+
+[[package]]
+name = "azure-storage-blob"
+version = "12.24.0"
+description = "Microsoft Azure Blob Storage Client Library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "azure_storage_blob-12.24.0-py3-none-any.whl", hash = "sha256:4f0bb4592ea79a2d986063696514c781c9e62be240f09f6397986e01755bc071"},
+ {file = "azure_storage_blob-12.24.0.tar.gz", hash = "sha256:eaaaa1507c8c363d6e1d1342bd549938fdf1adec9b1ada8658c8f5bf3aea844e"},
+]
+
+[package.dependencies]
+azure-core = ">=1.30.0"
+cryptography = ">=2.1.4"
+isodate = ">=0.6.1"
+typing-extensions = ">=4.6.0"
+
+[package.extras]
+aio = ["azure-core[aio] (>=1.30.0)"]
+
[[package]]
name = "babel"
version = "2.16.0"
@@ -1452,6 +1584,17 @@ widgetsnbextension = ">=4.0.12,<4.1.0"
[package.extras]
test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
+[[package]]
+name = "isodate"
+version = "0.7.2"
+description = "An ISO 8601 date/time/duration parser and formatter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"},
+ {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"},
+]
+
[[package]]
name = "isoduration"
version = "20.11.0"
@@ -1984,6 +2127,40 @@ files = [
{file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"},
]
+[[package]]
+name = "msal"
+version = "1.31.1"
+description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "msal-1.31.1-py3-none-any.whl", hash = "sha256:29d9882de247e96db01386496d59f29035e5e841bcac892e6d7bf4390bf6bd17"},
+ {file = "msal-1.31.1.tar.gz", hash = "sha256:11b5e6a3f802ffd3a72107203e20c4eac6ef53401961b880af2835b723d80578"},
+]
+
+[package.dependencies]
+cryptography = ">=2.5,<46"
+PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]}
+requests = ">=2.0.0,<3"
+
+[package.extras]
+broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"]
+
+[[package]]
+name = "msal-extensions"
+version = "1.2.0"
+description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"},
+ {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"},
+]
+
+[package.dependencies]
+msal = ">=1.29,<2"
+portalocker = ">=1.4,<3"
+
[[package]]
name = "multidict"
version = "6.1.0"
@@ -2506,6 +2683,25 @@ timezone = ["backports-zoneinfo", "tzdata"]
xlsx2csv = ["xlsx2csv (>=0.8.0)"]
xlsxwriter = ["xlsxwriter"]
+[[package]]
+name = "portalocker"
+version = "2.10.1"
+description = "Wraps the portalocker recipe for easy usage"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"},
+ {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"},
+]
+
+[package.dependencies]
+pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+docs = ["sphinx (>=1.7.1)"]
+redis = ["redis"]
+tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"]
+
[[package]]
name = "prometheus-client"
version = "0.21.0"
@@ -2863,6 +3059,26 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
+ {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -2877,6 +3093,20 @@ files = [
[package.dependencies]
six = ">=1.5"
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
+ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
[[package]]
name = "python-json-logger"
version = "2.0.7"
@@ -4000,4 +4230,4 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.0"
python-versions = ">=3.10"
-content-hash = "dab26b4b5c0d8b06086270c4700cdba75ef193bb6865ffe74992ea9c9848d820"
+content-hash = "c3b534a33f06b7123370d8957387ec9aac9c91b87d5c475650c24f2ed8be6db0"
diff --git a/pyproject.toml b/pyproject.toml
index 93ff40df..5608fdc2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,12 @@ jupyter = "^1.0.0"
numpy = "^2.1.3"
scipy = "^1.14.1"
tulipy = "^0.4.0"
+azure-storage-blob = "^12.24.0"
+azure-identity = "^1.19.0"
+azure-mgmt-storage = "^21.2.1"
+azure-mgmt-web = "^7.3.1"
+azure-mgmt-resource = "^23.2.0"
+python-dotenv = "^1.0.1"
[tool.poetry.group.test.dependencies]
coverage= "7.4.2"
@@ -38,4 +44,8 @@ ipykernel = "^6.29.5"
[build-system]
requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
\ No newline at end of file
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.scripts]
+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"
diff --git a/tests/app/algorithm/test_create_market_sell_order.py b/tests/app/algorithm/test_create_market_sell_order.py
index 05289e8a..d20f17a4 100644
--- a/tests/app/algorithm/test_create_market_sell_order.py
+++ b/tests/app/algorithm/test_create_market_sell_order.py
@@ -41,7 +41,7 @@ def test_create_market_sell_order(self):
order_service = self.app.container.order_service()
trading_symbol_position = position_service.find({"symbol": "EUR"})
self.assertEqual(990, trading_symbol_position.get_amount())
- self.app.run(number_of_iterations=1, sync=False)
+ self.app.run(number_of_iterations=1)
trading_symbol_position = position_service.find({"symbol": "EUR"})
self.assertEqual(990, trading_symbol_position.get_amount())
btc_position = position_service.find({"symbol": "BTC"})
diff --git a/tests/app/algorithm/test_get_data.py b/tests/app/algorithm/test_get_data.py
deleted file mode 100644
index eaa34407..00000000
--- a/tests/app/algorithm/test_get_data.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# import os
-# from datetime import datetime, timedelta
-# from investing_algorithm_framework import create_app,
-# TradingStrategy, TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration,
-# TradingDataType, TradingTimeFrame
-# from tests.resources import TestBase, MarketServiceStub
-#
-#
-# class StrategyOne(TradingStrategy):
-# time_unit = TimeUnit.SECOND
-# interval = 2
-# trading_data_types = [
-# TradingDataType.OHLCV,
-# TradingDataType.TICKER,
-# TradingDataType.ORDER_BOOK
-# ]
-# trading_time_frame = TradingTimeFrame.ONE_MINUTE
-# trading_time_frame_start_date = datetime.utcnow() - timedelta(days=1)
-# symbols = ["ETH/EUR", "BTC/EUR"]
-# market = "BITVAVO"
-# market_data = False
-#
-# def apply_strategy(self, algorithm, market_data):
-# algorithm.create_limit_order(
-# target_symbol="BTC",
-# amount=1,
-# order_side="BUY",
-# price=10,
-# )
-#
-#
-# class Test(TestBase):
-#
-# 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"
-# )
-# )
-# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
-# self.app.add_portfolio_configuration(
-# PortfolioConfiguration(
-# market="bitvavo",
-# api_key="test",
-# secret_key="test",
-# trading_symbol="EUR"
-# )
-# )
-# self.app.container.market_service.override(MarketServiceStub(None))
-# self.app.add_strategy(StrategyOne)
-# self.app.initialize()
-#
-# def test_get_data(self):
-# self.app.run(number_of_iterations=1, sync=False)
-# order_service = self.app.container.order_service()
-# order_service.check_pending_orders()
diff --git a/tests/app/algorithm/test_get_pending_orders.py b/tests/app/algorithm/test_get_pending_orders.py
index f661c4b3..559d30a3 100644
--- a/tests/app/algorithm/test_get_pending_orders.py
+++ b/tests/app/algorithm/test_get_pending_orders.py
@@ -18,7 +18,7 @@ class TestPortfolioService(TestBase):
secret_key="secret_key",
)
]
- external_orders = [
+ initial_orders = [
Order.from_dict(
{
"id": "1323",
@@ -65,14 +65,9 @@ class TestPortfolioService(TestBase):
},
),
]
- external_available_symbols = [
- "BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"
- ]
external_balances = {
"EUR": 700,
- "BTC": 10,
}
- config = {SYMBOLS: ["BTC/EUR", "DOT/EUR", "ETH/EUR", "ADA/EUR"]}
def test_get_pending_orders(self):
"""
@@ -115,7 +110,7 @@ def test_get_pending_orders(self):
# Check if all positions are made
position_service = self.app.container.position_service()
- self.assertEqual(5, position_service.count())
+ self.assertEqual(4, position_service.count())
# Check if btc position exists
btc_position = position_service.find(
@@ -139,7 +134,7 @@ def test_get_pending_orders(self):
eur_position = position_service.find(
{"portfolio_id": portfolio.id, "symbol": "EUR"}
)
- self.assertEqual(700, eur_position.amount)
+ self.assertEqual(400, eur_position.amount)
pending_orders = self.app.algorithm.get_pending_orders()
self.assertEqual(2, len(pending_orders))
diff --git a/tests/app/algorithm/test_get_portfolio.py b/tests/app/algorithm/test_get_portfolio.py
index 07d4e8a8..a669d60c 100644
--- a/tests/app/algorithm/test_get_portfolio.py
+++ b/tests/app/algorithm/test_get_portfolio.py
@@ -24,6 +24,6 @@ class Test(TestBase):
)
]
- def test_create_limit_buy_order_with_percentage_of_portfolio(self):
+ def test_get_portfolio(self):
portfolio = self.app.algorithm.get_portfolio()
self.assertEqual(Decimal(1000), portfolio.get_unallocated())
diff --git a/tests/app/algorithm/test_get_unfilled_buy_value.py b/tests/app/algorithm/test_get_unfilled_buy_value.py
index 2861e9b0..806745b4 100644
--- a/tests/app/algorithm/test_get_unfilled_buy_value.py
+++ b/tests/app/algorithm/test_get_unfilled_buy_value.py
@@ -8,7 +8,7 @@ class Test(TestBase):
"""
Test for functionality of algorithm get_unfilled_buy_value
"""
- external_orders = [
+ initial_orders = [
Order.from_dict(
{
"id": "1323",
@@ -56,15 +56,8 @@ class Test(TestBase):
)
]
external_balances = {
- "EUR": 1000,
- "BTC": 10,
- "DOT": 0,
- "ETH": 0
+ "EUR": 1000
}
- config = {
- SYMBOLS: ["BTC/EUR", "DOT/EUR", "ETH/EUR"],
- }
- external_available_symbols = ["BTC/EUR", "DOT/EUR", "ETH/EUR"]
portfolio_configurations = [
PortfolioConfiguration(
market="BITVAVO",
@@ -85,6 +78,14 @@ def test_get_unfilled_buy_value(self):
The test should make sure that the portfolio service can sync
existing orders from the market service to the order service.
+
+ Orders overview:
+ - BTC/EUR: 10 10.0 (filled)
+ - DOT/EUR: 10 10.0 (unfilled)
+ - ETH/EUR: 10 10.0 (unfilled)
+
+ The unfilled buy value should be 200
+ The unallocated amount should be 700
"""
portfolio_service: PortfolioService \
= self.app.container.portfolio_service()
@@ -144,7 +145,7 @@ def test_get_unfilled_buy_value(self):
eur_position = position_service.find(
{"portfolio_id": portfolio.id, "symbol": "EUR"}
)
- self.assertEqual(1000, eur_position.amount)
+ self.assertEqual(700, eur_position.amount)
pending_orders = self.app.algorithm.get_pending_orders()
self.assertEqual(2, len(pending_orders))
diff --git a/tests/app/algorithm/test_get_unfilled_sell_value.py b/tests/app/algorithm/test_get_unfilled_sell_value.py
index 55960ba3..96940771 100644
--- a/tests/app/algorithm/test_get_unfilled_sell_value.py
+++ b/tests/app/algorithm/test_get_unfilled_sell_value.py
@@ -22,11 +22,7 @@ class Test(TestBase):
secret_key="secret_key",
)
]
- config = {
- SYMBOLS: ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"]
- }
- external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"]
- external_orders = [
+ initial_orders = [
Order.from_dict(
{
"id": "1323",
@@ -116,10 +112,7 @@ class Test(TestBase):
),
]
external_balances = {
- "EUR": 1000,
- "BTC": 0,
- "DOT": 0,
- "ETH": 0,
+ "EUR": 1000
}
def test_get_unfilled_sell_value(self):
@@ -128,6 +121,16 @@ def test_get_unfilled_sell_value(self):
The test should make sure that the portfolio service can sync
existing orders from the market service to the order service.
+
+ Order overview:
+ - BTC/EUR BUY 10 10.0
+ - BTC/EUR SELL 10 20.0 (filled, profit 10*10=100)
+ - DOT/EUR BUY 10 10.0
+ - ETH/EUR BUY 10 10.0
+ - ETH/EUR SELL 10 10.0 (unfilled)
+ - DOT/EUR SELL 10 10.0 (unfilled)
+
+ At the end of the order history unallocated amount should be 900
"""
portfolio_service: PortfolioService \
= self.app.container.portfolio_service()
@@ -163,7 +166,7 @@ def test_get_unfilled_sell_value(self):
# Check if all positions are made
position_service = self.app.container.position_service()
- self.assertEqual(5, position_service.count())
+ self.assertEqual(4, position_service.count())
# Check if btc position exists
btc_position = position_service.find(
@@ -187,12 +190,12 @@ def test_get_unfilled_sell_value(self):
eur_position = position_service.find(
{"portfolio_id": portfolio.id, "symbol": "EUR"}
)
- self.assertEqual(1000, eur_position.amount)
+ self.assertEqual(900, eur_position.amount)
pending_orders = self.app.algorithm.get_pending_orders()
self.assertEqual(2, len(pending_orders))
- # Check the unfilled buy value
+ # Check the unfilled sell value
unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value()
self.assertEqual(200, unfilled_sell_value)
@@ -215,3 +218,9 @@ def test_get_unfilled_sell_value(self):
# Check the unfilled buy value
unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value()
self.assertEqual(100, unfilled_sell_value)
+
+ # Check if eur position exists
+ eur_position = position_service.find(
+ {"portfolio_id": portfolio.id, "symbol": "EUR"}
+ )
+ self.assertEqual(1000, eur_position.amount)
diff --git a/tests/app/algorithm/test_has_open_buy_orders.py b/tests/app/algorithm/test_has_open_buy_orders.py
index a2dd1d7e..dfc236a4 100644
--- a/tests/app/algorithm/test_has_open_buy_orders.py
+++ b/tests/app/algorithm/test_has_open_buy_orders.py
@@ -1,7 +1,7 @@
from decimal import Decimal
from investing_algorithm_framework import PortfolioConfiguration, \
- MarketCredential, SYMBOLS, Order
+ MarketCredential
from tests.resources import TestBase
@@ -20,117 +20,24 @@ class Test(TestBase):
secret_key="secret_key",
)
]
- config = {
- SYMBOLS: ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"]
- }
- external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"]
- external_orders = [
- Order.from_dict(
- {
- "id": "1323",
- "side": "buy",
- "symbol": "BTC/EUR",
- "amount": 10,
- "price": 10.0,
- "status": "CLOSED",
- "order_type": "limit",
- "order_side": "buy",
- "created_at": "2023-08-08T14:40:56.626362Z",
- "filled": 10,
- "remaining": 0,
- },
- ),
- Order.from_dict(
- {
- "id": "132343",
- "order_side": "SELL",
- "symbol": "BTC/EUR",
- "amount": 10,
- "price": 20.0,
- "status": "CLOSED",
- "order_type": "limit",
- "created_at": "2023-08-08T14:40:56.626362Z",
- "filled": 10,
- "remaining": 0,
- },
- ),
- Order.from_dict(
- {
- "id": "14354",
- "side": "buy",
- "symbol": "DOT/EUR",
- "amount": 10,
- "price": 10.0,
- "status": "CLOSED",
- "order_type": "limit",
- "order_side": "buy",
- "created_at": "2023-09-22T14:40:56.626362Z",
- "filled": 10,
- "remaining": 0,
- },
- ),
- Order.from_dict(
- {
- "id": "49394",
- "side": "buy",
- "symbol": "ETH/EUR",
- "amount": 10,
- "price": 10.0,
- "status": "CLOSED",
- "order_type": "limit",
- "order_side": "buy",
- "created_at": "2023-08-08T14:40:56.626362Z",
- "filled": 10,
- "remaining": 0,
- },
- ),
- Order.from_dict(
- {
- "id": "4939424",
- "order_side": "sell",
- "symbol": "ETH/EUR",
- "amount": 10,
- "price": 10.0,
- "status": "OPEN",
- "order_type": "limit",
- "created_at": "2023-08-08T14:40:56.626362Z",
- "filled": 0,
- "remaining": 0,
- },
- ),
- Order.from_dict(
- {
- "id": "493943434",
- "order_side": "sell",
- "symbol": "DOT/EUR",
- "amount": 10,
- "price": 10.0,
- "status": "OPEN",
- "order_type": "limit",
- "created_at": "2023-08-08T14:40:56.626362Z",
- "filled": 0,
- "remaining": 0,
- },
- ),
- ]
+ external_orders = []
external_balances = {
"EUR": 1000,
- "BTC": 0,
- "DOT": 0,
- "ETH": 0,
}
def test_has_open_buy_orders(self):
trading_symbol_position = self.app.algorithm.get_position("EUR")
self.assertEqual(Decimal(1000), trading_symbol_position.get_amount())
- self.assertTrue(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ order = self.app.algorithm.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
order_service = self.app.container.order_service()
+ order = order_service.find({"symbol": "BTC/EUR"})
+ position_service = self.app.container.position_service()
+ position = position_service.find({"symbol": "BTC"})
self.assertTrue(self.app.algorithm.has_open_buy_orders("BTC"))
order_service.check_pending_orders()
self.assertFalse(self.app.algorithm.has_open_buy_orders("BTC"))
diff --git a/tests/app/algorithm/test_run_strategy.py b/tests/app/algorithm/test_run_strategy.py
index 19e18269..07262d9b 100644
--- a/tests/app/algorithm/test_run_strategy.py
+++ b/tests/app/algorithm/test_run_strategy.py
@@ -38,7 +38,6 @@ class Test(TestCase):
secret_key="secret_key",
)
]
- external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"]
external_balances = {
"EUR": 1000,
}
@@ -61,6 +60,20 @@ def setUp(self) -> None:
)
)
+ def tearDown(self) -> None:
+ super().tearDown()
+ # Delete the resources database directory
+
+ 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_with_strategy_object(self):
app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
app.container.market_service.override(MarketServiceStub(None))
@@ -119,7 +132,7 @@ def run_strategy(algorithm, market_data):
self.assertTrue(strategy_orchestration_service.has_run("run_strategy"))
def test_stateless(self):
- app = create_app(stateless=True)
+ app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
app.container.market_service.override(MarketServiceStub(None))
app.container.portfolio_configuration_service().clear()
app.add_portfolio_configuration(
diff --git a/tests/app/backtesting/test_backtest_report.py b/tests/app/backtesting/test_backtest_report.py
index 720cccbe..b47f3da5 100644
--- a/tests/app/backtesting/test_backtest_report.py
+++ b/tests/app/backtesting/test_backtest_report.py
@@ -38,13 +38,23 @@ def setUp(self) -> None:
)
)
- def test_report_csv_creation(self):
+ 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_report_json_creation(self):
"""
Test if the backtest report is created as a CSV file
"""
- app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
- )
+ app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
algorithm = Algorithm()
algorithm.add_strategy(TestStrategy())
app.add_algorithm(algorithm)
@@ -71,89 +81,13 @@ def test_report_csv_creation(self):
os.path.isfile(os.path.join(self.resource_dir, file_path))
)
- def test_report_csv_creation_without_strategy_identifier(self):
- """
- Test if the backtest report is created as a CSV file
- when the strategy does not have an identifier
- """
- app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
- )
- strategy = TestStrategy()
- strategy.strategy_id = None
- algorithm = Algorithm()
- algorithm.add_strategy(strategy)
- 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
- )
- file_path = BacktestReportWriterService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
- )
- # Check if the backtest report exists
- self.assertTrue(
- os.path.isfile(os.path.join(self.resource_dir, 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={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
- )
- strategy = TestStrategy()
- strategy.strategy_id = None
- algorithm = Algorithm()
- algorithm.add_strategy(strategy)
-
- @algorithm.strategy()
- def run_strategy(algorithm, market_data):
- pass
-
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="bitvavo",
- trading_symbol="EUR",
- initial_balance=1000
- )
- )
-
- 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 = BacktestReportWriterService.create_report_name(
- report, os.path.join(self.resource_dir, "backtest_reports")
- )
- # Check if the backtest report exists
- self.assertTrue(
- os.path.isfile(os.path.join(self.resource_dir, file_path))
- )
-
def test_report_json_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
"""
app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ config={RESOURCE_DIRECTORY: self.resource_dir}
)
algorithm = Algorithm()
diff --git a/tests/app/backtesting/test_run_backtest.py b/tests/app/backtesting/test_run_backtest.py
index 33168bbf..308803c1 100644
--- a/tests/app/backtesting/test_run_backtest.py
+++ b/tests/app/backtesting/test_run_backtest.py
@@ -38,6 +38,18 @@ def setUp(self) -> None:
)
)
+ 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_report_csv_creation(self):
"""
Test if the backtest report is created as a CSV file
@@ -74,7 +86,7 @@ def test_report_csv_creation_without_strategy_identifier(self):
when the strategy does not have an identifier
"""
app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ config={RESOURCE_DIRECTORY: self.resource_dir}
)
strategy = TestStrategy()
strategy.strategy_id = None
@@ -106,7 +118,7 @@ def test_report_csv_creation_with_multiple_strategies(self):
when there are multiple strategies
"""
app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ config={RESOURCE_DIRECTORY: self.resource_dir}
)
strategy = TestStrategy()
strategy.strategy_id = None
@@ -145,7 +157,7 @@ def test_report_csv_creation_with_multiple_strategies_with_id(self):
when there are multiple strategies with identifiers
"""
app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ config={RESOURCE_DIRECTORY: self.resource_dir}
)
algorithm = Algorithm()
diff --git a/tests/app/backtesting/test_run_backtests.py b/tests/app/backtesting/test_run_backtests.py
index 2fd1a159..387d2ea3 100644
--- a/tests/app/backtesting/test_run_backtests.py
+++ b/tests/app/backtesting/test_run_backtests.py
@@ -38,12 +38,22 @@ def setUp(self) -> None:
)
)
+ 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_run_backtests(self):
"""
Test if all backtests are run when multiple algorithms are provided
"""
app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+ config={RESOURCE_DIRECTORY: self.resource_dir}
)
# Add all algorithms
diff --git a/tests/app/test_add_config.py b/tests/app/test_add_config.py
index 4b43b489..9c3c32a3 100644
--- a/tests/app/test_add_config.py
+++ b/tests/app/test_add_config.py
@@ -1,29 +1,29 @@
-import os
-from unittest import TestCase
+# import os
+# from unittest import TestCase
-from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY
+# from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY
-class Test(TestCase):
+# class Test(TestCase):
- def setUp(self) -> None:
- self.resource_dir = os.path.abspath(
- os.path.join(
- os.path.join(
- os.path.join(
- os.path.realpath(__file__),
- os.pardir
- ),
- os.pardir
- ),
- "resources"
- )
- )
+# def setUp(self) -> None:
+# self.resource_dir = os.path.abspath(
+# os.path.join(
+# os.path.join(
+# os.path.join(
+# os.path.realpath(__file__),
+# os.pardir
+# ),
+# os.pardir
+# ),
+# "resources"
+# )
+# )
- def test_add(self):
- app = create_app(
- config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
- )
- self.assertIsNotNone(app.config)
- self.assertIsNotNone(app.config.get("test"))
- self.assertIsNotNone(app.config.get(RESOURCE_DIRECTORY))
+# def test_add(self):
+# app = create_app(
+# config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir}
+# )
+# self.assertIsNotNone(app.config)
+# self.assertIsNotNone(app.config.get("test"))
+# self.assertIsNotNone(app.config.get(RESOURCE_DIRECTORY))
diff --git a/tests/app/test_add_portfolio_configuration.py b/tests/app/test_add_portfolio_configuration.py
index 491ddfae..a71c2ab2 100644
--- a/tests/app/test_add_portfolio_configuration.py
+++ b/tests/app/test_add_portfolio_configuration.py
@@ -1,27 +1,31 @@
-from investing_algorithm_framework import PortfolioConfiguration, \
- MarketCredential
-from tests.resources import TestBase
+# from investing_algorithm_framework import PortfolioConfiguration, \
+# MarketCredential
+# from tests.resources import TestBase
-class Test(TestBase):
- portfolio_configurations = [
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- ]
- market_credentials = [
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- ]
- external_balances = {
- "EUR": 1000,
- }
+# class Test(TestBase):
+# portfolio_configurations = [
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR"
+# )
+# ]
+# market_credentials = [
+# MarketCredential(
+# market="BITVAVO",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# ]
+# external_balances = {
+# "EUR": 1000,
+# }
- def test_add(self):
- self.assertEqual(1, self.app.algorithm.portfolio_service.count())
- self.assertEqual(1, self.app.algorithm.position_service.count())
- self.assertEqual(1000, self.app.algorithm.get_unallocated())
+# def test_add(self):
+# self.assertEqual(1, self.app.algorithm.portfolio_service.count())
+# self.assertEqual(1, self.app.algorithm.position_service.count())
+# self.assertEqual(1000, self.app.algorithm.get_unallocated())
+
+# # Make sure that the portfolio is initialized
+# portfolio = self.app.algorithm.get_portfolio()
+# self.assertTrue(portfolio.initialized)
diff --git a/tests/app/test_app_initialize.py b/tests/app/test_app_initialize.py
index 6fb325c4..66b5624f 100644
--- a/tests/app/test_app_initialize.py
+++ b/tests/app/test_app_initialize.py
@@ -1,126 +1,97 @@
-import os
-from unittest import TestCase
+# import os
+# from unittest import TestCase
-from investing_algorithm_framework import create_app, PortfolioConfiguration, \
- MarketCredential, Algorithm, AppMode, APP_MODE
-from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI
-from tests.resources import MarketServiceStub
+# from investing_algorithm_framework import create_app, PortfolioConfiguration, \
+# MarketCredential, Algorithm, AppMode, APP_MODE, RESOURCE_DIRECTORY
+# from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI
+# from tests.resources import MarketServiceStub
-class TestAppInitialize(TestCase):
- portfolio_configurations = [
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- ]
- market_credentials = [
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- ]
- external_balances = {
- "EUR": 1000,
- }
+# class TestAppInitialize(TestCase):
+# portfolio_configurations = [
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR"
+# )
+# ]
+# market_credentials = [
+# MarketCredential(
+# market="BITVAVO",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# ]
+# external_balances = {
+# "EUR": 1000,
+# }
- def setUp(self) -> None:
- self.resource_dir = os.path.abspath(
- os.path.join(
- os.path.join(
- os.path.join(
- os.path.realpath(__file__),
- os.pardir
- ),
- os.pardir
- ),
- "resources"
- )
- )
+# def setUp(self) -> None:
+# self.resource_dir = os.path.abspath(
+# os.path.join(
+# os.path.join(
+# os.path.join(
+# os.path.realpath(__file__),
+# os.pardir
+# ),
+# os.pardir
+# ),
+# "resources"
+# )
+# )
- def test_app_initialize_default(self):
- app = create_app(
- config={"test": "test", 'resource_directory': self.resource_dir}
- )
- app.container.market_service.override(
- MarketServiceStub(app.container.market_credential_service())
- )
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR",
- )
- )
- algorithm = Algorithm()
- app.add_algorithm(algorithm)
- app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
- app.initialize()
- self.assertIsNotNone(app.config)
- self.assertIsNotNone(app._flask_app)
- self.assertTrue(AppMode.DEFAULT.equals(app.config[APP_MODE]))
- order_service = app.container.order_service()
- self.assertEqual(0, order_service.count())
+# def test_app_initialize_default(self):
+# app = create_app(
+# config={RESOURCE_DIRECTORY: self.resource_dir}
+# )
+# app.container.market_service.override(
+# MarketServiceStub(app.container.market_credential_service())
+# )
+# app.add_portfolio_configuration(
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR",
+# )
+# )
+# algorithm = Algorithm()
+# app.add_algorithm(algorithm)
+# app.add_market_credential(
+# MarketCredential(
+# market="BITVAVO",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# )
+# app.initialize()
+# self.assertIsNotNone(app.config)
+# self.assertIsNone(app._flask_app)
+# self.assertTrue(AppMode.DEFAULT.equals(app.config[APP_MODE]))
+# order_service = app.container.order_service()
+# self.assertEqual(0, order_service.count())
- def test_app_initialize_web(self):
- app = create_app(
- config={"test": "test", 'resource_directory': self.resource_dir},
- web=True
- )
- app.container.market_service.override(MarketServiceStub(None))
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR",
- )
- )
- algorithm = Algorithm()
- app.add_algorithm(algorithm)
- app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
- app.initialize()
- self.assertIsNotNone(app.config)
- self.assertIsNotNone(app._flask_app)
- self.assertTrue(AppMode.WEB.equals(app.config[APP_MODE]))
- order_service = app.container.order_service()
- self.assertEqual(0, order_service.count())
-
- def test_app_initialize_stateless(self):
- app = create_app(
- config={"test": "test"},
- stateless=True
- )
- app.container.market_service.override(MarketServiceStub(None))
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- )
- algorithm = Algorithm()
- app.add_algorithm(algorithm)
- app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
- app.initialize()
- order_service = app.container.order_service()
- self.assertIsNotNone(app.config)
- self.assertIsNotNone(app._flask_app)
- self.assertTrue(AppMode.STATELESS.equals(app.config[APP_MODE]))
- self.assertEqual(app.config[SQLALCHEMY_DATABASE_URI], "sqlite://")
- self.assertEqual(0, order_service.count())
+# def test_app_initialize_web(self):
+# app = create_app(
+# config={RESOURCE_DIRECTORY: self.resource_dir},
+# web=True
+# )
+# app.container.market_service.override(MarketServiceStub(None))
+# app.add_portfolio_configuration(
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR",
+# )
+# )
+# algorithm = Algorithm()
+# app.add_algorithm(algorithm)
+# app.add_market_credential(
+# MarketCredential(
+# market="BITVAVO",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# )
+# app.initialize()
+# self.assertIsNotNone(app.config)
+# self.assertIsNotNone(app._flask_app)
+# self.assertTrue(AppMode.WEB.equals(app.config[APP_MODE]))
+# order_service = app.container.order_service()
+# self.assertEqual(0, order_service.count())
diff --git a/tests/app/test_app_stateless.py b/tests/app/test_app_stateless.py
deleted file mode 100644
index 4fee8c51..00000000
--- a/tests/app/test_app_stateless.py
+++ /dev/null
@@ -1,99 +0,0 @@
-from investing_algorithm_framework import PortfolioConfiguration, \
- MarketCredential, APP_MODE, AppMode, StatelessAction, create_app
-from tests.resources import MarketServiceStub
-from tests.resources import TestBase
-
-
-class Test(TestBase):
- config = {
- APP_MODE: "stateless"
- }
- portfolio_configurations = [
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- ]
- market_credentials = [
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- ]
- external_balances = {
- "EUR": 1000,
- }
-
- def test_run(self):
- config = self.app.config
- self.assertTrue(AppMode.STATELESS.equals(config[APP_MODE]))
- self.app.run(
- number_of_iterations=1,
- payload={"action": StatelessAction.RUN_STRATEGY.value}
- )
- self.assertEqual(1000, self.app.algorithm.get_unallocated())
- trading_symbol_position = self.app.algorithm.get_position(
- symbol="EUR"
- )
- self.assertEqual(1000, trading_symbol_position.get_amount())
-
- def test_run_with_changed_external_positions(self):
- app = create_app(config={APP_MODE: AppMode.STATELESS.value})
- app.add_algorithm(self.algorithm)
- app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- )
- market_service = MarketServiceStub(None)
- market_service.balances = {
- "EUR": 1000,
- "BTC": 1
- }
- app.container.market_service.override(market_service)
- app.initialize()
- self.assertTrue(AppMode.STATELESS.equals(app.config[APP_MODE]))
- self.app.run(payload={
- "action": StatelessAction.RUN_STRATEGY.value,
- })
- self.assertEqual(1000, app.algorithm.get_unallocated())
- trading_symbol_position = self.app.algorithm.get_position(
- symbol="EUR"
- )
- self.assertEqual(1000, trading_symbol_position.get_amount())
- app = create_app(config={APP_MODE: AppMode.STATELESS.value})
- app.add_algorithm(self.algorithm)
- app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
- app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- )
- market_service = MarketServiceStub(None)
- market_service.balances = {
- "EUR": 2000,
- "BTC": 1
- }
- app.container.market_service.override(market_service)
- app.initialize()
- self.assertEqual(2000, app.algorithm.get_unallocated())
- trading_symbol_position = self.app.algorithm.get_position(
- symbol="EUR"
- )
- self.assertEqual(2000, trading_symbol_position.get_amount())
diff --git a/tests/app/test_backtesting.py b/tests/app/test_backtesting.py
index 735090d6..f4ecb8c9 100644
--- a/tests/app/test_backtesting.py
+++ b/tests/app/test_backtesting.py
@@ -1,43 +1,43 @@
-from investing_algorithm_framework import TradingStrategy, Algorithm
+# from investing_algorithm_framework import TradingStrategy, Algorithm
-class SimpleTradingStrategy(TradingStrategy):
- interval = 2
- time_unit = "hour"
+# class SimpleTradingStrategy(TradingStrategy):
+# interval = 2
+# time_unit = "hour"
- def apply_strategy(self, algorithm: Algorithm, market_data):
+# def apply_strategy(self, algorithm: Algorithm, market_data):
- if algorithm.has_open_orders():
- return
+# if algorithm.has_open_orders():
+# return
- algorithm.create_limit_order(
- target_symbol="BTC",
- amount=0.01,
- price=10000,
- order_side="buy"
- )
+# algorithm.create_limit_order(
+# target_symbol="BTC",
+# amount=0.01,
+# price=10000,
+# order_side="buy"
+# )
-# class Test(TestCase):
-#
-# def setUp(self) -> None:
-# self.resource_dir = os.path.abspath(
-# os.path.join(
-# os.path.join(
-# os.path.join(
-# os.path.realpath(__file__),
-# os.pardir
-# ),
-# os.pardir
-# ),
-# "resources"
-# )
-# )
-# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
-# self.app.add_portfolio_configuration(
-# PortfolioConfiguration(
-# market="BITVAVO",
-# trading_symbol="USDT"
-# )
-# )
-# self.app.add_strategy(SimpleTradingStrategy)
+# # class Test(TestCase):
+# #
+# # def setUp(self) -> None:
+# # self.resource_dir = os.path.abspath(
+# # os.path.join(
+# # os.path.join(
+# # os.path.join(
+# # os.path.realpath(__file__),
+# # os.pardir
+# # ),
+# # os.pardir
+# # ),
+# # "resources"
+# # )
+# # )
+# # self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
+# # self.app.add_portfolio_configuration(
+# # PortfolioConfiguration(
+# # market="BITVAVO",
+# # trading_symbol="USDT"
+# # )
+# # )
+# # self.app.add_strategy(SimpleTradingStrategy)
diff --git a/tests/app/test_config.py b/tests/app/test_config.py
index 9414e122..02e10ae7 100644
--- a/tests/app/test_config.py
+++ b/tests/app/test_config.py
@@ -1,34 +1,25 @@
-from unittest import TestCase
+# from unittest import TestCase
-from investing_algorithm_framework import create_app
-from investing_algorithm_framework.domain import BACKTEST_DATA_DIRECTORY_NAME
-from tests.resources import random_string
+# from investing_algorithm_framework import create_app
+# from investing_algorithm_framework.domain import RESOURCE_DIRECTORY
+# from tests.resources import random_string
-TEST_VALUE = random_string(10)
+# TEST_VALUE = random_string(10)
-class TestConfig(TestCase):
- ATTRIBUTE_ONE = "ATTRIBUTE_ONE"
+# class TestConfig(TestCase):
+# ATTRIBUTE_ONE = "ATTRIBUTE_ONE"
- def test_config(self):
- app = create_app(
- config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE}
- )
- self.assertIsNotNone(app.config)
+# def test_config(self):
+# app = create_app(
+# config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE}
+# )
+# app.initialize_config()
+# self.assertIsNotNone(app.config)
+# self.assertIsNotNone(app.config[self.ATTRIBUTE_ONE])
- def test_get_item(self):
- app = create_app(
- config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE}
- )
- self.assertIsNotNone(app.config)
- self.assertIsNotNone(app.config.get(self.ATTRIBUTE_ONE))
- self.assertIsNotNone(app.config.get(BACKTEST_DATA_DIRECTORY_NAME))
-
- def test_set_item(self):
- app = create_app(
- config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE}
- )
- self.assertIsNotNone(app.config)
- new_value = random_string(10)
- app.config.set(self.ATTRIBUTE_ONE, new_value)
- self.assertEqual(app.config.get(self.ATTRIBUTE_ONE), new_value)
+# def test_resource_directory_exists(self):
+# app = create_app()
+# app.initialize_config()
+# self.assertIsNotNone(app.config)
+# self.assertIsNotNone(app.config[RESOURCE_DIRECTORY])
diff --git a/tests/app/test_start.py b/tests/app/test_start.py
index 14a72305..5b5bba0e 100644
--- a/tests/app/test_start.py
+++ b/tests/app/test_start.py
@@ -1,94 +1,95 @@
-import os
+# import os
-from investing_algorithm_framework import create_app, TradingStrategy, \
- TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \
- MarketCredential
-from tests.resources import TestBase, MarketServiceStub
+# from investing_algorithm_framework import create_app, TradingStrategy, \
+# TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \
+# MarketCredential
+# from tests.resources import TestBase, MarketServiceStub
-class StrategyOne(TradingStrategy):
- time_unit = TimeUnit.SECOND
- interval = 2
+# class StrategyOne(TradingStrategy):
+# time_unit = TimeUnit.SECOND
+# interval = 2
- def apply_strategy(
- self,
- algorithm,
- market_date=None,
- **kwargs
- ):
- pass
+# def apply_strategy(
+# self,
+# algorithm,
+# market_date=None,
+# **kwargs
+# ):
+# pass
-class StrategyTwo(TradingStrategy):
- time_unit = TimeUnit.SECOND
- interval = 2
+# class StrategyTwo(TradingStrategy):
+# time_unit = TimeUnit.SECOND
+# interval = 2
- def apply_strategy(
- self,
- algorithm,
- market_date=None,
- **kwargs
- ):
- pass
+# def apply_strategy(
+# self,
+# algorithm,
+# market_date=None,
+# **kwargs
+# ):
+# pass
-class Test(TestBase):
+# class Test(TestBase):
- def setUp(self) -> None:
- self.resource_dir = os.path.abspath(
- os.path.join(
- os.path.join(
- os.path.join(
- os.path.realpath(__file__),
- os.pardir
- ),
- os.pardir
- ),
- "resources"
- )
- )
- self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="USDT"
- )
- )
- self.app.container.market_service.override(MarketServiceStub(None))
- algorithm = Algorithm()
- algorithm.add_strategy(StrategyOne)
- algorithm.add_strategy(StrategyTwo)
- self.app.add_algorithm(algorithm)
- self.app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
- )
+# def setUp(self) -> None:
+# self.resource_dir = os.path.abspath(
+# os.path.join(
+# os.path.join(
+# os.path.join(
+# os.path.realpath(__file__),
+# os.pardir
+# ),
+# os.pardir
+# ),
+# "resources"
+# )
+# )
+# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
+# self.app.add_portfolio_configuration(
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR"
+# )
+# )
- def test_default(self):
- self.app.run(number_of_iterations=2)
- self.assertFalse(self.app.running)
- strategy_orchestrator_service = self.app \
- .algorithm.strategy_orchestrator_service
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
+# self.app.container.market_service.override(MarketServiceStub(None))
+# algorithm = Algorithm()
+# algorithm.add_strategy(StrategyOne)
+# algorithm.add_strategy(StrategyTwo)
+# self.app.add_algorithm(algorithm)
+# self.app.add_market_credential(
+# MarketCredential(
+# market="BITVAVO",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# )
- def test_web(self):
- self.app.run(number_of_iterations=2)
- self.assertFalse(self.app.running)
- strategy_orchestrator_service = self.app \
- .algorithm.strategy_orchestrator_service
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
+# def test_default(self):
+# self.app.run(number_of_iterations=2)
+# self.assertFalse(self.app.running)
+# strategy_orchestrator_service = self.app \
+# .algorithm.strategy_orchestrator_service
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
- def test_stateless(self):
- self.app.run(
- number_of_iterations=2,
- payload={"ACTION": "RUN_STRATEGY"},
- )
- strategy_orchestrator_service = self.app\
- .algorithm.strategy_orchestrator_service
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
- self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
+# def test_web(self):
+# self.app.run(number_of_iterations=2)
+# self.assertFalse(self.app.running)
+# strategy_orchestrator_service = self.app \
+# .algorithm.strategy_orchestrator_service
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
+
+# def test_stateless(self):
+# self.app.run(
+# number_of_iterations=2,
+# payload={"ACTION": "RUN_STRATEGY"},
+# )
+# strategy_orchestrator_service = self.app\
+# .algorithm.strategy_orchestrator_service
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne"))
+# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo"))
diff --git a/tests/app/test_start_with_new_external_orders.py b/tests/app/test_start_with_new_external_orders.py
new file mode 100644
index 00000000..69f98538
--- /dev/null
+++ b/tests/app/test_start_with_new_external_orders.py
@@ -0,0 +1,132 @@
+# from investing_algorithm_framework import Order, PortfolioConfiguration, \
+# MarketCredential
+# from tests.resources import TestBase
+
+
+# class Test(TestBase):
+# initial_orders = [
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "BTC/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# Order.from_dict(
+# {
+# "id": "14354",
+# "side": "buy",
+# "symbol": "DOT/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-09-22T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "ETH/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "OPEN",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 0,
+# "remaining": 0,
+# },
+# ),
+# ]
+# external_orders = [
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "ETH/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# # Order that is not tracked by the trading bot
+# Order.from_dict(
+# {
+# "id": "133423",
+# "side": "buy",
+# "symbol": "KSM/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# ]
+# external_balances = {"EUR": 1000}
+# portfolio_configurations = [
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR"
+# )
+# ]
+# market_credentials = [
+# MarketCredential(
+# market="bitvavo",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# ]
+
+# def test_start_with_new_external_positions(self):
+# """
+# Test how the framework handles new external positions on an broker or
+# exchange.
+
+# If the positions where not in the database, the algorithm should
+# not include them, because they could be positions from another
+# user or from another algorithm.
+# """
+# self.assertTrue(self.app.algorithm.has_position("BTC"))
+# btc_position = self.app.algorithm.get_position("BTC")
+# self.assertEqual(10, btc_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("DOT"))
+# dot_position = self.app.algorithm.get_position("DOT")
+# self.assertEqual(10, dot_position.get_amount())
+# # Eth position still has open order
+# self.assertFalse(self.app.algorithm.has_position("ETH"))
+# eth_position = self.app.algorithm.get_position("ETH")
+# self.assertEqual(0, eth_position.get_amount())
+# self.assertFalse(self.app.algorithm.has_position("KSM"))
+# self.app.run(number_of_iterations=1)
+# self.assertTrue(self.app.algorithm.has_position("BTC"))
+# btc_position = self.app.algorithm.get_position("BTC")
+# self.assertEqual(10, btc_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("DOT"))
+# dot_position = self.app.algorithm.get_position("DOT")
+# self.assertEqual(10, dot_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("ETH"))
+# eth_position = self.app.algorithm.get_position("ETH")
+# self.assertEqual(10, eth_position.get_amount())
+# self.assertFalse(self.app.algorithm.has_position("KSM"))
diff --git a/tests/app/test_start_with_new_external_positions.py b/tests/app/test_start_with_new_external_positions.py
new file mode 100644
index 00000000..69f98538
--- /dev/null
+++ b/tests/app/test_start_with_new_external_positions.py
@@ -0,0 +1,132 @@
+# from investing_algorithm_framework import Order, PortfolioConfiguration, \
+# MarketCredential
+# from tests.resources import TestBase
+
+
+# class Test(TestBase):
+# initial_orders = [
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "BTC/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# Order.from_dict(
+# {
+# "id": "14354",
+# "side": "buy",
+# "symbol": "DOT/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-09-22T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "ETH/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "OPEN",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 0,
+# "remaining": 0,
+# },
+# ),
+# ]
+# external_orders = [
+# Order.from_dict(
+# {
+# "id": "1323",
+# "side": "buy",
+# "symbol": "ETH/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# # Order that is not tracked by the trading bot
+# Order.from_dict(
+# {
+# "id": "133423",
+# "side": "buy",
+# "symbol": "KSM/EUR",
+# "amount": 10,
+# "price": 10.0,
+# "status": "CLOSED",
+# "order_type": "limit",
+# "order_side": "buy",
+# "created_at": "2023-08-08T14:40:56.626362Z",
+# "filled": 10,
+# "remaining": 0,
+# },
+# ),
+# ]
+# external_balances = {"EUR": 1000}
+# portfolio_configurations = [
+# PortfolioConfiguration(
+# market="BITVAVO",
+# trading_symbol="EUR"
+# )
+# ]
+# market_credentials = [
+# MarketCredential(
+# market="bitvavo",
+# api_key="api_key",
+# secret_key="secret_key"
+# )
+# ]
+
+# def test_start_with_new_external_positions(self):
+# """
+# Test how the framework handles new external positions on an broker or
+# exchange.
+
+# If the positions where not in the database, the algorithm should
+# not include them, because they could be positions from another
+# user or from another algorithm.
+# """
+# self.assertTrue(self.app.algorithm.has_position("BTC"))
+# btc_position = self.app.algorithm.get_position("BTC")
+# self.assertEqual(10, btc_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("DOT"))
+# dot_position = self.app.algorithm.get_position("DOT")
+# self.assertEqual(10, dot_position.get_amount())
+# # Eth position still has open order
+# self.assertFalse(self.app.algorithm.has_position("ETH"))
+# eth_position = self.app.algorithm.get_position("ETH")
+# self.assertEqual(0, eth_position.get_amount())
+# self.assertFalse(self.app.algorithm.has_position("KSM"))
+# self.app.run(number_of_iterations=1)
+# self.assertTrue(self.app.algorithm.has_position("BTC"))
+# btc_position = self.app.algorithm.get_position("BTC")
+# self.assertEqual(10, btc_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("DOT"))
+# dot_position = self.app.algorithm.get_position("DOT")
+# self.assertEqual(10, dot_position.get_amount())
+# self.assertTrue(self.app.algorithm.has_position("ETH"))
+# eth_position = self.app.algorithm.get_position("ETH")
+# self.assertEqual(10, eth_position.get_amount())
+# self.assertFalse(self.app.algorithm.has_position("KSM"))
diff --git a/tests/app/web/controllers/position_controller/test_list_positions.py b/tests/app/web/controllers/position_controller/test_list_positions.py
index f507524c..c601d699 100644
--- a/tests/app/web/controllers/position_controller/test_list_positions.py
+++ b/tests/app/web/controllers/position_controller/test_list_positions.py
@@ -23,9 +23,6 @@ class Test(FlaskTestBase):
"EUR": 1000
}
- def setUp(self) -> None:
- super(Test, self).setUp()
-
def test_list_portfolios(self):
self.iaf_app.algorithm.create_limit_order(
amount=10,
diff --git a/tests/app/web/schemas/test_order_schema.py b/tests/app/web/schemas/test_order_schema.py
deleted file mode 100644
index efdd6ec8..00000000
--- a/tests/app/web/schemas/test_order_schema.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# from investing_algorithm_framework import SQLLiteOrder
-# from investing_algorithm_framework.schemas import OrderSerializer
-# from tests.resources import TestBase, TestOrderAndPositionsObjectsMixin
-# from tests.resources.serialization_dicts import order_serialization_dict
-#
-#
-# class Test(TestBase, TestOrderAndPositionsObjectsMixin):
-#
-# def setUp(self):
-# super(Test, self).setUp()
-#
-# self.start_algorithm()
-# self.create_buy_order(
-# 10,
-# self.TARGET_SYMBOL_A,
-# self.BASE_SYMBOL_A_PRICE,
-# self.algo_app.algorithm.get_portfolio_manager(),
-# 10
-# )
-#
-# def test(self):
-# order = SQLLiteOrder.query.first()
-# serializer = OrderSerializer()
-# data = serializer.dump(order)
-# self.assertEqual(set(data), order_serialization_dict)
diff --git a/tests/domain/test_load_backtest_reports.py b/tests/domain/test_load_backtest_reports.py
index 7cd7d099..184fe85c 100644
--- a/tests/domain/test_load_backtest_reports.py
+++ b/tests/domain/test_load_backtest_reports.py
@@ -23,4 +23,3 @@ def setUp(self) -> None:
def test_backtest_reports_evaluation(self):
path = os.path.join(self.resource_dir, "backtest_reports_for_testing")
reports = load_backtest_reports(path)
- print(len(reports))
diff --git a/tests/infrastructure/models/test_order.py b/tests/infrastructure/models/test_order.py
index 4a4c8835..872eb3d6 100644
--- a/tests/infrastructure/models/test_order.py
+++ b/tests/infrastructure/models/test_order.py
@@ -1,49 +1,26 @@
-import os
-
-from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \
- PortfolioConfiguration, Algorithm, MarketCredential
+from investing_algorithm_framework import \
+ PortfolioConfiguration, MarketCredential
from investing_algorithm_framework.infrastructure.models import SQLOrder
-from tests.resources import TestBase, MarketServiceStub
+from tests.resources import TestBase
class Test(TestBase):
-
- 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"
- )
- )
- self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="binance",
- trading_symbol="USDT"
- )
- )
- self.app.container.market_service.override(
- MarketServiceStub(self.app.container.market_credential_service())
+ portfolio_configurations = [
+ PortfolioConfiguration(
+ market="binance",
+ trading_symbol="USDT"
)
- algorithm = Algorithm()
- self.app.add_algorithm(algorithm)
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="api_key",
- secret_key="secret_key",
- )
+ ]
+ external_balances = {
+ "USDT": 1000
+ }
+ market_credentials = [
+ MarketCredential(
+ market="binance",
+ api_key="api_key",
+ secret_key="secret_key"
)
- self.app.initialize()
+ ]
def test_creation(self):
order = SQLOrder(
diff --git a/tests/infrastructure/models/test_position.py b/tests/infrastructure/models/test_position.py
index bdb344f1..410fe324 100644
--- a/tests/infrastructure/models/test_position.py
+++ b/tests/infrastructure/models/test_position.py
@@ -6,43 +6,62 @@
class Test(TestBase):
-
- 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"
- )
- )
- self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="USDT"
- )
- )
- self.app.container.market_service.override(
- MarketServiceStub(self.app.container.market_credential_service())
+ portfolio_configurations = [
+ PortfolioConfiguration(
+ market="BITVAVO",
+ trading_symbol="USDT"
)
- algorithm = Algorithm()
- self.app.add_algorithm(algorithm)
- self.app.add_market_credential(
- MarketCredential(
- market="BITVAVO",
- api_key="api_key",
- secret_key="secret_key"
- )
+ ]
+ external_balances = {
+ "USDT": 1000
+ }
+ market_credentials = [
+ MarketCredential(
+ market="BITVAVO",
+ api_key="",
+ secret_key=""
)
- self.app.initialize()
+ ]
+
+ # 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"
+ # )
+ # )
+ # self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
+ # self.app.add_portfolio_configuration(
+ # PortfolioConfiguration(
+ # market="BITVAVO",
+ # trading_symbol="USDT"
+ # )
+ # )
+ # self.app.container.market_service.override(
+ # MarketServiceStub(self.app.container.market_credential_service())
+ # )
+ # algorithm = Algorithm()
+ # self.app.add_algorithm(algorithm)
+ # self.app.add_market_credential(
+ # MarketCredential(
+ # market="BITVAVO",
+ # api_key="api_key",
+ # secret_key="secret_key"
+ # )
+ # )
+ # self.app.initialize()
+
+ # def tearDown(self):
+ # return super().tearDown()
def test_store_position_amount(self):
self.portfolio_service = self.app.container.portfolio_service()
diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py
index d6c80d8e..cdfaf627 100644
--- a/tests/resources/test_base.py
+++ b/tests/resources/test_base.py
@@ -1,12 +1,14 @@
import logging
import os
+from decimal import Decimal
from unittest import TestCase
from flask_testing import TestCase as FlaskTestCase
from investing_algorithm_framework import create_app, Algorithm, App, \
- TradingStrategy, TimeUnit
-from investing_algorithm_framework.domain import RESOURCE_DIRECTORY
+ TradingStrategy, TimeUnit, OrderStatus
+from investing_algorithm_framework.domain import RESOURCE_DIRECTORY, \
+ ENVIRONMENT, Environment
from investing_algorithm_framework.infrastructure.database import Session
from tests.resources.stubs import MarketServiceStub
@@ -33,7 +35,7 @@ class TestBase(TestCase):
algorithm = Algorithm()
external_balances = {}
external_orders = []
- external_available_symbols = []
+ initial_orders = []
market_credentials = []
market_service = MarketServiceStub(None)
market_data_source_service = None
@@ -45,8 +47,8 @@ def setUp(self) -> None:
self.resource_directory = os.path.dirname(__file__)
config = self.config
config[RESOURCE_DIRECTORY] = self.resource_directory
+ config[ENVIRONMENT] = Environment.TEST.value
self.app: App = create_app(config=config)
- self.market_service.symbols = self.external_available_symbols
self.market_service.balances = self.external_balances
self.market_service.orders = self.external_orders
self.app.container.market_service.override(self.market_service)
@@ -71,32 +73,66 @@ def setUp(self) -> None:
if self.initialize:
self.app.initialize()
+ if self.initial_orders is not None:
+ for order in self.initial_orders:
+ created_order = self.app.algorithm.create_order(
+ target_symbol=order.get_target_symbol(),
+ amount=order.get_amount(),
+ price=order.get_price(),
+ order_side=order.get_order_side(),
+ order_type=order.get_order_type()
+ )
+
+ # Update the order to the correct status
+ order_service = self.app.container.order_service()
+
+ if OrderStatus.CLOSED.value == order.get_status():
+ order_service.update(
+ created_order.get_id(),
+ {
+ "status": "CLOSED",
+ "filled": order.get_filled(),
+ "remaining": Decimal('0'),
+ }
+ )
+
def tearDown(self) -> None:
- database_path = os.path.join(
- self.resource_directory, "databases/prod-database.sqlite3"
+ database_dir = os.path.join(
+ self.resource_directory, "databases"
)
- if os.path.exists(database_path):
- session = Session()
- session.commit()
- session.close()
-
- try:
- os.remove(database_path)
- except Exception as e:
- logger.error(e)
+ 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 remove_database(self):
- try:
- database_path = os.path.join(
- self.resource_directory, "databases/prod-database.sqlite3"
- )
+ database_dir = os.path.join(
+ self.resource_directory, "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))
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ database_dir = os.path.join(
+ cls.resource_directory, "databases"
+ )
- if os.path.exists(database_path):
- os.remove(database_path)
- except Exception as e:
- logger.error(e)
+ 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))
class FlaskTestBase(FlaskTestCase):
@@ -106,17 +142,17 @@ class FlaskTestBase(FlaskTestCase):
config = {}
algorithm = Algorithm()
external_balances = {}
+ initial_orders = []
external_orders = []
- external_available_symbols = []
market_service = MarketServiceStub(None)
initialize = True
+ resource_directory = os.path.dirname(__file__)
def create_app(self):
self.resource_directory = os.path.dirname(__file__)
self.iaf_app: App = create_app(
{RESOURCE_DIRECTORY: self.resource_directory}, web=True
)
- self.market_service.symbols = self.external_available_symbols
self.market_service.balances = self.external_balances
self.market_service.orders = self.external_orders
self.iaf_app.container.market_service.override(self.market_service)
@@ -138,19 +174,52 @@ def create_app(self):
self.iaf_app.initialize()
+ if self.initial_orders is not None:
+ for order in self.initial_orders:
+ created_order = self.app.algorithm.create_order(
+ target_symbol=order.get_target_symbol(),
+ amount=order.get_amount(),
+ price=order.get_price(),
+ order_side=order.get_order_side(),
+ order_type=order.get_order_type()
+ )
+
+ # Update the order to the correct status
+ order_service = self.app.container.order_service()
+
+ if OrderStatus.CLOSED.value == order.get_status():
+ order_service.update(
+ created_order.get_id(),
+ {
+ "status": "CLOSED",
+ "filled": order.get_filled(),
+ "remaining": Decimal('0'),
+ }
+ )
+
return self.iaf_app._flask_app
def tearDown(self) -> None:
- database_path = os.path.join(
- os.path.dirname(__file__), "databases/prod-database.sqlite3"
+ database_dir = os.path.join(
+ self.resource_directory, "databases"
)
- if os.path.exists(database_path):
- session = Session()
- session.commit()
- session.close()
+ 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))
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ database_dir = os.path.join(
+ cls.resource_directory, "databases"
+ )
- try:
- os.remove(database_path)
- except Exception as e:
- logger.error(e)
+ 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))
diff --git a/tests/services/test_market_data_source_service.py b/tests/services/test_market_data_source_service.py
index 7c72dfee..52cfca95 100644
--- a/tests/services/test_market_data_source_service.py
+++ b/tests/services/test_market_data_source_service.py
@@ -1,56 +1,42 @@
import os
-from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \
- PortfolioConfiguration, CSVTickerMarketDataSource, MarketCredential, \
- Algorithm
-from tests.resources import TestBase, MarketServiceStub
+from investing_algorithm_framework import RESOURCE_DIRECTORY, \
+ PortfolioConfiguration, CSVTickerMarketDataSource, MarketCredential
+from tests.resources import TestBase
class TestMarketDataSourceService(TestBase):
-
- def setUp(self) -> None:
- self.resource_dir = os.path.abspath(
- os.path.join(
- os.path.join(
- os.path.join(
- os.path.realpath(__file__),
- os.pardir
- ),
- os.pardir
- ),
- "resources"
- )
- )
- self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- market="binance",
- trading_symbol="USDT"
- )
+ portfolio_configurations = [
+ PortfolioConfiguration(
+ market="binance",
+ trading_symbol="USDT"
)
- self.app.container.market_service.override(
- MarketServiceStub(self.app.container.market_credential_service())
+ ]
+ market_credentials = [
+ MarketCredential(
+ market="binance",
+ api_key="api_key",
+ secret_key="secret_key"
)
+ ]
+ external_balances = {
+ "USDT": 1000
+ }
+
+ def setUp(self) -> None:
+ super(TestMarketDataSourceService, self).setUp()
+ configuration_service = self.app.container.configuration_service()
+ config = configuration_service.get_config()
self.app.add_market_data_source(CSVTickerMarketDataSource(
identifier="BTC/EUR-ticker",
market="BITVAVO",
symbol="BTC/EUR",
csv_file_path=os.path.join(
- self.resource_dir,
+ config[RESOURCE_DIRECTORY],
"market_data_sources",
"TICKER_BTC-EUR_BINANCE_2023-08-23:22:00_2023-12-02:00:00.csv"
)
))
- algorithm = Algorithm()
- self.app.add_algorithm(algorithm)
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="api_key",
- secret_key="secret_key",
- )
- )
- self.app.initialize()
def test_get_ticker_market_data_source(self):
market_data_source_service = self.app.container\
diff --git a/tests/services/test_order_backtest_service.py b/tests/services/test_order_backtest_service.py
index 50c58db3..c8bc5d53 100644
--- a/tests/services/test_order_backtest_service.py
+++ b/tests/services/test_order_backtest_service.py
@@ -77,8 +77,12 @@ def setUp(self) -> None:
def test_create_limit_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
+
order = order_service.create(
{
"target_symbol": "ADA",
@@ -104,8 +108,12 @@ def test_create_limit_order(self):
def test_update_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
+
order = order_service.create(
{
"target_symbol": "ADA",
@@ -136,8 +144,11 @@ def test_update_order(self):
def test_create_limit_buy_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
order = order_service.create(
{
"target_symbol": "ADA",
@@ -163,9 +174,11 @@ def test_create_limit_buy_order(self):
def test_create_limit_sell_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
-
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
order = order_service.create(
{
"target_symbol": "ADA",
@@ -232,8 +245,11 @@ def test_update_sell_order_with_successful_order(self):
def test_update_closing_partial_buy_orders(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
buy_order_one = order_service.create(
{
"target_symbol": "ADA",
@@ -356,8 +372,11 @@ def test_update_sell_order_with_cancelled_order(self):
def test_trade_closing_winning_trade(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
buy_order = order_service.create(
{
"target_symbol": "ADA",
@@ -416,8 +435,11 @@ def test_trade_closing_winning_trade(self):
def test_trade_closing_losing_trade(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
buy_order = order_service.create(
{
"target_symbol": "ADA",
@@ -476,8 +498,11 @@ def test_trade_closing_losing_trade(self):
def test_has_executed_buy_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
# Create the buy order
order = order_service.create(
@@ -604,8 +629,11 @@ def test_has_executed_buy_order(self):
def test_has_executed_sell_order(self):
order_service = self.app.container.order_service()
- config = self.app.config
- config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow()
+ configuration_service = self.app.container.configuration_service()
+ configuration_service.add_value(
+ BACKTESTING_INDEX_DATETIME,
+ datetime.utcnow()
+ )
# Create the buy order
order = order_service.create(
diff --git a/tests/services/test_portfolio_sync_service.py b/tests/services/test_portfolio_sync_service.py
index 8f7b3b81..1ba3a387 100644
--- a/tests/services/test_portfolio_sync_service.py
+++ b/tests/services/test_portfolio_sync_service.py
@@ -75,9 +75,16 @@ def test_sync_unallocated_with_no_balance(self):
self.market_service.balances = {"EUR": 0}
self.app.add_algorithm(Algorithm())
- with self.assertRaises(OperationalException):
+ with self.assertRaises(OperationalException) as context:
self.app.initialize()
+ self.assertEqual(
+ "The initial balance of the portfolio configuration is more than" " the available balance on the exchange. Please make sure"
+ " that the initial balance of the portfolio configuration"
+ " is less than the available balance on the exchange.",
+ str(context.exception)
+ )
+
def test_sync_unallocated_with_reserved(self):
configuration_service = self.app.container.configuration_service()
configuration_service.config[RESERVED_BALANCES] = {"EUR": 500}
@@ -104,39 +111,6 @@ def test_sync_unallocated_with_reserved(self):
.find({"identifier": "test"})
self.assertEqual(500, portfolio.unallocated)
- def test_sync_unallocated_with_reserved_error(self):
- """
- Test the sync_unallocated method of the PortfolioSyncService class.
-
- 1. Create a PortfolioSyncService object.
- 2. Create a portfolio configuration with 500 unallocated balance.
- 3. Set the reserved balance to 500.
- 3. Set the market balance to 700
- 4. Check if sync method raises an OperationalException.
- """
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[RESERVED_BALANCES] = {"EUR": 500}
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {"EUR": 700}
- self.app.add_algorithm(Algorithm())
-
- with self.assertRaises(OperationalException):
- self.app.initialize()
-
def test_sync_unallocated_with_initial_size(self):
self.app.add_portfolio_configuration(
PortfolioConfiguration(
@@ -160,686 +134,3 @@ def test_sync_unallocated_with_initial_size(self):
portfolio = self.app.container.portfolio_service() \
.find({"identifier": "test"})
self.assertEqual(500, portfolio.unallocated)
-
- def test_sync_unallocated_with_stateless(self):
- """
- Test to sync the unallocated amount with initial load set to false.
- This means that if the available balance is less than the
- initial balance of the portfolio configuration, the
- unallocated balance should be set to the available balance. It
- should not raise an OperationalException.
- """
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {"EUR": 1200}
- self.app.add_algorithm(Algorithm())
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[APP_MODE] = AppMode.STATELESS.value
- self.app.config[APP_MODE] = AppMode.STATELESS.value
- self.app.initialize()
-
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[APP_MODE] = "STATELESS"
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- self.assertEqual(1200, portfolio.unallocated)
-
- def test_sync_positions(self):
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.app.add_algorithm(Algorithm())
- self.app.initialize()
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[APP_MODE] = "STATELESS"
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- self.assertEqual(500, portfolio.unallocated)
- btc_position = self.app.container.position_service().find(
- {"symbol": "BTC", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0.5, btc_position.amount)
- eth_position = self.app.container.position_service().find(
- {"symbol": "ETH", "portfolio_id": portfolio.id}
- )
- self.assertEqual(199, eth_position.amount)
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- self.assertEqual(4023, ada_position.amount)
- xrp_position = self.app.container.position_service().find(
- {"symbol": "XRP", "portfolio_id": portfolio.id}
- )
- self.assertEqual(10, xrp_position.amount)
-
- def test_sync_positions_with_reserved(self):
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.app.add_algorithm(Algorithm())
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[APP_MODE] = AppMode.DEFAULT.value
- configuration_service.config[RESERVED_BALANCES] = {
- "BTC": 0.5,
- "ETH": 1,
- "ADA": 1000,
- "XRP": 5
- }
- self.app.initialize()
-
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- self.assertEqual(500, portfolio.unallocated)
- btc_position = self.app.container.position_service().find(
- {"symbol": "BTC", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0, btc_position.amount)
- eth_position = self.app.container.position_service().find(
- {"symbol": "ETH", "portfolio_id": portfolio.id}
- )
- self.assertEqual(198, eth_position.amount)
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- self.assertEqual(3023, ada_position.amount)
- xrp_position = self.app.container.position_service().find(
- {"symbol": "XRP", "portfolio_id": portfolio.id}
- )
- self.assertEqual(5, xrp_position.amount)
-
- self.market_service.balances = {
- "BTC": 0.6,
- "ETH": 200,
- "ADA": 1000,
- "XRP": 5
- }
- portfolio_sync_service = self.app.container.portfolio_sync_service()
- portfolio_sync_service.sync_positions(portfolio)
- btc_position = self.app.container.position_service().find(
- {"symbol": "BTC", "portfolio_id": portfolio.id}
- )
- self.assertAlmostEqual(0.1, btc_position.amount)
- eth_position = self.app.container.position_service().find(
- {"symbol": "ETH", "portfolio_id": portfolio.id}
- )
- self.assertEqual(199, eth_position.amount)
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0, ada_position.amount)
- xrp_position = self.app.container.position_service().find(
- {"symbol": "XRP", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0, xrp_position.amount)
-
- def test_sync_orders(self):
- """
- Test the sync_orders method of the PortfolioSyncService class.
-
- 1. Create a PortfolioSyncService object.
- 2. Create a portfolio with 1000 unallocated balance.
- 3. 4 orders are synced to the portfolio.
- 4. Check if the portfolio still has the 1000eu
- """
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[SYMBOLS] = ["KSM/EUR"]
- self.market_service.symbols = ["KSM/EUR"]
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.market_service.orders = [
- Order.from_ccxt_order(
- {
- "id": "12333535",
- "symbol": "BTC/EUR",
- "amount": 0.5,
- "price": 50000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-10-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233353",
- "symbol": "ETH/EUR",
- "amount": 199,
- "price": 2000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-10-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "price": 1,
- "side": "buy",
- "status": "closed",
- "type": "limit",
- "datetime": "2021-10-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "123",
- "symbol": "XRP/EUR",
- "amount": 10,
- "price": 1,
- "side": "buy",
- "type": "limit",
- "status": "closed",
- "datetime": "2021-10-10T10:10:10"
- }
- )
- ]
-
- self.app.add_algorithm(Algorithm())
- self.app.initialize()
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- order_service = self.app.container.order_service()
- position_service = self.app.container.position_service()
- btc_position = position_service.find(
- {"symbol": "BTC", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": btc_position.id}))
- eth_position = self.app.container.position_service().find(
- {"symbol": "ETH", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": eth_position.id}))
- orders = order_service.get_all(
- {"position": eth_position.id}
- )
- order = orders[0]
- self.assertEqual(199, order.amount)
- self.assertEqual(2000, order.price)
- self.assertEqual("BUY", order.order_side)
- self.assertEqual("LIMIT", order.order_type)
- self.assertEqual("OPEN", order.status)
-
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": ada_position.id}))
- orders = order_service.get_all(
- {"position": ada_position.id}
- )
- order = orders[0]
- self.assertEqual(4023, order.amount)
- self.assertEqual(1, order.price)
- self.assertEqual("BUY", order.order_side)
- self.assertEqual("LIMIT", order.order_type)
- self.assertEqual("CLOSED", order.status)
-
- xrp_position = self.app.container.position_service().find(
- {"symbol": "XRP", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": xrp_position.id}))
- orders = order_service.get_all(
- {"position": xrp_position.id}
- )
- order = orders[0]
- self.assertEqual(10, order.amount)
- self.assertEqual(1, order.price)
- self.assertEqual("BUY", order.order_side)
- self.assertEqual("LIMIT", order.order_type)
- self.assertEqual("CLOSED", order.status)
-
- ksm_position = self.app.container.position_service().find(
- {"symbol": "KSM", "portfolio_id": portfolio.id}
- )
- self.assertIsNotNone(ksm_position)
- self.assertEqual(0, order_service.count({"position": ksm_position.id}))
-
- def test_sync_orders_with_track_from_attribute_set(self):
- """
- Test the sync_orders method of the PortfolioSyncService class with
- the track_from attribute set.
-
- 1. Create a PortfolioSyncService object.
- 2. Create a portfolio with 1000 unallocated balance.
- 3. 4 orders are synced to the portfolio.
- 4. Check if the portfolio still has the 1000eu
- """
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[SYMBOLS] = ["KSM/EUR"]
- self.market_service.symbols = ["KSM/EUR"]
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500,
- track_from="2021-10-10T10:10:10"
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.market_service.orders = [
- Order.from_ccxt_order(
- {
- "id": "12333535",
- "symbol": "BTC/EUR",
- "amount": 0.5,
- "price": 50000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233353",
- "symbol": "ETH/EUR",
- "amount": 199,
- "price": 2000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "price": 1,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-10-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "123",
- "symbol": "XRP/EUR",
- "amount": 10,
- "price": 1,
- "side": "buy",
- "type": "limit",
- "status": "open",
- "datetime": "2021-10-10T10:10:10"
- }
- )
- ]
-
- self.app.add_algorithm(Algorithm())
- self.app.initialize()
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- self.assertEqual(500, portfolio.unallocated)
- btc_position = self.app.container.position_service().find(
- {"symbol": "BTC", "portfolio_id": portfolio.id}
- )
- order_service = self.app.container.order_service()
- self.assertEqual(0, order_service.count({"position": btc_position.id}))
-
- eth_position = self.app.container.position_service().find(
- {"symbol": "ETH", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0, order_service.count({"position": eth_position.id}))
-
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": ada_position.id}))
-
- xrp_position = self.app.container.position_service().find(
- {"symbol": "XRP", "portfolio_id": portfolio.id}
- )
- self.assertEqual(1, order_service.count({"position": xrp_position.id}))
-
- ksm_position = self.app.container.position_service().find(
- {"symbol": "KSM", "portfolio_id": portfolio.id}
- )
- self.assertEqual(0, order_service.count({"position": ksm_position.id}))
-
- def test_sync_trades(self):
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[SYMBOLS] = ["KSM/EUR"]
- self.market_service.symbols = ["KSM/EUR"]
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500,
- track_from="2021-10-10T10:10:10"
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.market_service.orders = [
- Order.from_ccxt_order(
- {
- "id": "12333535",
- "symbol": "BTC/EUR",
- "amount": 0.5,
- "filled": 0.3,
- "remaining": 0.2,
- "price": 50000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233353",
- "symbol": "ETH/EUR",
- "amount": 199,
- "filled": 100,
- "remaining": 99,
- "price": 2000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "filled": 4023,
- "remaining": 0,
- "price": 1,
- "side": "buy",
- "status": "closed",
- "type": "limit",
- "datetime": "2021-10-12T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233324",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "filled": 4023,
- "remaining": 0,
- "price": 1.10,
- "side": "sell",
- "status": "closed",
- "type": "limit",
- "datetime": "2021-10-13T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "123",
- "symbol": "XRP/EUR",
- "amount": 10,
- "filled": 5,
- "remaining": 5,
- "price": 1,
- "side": "buy",
- "type": "limit",
- "status": "open",
- "datetime": "2021-10-10T10:10:10"
- }
- )
- ]
-
- self.app.add_algorithm(Algorithm())
- self.app.initialize()
-
- # Get ada buy order
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- order_service = self.app.container.order_service()
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- orders = order_service.get_all(
- {"position": ada_position.id, "order_side": "buy"}
- )
- ada_buy_order = orders[0]
- self.assertEqual(4023, ada_buy_order.amount)
- self.assertEqual(1, ada_buy_order.price)
- self.assertEqual("BUY", ada_buy_order.order_side)
- self.assertEqual("LIMIT", ada_buy_order.order_type)
- self.assertEqual("CLOSED", ada_buy_order.status)
- self.assertEqual(
- datetime(
- year=2021, month=10, day=13, hour=10, minute=10, second=10
- ),
- ada_buy_order.get_trade_closed_at()
- )
- self.assertEqual(4023, ada_buy_order.get_filled())
- self.assertEqual(0, ada_buy_order.get_remaining())
- self.assertAlmostEqual(4023 * 0.1, ada_buy_order.get_net_gain())
-
- def test_sync_trades_stateless(self):
- configuration_service = self.app.container.configuration_service()
- configuration_service.config[SYMBOLS] = ["KSM/EUR"]
- self.market_service.symbols = ["KSM/EUR"]
- configuration_service.config[APP_MODE] = "STATELESS"
-
- self.app.add_portfolio_configuration(
- PortfolioConfiguration(
- identifier="test",
- market="binance",
- trading_symbol="EUR",
- initial_balance=500,
- track_from="2021-10-10T10:10:10"
- )
- )
- self.app.add_market_credential(
- MarketCredential(
- market="binance",
- api_key="test",
- secret_key="test"
- )
- )
- self.market_service.balances = {
- "EUR": 1200,
- "BTC": 0.5,
- "ETH": 199,
- "ADA": 4023,
- "XRP": 10,
- }
- self.market_service.orders = [
- Order.from_ccxt_order(
- {
- "id": "12333535",
- "symbol": "BTC/EUR",
- "amount": 0.5,
- "filled": 0.3,
- "remaining": 0.2,
- "price": 50000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233353",
- "symbol": "ETH/EUR",
- "amount": 199,
- "filled": 100,
- "remaining": 99,
- "price": 2000,
- "side": "buy",
- "status": "open",
- "type": "limit",
- "datetime": "2021-09-10T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "filled": 4023,
- "remaining": 0,
- "price": 1,
- "side": "buy",
- "status": "closed",
- "type": "limit",
- "datetime": "2021-10-12T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "1233324",
- "symbol": "ADA/EUR",
- "amount": 4023,
- "filled": 4023,
- "remaining": 0,
- "price": 1.10,
- "side": "sell",
- "status": "closed",
- "type": "limit",
- "datetime": "2021-10-13T10:10:10"
- },
- ),
- Order.from_ccxt_order(
- {
- "id": "123",
- "symbol": "XRP/EUR",
- "amount": 10,
- "filled": 5,
- "remaining": 5,
- "price": 1,
- "side": "buy",
- "type": "limit",
- "status": "open",
- "datetime": "2021-10-10T10:10:10"
- }
- )
- ]
-
- self.app.add_algorithm(Algorithm())
- self.app.initialize()
-
- # Get ada buy order
- portfolio = self.app.container.portfolio_service() \
- .find({"identifier": "test"})
- order_service = self.app.container.order_service()
- ada_position = self.app.container.position_service().find(
- {"symbol": "ADA", "portfolio_id": portfolio.id}
- )
- orders = order_service.get_all(
- {"position": ada_position.id, "order_side": "buy"}
- )
- ada_buy_order = orders[0]
- self.assertEqual(4023, ada_buy_order.amount)
- self.assertEqual(1, ada_buy_order.price)
- self.assertEqual("BUY", ada_buy_order.order_side)
- self.assertEqual("LIMIT", ada_buy_order.order_type)
- self.assertEqual("CLOSED", ada_buy_order.status)
- self.assertEqual(
- datetime(
- year=2021, month=10, day=13, hour=10, minute=10, second=10
- ),
- ada_buy_order.get_trade_closed_at()
- )
- self.assertEqual(4023, ada_buy_order.get_filled())
- self.assertEqual(0, ada_buy_order.get_remaining())
- self.assertAlmostEqual(4023 * 0.1, ada_buy_order.get_net_gain())
diff --git a/tests/test_create_app.py b/tests/test_create_app.py
index dc1cd7e8..d33fafc7 100644
--- a/tests/test_create_app.py
+++ b/tests/test_create_app.py
@@ -1,7 +1,7 @@
import os
from unittest import TestCase
-from investing_algorithm_framework import create_app, Config, \
+from investing_algorithm_framework import create_app, \
PortfolioConfiguration, Algorithm, MarketCredential
from investing_algorithm_framework.domain import RESOURCE_DIRECTORY
from tests.resources import MarketServiceStub
@@ -25,29 +25,15 @@ def test_create_app(self):
app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
self.assertIsNotNone(app)
self.assertIsNone(app._flask_app)
- self.assertFalse(app._stateless)
self.assertIsNotNone(app.container)
self.assertIsNone(app.algorithm)
def test_create_app_with_config(self):
- config = Config(
- resource_directory=self.resource_dir
- )
- app = create_app(config=config)
+ app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
self.assertIsNotNone(app)
self.assertIsNotNone(app.config)
self.assertIsNone(app._flask_app)
- self.assertFalse(app._stateless)
- self.assertIsNotNone(app.container)
- self.assertIsNone(app.algorithm)
-
- def test_create_app_stateless(self):
- app = create_app(stateless=True, config={})
- self.assertIsNotNone(app)
- self.assertIsNotNone(app.config)
- self.assertTrue(app._stateless)
self.assertIsNotNone(app.container)
- self.assertIsNotNone(app.config)
self.assertIsNone(app.algorithm)
def test_create_app_web(self):
@@ -76,8 +62,12 @@ def test_create_app_web(self):
secret_key="secret_key"
)
)
+ market_service = MarketServiceStub(app.container.market_credential_service())
+ market_service.balances = {
+ "USDT": 1000
+ }
app.container.market_service.override(
- MarketServiceStub(app.container.market_credential_service())
+ market_service
)
app.initialize()
self.assertIsNotNone(app)