diff --git a/README.md b/README.md
index f2ee2a15..ac4ce71f 100644
--- a/README.md
+++ b/README.md
@@ -6,43 +6,58 @@
[](https://github.com/SeaQL/sea-orm/stargazers/) If you like what we do, consider starring, sharing and contributing!
-###### Sponsors
-
-
-
-
-
-
-
# [Investing Algorithm Framework](https://github.com/coding-kitties/investing-algorithm-framework)
-The Investing Algorithm Framework is a Python framework that enables swift and elegant development of trading bots. It comes with all the necessary components for creating trading strategies, including data management, portfolio, order, position and trades management.
+The Investing Algorithm Framework is a Python framework that enables swift and elegant development of trading bots.
-Features:
+## Sponsors
+
+
+
+
+
+
+
+
-* Indicators module: A collection of indicators and utility functions that can be used in your trading strategies.
-* Order execution and tracking
-* Broker and exchange connections through [ccxt](https://github.com/ccxt/ccxt)
-* Backtesting and performance analysis reports [example](./examples/backtest_example)
-* Backtesting multiple algorithms with different backtest date ranges [example](./examples/backtests_example)
-* Portfolio management and tracking
-* Tracing for analyzing and debugging your trading bot
-* Web API for interacting with your deployed trading bot
-* Data persistence through sqlite db or an in-memory db
-* Stateless running for cloud function deployments
-* Polars dataframes support out of the box for fast data processing [pola.rs](https://pola.rs/)
+## Features and planned features:
+
+- [x] **Based on Python 3.9+**: Windows, macOS and Linux.
+- [x] **Documentation**: [Documentation](https://investing-algorithm-framework.com)
+- [x] **Persistence of portfolios, orders, positions and trades**: Persistence is achieved through sqlite.
+- [x] **Limit orders**: Create limit orders for buying and selling.
+- [x] **Trade models**: Models and functionality for trades, trades stop losses (fixed and trailing) and take profits (fixed and trailing).
+- [x] **Market data sources**: Market data sources for OHLCV data and ticker data, and extendible with custom data and events.
+- [x] **Polars and Pandas dataframes support** Out of the box dataframes support for fast data processing ([pola.rs](https://pola.rs/), [pandas](https://pandas.pydata.org/)).
+- [x] **Azure Functions support**: Stateless running for cloud function deployments in Azure.
+- [x] **Live trading**: Live trading.
+- [x] **Backtesting and performance analysis reports** [example](./examples/backtest_example)
+- [x] **Backtesting multiple algorithms with different backtest date ranges** [example](./examples/backtests_example)
+- [x] **Backtest comparison and experiments**: Compare multiple backtests and run experiments.
+- [x] **Order execution**: Currently support for a wide range of crypto exchanges through [ccxt](https://github.com/ccxt/ccxt) (Support for traditional asset brokers is planned).
+- [x] **Web API**: Rest API for interacting with your deployed trading bot
+- [x] **PyIndicators**: Works natively with [PyIndicators](https://github.com/coding-kitties/PyIndicators) for technical analysis on your Pandas and Polars dataframes.
+- [ ] **Builtin WebUI (Planned)**: Builtin web UI to manage your bot and evaluate your backtests.
+- [ ] **Manageable via Telegram (Planned)**: Manage the bot with Telegram.
+- [ ] **Performance status report via Web UI and telegram(Planned)**: Provide a performance status of your current trades.
+- [ ] **CI/CD integration (Planned)**: Tools for continuous integration and deployment (version tracking, comparison of backtests, automatic deployments).
+- [ ] **Tracing and replaying of strategies in backtests (Planned)**: Tracing and replaying of strategies in backtests for specific dates and date ranges so you can evaluate your strategy step by step.
+- [ ] **AWS Lambda support (Planned)**: Stateless running for cloud function deployments in AWS.
+- [ ] **Azure App services support (Planned)**: deployments in Azure app services with Web UI.
## Example implementation
-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.
+The following algorithm connects to binance and buys BTC every 2 hours.
```python
import logging.config
+from dotenv import load_dotenv
from investing_algorithm_framework import create_app, PortfolioConfiguration, \
- TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \
- CCXTTickerMarketDataSource, MarketCredential, DEFAULT_LOGGING_CONFIG
+ TimeUnit, CCXTOHLCVMarketDataSource, Context, CCXTTickerMarketDataSource, \
+ MarketCredential, DEFAULT_LOGGING_CONFIG, Algorithm, Context
+load_dotenv()
logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
# OHLCV data for candles
@@ -60,50 +75,35 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource(
symbol="BTC/EUR",
)
app = create_app()
-# Bitvavo market credentials are read from .env file
+
+# Bitvavo market credentials are read from .env file, or you can
+# set them manually as params
app.add_market_credential(MarketCredential(market="bitvavo"))
app.add_portfolio_configuration(
PortfolioConfiguration(
- market="bitvavo",
- trading_symbol="EUR",
- initial_balance=400
+ market="bitvavo", trading_symbol="EUR", initial_balance=40
)
)
-# Run every two hours and register the data sources
-@app.strategy(
- time_unit=TimeUnit.HOUR,
- interval=2,
+algorithm = Algorithm(name="test_algorithm")
+
+# Define a strategy for the algorithm that will run every 10 seconds
+@algorithm.strategy(
+ time_unit=TimeUnit.SECOND,
+ interval=10,
market_data_sources=[bitvavo_btc_eur_ticker, bitvavo_btc_eur_ohlcv_2h]
)
-def perform_strategy(algorithm: Algorithm, market_data: dict):
+def perform_strategy(context: Context, market_data: dict):
# Access the data sources with the indentifier
polars_df = market_data["BTC-ohlcv"]
- # 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()
- trades = algorithm.get_trades()
- open_trades = algorithm.get_open_trades()
- closed_trades = algorithm.get_closed_trades()
-
- # Create a buy oder
- order = algorithm.create_limit_order(
- target_symbol="BTC/EUR",
- order_side="buy",
- amount=0.01,
- price=ticker_data["ask"],
- )
- trade = algorithm.get_trade(order_id=order.id)
- algorithm.add_trailing_stop_loss(trade=trade, percentage=5)
+ unallocated_balance = context.get_unallocated()
+ positions = context.get_positions()
+ trades = context.get_trades()
+ open_trades = context.get_open_trades()
+ closed_trades = context.get_closed_trades()
- # Close a trade
- algorithm.close_trade(trade=trade)
-
- # Close a position
- position = algorithm.get_position(symbol="BTC/EUR")
- algorithm.close_position(position)
+app.add_algorithm(algorithm)
if __name__ == "__main__":
app.run()
@@ -127,7 +127,7 @@ python examples/backtest_example/run_backtest.py
### Backtesting report
You can use the ```pretty_print_backtest``` function to print a backtest report.
-For example if you run the [moving average example trading bot](./examples/crossover_moving_average_trading_bot)
+For example if you run the [moving average example trading bot](./examples/backtest_example/run_backtest.py)
you will get the following backtesting report:
```bash
@@ -138,77 +138,91 @@ you will get the following backtesting report:
.%%%%%%%%%%%%%%%%%%%%%%# End date: 2023-12-02 00:00:00
#%%%####%%%%%%%%**#%%%+ Number of days: 100
.:-+*%%%%- -+..#%%%+.+- +%%%#*=-: Number of runs: 1201
- .:-=*%%%%. += .%%# -+.-%%%%=-:.. Number of orders: 40
+ .:-=*%%%%. += .%%# -+.-%%%%=-:.. Number of orders: 14
.:=+#%%%%%*###%%%%#*+#%%%%%%*+-: Initial balance: 400.0
- +%%%%%%%%%%%%%%%%%%%= Final balance: 428.2434
- :++ .=#%%%%%%%%%%%%%*- Total net gain: 28.2434 7.061%
- :++: :+%%%%%%#-. Growth: 28.2434 7.061%
- :++: .%%%%%#= Number of trades closed: 20
- :++: .#%%%%%#*= Number of trades open(end of backtest): 0
- :++- :%%%%%%%%%+= Percentage positive trades: 30.0%
- .++- -%%%%%%%%%%%+= Percentage negative trades: 70.0%
- .++- .%%%%%%%%%%%%%+= Average trade size: 100.9692 EUR
- .++- *%%%%%%%%%%%%%*+: Average trade duration: 83.6 hours
+ +%%%%%%%%%%%%%%%%%%%= Final balance: 417.8982
+ :++ .=#%%%%%%%%%%%%%*- Total net gain: 15.4755 3.869%
+ :++: :+%%%%%%#-. Growth: 17.8982 4.475%
+ :++: .%%%%%#= Number of trades closed: 2
+ :++: .#%%%%%#*= Number of trades open(end of backtest): 2
+ :++- :%%%%%%%%%+= Percentage positive trades: 75.0%
+ .++- -%%%%%%%%%%%+= Percentage negative trades: 25.0%
+ .++- .%%%%%%%%%%%%%+= Average trade size: 98.8050 EUR
+ .++- *%%%%%%%%%%%%%*+: Average trade duration: 11665.866590240556 hours
.++- %%%%%%%%%%%%%%#+=
=++........:::%%%%%%%%%%%%%%*+-
.=++++++++++**#%%%%%%%%%%%%%++.
-Price noise
-
Positions overview
╭────────────┬──────────┬──────────────────────┬───────────────────────┬──────────────┬───────────────┬───────────────────────────┬────────────────┬───────────────╮
│ Position │ Amount │ Pending buy amount │ Pending sell amount │ Cost (EUR) │ Value (EUR) │ Percentage of portfolio │ Growth (EUR) │ Growth_rate │
├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤
-│ EUR │ 428.243 │ 0 │ 0 │ 428.243 │ 428.243 │ 100.0000% │ 0 │ 0.0000% │
+│ EUR │ 218.062 │ 0 │ 0 │ 218.062 │ 218.062 │ 52.1806% │ 0 │ 0.0000% │
├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤
-│ DOT │ 0 │ 0 │ 0 │ 0 │ 0 │ 0.0000% │ 0 │ 0.0000% │
+│ BTC │ 0.0028 │ 0 │ 0 │ 97.4139 │ 99.7171 │ 23.8616% │ 2.3032 │ 2.3644% │
├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤
-│ BTC │ 0 │ 0 │ 0 │ 0 │ 0 │ 0.0000% │ 0 │ 0.0000% │
+│ DOT │ 19.9084 │ 0 │ 0 │ 99.9999 │ 100.119 │ 23.9578% │ 0.1195 │ 0.1195% │
╰────────────┴──────────┴──────────────────────┴───────────────────────┴──────────────┴───────────────┴───────────────────────────┴────────────────┴───────────────╯
Trades overview
-╭─────────┬─────────────────────┬─────────────────────┬────────────────────┬──────────────┬──────────────────┬───────────────────────┬────────────────────┬─────────────────────╮
-│ Pair │ Open date │ Close date │ Duration (hours) │ Size (EUR) │ Net gain (EUR) │ Net gain percentage │ Open price (EUR) │ Close price (EUR) │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-11-24 12:00:00 │ 2023-11-27 14:00:00 │ 74 │ 107.55 │ -1.9587 │ -1.8212% │ 4.777 │ 4.69 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-11-20 00:00:00 │ 2023-11-21 08:00:00 │ 32 │ 109.39 │ -4.5949 │ -4.2005% │ 4.9875 │ 4.778 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-11-19 22:00:00 │ 2023-11-22 00:00:00 │ 50 │ 109.309 │ -2.7624 │ -2.5272% │ 34159.1 │ 33295.9 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-11-06 12:00:00 │ 2023-11-13 14:00:00 │ 170 │ 107.864 │ 6.1015 │ 5.6567% │ 32685.9 │ 34534.9 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-10-20 12:00:00 │ 2023-10-27 08:00:00 │ 164 │ 99.085 │ 10.9799 │ 11.0813% │ 3.5465 │ 3.9395 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-10-14 04:00:00 │ 2023-10-27 22:00:00 │ 330 │ 97.4278 │ 24.137 │ 24.7742% │ 25638.9 │ 31990.7 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-10-14 04:00:00 │ 2023-10-17 14:00:00 │ 82 │ 99.5572 │ -1.8877 │ -1.8961% │ 3.56 │ 3.4925 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-10-07 08:00:00 │ 2023-10-08 08:00:00 │ 24 │ 99.9498 │ -1.5708 │ -1.5716% │ 3.8815 │ 3.8205 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-09-27 10:00:00 │ 2023-10-05 20:00:00 │ 202 │ 98.2888 │ 3.433 │ 3.4927% │ 25202.2 │ 26082.5 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-27 10:00:00 │ 2023-10-03 20:00:00 │ 154 │ 98.7893 │ 1.2085 │ 1.2233% │ 3.842 │ 3.889 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-25 12:00:00 │ 2023-09-27 04:00:00 │ 40 │ 98.9193 │ -0.5194 │ -0.5251% │ 3.809 │ 3.789 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-14 16:00:00 │ 2023-09-18 02:00:00 │ 82 │ 98.9419 │ -0.0912 │ -0.0921% │ 3.799 │ 3.7955 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-09-07 06:00:00 │ 2023-09-10 16:00:00 │ 82 │ 98.6093 │ 0.3412 │ 0.3460% │ 24051 │ 24134.3 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-07 00:00:00 │ 2023-09-09 02:00:00 │ 50 │ 98.9158 │ -0.2358 │ -0.2383% │ 3.986 │ 3.9765 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-05 14:00:00 │ 2023-09-06 12:00:00 │ 22 │ 99.2132 │ -1.1909 │ -1.2003% │ 3.999 │ 3.951 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-04 16:00:00 │ 2023-09-04 22:00:00 │ 6 │ 99.355 │ -0.5671 │ -0.5708% │ 3.942 │ 3.9195 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-09-04 10:00:00 │ 2023-09-04 14:00:00 │ 4 │ 99.4774 │ -0.4889 │ -0.4914% │ 3.968 │ 3.9485 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ BTC-EUR │ 2023-08-26 10:00:00 │ 2023-08-26 18:00:00 │ 8 │ 99.0829 │ -0.03 │ -0.0302% │ 24166.6 │ 24159.3 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-08-25 10:00:00 │ 2023-08-28 10:00:00 │ 72 │ 99.659 │ -0.6975 │ -0.6999% │ 4.1435 │ 4.1145 │
-├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼──────────────────┼───────────────────────┼────────────────────┼─────────────────────┤
-│ DOT-EUR │ 2023-08-24 00:00:00 │ 2023-08-25 00:00:00 │ 24 │ 99.9999 │ -1.3626 │ -1.3626% │ 4.1465 │ 4.09 │
-╰─────────┴─────────────────────┴─────────────────────┴────────────────────┴──────────────┴──────────────────┴───────────────────────┴────────────────────┴─────────────────────╯
+╭───────────────────┬────────────┬─────────────────────────────────┬─────────────────────┬────────────────────────────┬──────────────────────────┬────────────────────┬─────────────────────────────────╮
+│ Pair (Trade id) │ Status │ Net gain (EUR) │ Open date │ Close date │ Duration │ Open price (EUR) │ Close price's (EUR) │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ BTC/EUR (1) │ CLOSED, TP │ 2.9820 (3.0460%) │ 2023-09-13 14:00:00 │ 2025-02-19 15:21:54.823674 │ 12601.365228798333 hours │ 24474.4 │ 25427.69, 25012.105 │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ DOT/EUR (2) │ CLOSED, TP │ 9.3097 (9.3097%) │ 2023-10-30 04:00:00 │ 2025-02-19 15:22:02.227035 │ 11483.3672852875 hours │ 4.0565 │ 4.233, 4.377, 4.807499999999999 │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ BTC/EUR (3) │ CLOSED │ -0.4248 (-0.4322%) │ 2023-11-06 14:00:00 │ 2025-02-19 15:21:59.823557 │ 11305.366617654721 hours │ 32761.8 │ 32620.225 │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ BTC/EUR (4) │ CLOSED, TP │ 3.6086 (3.6364%) │ 2023-11-07 22:00:00 │ 2025-02-19 15:22:02.025198 │ 11273.367229221665 hours │ 33077.9 │ 34637.09, 33924.39 │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ BTC/EUR (5) │ OPEN │ 2.3880 (2.4514%) (unrealized) │ 2023-11-29 12:00:00 │ │ 60.0 hours │ 34790.7 │ │
+├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤
+│ DOT/EUR (6) │ OPEN │ -0.0398 (-0.0398%) (unrealized) │ 2023-11-30 18:00:00 │ │ 30.0 hours │ 5.023 │ │
+╰───────────────────┴────────────┴─────────────────────────────────┴─────────────────────┴────────────────────────────┴──────────────────────────┴────────────────────┴─────────────────────────────────╯
+Stop losses overview
+╭────────────────────┬───────────────┬──────────┬────────┬──────────────────────┬────────────────┬────────────────┬───────────────────┬──────────────┬─────────────┬───────────────╮
+│ Trade (Trade id) │ Status │ Active │ Type │ stop loss │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ BTC/EUR (1) │ NOT TRIGGERED │ True │ FIXED │ 23250.6847(5.0%) EUR │ 24474.4050 EUR │ None │ 24474.4 │ 50.0% │ 0.0020 BTC │ │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ DOT/EUR (2) │ NOT TRIGGERED │ True │ FIXED │ 3.8537(5.0%) EUR │ 4.0565 EUR │ None │ 4.0565 │ 50.0% │ 12.3259 DOT │ │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ BTC/EUR (3) │ NOT TRIGGERED │ True │ FIXED │ 31123.7242(5.0%) EUR │ 32761.8150 EUR │ None │ 32761.8 │ 50.0% │ 0.0015 BTC │ │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ BTC/EUR (4) │ NOT TRIGGERED │ True │ FIXED │ 31423.9908(5.0%) EUR │ 33077.8850 EUR │ None │ 33077.9 │ 50.0% │ 0.0015 BTC │ │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ BTC/EUR (5) │ NOT TRIGGERED │ True │ FIXED │ 33051.1460(5.0%) EUR │ 34790.6800 EUR │ None │ 34790.7 │ 50.0% │ 0.0014 BTC │ │
+├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤
+│ DOT/EUR (6) │ NOT TRIGGERED │ True │ FIXED │ 4.7718(5.0%) EUR │ 5.0230 EUR │ None │ 5.023 │ 50.0% │ 9.9542 DOT │ │
+╰────────────────────┴───────────────┴──────────┴────────┴──────────────────────┴────────────────┴────────────────┴───────────────────┴──────────────┴─────────────┴───────────────╯
+Take profits overview
+╭────────────────────┬───────────────┬──────────┬──────────┬───────────────────────┬────────────────┬────────────────┬───────────────────┬──────────────┬─────────────┬───────────────────╮
+│ Trade (Trade id) │ Status │ Active │ Type │ Take profit │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (1) │ TRIGGERED │ False │ TRAILING │ 25698.1253(5.0)% EUR │ 24474.4050 EUR │ 25427.69 │ 25703.77 │ 50.0% │ 0.0020 BTC │ 0.002 │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (1) │ NOT TRIGGERED │ True │ TRAILING │ 26921.8455(10.0)% EUR │ 24474.4050 EUR │ None │ │ 20.0% │ 0.0008 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 5.1756(5.0)% EUR │ 4.0565 EUR │ 4.233 │ 5.448 │ 50.0% │ 12.3259 DOT │ 12.32585 │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 4.9032(10.0)% EUR │ 4.0565 EUR │ 4.377 │ 5.448 │ 20.0% │ 4.9303 DOT │ 4.930340000000001 │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (3) │ NOT TRIGGERED │ True │ TRAILING │ 34399.9057(5.0)% EUR │ 32761.8150 EUR │ None │ │ 50.0% │ 0.0015 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (3) │ NOT TRIGGERED │ True │ TRAILING │ 36037.9965(10.0)% EUR │ 32761.8150 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (4) │ TRIGGERED │ False │ TRAILING │ 34731.7793(5.0)% EUR │ 33077.8850 EUR │ 34637.09 │ 34967.12 │ 50.0% │ 0.0015 BTC │ 0.0015 │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (4) │ NOT TRIGGERED │ True │ TRAILING │ 36385.6735(10.0)% EUR │ 33077.8850 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 36530.2140(5.0)% EUR │ 34790.6800 EUR │ None │ │ 50.0% │ 0.0014 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 38269.7480(10.0)% EUR │ 34790.6800 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.2741(5.0)% EUR │ 5.0230 EUR │ None │ │ 50.0% │ 9.9542 DOT │ │
+├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤
+│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.5253(10.0)% EUR │ 5.0230 EUR │ None │ │ 20.0% │ 3.9817 DOT │ │
+╰────────────────────┴───────────────┴──────────┴──────────┴───────────────────────┴────────────────┴────────────────┴───────────────────┴──────────────┴─────────────┴───────────────────╯
```
### Backtest experiments
@@ -325,4 +339,4 @@ You can pick up a task by assigning yourself to it.
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do*.
This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
-**Important:** Always create your feature or hotfix against the `develop` branch, not `master`.
+**Important:** Always create your feature or hotfix against the `develop` branch, not `main`.
diff --git a/examples/backtest_example/run_backtest.py b/examples/backtest_example/run_backtest.py
index b98526ee..4fa7bfd7 100644
--- a/examples/backtest_example/run_backtest.py
+++ b/examples/backtest_example/run_backtest.py
@@ -4,9 +4,9 @@
from investing_algorithm_framework import CCXTOHLCVMarketDataSource, \
- CCXTTickerMarketDataSource, Algorithm, PortfolioConfiguration, \
+ CCXTTickerMarketDataSource, PortfolioConfiguration, \
create_app, pretty_print_backtest, BacktestDateRange, TimeUnit, \
- TradingStrategy, OrderSide, DEFAULT_LOGGING_CONFIG
+ TradingStrategy, OrderSide, DEFAULT_LOGGING_CONFIG, Context
import tulipy as ti
@@ -96,12 +96,12 @@ class CrossOverStrategy(TradingStrategy):
trend = 150
stop_loss_percentage = 7
- def apply_strategy(self, algorithm: Algorithm, market_data):
+ def apply_strategy(self, context: Context, market_data):
for symbol in self.symbols:
target_symbol = symbol.split('/')[0]
- if algorithm.has_open_orders(target_symbol):
+ if context.has_open_orders(target_symbol):
continue
df = market_data[f"{symbol}-ohlcv"]
@@ -111,36 +111,47 @@ def apply_strategy(self, algorithm: Algorithm, market_data):
trend = ti.sma(df['Close'].to_numpy(), self.trend)
price = ticker_data["bid"]
- if not algorithm.has_position(target_symbol) \
+ if not context.has_position(target_symbol) \
and is_crossover(fast, slow) \
and is_above_trend(fast, trend):
- order = algorithm.create_limit_order(
+ order = context.create_limit_order(
target_symbol=target_symbol,
order_side=OrderSide.BUY,
price=price,
percentage_of_portfolio=25,
precision=4,
)
- trade = algorithm.get_trade(order_id=order.id)
- algorithm.add_trailing_stop_loss(
- trade=trade, percentage=5
+ trade = context.get_trade(order_id=order.id)
+ context.add_stop_loss(
+ trade=trade,
+ percentage=5,
+ sell_percentage=50
+ )
+ context.add_take_profit(
+ trade=trade,
+ percentage=5,
+ trade_risk_type="trailing",
+ sell_percentage=50
+ )
+ context.add_take_profit(
+ trade=trade,
+ percentage=10,
+ trade_risk_type="trailing",
+ sell_percentage=20
)
-
- if algorithm.has_position(target_symbol) \
+ if context.has_position(target_symbol) \
and is_below_trend(fast, slow):
- open_trades = algorithm.get_open_trades(
+ open_trades = context.get_open_trades(
target_symbol=target_symbol
)
for trade in open_trades:
- algorithm.close_trade(trade)
+ context.close_trade(trade)
-app = create_app()
-algorithm = Algorithm("GoldenCrossStrategy")
-algorithm.add_strategy(CrossOverStrategy)
-app.add_algorithm(algorithm)
+app = create_app(name="GoldenCrossStrategy")
+app.add_strategy(CrossOverStrategy)
app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h)
app.add_market_data_source(bitvavo_dot_eur_ohlcv_2h)
app.add_market_data_source(bitvavo_btc_eur_ticker)
@@ -157,17 +168,13 @@ def apply_strategy(self, algorithm: Algorithm, market_data):
if __name__ == "__main__":
end_date = datetime(2023, 12, 2)
- start_date = end_date - timedelta(days=400)
+ start_date = end_date - timedelta(days=100)
date_range = BacktestDateRange(
start_date=start_date,
end_date=end_date
)
start_time = time.time()
-
- backtest_report = app.run_backtest(
- algorithm=algorithm,
- backtest_date_range=date_range,
- )
+ backtest_report = app.run_backtest(backtest_date_range=date_range)
pretty_print_backtest(backtest_report)
end_time = time.time()
print(f"Execution Time: {end_time - start_time:.6f} seconds")
diff --git a/examples/backtests_example/backtests.ipynb b/examples/backtests_example/backtests.ipynb
index c3c6d9e4..75450504 100644
--- a/examples/backtests_example/backtests.ipynb
+++ b/examples/backtests_example/backtests.ipynb
@@ -39,7 +39,6 @@
" time_frame=\"2h\",\n",
" window_size=200,\n",
")\n",
- "\n",
"btc_eur_ticker_data = CCXTTickerMarketDataSource(\n",
" identifier=\"BTC/EUR_ticker\",\n",
" symbol=\"BTC/EUR\",\n",
@@ -50,13 +49,13 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"from investing_algorithm_framework import Algorithm, TradingStrategy, \\\n",
" TimeUnit, OrderSide, get_ema, is_below, is_crossover, \\\n",
- " convert_polars_to_pandas\n",
+ " convert_polars_to_pandas, Context\n",
"\n",
"\n",
"class AlternativeStrategy(TradingStrategy):\n",
@@ -77,12 +76,12 @@
" self.slow = long_period\n",
" super().__init__()\n",
"\n",
- " def apply_strategy(self, algorithm: Algorithm, market_data):\n",
+ " def apply_strategy(self, context: Context, market_data):\n",
"\n",
" for symbol in self.symbols:\n",
" target_symbol = symbol.split('/')[0]\n",
"\n",
- " if algorithm.has_open_orders(target_symbol):\n",
+ " if context.has_open_orders(target_symbol):\n",
" continue\n",
"\n",
" polars_df = market_data[f\"{symbol}_ohlcv_2h\"]\n",
@@ -92,27 +91,39 @@
" ticker_data = market_data[f\"{symbol}_ticker\"]\n",
" price = ticker_data['bid']\n",
"\n",
- " if not algorithm.has_position(target_symbol) \\\n",
+ " if not context.has_position(target_symbol) \\\n",
" and is_crossover(\n",
" df, f\"EMA_{self.fast}\", f\"EMA_{self.slow}\"\n",
" ):\n",
- " algorithm.create_limit_order(\n",
+ " order = context.create_limit_order(\n",
" target_symbol=target_symbol,\n",
" order_side=OrderSide.BUY,\n",
" price=price,\n",
" percentage_of_portfolio=25,\n",
" precision=4,\n",
" )\n",
- "\n",
- " if algorithm.has_position(target_symbol) \\\n",
+ " trade = context.get_trade(order_id=order.id)\n",
+ " trade = context.add_stop_loss(\n",
+ " trade=trade,\n",
+ " percentage=5,\n",
+ " sell_percentage=100,\n",
+ " trade_risk_type=\"trailing\"\n",
+ " )\n",
+ " trade = context.add_take_profit(\n",
+ " trade=trade,\n",
+ " percentage=10,\n",
+ " sell_percentage=50,\n",
+ " trade_risk_type=\"trailing\"\n",
+ " )\n",
+ " if context.has_position(target_symbol) \\\n",
" and is_below(\n",
" df, f\"EMA_{self.fast}\", f\"EMA_{self.slow}\"\n",
" ):\n",
- " open_trades = algorithm.get_open_trades(\n",
+ " open_trades = context.get_open_trades(\n",
" target_symbol=target_symbol\n",
" )\n",
" for trade in open_trades:\n",
- " algorithm.close_trade(trade)\n",
+ " context.close_trade(trade)\n",
"\n",
"\n",
"def create_algorithm(\n",
@@ -136,13 +147,13 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"from investing_algorithm_framework import Algorithm, TradingStrategy, \\\n",
" TimeUnit, OrderSide, get_ema, get_rsi, is_crossover, is_below, \\\n",
- " convert_polars_to_pandas\n",
+ " convert_polars_to_pandas, Context\n",
"\n",
"\n",
"class Strategy(TradingStrategy):\n",
@@ -169,12 +180,12 @@
" self.rsi_sell_threshold = rsi_sell_threshold\n",
" super().__init__()\n",
"\n",
- " def apply_strategy(self, algorithm: Algorithm, market_data):\n",
+ " def apply_strategy(self, context: Context, market_data):\n",
"\n",
" for symbol in self.symbols:\n",
" target_symbol = symbol.split('/')[0]\n",
"\n",
- " if algorithm.has_open_orders(target_symbol):\n",
+ " if context.has_open_orders(target_symbol):\n",
" continue\n",
"\n",
" polars_df = market_data[f\"{symbol}_ohlcv_2h\"]\n",
@@ -185,27 +196,40 @@
" ticker_data = market_data[f\"{symbol}_ticker\"]\n",
" price = ticker_data['bid']\n",
"\n",
- " if not algorithm.has_position(target_symbol) \\\n",
+ " if not context.has_position(target_symbol) \\\n",
" and is_crossover(\n",
" df, f\"EMA_{self.fast}\", f\"EMA_{self.slow}\"\n",
" ) and df[f\"RSI_{self.rsi_period}\"].iloc[-1] <= self.rsi_buy_threshold:\n",
- " algorithm.create_limit_order(\n",
+ " order = context.create_limit_order(\n",
" target_symbol=target_symbol,\n",
" order_side=OrderSide.BUY,\n",
" price=price,\n",
" percentage_of_portfolio=25,\n",
" precision=4,\n",
" )\n",
+ " trade = context.get_trade(order_id=order.id)\n",
+ " trade = context.add_stop_loss(\n",
+ " trade=trade,\n",
+ " percentage=5,\n",
+ " sell_percentage=100,\n",
+ " trade_risk_type=\"trailing\"\n",
+ " )\n",
+ " trade = context.add_take_profit(\n",
+ " trade=trade,\n",
+ " percentage=10,\n",
+ " sell_percentage=50,\n",
+ " trade_risk_type=\"trailing\"\n",
+ " )\n",
"\n",
- " if algorithm.has_position(target_symbol) \\\n",
+ " if context.has_position(target_symbol) \\\n",
" and is_below(\n",
" df, f\"EMA_{self.fast}\", f\"EMA_{self.slow}\"\n",
" ) and df[f\"RSI_{self.rsi_period}\"].iloc[-1] > self.rsi_sell_threshold:\n",
- " open_trades = algorithm.get_open_trades(\n",
+ " open_trades = context.get_open_trades(\n",
" target_symbol=target_symbol\n",
" )\n",
" for trade in open_trades:\n",
- " algorithm.close_trade(trade)\n",
+ " context.close_trade(trade)\n",
"\n",
"def create_alternative_algorithm(\n",
" name,\n",
@@ -243,7 +267,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 7,
"metadata": {
"collapsed": false
},
@@ -264,6 +288,7 @@
"name": "Close down turn",
"type": "scatter",
"x": [
+ "2021-12-21T00:00:00",
"2021-12-21T02:00:00",
"2021-12-21T04:00:00",
"2021-12-21T06:00:00",
@@ -1704,6 +1729,7 @@
"2022-04-19T20:00:00",
"2022-04-19T22:00:00",
"2022-04-20T00:00:00",
+ "2022-04-20T02:00:00",
"2022-04-20T04:00:00",
"2022-04-20T06:00:00",
"2022-04-20T08:00:00",
@@ -2438,6 +2464,7 @@
],
"xaxis": "x",
"y": [
+ 41656,
42439,
43107,
43012,
@@ -3878,6 +3905,7 @@
38287,
38434,
38314,
+ 38288,
38307,
38266,
38299,
@@ -8548,6 +8576,7 @@
"name": "Close side ways",
"type": "scatter",
"x": [
+ "2022-06-10T00:00:00",
"2022-06-10T02:00:00",
"2022-06-10T04:00:00",
"2022-06-10T06:00:00",
@@ -9988,6 +10017,7 @@
"2022-10-07T20:00:00",
"2022-10-07T22:00:00",
"2022-10-08T00:00:00",
+ "2022-10-08T02:00:00",
"2022-10-08T04:00:00",
"2022-10-08T06:00:00",
"2022-10-08T08:00:00",
@@ -11118,6 +11148,7 @@
],
"xaxis": "x3",
"y": [
+ 28125,
28304,
28309,
28268,
@@ -12558,6 +12589,7 @@
20076,
20071,
20136,
+ 20046,
20040,
20027,
20033,
@@ -14702,7 +14734,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 11,
"metadata": {
"collapsed": false
},
@@ -14718,44 +14750,103 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 73.83it/s]\n",
- "Running backtest for algorithm with name primary: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:21<00:00, 102.26it/s]\n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 321.02it/s]\n",
- "Running backtest for algorithm with name secondary: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:18<00:00, 119.51it/s]\n"
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 153.90it/s]\n",
+ "Running backtest for algorithm with name primary: 0%|\u001b[32m \u001b[0m| 0/2173 [00:00, ?it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001b[93mRunning backtests for date range:\u001b[0m \u001b[92mup_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 for a total of 2 algorithms.\u001b[0m\n"
+ "\n",
+ "\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 113.81it/s]\n",
- "Running backtest for algorithm with name primary: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:21<00:00, 92.46it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 198.18it/s]\n",
- "Running backtest for algorithm with name secondary: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:18<00:00, 103.83it/s]\n"
+ "Running backtest for algorithm with name primary: 13%|\u001b[32m█▎ \u001b[0m| 273/2173 [00:02<00:12, 157.47it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
- "\u001b[93mRunning backtests for date range:\u001b[0m \u001b[92msideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 for a total of 2 algorithms.\u001b[0m\n"
+ "\n",
+ "\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 194.67it/s]\n",
- "Running backtest for algorithm with name primary: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:29<00:00, 86.08it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 286.86it/s]\n",
- "Running backtest for algorithm with name secondary: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:29<00:00, 86.29it/s] \n"
+ "Running backtest for algorithm with name primary: 14%|\u001b[32m█▍ \u001b[0m| 305/2173 [00:02<00:20, 93.14it/s] "
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Running backtest for algorithm with name primary: 17%|\u001b[32m█▋ \u001b[0m| 380/2173 [00:03<00:18, 96.33it/s] "
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Running backtest for algorithm with name primary: 20%|\u001b[32m██ \u001b[0m| 437/2173 [00:03<00:13, 131.25it/s]"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Running backtest for algorithm with name primary: 25%|\u001b[32m██▍ \u001b[0m| 538/2173 [00:04<00:13, 123.38it/s]\n"
+ ]
+ },
+ {
+ "ename": "KeyboardInterrupt",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[11], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m reports \u001b[38;5;241m=\u001b[39m \u001b[43mapp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_backtests\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43malgorithms\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mchampion\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mchallenger\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mdate_ranges\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mdown_turn_date_range\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mup_turn_date_range\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msideways_date_range\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m)\u001b[49m\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/app/app.py:803\u001b[0m, in \u001b[0;36mApp.run_backtests\u001b[0;34m(self, algorithms, initial_amount, date_ranges, pending_order_check_interval, output_directory, checkpoint)\u001b[0m\n\u001b[1;32m 797\u001b[0m backtest_service\u001b[38;5;241m.\u001b[39mresource_directory \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig[\n\u001b[1;32m 798\u001b[0m RESOURCE_DIRECTORY\n\u001b[1;32m 799\u001b[0m ]\n\u001b[1;32m 801\u001b[0m \u001b[38;5;66;03m# Run the backtest with the backtest_service\u001b[39;00m\n\u001b[1;32m 802\u001b[0m \u001b[38;5;66;03m# and collect the report\u001b[39;00m\n\u001b[0;32m--> 803\u001b[0m report \u001b[38;5;241m=\u001b[39m \u001b[43mbacktest_service\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_backtest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 804\u001b[0m \u001b[43m \u001b[49m\u001b[43malgorithm\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43malgorithm\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 805\u001b[0m \u001b[43m \u001b[49m\u001b[43minitial_amount\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minitial_amount\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 806\u001b[0m \u001b[43m \u001b[49m\u001b[43mbacktest_date_range\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdate_range\u001b[49m\n\u001b[1;32m 807\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 809\u001b[0m \u001b[38;5;66;03m# Add date range name to report if present\u001b[39;00m\n\u001b[1;32m 810\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m date_range\u001b[38;5;241m.\u001b[39mname \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/services/backtesting/backtest_service.py:174\u001b[0m, in \u001b[0;36mBacktestService.run_backtest\u001b[0;34m(self, algorithm, backtest_date_range, initial_amount)\u001b[0m\n\u001b[1;32m 166\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_configuration_service\u001b[38;5;241m.\u001b[39madd_value(\n\u001b[1;32m 167\u001b[0m BACKTESTING_INDEX_DATETIME, index_date\n\u001b[1;32m 168\u001b[0m )\n\u001b[1;32m 169\u001b[0m \u001b[38;5;66;03m# self.run_backtest_for_profile(\u001b[39;00m\n\u001b[1;32m 170\u001b[0m \u001b[38;5;66;03m# algorithm=algorithm,\u001b[39;00m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;66;03m# strategy=algorithm.get_strategy(strategy_profile.strategy_id),\u001b[39;00m\n\u001b[1;32m 172\u001b[0m \u001b[38;5;66;03m# index_date=index_date,\u001b[39;00m\n\u001b[1;32m 173\u001b[0m \u001b[38;5;66;03m# )\u001b[39;00m\n\u001b[0;32m--> 174\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_backtest_v2\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 175\u001b[0m \u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43malgorithm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcontext\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 176\u001b[0m \u001b[43m \u001b[49m\u001b[43mstrategy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43malgorithm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_strategy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstrategy_profile\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstrategy_id\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 177\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 179\u001b[0m report \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcreate_backtest_report(\n\u001b[1;32m 180\u001b[0m algorithm, \u001b[38;5;28mlen\u001b[39m(schedule), backtest_date_range, initial_unallocated\n\u001b[1;32m 181\u001b[0m )\n\u001b[1;32m 183\u001b[0m \u001b[38;5;66;03m# Cleanup backtest portfolio\u001b[39;00m\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/services/backtesting/backtest_service.py:251\u001b[0m, in \u001b[0;36mBacktestService.run_backtest_v2\u001b[0;34m(self, strategy, context)\u001b[0m\n\u001b[1;32m 249\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mrun_backtest_v2\u001b[39m(\u001b[38;5;28mself\u001b[39m, strategy, context):\n\u001b[1;32m 250\u001b[0m config \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_configuration_service\u001b[38;5;241m.\u001b[39mget_config()\n\u001b[0;32m--> 251\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_strategy_orchestrator_service\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_backtest_strategy\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 252\u001b[0m \u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcontext\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstrategy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstrategy\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\n\u001b[1;32m 253\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/services/strategy_orchestrator_service.py:98\u001b[0m, in \u001b[0;36mStrategyOrchestratorService.run_backtest_strategy\u001b[0;34m(self, strategy, context, config)\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mrun_backtest_strategy\u001b[39m(\u001b[38;5;28mself\u001b[39m, strategy, context, config):\n\u001b[1;32m 97\u001b[0m data \u001b[38;5;241m=\u001b[39m \\\n\u001b[0;32m---> 98\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmarket_data_source_service\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_data_for_strategy\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstrategy\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 100\u001b[0m strategy\u001b[38;5;241m.\u001b[39mrun_strategy(\n\u001b[1;32m 101\u001b[0m market_data\u001b[38;5;241m=\u001b[39mdata,\n\u001b[1;32m 102\u001b[0m context\u001b[38;5;241m=\u001b[39mcontext,\n\u001b[1;32m 103\u001b[0m )\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py:144\u001b[0m, in \u001b[0;36mMarketDataSourceService.get_data_for_strategy\u001b[0;34m(self, strategy)\u001b[0m\n\u001b[1;32m 136\u001b[0m market_data \u001b[38;5;241m=\u001b[39m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmetadata\u001b[39m\u001b[38;5;124m\"\u001b[39m: {\n\u001b[1;32m 137\u001b[0m MarketDataType\u001b[38;5;241m.\u001b[39mOHLCV: {},\n\u001b[1;32m 138\u001b[0m MarketDataType\u001b[38;5;241m.\u001b[39mTICKER: {},\n\u001b[1;32m 139\u001b[0m MarketDataType\u001b[38;5;241m.\u001b[39mORDER_BOOK: {},\n\u001b[1;32m 140\u001b[0m MarketDataType\u001b[38;5;241m.\u001b[39mCUSTOM: {}\n\u001b[1;32m 141\u001b[0m }}\n\u001b[1;32m 143\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m identifier \u001b[38;5;129;01min\u001b[39;00m identifiers:\n\u001b[0;32m--> 144\u001b[0m result_data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_data\u001b[49m\u001b[43m(\u001b[49m\u001b[43midentifier\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 146\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msymbol\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m result_data \u001b[38;5;129;01mand\u001b[39;00m result_data[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msymbol\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \\\n\u001b[1;32m 147\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m result_data \\\n\u001b[1;32m 148\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m result_data[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 149\u001b[0m \u001b[38;5;28mtype\u001b[39m \u001b[38;5;241m=\u001b[39m result_data[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py:102\u001b[0m, in \u001b[0;36mBacktestMarketDataSourceService.get_data\u001b[0;34m(self, identifier)\u001b[0m\n\u001b[1;32m 100\u001b[0m config \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_configuration_service\u001b[38;5;241m.\u001b[39mget_config()\n\u001b[1;32m 101\u001b[0m backtest_index_date \u001b[38;5;241m=\u001b[39m config[BACKTESTING_INDEX_DATETIME]\n\u001b[0;32m--> 102\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[43mmarket_data_source\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_data\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 103\u001b[0m \u001b[43m \u001b[49m\u001b[43mdate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbacktest_index_date\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\n\u001b[1;32m 104\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 106\u001b[0m result \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 107\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdata\u001b[39m\u001b[38;5;124m\"\u001b[39m: data,\n\u001b[1;32m 108\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 109\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msymbol\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 110\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtime_frame\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 111\u001b[0m }\n\u001b[1;32m 113\u001b[0m \u001b[38;5;66;03m# Add metadata to the data\u001b[39;00m\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py:395\u001b[0m, in \u001b[0;36mCCXTTickerBacktestMarketDataSource.get_data\u001b[0;34m(self, date, config)\u001b[0m\n\u001b[1;32m 393\u001b[0m \u001b[38;5;66;03m# Filter the data based on the backtest index date and the end date\u001b[39;00m\n\u001b[1;32m 394\u001b[0m df \u001b[38;5;241m=\u001b[39m polars\u001b[38;5;241m.\u001b[39mread_csv(file_path)\n\u001b[0;32m--> 395\u001b[0m filtered_df \u001b[38;5;241m=\u001b[39m \u001b[43mdf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfilter\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 396\u001b[0m \u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mDatetime\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m>\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mdate\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstrftime\u001b[49m\u001b[43m(\u001b[49m\u001b[43mDATETIME_FORMAT\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 397\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 399\u001b[0m \u001b[38;5;66;03m# If nothing is found, get all dates before the index date\u001b[39;00m\n\u001b[1;32m 400\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(filtered_df) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/.venv/lib/python3.10/site-packages/polars/dataframe/frame.py:4268\u001b[0m, in \u001b[0;36mDataFrame.filter\u001b[0;34m(self, *predicates, **constraints)\u001b[0m\n\u001b[1;32m 4168\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mfilter\u001b[39m(\n\u001b[1;32m 4169\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 4170\u001b[0m \u001b[38;5;241m*\u001b[39mpredicates: (\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 4177\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mconstraints: Any,\n\u001b[1;32m 4178\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DataFrame:\n\u001b[1;32m 4179\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 4180\u001b[0m \u001b[38;5;124;03m Filter the rows in the DataFrame based on one or more predicate expressions.\u001b[39;00m\n\u001b[1;32m 4181\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 4266\u001b[0m \u001b[38;5;124;03m └─────┴─────┴─────┘\u001b[39;00m\n\u001b[1;32m 4267\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 4268\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlazy\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfilter\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mpredicates\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mconstraints\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m_eager\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n",
+ "File \u001b[0;32m~/Projects/LogicFunds/investing-algorithm-framework/.venv/lib/python3.10/site-packages/polars/lazyframe/frame.py:1967\u001b[0m, in \u001b[0;36mLazyFrame.collect\u001b[0;34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, no_optimization, streaming, background, _eager, **_kwargs)\u001b[0m\n\u001b[1;32m 1964\u001b[0m \u001b[38;5;66;03m# Only for testing purposes atm.\u001b[39;00m\n\u001b[1;32m 1965\u001b[0m callback \u001b[38;5;241m=\u001b[39m _kwargs\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpost_opt_callback\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m-> 1967\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n",
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
]
}
],
@@ -14779,7 +14870,7 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 18,
"metadata": {
"collapsed": false
},
@@ -14870,7 +14961,7 @@
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
@@ -14930,7 +15021,7 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 20,
"metadata": {
"collapsed": false
},
@@ -14957,7 +15048,7 @@
" :++- :%%%%%%%%%+= \u001b[93mPercentage positive trades:\u001b[0m\u001b[92m 26.31578947368421%\u001b[0m\n",
" .++- -%%%%%%%%%%%+= \u001b[93mPercentage negative trades:\u001b[0m\u001b[92m 73.68421052631578%\u001b[0m\n",
" .++- .%%%%%%%%%%%%%+= \u001b[93mAverage trade size:\u001b[0m\u001b[92m 248.6320 EUR\u001b[0m\n",
- " .++- *%%%%%%%%%%%%%*+: \u001b[93mAverage trade duration:\u001b[0m\u001b[92m 106.10526315789474 hours\u001b[0m\n",
+ " .++- *%%%%%%%%%%%%%*+: \u001b[93mAverage trade duration:\u001b[0m\u001b[92m 16810.421329006214 hours\u001b[0m\n",
" .++- %%%%%%%%%%%%%%#+=\n",
" =++........:::%%%%%%%%%%%%%%*+-\n",
" .=++++++++++**#%%%%%%%%%%%%%++.\n",
@@ -14971,49 +15062,59 @@
"│ BTC │ 0.01 │ 0 │ 0 │ 249.215 │ 254.525 │ 22.6189% │ 5.31 │ 2.1307% │\n",
"╰────────────┴──────────┴──────────────────────┴───────────────────────┴──────────────┴───────────────┴───────────────────────────┴────────────────┴───────────────╯\n",
"\u001b[93mTrades overview\u001b[0m\n",
- "╭─────────┬─────────────────────┬─────────────────────┬────────────────────┬──────────────┬─────────────────────┬───────────────────────┬────────────────────┬─────────────────────────────┬───────────────────────╮\n",
- "│ Pair │ Open date │ Close date │ Duration (hours) │ Cost (EUR) │ Net gain (EUR) │ Net gain percentage │ Open price (EUR) │ Last reported price (EUR) │ Stop loss triggered │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2022-12-26 02:00:00 │ 2022-12-26 22:00:00 │ 20:00:00 │ 249.489 │ -1.1461 │ -0.4594% │ 15891 │ 15815 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2022-12-26 22:00:00 │ 2022-12-27 06:00:00 │ 8:00:00 │ 248.814 │ -0.3690 │ -0.1483% │ 15848 │ 15802 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-01-02 12:00:00 │ 2023-01-31 06:00:00 │ 28 days, 18:00:00 │ 248.716 │ 85.8918 │ 34.5341% │ 15642.5 │ 21000 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-02-01 22:00:00 │ 2023-02-05 20:00:00 │ 3 days, 22:00:00 │ 248.222 │ -4.5597 │ -1.8370% │ 21584.5 │ 21216 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-02-07 22:00:00 │ 2023-02-09 02:00:00 │ 1 day, 4:00:00 │ 248.745 │ -2.3518 │ -0.9454% │ 21630 │ 21378 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-02-15 08:00:00 │ 2023-02-22 16:00:00 │ 7 days, 8:00:00 │ 249.677 │ 21.8647 │ 8.7572% │ 20634.5 │ 22338 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-03-13 02:00:00 │ 2023-03-23 14:00:00 │ 10 days, 12:00:00 │ 248.02 │ 53.3655 │ 21.5166% │ 20842 │ 25207 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-03-23 14:00:00 │ 2023-03-25 12:00:00 │ 1 day, 22:00:00 │ 247.92 │ -2.3376 │ -0.9429% │ 25825 │ 25581 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-03-26 12:00:00 │ 2023-03-27 16:00:00 │ 1 day, 4:00:00 │ 249.883 │ -8.2176 │ -3.2886% │ 26029.5 │ 24973 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-03-29 14:00:00 │ 2023-04-03 08:00:00 │ 4 days, 18:00:00 │ 248.587 │ -4.1040 │ -1.6509% │ 26167 │ 25796 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-04-05 10:00:00 │ 2023-04-05 16:00:00 │ 6:00:00 │ 247.622 │ -2.5460 │ -1.0282% │ 26065.5 │ 25718 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-04-09 22:00:00 │ 2023-04-17 12:00:00 │ 7 days, 14:00:00 │ 247.484 │ 9.6283 │ 3.8904% │ 26051 │ 26973 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-04-18 20:00:00 │ 2023-04-19 12:00:00 │ 16:00:00 │ 248.656 │ -7.7445 │ -3.1145% │ 27628.5 │ 26844 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-04-26 06:00:00 │ 2023-05-01 12:00:00 │ 5 days, 6:00:00 │ 248.05 │ 1.2336 │ 0.4973% │ 25838.5 │ 25948 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-04 08:00:00 │ 2023-05-08 00:00:00 │ 3 days, 16:00:00 │ 249.845 │ -2.5033 │ -1.0019% │ 26299.5 │ 25868 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-15 06:00:00 │ 2023-05-17 06:00:00 │ 2 days, 0:00:00 │ 247.626 │ -3.6897 │ -1.4900% │ 25268 │ 24851 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-17 20:00:00 │ 2023-05-19 12:00:00 │ 1 day, 16:00:00 │ 249.822 │ -3.6036 │ -1.4425% │ 25234.5 │ 24865 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-20 22:00:00 │ 2023-05-21 18:00:00 │ 20:00:00 │ 247.926 │ -1.6335 │ -0.6589% │ 25043 │ 24858 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-23 04:00:00 │ 2023-05-24 14:00:00 │ 1 day, 10:00:00 │ 248.322 │ -7.2128 │ -2.9046% │ 25339 │ 24579 │ False │\n",
- "├─────────┼─────────────────────┼─────────────────────┼────────────────────┼──────────────┼─────────────────────┼───────────────────────┼────────────────────┼─────────────────────────────┼───────────────────────┤\n",
- "│ BTC-EUR │ 2023-05-27 02:00:00 │ │ 4 days, 22:00:00 │ 249.215 │ 4.3650 (unrealized) │ 1.7515% (unrealized) │ 24921.5 │ 25358 │ False │\n",
- "╰─────────┴─────────────────────┴─────────────────────┴────────────────────┴──────────────┴─────────────────────┴───────────────────────┴────────────────────┴─────────────────────────────┴───────────────────────╯\n"
+ "╭───────────────────┬──────────┬───────────────────────────────┬─────────────────────┬────────────────────────────┬──────────────────────────┬────────────────────┬───────────────────────╮\n",
+ "│ Pair (Trade id) │ Status │ Net gain (EUR) │ Open date │ Close date │ Duration │ Open price (EUR) │ Close price's (EUR) │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (1) │ CLOSED │ -1.1461 (-0.4594%) │ 2022-12-26 02:00:00 │ 2025-02-19 14:37:41.267841 │ 18876.628129955832 hours │ 15891 │ 15818.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (2) │ CLOSED │ -0.3690 (-0.1483%) │ 2022-12-26 22:00:00 │ 2025-02-19 14:37:41.615564 │ 18856.628226545556 hours │ 15848 │ 15824.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (3) │ CLOSED │ 85.8918 (34.5341%) │ 2023-01-02 12:00:00 │ 2025-02-19 14:37:47.238045 │ 18698.629788345836 hours │ 15642.5 │ 21044.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (4) │ CLOSED │ -4.5597 (-1.8370%) │ 2023-02-01 22:00:00 │ 2025-02-19 14:37:48.132302 │ 17968.630036750557 hours │ 21584.5 │ 21188.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (5) │ CLOSED │ -2.3518 (-0.9454%) │ 2023-02-07 22:00:00 │ 2025-02-19 14:37:48.654490 │ 17824.63018180278 hours │ 21630 │ 21425.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (6) │ CLOSED │ 21.8647 (8.7572%) │ 2023-02-15 08:00:00 │ 2025-02-19 14:37:50.552297 │ 17646.63070897139 hours │ 20634.5 │ 22441.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (7) │ CLOSED │ 53.3655 (21.5166%) │ 2023-03-13 02:00:00 │ 2025-02-19 14:37:53.757568 │ 17028.631599324446 hours │ 20842 │ 25326.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (8) │ CLOSED │ -2.3376 (-0.9429%) │ 2023-03-23 14:00:00 │ 2025-02-19 14:37:54.246152 │ 16776.63173504222 hours │ 25825 │ 25581.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (9) │ CLOSED │ -8.2176 (-3.2886%) │ 2023-03-26 12:00:00 │ 2025-02-19 14:37:54.664445 │ 16706.63185123472 hours │ 26029.5 │ 25173.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (10) │ CLOSED │ -4.1040 (-1.6509%) │ 2023-03-29 14:00:00 │ 2025-02-19 14:37:55.700535 │ 16632.6321390375 hours │ 26167 │ 25735.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (11) │ CLOSED │ -2.5460 (-1.0282%) │ 2023-04-05 10:00:00 │ 2025-02-19 14:37:56.117703 │ 16468.6322549175 hours │ 26065.5 │ 25797.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (12) │ CLOSED │ 9.6283 (3.8904%) │ 2023-04-09 22:00:00 │ 2025-02-19 14:37:57.729326 │ 16360.632702590556 hours │ 26051 │ 27064.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (13) │ CLOSED │ -7.7445 (-3.1145%) │ 2023-04-18 20:00:00 │ 2025-02-19 14:37:58.096395 │ 16146.632804554167 hours │ 27628.5 │ 26768.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (14) │ CLOSED │ 1.2336 (0.4973%) │ 2023-04-26 06:00:00 │ 2025-02-19 14:37:59.720684 │ 15968.633255745555 hours │ 25838.5 │ 25967.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (15) │ CLOSED │ -2.5033 (-1.0019%) │ 2023-05-04 08:00:00 │ 2025-02-19 14:38:00.822668 │ 15774.633561852223 hours │ 26299.5 │ 26036.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (16) │ CLOSED │ -3.6897 (-1.4900%) │ 2023-05-15 06:00:00 │ 2025-02-19 14:38:01.946701 │ 15512.63387408361 hours │ 25268 │ 24891.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (17) │ CLOSED │ -3.6036 (-1.4425%) │ 2023-05-17 20:00:00 │ 2025-02-19 14:38:02.405810 │ 15450.63400161389 hours │ 25234.5 │ 24870.5 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (18) │ CLOSED │ -1.6335 (-0.6589%) │ 2023-05-20 22:00:00 │ 2025-02-19 14:38:02.844927 │ 15376.634123590833 hours │ 25043 │ 24878.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (19) │ CLOSED │ -7.2128 (-2.9046%) │ 2023-05-23 04:00:00 │ 2025-02-19 14:38:03.390572 │ 15322.634275158887 hours │ 25339 │ 24603.0 │\n",
+ "├───────────────────┼──────────┼───────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼───────────────────────┤\n",
+ "│ BTC/EUR (20) │ OPEN │ 4.3650 (1.7515%) (unrealized) │ 2023-05-27 02:00:00 │ │ 118.0 hours │ 24921.5 │ │\n",
+ "╰───────────────────┴──────────┴───────────────────────────────┴─────────────────────┴────────────────────────────┴──────────────────────────┴────────────────────┴───────────────────────╯\n",
+ "\u001b[93mStop losses overview\u001b[0m\n",
+ "╭────────────────────┬──────────┬──────────┬────────┬─────────────┬──────────────┬────────────────┬───────────────────┬──────────────┬────────┬───────────────╮\n",
+ "│ Trade (Trade id) │ Status │ Active │ Type │ stop loss │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │\n",
+ "├────────────────────┼──────────┼──────────┼────────┼─────────────┼──────────────┼────────────────┼───────────────────┼──────────────┼────────┼───────────────┤\n",
+ "╰────────────────────┴──────────┴──────────┴────────┴─────────────┴──────────────┴────────────────┴───────────────────┴──────────────┴────────┴───────────────╯\n",
+ "\u001b[93mTake profits overview\u001b[0m\n",
+ "╭────────────────────┬──────────┬──────────┬────────┬───────────────┬──────────────┬────────────────┬───────────────────┬──────────────┬────────┬───────────────╮\n",
+ "│ Trade (Trade id) │ Status │ Active │ Type │ Take profit │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │\n",
+ "├────────────────────┼──────────┼──────────┼────────┼───────────────┼──────────────┼────────────────┼───────────────────┼──────────────┼────────┼───────────────┤\n",
+ "╰────────────────────┴──────────┴──────────┴────────┴───────────────┴──────────────┴────────────────┴───────────────────┴──────────────┴────────┴───────────────╯\n"
]
},
{
@@ -16987,8 +17088,7 @@
"2023-05-31T18:00:00",
"2023-05-31T20:00:00",
"2023-05-31T22:00:00",
- "2023-06-01T00:00:00",
- null
+ "2023-06-01T00:00:00"
],
"xaxis": "x",
"y": [
@@ -18947,8 +19047,7 @@
25292,
25350,
25469,
- 25358,
- null
+ 25358
],
"yaxis": "y"
},
@@ -19018,48 +19117,48 @@
"name": "Sell Signal",
"type": "scatter",
"x": [
- "2022-12-26T22:00:00",
- "2022-12-27T06:00:00",
- "2023-01-31T06:00:00",
- "2023-02-05T20:00:00",
- "2023-02-09T02:00:00",
- "2023-02-22T16:00:00",
- "2023-03-23T14:00:00",
- "2023-03-25T12:00:00",
- "2023-03-27T16:00:00",
- "2023-04-03T08:00:00",
- "2023-04-05T16:00:00",
- "2023-04-17T12:00:00",
- "2023-04-19T12:00:00",
- "2023-05-01T12:00:00",
- "2023-05-08T00:00:00",
- "2023-05-17T06:00:00",
- "2023-05-19T12:00:00",
- "2023-05-21T18:00:00",
- "2023-05-24T14:00:00",
+ "2025-02-19T14:37:41.267841",
+ "2025-02-19T14:37:41.615564",
+ "2025-02-19T14:37:47.238045",
+ "2025-02-19T14:37:48.132302",
+ "2025-02-19T14:37:48.654490",
+ "2025-02-19T14:37:50.552297",
+ "2025-02-19T14:37:53.757568",
+ "2025-02-19T14:37:54.246152",
+ "2025-02-19T14:37:54.664445",
+ "2025-02-19T14:37:55.700535",
+ "2025-02-19T14:37:56.117703",
+ "2025-02-19T14:37:57.729326",
+ "2025-02-19T14:37:58.096395",
+ "2025-02-19T14:37:59.720684",
+ "2025-02-19T14:38:00.822668",
+ "2025-02-19T14:38:01.946701",
+ "2025-02-19T14:38:02.405810",
+ "2025-02-19T14:38:02.844927",
+ "2025-02-19T14:38:03.390572",
null
],
"xaxis": "x",
"y": [
- 15897,
- 15821,
- 21206,
- 21248,
- 20992,
- 22352,
- 26296,
- 25679,
- 25000,
- 26128,
- 25755,
- 26894,
- 26801,
- 25865,
- 25639,
- 24781,
- 24903,
- 24901,
- 24458,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
null
],
"yaxis": "y"
@@ -19959,7 +20058,7 @@
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 21,
"metadata": {
"collapsed": false
},
@@ -19975,14 +20074,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 81.52it/s]\n",
- "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:19<00:00, 109.79it/s]\n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 195.99it/s]\n",
- "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:23<00:00, 93.20it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 61.48it/s]\n",
- "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:20<00:00, 104.58it/s]\n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 276.06it/s]\n",
- "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:19<00:00, 112.87it/s]\n"
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 84.31it/s]\n",
+ "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:24<00:00, 88.07it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 184.93it/s]\n",
+ "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:23<00:00, 93.05it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 113.39it/s]\n",
+ "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:20<00:00, 107.07it/s]\n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 194.65it/s]\n",
+ "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 2173/2173 [00:17<00:00, 125.81it/s]\n"
]
},
{
@@ -19996,14 +20095,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 181.62it/s]\n",
- "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:22<00:00, 88.41it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 151.12it/s]\n",
- "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:21<00:00, 92.59it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 260.08it/s]\n",
- "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:17<00:00, 109.98it/s]\n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 230.50it/s]\n",
- "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:19<00:00, 102.87it/s]\n"
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 197.40it/s]\n",
+ "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:23<00:00, 83.23it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 132.58it/s]\n",
+ "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:21<00:00, 89.83it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 194.67it/s]\n",
+ "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:19<00:00, 101.23it/s]\n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 98.48it/s]\n",
+ "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 1957/1957 [00:20<00:00, 96.49it/s] \n"
]
},
{
@@ -20017,14 +20116,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 150.94it/s]\n",
- "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:30<00:00, 85.42it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 361.66it/s]\n",
- "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:27<00:00, 93.56it/s] \n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 333.78it/s]\n",
- "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:23<00:00, 110.81it/s]\n",
- "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 113.52it/s]\n",
- "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:21<00:00, 118.72it/s]"
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 180.60it/s]\n",
+ "Running backtest for algorithm with name primary_21_50: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:37<00:00, 69.35it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 100.55it/s]\n",
+ "Running backtest for algorithm with name primary_21_75: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:31<00:00, 81.22it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 123.20it/s]\n",
+ "Running backtest for algorithm with name primary_50_100: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:26<00:00, 98.40it/s] \n",
+ "Preparing backtest market data: 100%|\u001b[32m██████████\u001b[0m| 2/2 [00:00<00:00, 146.06it/s]\n",
+ "Running backtest for algorithm with name primary_50_200: 100%|\u001b[32m██████████\u001b[0m| 2569/2569 [00:27<00:00, 94.38it/s] "
]
},
{
@@ -20035,8 +20134,8 @@
" :%%%#+- .=*#%%% \u001b[92mBacktest reports evaluation\u001b[0m\n",
" *%%%%%%%+------=*%%%%%%%- \u001b[92m---------------------------\u001b[0m\n",
" *%%%%%%%%%%%%%%%%%%%%%%%- \u001b[93mNumber of reports:\u001b[0m \u001b[92m4 backtest reports\u001b[0m\n",
- " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_21_50) 119.9648 EUR 11.9965% (up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00)\u001b[0m\n",
- " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_21_50) 125.2748 EUR 12.5275% (up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00)\u001b[0m\n",
+ " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_21_75) 122.7062 EUR 12.2706% (up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00)\u001b[0m\n",
+ " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_21_75) 128.2862 EUR 12.8286% (up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00)\u001b[0m\n",
" .:-+*%%%%- \u001b[95m-+..#\u001b[0m%%%+.\u001b[95m+- +\u001b[0m%%%#*=-:\n",
" .:-=*%%%%. \u001b[95m+=\u001b[0m .%%# \u001b[95m-+.-\u001b[0m%%%%=-:..\n",
" .:=+#%%%%%*###%%%%#*+#%%%%%%*+-:\n",
@@ -20059,33 +20158,33 @@
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬───────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Profit │ Profit percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_50 │ 119.9648 EUR │ 11.9965% │ 26% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1125.27 │\n",
+ "│ primary_21_75 │ 122.7062 EUR │ 12.2706% │ 29% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1128.29 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ 118.5247 EUR │ 11.8525% │ 29% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1124.1 │\n",
+ "│ primary_21_50 │ 119.9648 EUR │ 11.9965% │ 26% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1125.27 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ 79.6898 EUR │ 7.9690% │ 67% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1079.69 │\n",
+ "│ primary_50_100 │ 85.8809 EUR │ 8.5881% │ 33% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1087.5 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ 9.7453 EUR │ 0.9745% │ 40% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1011.36 │\n",
+ "│ primary_50_200 │ 70.8413 EUR │ 7.0841% │ 57% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1072.08 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴───────────────────────────────────────────────────┴───────────────╯\n",
"\u001b[93mAll growths ordered\u001b[0m\n",
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬───────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Growth │ Growth percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_50 │ 125.2748 EUR │ 12.5275% │ 26% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1125.27 │\n",
+ "│ primary_21_75 │ 128.2862 EUR │ 12.8286% │ 29% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1128.29 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ 124.1047 EUR │ 12.4105% │ 29% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1124.1 │\n",
+ "│ primary_21_50 │ 125.2748 EUR │ 12.5275% │ 26% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1125.27 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ 79.6898 EUR │ 7.9690% │ 67% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1079.69 │\n",
+ "│ primary_50_100 │ 87.4980 EUR │ 8.7498% │ 33% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1087.5 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ 11.3623 EUR │ 1.1362% │ 40% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1011.36 │\n",
+ "│ primary_50_200 │ 72.0810 EUR │ 7.2081% │ 57% │ up_turn 2022-12-20 00:00:00 - 2023-06-01 00:00:00 │ 1072.08 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴───────────────────────────────────────────────────┴───────────────╯\n",
- "The winning configuration for the up turn backtest date range is primary_21_50\n",
+ "The winning configuration for the up turn backtest date range is primary_21_75\n",
"\n",
" :%%%#+- .=*#%%% \u001b[92mBacktest reports evaluation\u001b[0m\n",
" *%%%%%%%+------=*%%%%%%%- \u001b[92m---------------------------\u001b[0m\n",
" *%%%%%%%%%%%%%%%%%%%%%%%- \u001b[93mNumber of reports:\u001b[0m \u001b[92m4 backtest reports\u001b[0m\n",
- " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_21_50) -8.2249 EUR -0.8225% (sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00)\u001b[0m\n",
- " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_21_50) -2.0557 EUR -0.2056% (sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00)\u001b[0m\n",
+ " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_21_75) -6.1955 EUR -0.6195% (sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00)\u001b[0m\n",
+ " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_21_75) -0.3761 EUR -0.0376% (sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00)\u001b[0m\n",
" .:-+*%%%%- \u001b[95m-+..#\u001b[0m%%%+.\u001b[95m+- +\u001b[0m%%%#*=-:\n",
" .:-=*%%%%. \u001b[95m+=\u001b[0m .%%# \u001b[95m-+.-\u001b[0m%%%%=-:..\n",
" .:=+#%%%%%*###%%%%#*+#%%%%%%*+-:\n",
@@ -20108,33 +20207,33 @@
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬────────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Profit │ Profit percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_50 │ -8.2249 EUR │ -0.8225% │ 32% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 997.944 │\n",
+ "│ primary_21_75 │ -6.1955 EUR │ -0.6195% │ 33% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 999.624 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ -10.4957 EUR │ -1.0496% │ 33% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 995.324 │\n",
+ "│ primary_21_50 │ -8.2249 EUR │ -0.8225% │ 32% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 997.944 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ -32.8569 EUR │ -3.2857% │ 50% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 969.977 │\n",
+ "│ primary_50_200 │ -32.1389 EUR │ -3.2139% │ 43% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 970.813 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ -47.5249 EUR │ -4.7525% │ 14% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 952.475 │\n",
+ "│ primary_50_100 │ -47.3143 EUR │ -4.7314% │ 12% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 956.667 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴────────────────────────────────────────────────────┴───────────────╯\n",
"\u001b[93mAll growths ordered\u001b[0m\n",
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬────────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Growth │ Growth percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_50 │ -2.0557 EUR │ -0.2056% │ 32% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 997.944 │\n",
+ "│ primary_21_75 │ -0.3761 EUR │ -0.0376% │ 33% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 999.624 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ -4.6763 EUR │ -0.4676% │ 33% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 995.324 │\n",
+ "│ primary_21_50 │ -2.0557 EUR │ -0.2056% │ 32% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 997.944 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ -30.0231 EUR │ -3.0023% │ 50% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 969.977 │\n",
+ "│ primary_50_200 │ -29.1873 EUR │ -2.9187% │ 43% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 970.813 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ -47.5249 EUR │ -4.7525% │ 14% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 952.475 │\n",
+ "│ primary_50_100 │ -43.3327 EUR │ -4.3333% │ 12% │ sideways 2022-06-10 00:00:00 - 2023-01-10 00:00:00 │ 956.667 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴────────────────────────────────────────────────────┴───────────────╯\n",
"The winning configuration for the side ways backtest date range is None\n",
"\n",
" :%%%#+- .=*#%%% \u001b[92mBacktest reports evaluation\u001b[0m\n",
" *%%%%%%%+------=*%%%%%%%- \u001b[92m---------------------------\u001b[0m\n",
" *%%%%%%%%%%%%%%%%%%%%%%%- \u001b[93mNumber of reports:\u001b[0m \u001b[92m4 backtest reports\u001b[0m\n",
- " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_50_200) -33.8225 EUR -3.3823% (down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00)\u001b[0m\n",
- " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_50_200) -33.8226 EUR -3.3823% (down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00)\u001b[0m\n",
+ " .%%%%%%%%%%%%%%%%%%%%%%# \u001b[93mLargest overall profit:\u001b[0m\u001b[92m\u001b[0m\u001b[92m (Algorithm primary_50_200) -48.1296 EUR -4.8130% (down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00)\u001b[0m\n",
+ " #%%%####%%%%%%%%**#%%%+ \u001b[93mLargest overall growth:\u001b[0m\u001b[92m (Algorithm primary_50_200) -48.1296 EUR -4.8130% (down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00)\u001b[0m\n",
" .:-+*%%%%- \u001b[95m-+..#\u001b[0m%%%+.\u001b[95m+- +\u001b[0m%%%#*=-:\n",
" .:-=*%%%%. \u001b[95m+=\u001b[0m .%%# \u001b[95m-+.-\u001b[0m%%%%=-:..\n",
" .:=+#%%%%%*###%%%%#*+#%%%%%%*+-:\n",
@@ -20157,11 +20256,11 @@
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬─────────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Profit │ Profit percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ -33.8225 EUR │ -3.3823% │ 20% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 966.177 │\n",
+ "│ primary_50_200 │ -48.1296 EUR │ -4.8130% │ 20% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 951.87 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ -68.4083 EUR │ -6.8408% │ 18% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 931.592 │\n",
+ "│ primary_50_100 │ -60.1076 EUR │ -6.0108% │ 10% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 939.893 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ -76.2687 EUR │ -7.6269% │ 16% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 923.731 │\n",
+ "│ primary_21_75 │ -74.0502 EUR │ -7.4050% │ 16% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 925.95 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
"│ primary_21_50 │ -95.0346 EUR │ -9.5035% │ 19% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 904.965 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴─────────────────────────────────────────────────────┴───────────────╯\n",
@@ -20169,11 +20268,11 @@
"╭──────────────────┬──────────────┬─────────────────────┬──────────────────────────────┬─────────────────────────────────────────────────────┬───────────────╮\n",
"│ Algorithm name │ Growth │ Growth percentage │ Percentage positive trades │ Date range │ Total value │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_200 │ -33.8226 EUR │ -3.3823% │ 20% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 966.177 │\n",
+ "│ primary_50_200 │ -48.1296 EUR │ -4.8130% │ 20% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 951.87 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_50_100 │ -68.4082 EUR │ -6.8408% │ 18% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 931.592 │\n",
+ "│ primary_50_100 │ -60.1075 EUR │ -6.0108% │ 10% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 939.893 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
- "│ primary_21_75 │ -76.2687 EUR │ -7.6269% │ 16% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 923.731 │\n",
+ "│ primary_21_75 │ -74.0502 EUR │ -7.4050% │ 16% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 925.95 │\n",
"├──────────────────┼──────────────┼─────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────┼───────────────┤\n",
"│ primary_21_50 │ -95.0346 EUR │ -9.5035% │ 19% │ down_turn 2021-12-21 00:00:00 - 2022-06-20 00:00:00 │ 904.965 │\n",
"╰──────────────────┴──────────────┴─────────────────────┴──────────────────────────────┴─────────────────────────────────────────────────────┴───────────────╯\n",
diff --git a/examples/bitvavo_trading_bot.py b/examples/bitvavo_trading_bot.py
index af0ed44d..1c87467b 100644
--- a/examples/bitvavo_trading_bot.py
+++ b/examples/bitvavo_trading_bot.py
@@ -3,7 +3,8 @@
from investing_algorithm_framework import MarketCredential, TimeUnit, \
CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \
- create_app, PortfolioConfiguration, Algorithm, DEFAULT_LOGGING_CONFIG
+ create_app, PortfolioConfiguration, Algorithm, DEFAULT_LOGGING_CONFIG, \
+ Context
"""
Bitvavo trading bot example with market data sources of bitvavo.
@@ -43,7 +44,7 @@ class BitvavoTradingStrategy(TradingStrategy):
interval = 10
market_data_sources = [bitvavo_btc_eur_ohlcv_2h, bitvavo_btc_eur_ticker]
- def apply_strategy(self, algorithm, market_data):
+ def apply_strategy(self, context: Context, market_data):
print(market_data["BTC/EUR-ohlcv"])
print(market_data["BTC/EUR-ticker"])
@@ -59,7 +60,7 @@ def apply_strategy(self, algorithm, market_data):
app.add_algorithm(algorithm)
app.add_portfolio_configuration(
PortfolioConfiguration(
- initial_balance=1000,
+ initial_balance=41,
trading_symbol="EUR",
market="bitvavo"
)
diff --git a/examples/coinbase_trading_bot.py b/examples/coinbase_trading_bot.py
index 66064625..c6091eba 100644
--- a/examples/coinbase_trading_bot.py
+++ b/examples/coinbase_trading_bot.py
@@ -2,7 +2,8 @@
import logging.config
from investing_algorithm_framework import MarketCredential, TimeUnit, \
CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \
- create_app, PortfolioConfiguration, Algorithm, DEFAULT_LOGGING_CONFIG
+ create_app, PortfolioConfiguration, Algorithm, DEFAULT_LOGGING_CONFIG, \
+ Context
"""
Coinbase market data sources example. Coinbase requires you to have an API key
and secret key to access their market data. You can create them here:
@@ -37,24 +38,32 @@
)
-class CoinBaseTradingStrategy(TradingStrategy):
+class BitvavoTradingStrategy(TradingStrategy):
time_unit = TimeUnit.SECOND
- interval = 5
- market_data_sources = [coinbase_btc_eur_ticker, coinbase_btc_eur_ohlcv_2h]
+ interval = 10
+ market_data_sources = [coinbase_btc_eur_ohlcv_2h, coinbase_btc_eur_ticker]
- def apply_strategy(self, algorithm, market_data):
- pass
+ def apply_strategy(self, context: Context, market_data):
+ print(market_data["BTC/EUR-ohlcv"])
+ print(market_data["BTC/EUR-ticker"])
+# Create an algorithm and link your trading strategy to it
algorithm = Algorithm()
-algorithm.add_strategy(CoinBaseTradingStrategy)
+algorithm.add_strategy(BitvavoTradingStrategy)
+
+# Create an app and add the market data sources and market credentials to it
app = create_app()
-app.add_algorithm(algorithm)
app.add_market_credential(coinbase_market_credential)
-app.add_portfolio_configuration(PortfolioConfiguration(
- initial_balance=1000,
- trading_symbol="EUR",
- market="coinbase"
-))
+
+# Register your algorithm and portfolio configuration to the app
+app.add_algorithm(algorithm)
+app.add_portfolio_configuration(
+ PortfolioConfiguration(
+ initial_balance=41,
+ trading_symbol="EUR",
+ market="coinbase"
+ )
+)
if __name__ == "__main__":
app.run()
diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py
index 57a00fa8..3a1db014 100644
--- a/investing_algorithm_framework/__init__.py
+++ b/investing_algorithm_framework/__init__.py
@@ -1,6 +1,5 @@
-from investing_algorithm_framework.app import App, Algorithm, AppHook
-from investing_algorithm_framework.app import TradingStrategy, \
- StatelessAction, Task
+from investing_algorithm_framework.app import App, Algorithm, \
+ TradingStrategy, StatelessAction, Task, AppHook, Context
from investing_algorithm_framework.domain import ApiException, \
TradingDataType, TradingTimeFrame, OrderType, OperationalException, \
OrderStatus, OrderSide, TimeUnit, TimeInterval, Order, Portfolio, \
@@ -12,7 +11,7 @@
RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \
load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \
DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG, \
- BacktestReport, TradeStatus, MarketDataType
+ BacktestReport, TradeStatus, MarketDataType, TradeRiskType
from investing_algorithm_framework.infrastructure import \
CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \
CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \
@@ -92,5 +91,7 @@
"DEFAULT_LOGGING_CONFIG",
"BacktestReport",
"TradeStatus",
- "MarketDataType"
+ "MarketDataType",
+ "TradeRiskType",
+ "Context"
]
diff --git a/investing_algorithm_framework/app/__init__.py b/investing_algorithm_framework/app/__init__.py
index 3f3e9c8f..a92a23ac 100644
--- a/investing_algorithm_framework/app/__init__.py
+++ b/investing_algorithm_framework/app/__init__.py
@@ -4,6 +4,7 @@
from investing_algorithm_framework.app.task import Task
from investing_algorithm_framework.app.web import create_flask_app
from .algorithm import Algorithm
+from .context import Context
__all__ = [
"Algorithm",
@@ -12,5 +13,6 @@
"TradingStrategy",
"StatelessAction",
"Task",
- "AppHook"
+ "AppHook",
+ "Context",
]
diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py
index c8349d2b..10fc80db 100644
--- a/investing_algorithm_framework/app/algorithm.py
+++ b/investing_algorithm_framework/app/algorithm.py
@@ -1,12 +1,10 @@
import inspect
import logging
-from typing import List, Dict
+from typing import Dict
import re
-from investing_algorithm_framework.domain import OrderStatus, \
- Position, Order, Portfolio, OrderType, OrderSide, TradeStatus, \
- BACKTESTING_FLAG, BACKTESTING_INDEX_DATETIME, MarketService, TimeUnit, \
- OperationalException, random_string, RoundingService, Trade
+from investing_algorithm_framework.domain import MarketService, TimeUnit, \
+ OperationalException, random_string
from investing_algorithm_framework.services import MarketCredentialService, \
MarketDataSourceService, PortfolioService, PositionService, TradeService, \
OrderService, ConfigurationService, StrategyOrchestratorService, \
@@ -111,6 +109,7 @@ def _validate_name(self, name):
def initialize_services(
self,
+ context,
configuration_service,
portfolio_configuration_service,
portfolio_service,
@@ -122,6 +121,7 @@ def initialize_services(
market_data_source_service,
trade_service
):
+ self.context = context
self.portfolio_service: PortfolioService = portfolio_service
self.position_service: PositionService = position_service
self.order_service: OrderService = order_service
@@ -160,7 +160,7 @@ def start(self, number_of_iterations: int = None):
None
"""
self.strategy_orchestrator_service.start(
- algorithm=self,
+ context=self.context,
number_of_iterations=number_of_iterations
)
@@ -214,26 +214,6 @@ def description(self):
"""
return self._description
- @property
- def context(self):
- """
- Function to get the context of the algorithm
- """
- return self._context
-
- def add_context(self, context: Dict):
- # Check if the context is a dictionary with only string,
- # float or int values
- for key, value in self.context.items():
- if not isinstance(key, str) or \
- not isinstance(value, (str, float, int)):
- raise OperationalException(
- "The context for an algorithm must be a dictionary with "
- "only string, float or int values."
- )
-
- self._context = context
-
@property
def running(self) -> bool:
"""
@@ -244,729 +224,16 @@ def running(self) -> bool:
"""
return self.strategy_orchestrator_service.running
- def run_jobs(self):
+ def run_jobs(self, context):
"""
Function run all pending jobs in the strategy orchestrator
"""
self.strategy_orchestrator_service.run_pending_jobs()
- def create_order(
- self,
- target_symbol,
- price,
- order_type,
- order_side,
- amount,
- market=None,
- execute=True,
- validate=True,
- sync=True
- ):
- """
- Function to create an order. This function will create an 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
-
- 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 = {
- "target_symbol": target_symbol,
- "price": price,
- "amount": amount,
- "order_type": order_type,
- "order_side": order_side,
- "portfolio_id": portfolio.id,
- "status": OrderStatus.CREATED.value,
- "trading_symbol": portfolio.trading_symbol,
- }
-
- if BACKTESTING_FLAG in self.configuration_service.config \
- and self.configuration_service.config[BACKTESTING_FLAG]:
- order_data["created_at"] = \
- self.configuration_service.config[BACKTESTING_INDEX_DATETIME]
-
- return self.order_service.create(
- order_data, execute=execute, validate=validate, sync=sync
- )
-
- def has_balance(self, symbol, amount, market=None):
- """
- Function to check if the portfolio has enough balance to
- create an order. This function will return True if the
- portfolio has enough balance to create an order, False
- otherwise.
-
- Parameters:
- symbol: The symbol of the asset
- amount: The amount of the asset
- market: The market of the asset
-
- Returns:
- Boolean: True if the portfolio has enough balance
- """
-
- portfolio = self.portfolio_service.find({"market": market})
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": symbol}
- )
-
- if position is None:
- return False
-
- return position.get_amount() >= amount
-
- def create_limit_order(
- self,
- target_symbol,
- price,
- order_side,
- amount=None,
- amount_trading_symbol=None,
- percentage=None,
- percentage_of_portfolio=None,
- percentage_of_position=None,
- precision=None,
- market=None,
- execute=True,
- validate=True,
- sync=True
- ):
- """
- Function to create a limit order. This function will create a limit
- 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
-
- Args:
- target_symbol: The symbol of the asset to trade
- price: The price of the asset
- order_side: The side of the order
- amount (optional): The amount of the asset to trade
- amount_trading_symbol (optional): The amount of the
- trading symbol to trade
- percentage (optional): The percentage of the portfolio
- to allocate to the
- order
- percentage_of_portfolio (optional): The percentage
- of the portfolio to allocate to the order
- percentage_of_position (optional): The percentage
- of the position to allocate to
- the order. (Only supported for SELL orders)
- precision (optional): The precision of the amount
- market (optional): The market to trade the asset
- execute (optional): Default True. If set to True,
- the order will be executed
- validate (optional): Default True. If set to
- True, the order will be validated
- sync (optional): Default True. If set to True,
- the created order will be synced with the
- portfolio of the algorithm
-
- Returns:
- Order: Instance of the order created
- """
- portfolio = self.portfolio_service.find({"market": market})
-
- if percentage_of_portfolio is not None:
- if not OrderSide.BUY.equals(order_side):
- raise OperationalException(
- "Percentage of portfolio is only supported for BUY orders."
- )
-
- net_size = portfolio.get_net_size()
- size = net_size * (percentage_of_portfolio / 100)
- amount = size / price
-
- elif percentage_of_position is not None:
-
- if not OrderSide.SELL.equals(order_side):
- raise OperationalException(
- "Percentage of position is only supported for SELL orders."
- )
-
- position = self.position_service.find(
- {
- "symbol": target_symbol,
- "portfolio": portfolio.id
- }
- )
- amount = position.get_amount() * (percentage_of_position / 100)
-
- elif percentage is not None:
- net_size = portfolio.get_net_size()
- size = net_size * (percentage / 100)
- amount = size / price
-
- if precision is not None:
- amount = RoundingService.round_down(amount, precision)
-
- if amount_trading_symbol is not None:
- amount = amount_trading_symbol / price
-
- if amount is None:
- raise OperationalException(
- "The amount parameter is required to create a limit order." +
- "Either the amount, amount_trading_symbol, percentage, " +
- "percentage_of_portfolio or percentage_of_position "
- "parameter must be specified."
- )
-
- order_data = {
- "target_symbol": target_symbol,
- "price": price,
- "amount": amount,
- "order_type": OrderType.LIMIT.value,
- "order_side": OrderSide.from_value(order_side).value,
- "portfolio_id": portfolio.id,
- "status": OrderStatus.CREATED.value,
- "trading_symbol": portfolio.trading_symbol,
- }
-
- if BACKTESTING_FLAG in self.configuration_service.config \
- and self.configuration_service.config[BACKTESTING_FLAG]:
- order_data["created_at"] = \
- self.configuration_service.config[BACKTESTING_INDEX_DATETIME]
-
- return self.order_service.create(
- order_data, execute=execute, validate=validate, sync=sync
- )
-
- def create_market_order(
- self,
- target_symbol,
- order_side,
- amount,
- market=None,
- execute=False,
- validate=False,
- sync=True
- ):
- """
- Function to create a market order. This function will create a market
- 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
-
- Parameters:
- target_symbol: The symbol of the asset to trade
- 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:
- Order: Instance of the order created
- """
-
- if market is None:
- portfolio = self.portfolio_service.get_all()[0]
- else:
- portfolio = self.portfolio_service.find({"market": market})
- order_data = {
- "target_symbol": target_symbol,
- "amount": amount,
- "order_type": OrderType.MARKET.value,
- "order_side": OrderSide.from_value(order_side).value,
- "portfolio_id": portfolio.id,
- "status": OrderStatus.CREATED.value,
- "trading_symbol": portfolio.trading_symbol,
- }
-
- if BACKTESTING_FLAG in self.configuration_service.config \
- and self.configuration_service.config[BACKTESTING_FLAG]:
- order_data["created_at"] = \
- self.configuration_service.config[BACKTESTING_INDEX_DATETIME]
-
- return self.order_service.create(
- order_data, execute=execute, validate=validate, sync=sync
- )
-
- def get_portfolio(self, market=None) -> Portfolio:
- """
- Function to get the portfolio of the algorithm. This function
- will return the portfolio of the algorithm. If the market
- parameter is specified, the portfolio of the specified market
- will be returned.
-
- Parameters:
- market: The market of the portfolio
-
- Returns:
- Portfolio: The portfolio of the algorithm
- """
-
- if market is None:
- return self.portfolio_service.get_all()[0]
-
- return self.portfolio_service.find({{"market": market}})
-
- def get_portfolios(self):
- """
- Function to get all portfolios of the algorithm. This function
- will return all portfolios of the algorithm.
-
- Returns:
- List[Portfolio]: A list of all portfolios of the algorithm
- """
- return self.portfolio_service.get_all()
-
- def get_unallocated(self, market=None) -> float:
- """
- Function to get the unallocated balance of the portfolio. This
- function will return the unallocated balance of the portfolio.
- If the market parameter is specified, the unallocated balance
- of the specified market will be returned.
-
- Args:
- market: The market of the portfolio
-
- Returns:
- float: The unallocated balance of the portfolio
- """
-
- if market:
- portfolio = self.portfolio_service.find({{"market": market}})
- else:
- portfolio = self.portfolio_service.get_all()[0]
-
- trading_symbol = portfolio.trading_symbol
- return self.position_service.find(
- {"portfolio": portfolio.id, "symbol": trading_symbol}
- ).get_amount()
-
- def get_total_size(self):
- """
- Returns the total size of the portfolio.
-
- The total size of the portfolio is the unallocated balance and the
- allocated balance of the portfolio.
-
- Returns:
- float: The total size of the portfolio
- """
- return self.get_unallocated() + self.get_allocated()
-
def reset(self):
self._workers = []
self._running_workers = []
- def get_order(
- self,
- reference_id=None,
- market=None,
- target_symbol=None,
- trading_symbol=None,
- order_side=None,
- order_type=None
- ) -> Order:
- """
- Function to retrieve an order.
-
- Exception is thrown when no param has been provided.
-
- Args:
- reference_id [optional] (int): id given by the external
- market or exchange.
- market [optional] (str): the market that the order was
- executed on.
- target_symbol [optional] (str): the symbol of the asset
- that the order was executed
- """
- query_params = {}
-
- if reference_id:
- query_params["reference_id"] = reference_id
-
- if target_symbol:
- query_params["target_symbol"] = target_symbol
-
- if trading_symbol:
- query_params["trading_symbol"] = trading_symbol
-
- if order_side:
- query_params["order_side"] = order_side
-
- if order_type:
- query_params["order_type"] = order_type
-
- if market:
- portfolio = self.portfolio_service.find({"market": market})
- positions = self.position_service.get_all(
- {"portfolio": portfolio.id}
- )
- query_params["position"] = [position.id for position in positions]
-
- if not query_params:
- raise OperationalException(
- "No parameters provided to get order."
- )
-
- return self.order_service.find(query_params)
-
- def get_orders(
- self,
- target_symbol=None,
- status=None,
- order_type=None,
- order_side=None,
- market=None
- ) -> List[Order]:
-
- if market is None:
- portfolio = self.portfolio_service.get_all()[0]
- else:
- portfolio = self.portfolio_service.find({"market": market})
-
- positions = self.position_service.get_all({"portfolio": portfolio.id})
- return self.order_service.get_all(
- {
- "position": [position.id for position in positions],
- "target_symbol": target_symbol,
- "status": status,
- "order_type": order_type,
- "order_side": order_side
- }
- )
-
- def get_positions(
- self,
- market=None,
- identifier=None,
- amount_gt=None,
- amount_gte=None,
- amount_lt=None,
- amount_lte=None
- ) -> List[Position]:
- """
- Function to get all positions. This function will return all
- positions that match the specified query parameters. If the
- market parameter is specified, the positions of the specified
- market will be returned. If the identifier parameter is
- specified, the positions of the specified portfolio will be
- returned. If the amount_gt parameter is specified, the positions
- with an amount greater than the specified amount will be returned.
- If the amount_gte parameter is specified, the positions with an
- amount greater than or equal to the specified amount will be
- returned. If the amount_lt parameter is specified, the positions
- with an amount less than the specified amount will be returned.
- If the amount_lte parameter is specified, the positions with an
- amount less than or equal to the specified amount will be returned.
-
- Parameters:
- market: The market of the portfolio where the positions are
- identifier: The identifier of the portfolio
- amount_gt: The amount of the asset must be greater than this
- amount_gte: The amount of the asset must be greater than or
- equal to this
- amount_lt: The amount of the asset must be less than this
- amount_lte: The amount of the asset must be less than or equal
- to this
-
- Returns:
- List[Position]: A list of positions that match the query parameters
- """
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if identifier is not None:
- query_params["identifier"] = identifier
-
- if amount_gt is not None:
- query_params["amount_gt"] = amount_gt
-
- if amount_gte is not None:
- query_params["amount_gte"] = amount_gte
-
- if amount_lt is not None:
- query_params["amount_lt"] = amount_lt
-
- if amount_lte is not None:
- query_params["amount_lte"] = amount_lte
-
- portfolios = self.portfolio_service.get_all(query_params)
-
- if not portfolios:
- raise OperationalException("No portfolio found.")
-
- portfolio = portfolios[0]
- return self.position_service.get_all(
- {"portfolio": portfolio.id}
- )
-
- def get_position(self, symbol, market=None, identifier=None) -> Position:
- """
- Function to get a position. This function will return the
- position that matches the specified query parameters. If the
- market parameter is specified, the position of the specified
- market will be returned. If the identifier parameter is
- specified, the position of the specified portfolio will be
- returned.
-
- Parameters:
- symbol: The symbol of the asset that represents the position
- market: The market of the portfolio where the position is located
- identifier: The identifier of the portfolio
-
- Returns:
- Position: The position that matches the query parameters
- """
-
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if identifier is not None:
- query_params["identifier"] = identifier
-
- portfolios = self.portfolio_service.get_all(query_params)
-
- if not portfolios:
- raise OperationalException("No portfolio found.")
-
- portfolio = portfolios[0]
-
- try:
- return self.position_service.find(
- {"portfolio": portfolio.id, "symbol": symbol}
- )
- except OperationalException:
- return None
-
- def has_position(
- self,
- symbol,
- market=None,
- identifier=None,
- amount_gt=0,
- amount_gte=None,
- amount_lt=None,
- amount_lte=None
- ):
- """
- Function to check if a position exists. This function will return
- True if a position exists, False otherwise. This function will check
- if the amount > 0 condition by default.
-
- Parameters:
- param symbol: The symbol of the asset
- param market: The market of the asset
- param identifier: The identifier of the portfolio
- param amount_gt: The amount of the asset must be greater than this
- param amount_gte: The amount of the asset must be greater than
- or equal to this
- param amount_lt: The amount of the asset must be less than this
- param amount_lte: The amount of the asset must be less than
- or equal to this
-
- Returns:
- Boolean: True if a position exists, False otherwise
- """
-
- return self.position_exists(
- symbol=symbol,
- market=market,
- identifier=identifier,
- amount_gt=amount_gt,
- amount_gte=amount_gte,
- amount_lt=amount_lt,
- amount_lte=amount_lte
- )
-
- def position_exists(
- self,
- symbol,
- market=None,
- identifier=None,
- amount_gt=None,
- amount_gte=None,
- amount_lt=None,
- amount_lte=None
- ) -> bool:
- """
- Function to check if a position exists. This function will return
- True if a position exists, False otherwise. This function will
- not check the amount > 0 condition by default. If you want to
- check if a position exists with an amount greater than 0, you
- can use the amount_gt parameter. If you want to check if a
- position exists with an amount greater than or equal to a
- certain amount, you can use the amount_gte parameter. If you
- want to check if a position exists with an amount less than a
- certain amount, you can use the amount_lt parameter. If you want
- to check if a position exists with an amount less than or equal
- to a certain amount, you can use the amount_lte parameter.
-
- It is not recommended to use this method directly because it can
- have adverse effects on the algorithm. It is recommended to use
- the has_position method instead.
-
- param symbol: The symbol of the asset
- param market: The market of the asset
- param identifier: The identifier of the portfolio
- param amount_gt: The amount of the asset must be greater than this
- param amount_gte: The amount of the asset must be greater than
- or equal to this
- param amount_lt: The amount of the asset must be less than this
- param amount_lte: The amount of the asset must be less than
- or equal to this
-
- return: True if a position exists, False otherwise
- """
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if identifier is not None:
- query_params["identifier"] = identifier
-
- if amount_gt is not None:
- query_params["amount_gt"] = amount_gt
-
- if amount_gte is not None:
- query_params["amount_gte"] = amount_gte
-
- if amount_lt is not None:
- query_params["amount_lt"] = amount_lt
-
- if amount_lte is not None:
- query_params["amount_lte"] = amount_lte
-
- query_params["symbol"] = symbol
- return self.position_service.exists(query_params)
-
- def get_position_percentage_of_portfolio(
- self, symbol, market=None, identifier=None
- ) -> float:
- """
- Returns the percentage of the current total value of the portfolio
- that is allocated to a position. This is calculated by dividing
- the current value of the position by the total current value
- of the portfolio.
- """
-
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if identifier is not None:
- query_params["identifier"] = identifier
-
- portfolios = self.portfolio_service.get_all(query_params)
-
- if not portfolios:
- raise OperationalException("No portfolio found.")
-
- portfolio = portfolios[0]
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": symbol}
- )
- full_symbol = f"{position.symbol.upper()}/" \
- f"{portfolio.trading_symbol.upper()}"
- ticker = self._market_data_source_service.get_ticker(
- symbol=full_symbol, market=market
- )
- total = self.get_unallocated() + self.get_allocated()
- return (position.amount * ticker["bid"] / total) * 100
-
- def get_position_percentage_of_portfolio_by_net_size(
- self, symbol, market=None, identifier=None
- ) -> float:
- """
- Returns the percentage of the portfolio that is allocated to a
- position. This is calculated by dividing the cost of the position
- by the total net size of the portfolio.
-
- The total net size of the portfolio is the initial balance of the
- portfolio plus the all the net gains of your trades.
- """
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if identifier is not None:
- query_params["identifier"] = identifier
-
- portfolios = self.portfolio_service.get_all(query_params)
-
- if not portfolios:
- raise OperationalException("No portfolio found.")
-
- portfolio = portfolios[0]
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": symbol}
- )
- net_size = portfolio.get_net_size()
- return (position.cost / net_size) * 100
-
- def close_position(
- self, symbol, market=None, identifier=None, precision=None
- ):
- """
- Function to close a position. This function will close a position
- by creating a market order to sell the position. If the precision
- parameter is specified, the amount of the order will be rounded
- down to the specified precision.
-
- Parameters:
- symbol: The symbol of the asset
- market: The market of the asset
- identifier: The identifier of the portfolio
- precision: The precision of the amount
-
- Returns:
- None
- """
- portfolio = self.portfolio_service.find(
- {"market": market, "identifier": identifier}
- )
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": symbol}
- )
-
- if position.get_amount() == 0:
- return
-
- for order in self.order_service \
- .get_all(
- {
- "position": position.id,
- "status": OrderStatus.OPEN.value
- }
- ):
- self.order_service.cancel_order(order)
-
- symbol = f"{symbol.upper()}/{portfolio.trading_symbol.upper()}"
- ticker = self._market_data_source_service.get_ticker(
- symbol=symbol, market=market
- )
- self.create_limit_order(
- target_symbol=position.symbol,
- amount=position.get_amount(),
- order_side=OrderSide.SELL.value,
- price=ticker["bid"],
- precision=precision,
- )
-
def add_strategies(self, strategies):
"""
Function to add multiple strategies to the algorithm.
@@ -1058,472 +325,6 @@ def get_strategy(self, strategy_id):
def add_tasks(self, tasks):
self.strategy_orchestrator_service.add_tasks(tasks)
- def get_allocated(self, market=None, identifier=None) -> float:
-
- if self.portfolio_configuration_service.count() > 1 \
- and identifier is None and market is None:
- raise OperationalException(
- "Multiple portfolios found. Please specify a "
- "portfolio identifier."
- )
-
- if market is not None and identifier is not None:
- portfolio_configurations = self.portfolio_configuration_service \
- .get_all()
-
- else:
- query_params = {"market": market, "identifier": identifier}
- portfolio_configuration = self.portfolio_configuration_service \
- .find(query_params)
-
- if not portfolio_configuration:
- raise OperationalException("No portfolio found.")
-
- portfolio_configurations = [portfolio_configuration]
-
- if len(portfolio_configurations) == 0:
- raise OperationalException("No portfolio found.")
-
- portfolios = []
-
- for portfolio_configuration in portfolio_configurations:
- portfolio = self.portfolio_service.find(
- {"identifier": portfolio_configuration.identifier}
- )
- portfolio.configuration = portfolio_configuration
- portfolios.append(portfolio)
-
- allocated = 0
-
- for portfolio in portfolios:
- positions = self.position_service.get_all(
- {"portfolio": portfolio.id}
- )
-
- for position in positions:
- if portfolio.trading_symbol == position.symbol:
- continue
-
- symbol = f"{position.symbol.upper()}/" \
- f"{portfolio.trading_symbol.upper()}"
- ticker = self._market_data_source_service.get_ticker(
- symbol=symbol, market=market,
- )
- allocated = allocated + \
- (position.get_amount() * ticker["bid"])
-
- return allocated
-
- def get_unfilled(self, market=None, identifier=None) -> float:
-
- if self.portfolio_configuration_service.count() > 1 \
- and identifier is None and market is None:
- raise OperationalException(
- "Multiple portfolios found. Please specify a "
- "portfolio identifier."
- )
-
- if market is not None and identifier is not None:
- portfolio_configurations = self.portfolio_configuration_service \
- .get_all()
-
- else:
- query_params = {
- "market": market,
- "identifier": identifier
- }
- portfolio_configurations = [self.portfolio_configuration_service
- .find(query_params)]
-
- portfolios = []
-
- for portfolio_configuration in portfolio_configurations:
- portfolio = self.portfolio_service.find(
- {"identifier": portfolio_configuration.identifier}
- )
- portfolios.append(portfolio)
-
- unfilled = 0
-
- for portfolio in portfolios:
- orders = self.order_service.get_all(
- {"status": OrderStatus.OPEN.value, "portfolio": portfolio.id}
- )
- unfilled = unfilled + sum(
- [order.get_amount() * order.get_price() for order in orders]
- )
-
- return unfilled
-
- def get_portfolio_configurations(self):
- return self.portfolio_configuration_service.get_all()
-
- def has_open_buy_orders(self, target_symbol, identifier=None, market=None):
- query_params = {}
-
- if identifier is not None:
- portfolio = self.portfolio_service.find(
- {"identifier": identifier}
- )
- query_params["portfolio"] = portfolio.id
-
- if market is not None:
- portfolio = self.portfolio_service.find(
- {"market": market}
- )
- query_params["portfolio"] = portfolio.id
-
- query_params["target_symbol"] = target_symbol
- query_params["order_side"] = OrderSide.BUY.value
- query_params["status"] = OrderStatus.OPEN.value
- return self.order_service.exists(query_params)
-
- def has_open_sell_orders(self, target_symbol, identifier=None,
- market=None):
- query_params = {}
-
- if identifier is not None:
- portfolio = self.portfolio_service.find(
- {"identifier": identifier}
- )
- query_params["portfolio"] = portfolio.id
-
- if market is not None:
- portfolio = self.portfolio_service.find(
- {"market": market}
- )
- query_params["portfolio"] = portfolio.id
-
- query_params["target_symbol"] = target_symbol
- query_params["order_side"] = OrderSide.SELL.value
- query_params["status"] = OrderStatus.OPEN.value
- return self.order_service.exists(query_params)
-
- def has_open_orders(
- self, target_symbol=None, identifier=None, market=None
- ):
- query_params = {}
-
- if identifier is not None:
- portfolio = self.portfolio_service.find(
- {"identifier": identifier}
- )
- query_params["portfolio"] = portfolio.id
-
- if market is not None:
- portfolio = self.portfolio_service.find(
- {"market": market}
- )
- query_params["portfolio"] = portfolio.id
-
- if target_symbol is not None:
- query_params["target_symbol"] = target_symbol
-
- query_params["status"] = OrderStatus.OPEN.value
- return self.order_service.exists(query_params)
-
- def get_trade(
- self,
- target_symbol=None,
- trading_symbol=None,
- market=None,
- portfolio=None,
- status=None,
- order_id=None
- ) -> List[Trade]:
- """
- Function to get all trades. This function will return all trades
- that match the specified query parameters. If the market parameter
- is specified, the trades with the specified market will be returned.
-
- Args:
- market: The market of the asset
- portfolio: The portfolio of the asset
- status: The status of the trade
- order_id: The order id of the trade
- target_symbol: The symbol of the asset
- trading_symbol: The trading symbol of the asset
-
- Returns:
- List[Trade]: A list of trades that match the query parameters
- """
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if portfolio is not None:
- query_params["portfolio"] = portfolio
-
- if status is not None:
- query_params["status"] = status
-
- if order_id is not None:
- query_params["order_id"] = order_id
-
- if target_symbol is not None:
- query_params["target_symbol"] = target_symbol
-
- if trading_symbol is not None:
- query_params["trading_symbol"] = trading_symbol
-
- return self.trade_service.find(query_params)
-
- def get_trades(
- self,
- target_symbol=None,
- trading_symbol=None,
- market=None,
- portfolio=None,
- status=None,
- ) -> List[Trade]:
- """
- Function to get all trades. This function will return all trades
- that match the specified query parameters. If the market parameter
- is specified, the trades with the specified market will be returned.
-
- Args:
- market: The market of the asset
- portfolio: The portfolio of the asset
- status: The status of the trade
- target_symbol: The symbol of the asset
- trading_symbol: The trading symbol of the asset
-
- Returns:
- List[Trade]: A list of trades that match the query parameters
- """
-
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if portfolio is not None:
- query_params["portfolio"] = portfolio
-
- if status is not None:
- query_params["status"] = status
-
- if target_symbol is not None:
- query_params["target_symbol"] = target_symbol
-
- if trading_symbol is not None:
- query_params["trading_symbol"] = trading_symbol
-
- return self.trade_service.get_all({"market": market})
-
- def get_closed_trades(self) -> List[Trade]:
- """
- Function to get all closed trades. This function will return all
- closed trades of the algorithm.
-
- Returns:
- List[Trade]: A list of closed trades
- """
- return self.trade_service.get_all({"status": TradeStatus.CLOSED.value})
-
- def count_trades(
- self,
- target_symbol=None,
- trading_symbol=None,
- market=None,
- portfolio=None
- ) -> int:
- """
- Function to count trades. This function will return the number of
- trades that match the specified query parameters.
-
- Args:
- target_symbol: The symbol of the asset
- trading_symbol: The trading symbol of the asset
- market: The market of the asset
- portfolio: The portfolio of the asset
-
- Returns:
- int: The number of trades that match the query parameters
- """
-
- query_params = {}
-
- if market is not None:
- query_params["market"] = market
-
- if portfolio is not None:
- query_params["portfolio"] = portfolio
-
- if target_symbol is not None:
- query_params["target_symbol"] = target_symbol
-
- if trading_symbol is not None:
- query_params["trading_symbol"] = trading_symbol
-
- return self.trade_service.count(query_params)
-
- def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]:
- """
- Function to get all open trades. This function will return all
- open trades that match the specified query parameters. If the
- target_symbol parameter is specified, the open trades with the
- specified target symbol will be returned. If the market parameter
- is specified, the open trades with the specified market will be
- returned.
-
- Args:
- target_symbol: The symbol of the asset
- market: The market of the asset
-
- Returns:
- List[Trade]: A list of open trades that match the query parameters
- """
- return self.trade_service.get_all(
- {
- "status": TradeStatus.OPEN.value,
- "target_symbol": target_symbol,
- "market": market
- }
- )
-
- def add_stop_loss(self, trade, percentage: int) -> None:
- """
- Function to add a stop loss to a trade. This function will add a
- stop loss to the specified trade. If the stop loss is triggered,
- the trade will be closed.
-
- Args:
- trade: Trade - The trade to add the stop loss to
- percentage: int - The stop loss of the trade
-
- Returns:
- None
- """
- self.trade_service.add_stop_loss(trade, percentage=percentage)
-
- def add_trailing_stop_loss(self, trade, percentage: int) -> None:
- """
- Function to add a trailing stop loss to a trade. This function will
- add a trailing stop loss to the specified trade. If the trailing
- stop loss is triggered, the trade will be closed.
-
- Args:
- trade: Trade - The trade to add the trailing stop loss to
- trailing_stop_loss: float - The trailing stop loss of the trade
-
- Returns:
- None
- """
- self.trade_service.add_trailing_stop_loss(trade, percentage=percentage)
-
- def close_trade(self, trade, precision=None) -> None:
- """
- Function to close a trade. This function will close a trade by
- creating a market order to sell the position. If the precision
- parameter is specified, the amount of the order will be rounded
- down to the specified precision.
-
- Args:
- trade: Trade - The trade to close
- precision: int - The precision of the amount
-
- Returns:
- None
- """
-
- trade = self.trade_service.get(trade.id)
-
- if TradeStatus.CLOSED.equals(trade.status):
- raise OperationalException("Trade already closed.")
-
- if trade.remaining <= 0:
- raise OperationalException("Trade has no amount to close.")
-
- position_id = trade.orders[0].position_id
- portfolio = self.portfolio_service.find({"position": position_id})
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": trade.target_symbol}
- )
- amount = trade.remaining
-
- if precision is not None:
- amount = RoundingService.round_down(amount, precision)
-
- if position.get_amount() < amount:
- logger.warning(
- f"Order amount {amount} is larger then amount "
- f"of available {position.symbol} "
- f"position: {position.get_amount()}, "
- f"changing order amount to size of position"
- )
- amount = position.get_amount()
-
- ticker = self._market_data_source_service.get_ticker(
- symbol=trade.symbol, market=portfolio.market
- )
-
- self.order_service.create(
- {
- "portfolio_id": portfolio.id,
- "trading_symbol": trade.trading_symbol,
- "target_symbol": trade.target_symbol,
- "amount": amount,
- "order_side": OrderSide.SELL.value,
- "order_type": OrderType.LIMIT.value,
- "price": ticker["bid"],
- }
- )
-
- def get_number_of_positions(self):
- """
- Returns the number of positions that have a positive amount.
-
- Returns:
- int: The number of positions
- """
- return self.position_service.count({"amount_gt": 0})
-
- def has_trading_symbol_position_available(
- self,
- amount_gt=None,
- amount_gte=None,
- percentage_of_portfolio=None,
- market=None
- ):
- """
- Checks if there is a position available for the trading symbol of the
- portfolio. If the amount_gt or amount_gte parameters are specified,
- the amount of the position must be greater than the specified amount.
- If the percentage_of_portfolio parameter is specified, the amount of
- the position must be greater than the net_size of the
- portfolio.
-
- Parameters:
- amount_gt: The amount of the position must be greater than this
- amount.
- :param amount_gte: The amount of the position must be greater than
- or equal to this amount.
- :param percentage_of_portfolio: The amount of the position must be
- greater than the net_size of the portfolio.
- :param market: The market of the portfolio.
- :return: True if there is a trading symbol position available with the
- specified parameters, False otherwise.
- """
- portfolio = self.portfolio_service.find({"market": market})
- position = self.position_service.find(
- {"portfolio": portfolio.id, "symbol": portfolio.trading_symbol}
- )
-
- if amount_gt is not None:
- return position.get_amount() > amount_gt
-
- if amount_gte is not None:
- return position.get_amount() >= amount_gte
-
- if percentage_of_portfolio is not None:
- net_size = portfolio.get_net_size()
- return position.get_amount() >= net_size \
- * percentage_of_portfolio / 100
-
- return position.get_amount() > 0
-
def strategy(
self,
function=None,
@@ -1575,53 +376,6 @@ def add_data_sources(self, data_sources):
def tasks(self):
return self._tasks
- def get_pending_orders(
- self, order_side=None, target_symbol=None, portfolio_id=None
- ):
- """
- Function to get all pending orders of the algorithm. If the
- portfolio_id parameter is specified, the function will return
- all pending orders of the portfolio with the specified id.
- """
- query_params = {}
-
- if portfolio_id:
- query_params["portfolio"] = portfolio_id
-
- if target_symbol:
- query_params["target_symbol"] = target_symbol
-
- if order_side:
- query_params["order_side"] = order_side
-
- return self.order_service.get_all({"status": OrderStatus.OPEN.value})
-
- def get_unfilled_buy_value(self):
- """
- Returns the total value of all unfilled buy orders.
- """
- pending_orders = self.get_pending_orders(
- order_side=OrderSide.BUY.value
- )
-
- return sum(
- [order.get_amount() * order.get_price()
- for order in pending_orders]
- )
-
- def get_unfilled_sell_value(self):
- """
- Returns the total value of all unfilled buy orders.
- """
- pending_orders = self.get_pending_orders(
- order_side=OrderSide.SELL.value
- )
-
- return sum(
- [order.get_amount() * order.get_price()
- for order in pending_orders]
- )
-
def get_trade_service(self):
return self.trade_service
diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py
index a28bed4b..b6dc5aa3 100644
--- a/investing_algorithm_framework/app/app.py
+++ b/investing_algorithm_framework/app/app.py
@@ -40,10 +40,9 @@ def on_run(self, app, algorithm: Algorithm):
class App:
- def __init__(self, state_handler=None):
+ def __init__(self, state_handler=None, name=None):
self._flask_app: Optional[Flask] = None
self.container = None
- self._algorithm: Optional[Algorithm] = None
self._started = False
self._tasks = []
self._configuration_service = None
@@ -55,6 +54,47 @@ def __init__(self, state_handler=None):
self._on_initialize_hooks = []
self._on_after_initialize_hooks = []
self._state_handler = state_handler
+ self._name = name
+ self._algorithm = Algorithm()
+
+ @property
+ def algorithm(self) -> Algorithm:
+ return self._algorithm
+
+ @property
+ def context(self):
+ return self.container.context()
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ self._name = name
+
+ @property
+ def started(self):
+ return self._started
+
+ @property
+ def config(self):
+ """
+ Function to get a config instance. This allows users when
+ having access to the app instance also to read the
+ configs of the app.
+ """
+ configuration_service = self.container.configuration_service()
+ return configuration_service.config
+
+ @config.setter
+ def config(self, config: dict):
+ configuration_service = self.container.configuration_service()
+ configuration_service.initialize_from_dict(config)
+
+ @property
+ def running(self):
+ return self.algorithm.running
def add_algorithm(self, algorithm: Algorithm) -> None:
"""
@@ -79,10 +119,6 @@ def initialize_services(self) -> None:
self._market_credential_service = \
self.container.market_credential_service()
- @property
- def algorithm(self) -> Algorithm:
- return self._algorithm
-
@algorithm.setter
def algorithm(self, algorithm: Algorithm) -> None:
self._algorithm = algorithm
@@ -289,6 +325,7 @@ def initialize(self):
raise OperationalException("No portfolios configured")
self.algorithm.initialize_services(
+ context=self.container.context(),
configuration_service=self.container.configuration_service(),
market_data_source_service=self._market_data_source_service,
market_credential_service=self.container
@@ -474,11 +511,13 @@ def run(
logger.info("Checking pending orders")
number_of_iterations_since_last_orders_check = 1
- self.algorithm.run_jobs()
+ self.algorithm.run_jobs(context=self.container.context())
number_of_iterations_since_last_orders_check += 1
sleep(1)
except KeyboardInterrupt:
exit(0)
+ except Exception as e:
+ logger.error(e)
finally:
self.algorithm.stop()
@@ -488,25 +527,6 @@ def run(
config = self.container.configuration_service().get_config()
self._state_handler.save(config[RESOURCE_DIRECTORY])
- @property
- def started(self):
- return self._started
-
- @property
- def config(self):
- """
- Function to get a config instance. This allows users when
- having access to the app instance also to read the
- configs of the app.
- """
- configuration_service = self.container.configuration_service()
- return configuration_service.config
-
- @config.setter
- def config(self, config: dict):
- configuration_service = self.container.configuration_service()
- configuration_service.initialize_from_dict(config)
-
def reset(self):
self._started = False
self.algorithm.reset()
@@ -516,10 +536,6 @@ def add_portfolio_configuration(self, portfolio_configuration):
.portfolio_configuration_service()
portfolio_configuration_service.add(portfolio_configuration)
- @property
- def running(self):
- return self.algorithm.running
-
def task(
self,
function=None,
@@ -863,6 +879,10 @@ def after_initialize(self, app_hook: AppHook):
self._on_after_initialize_hooks.append(app_hook)
- def clear(self) -> None:
- self.algorithm = Algorithm()
- self.mark
+ def add_strategy(self, strategy):
+ """
+ """
+ if self.algorithm is None:
+ self.algorithm = Algorithm(name=self._name)
+
+ self.algorithm.add_strategy(strategy)
diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py
new file mode 100644
index 00000000..6b539491
--- /dev/null
+++ b/investing_algorithm_framework/app/context.py
@@ -0,0 +1,1296 @@
+import logging
+from typing import List
+
+from investing_algorithm_framework.services import ConfigurationService, \
+ MarketCredentialService, MarketDataSourceService, \
+ OrderService, PortfolioConfigurationService, PortfolioService, \
+ PositionService, TradeService
+from investing_algorithm_framework.domain import OrderStatus, OrderType, \
+ OrderSide, OperationalException, Portfolio, RoundingService, \
+ BACKTESTING_FLAG, BACKTESTING_INDEX_DATETIME, TradeRiskType, Order, \
+ Position, Trade, TradeStatus, MarketService
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
+class Context:
+ """
+ Context class to store the state of the algorithm and
+ give access to objects such as orders, positions, trades and
+ portfolio.
+ """
+
+ def __init__(
+ self,
+ configuration_service: ConfigurationService,
+ portfolio_configuration_service: PortfolioConfigurationService,
+ portfolio_service: PortfolioService,
+ position_service: PositionService,
+ order_service: OrderService,
+ market_credential_service: MarketCredentialService,
+ market_data_source_service: MarketDataSourceService,
+ market_service: MarketService,
+ trade_service: TradeService,
+ ):
+ self.configuration_service: ConfigurationService = \
+ configuration_service
+ self.portfolio_configuration_service: PortfolioConfigurationService = \
+ portfolio_configuration_service
+ self.portfolio_service: PortfolioService = portfolio_service
+ self.position_service: PositionService = position_service
+ self.order_service: OrderService = order_service
+ self.market_credential_service: MarketCredentialService = \
+ market_credential_service
+ self.market_data_source_service: MarketDataSourceService = \
+ market_data_source_service
+ self.market_service: MarketService = market_service
+ self.trade_service: TradeService = trade_service
+
+ @property
+ def config(self):
+ """
+ Function to get a config instance. This allows users when
+ having access to the algorithm instance also to read the
+ configs of the app.
+ """
+ return self.configuration_service.get_config()
+
+ def get_config(self):
+ """
+ Function to get a config instance. This allows users when
+ having access to the algorithm instance also to read the
+ configs of the app.
+ """
+ return self.configuration_service.get_config()
+
+ def create_order(
+ self,
+ target_symbol,
+ price,
+ order_type,
+ order_side,
+ amount,
+ market=None,
+ execute=True,
+ validate=True,
+ sync=True
+ ):
+ """
+ Function to create an order. This function will create an 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
+
+ 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 = {
+ "target_symbol": target_symbol,
+ "price": price,
+ "amount": amount,
+ "order_type": order_type,
+ "order_side": order_side,
+ "portfolio_id": portfolio.id,
+ "status": OrderStatus.CREATED.value,
+ "trading_symbol": portfolio.trading_symbol,
+ }
+
+ if BACKTESTING_FLAG in self.configuration_service.config \
+ and self.configuration_service.config[BACKTESTING_FLAG]:
+ order_data["created_at"] = \
+ self.configuration_service.config[BACKTESTING_INDEX_DATETIME]
+
+ return self.order_service.create(
+ order_data, execute=execute, validate=validate, sync=sync
+ )
+
+ def has_balance(self, symbol, amount, market=None):
+ """
+ Function to check if the portfolio has enough balance to
+ create an order. This function will return True if the
+ portfolio has enough balance to create an order, False
+ otherwise.
+
+ Parameters:
+ symbol: The symbol of the asset
+ amount: The amount of the asset
+ market: The market of the asset
+
+ Returns:
+ Boolean: True if the portfolio has enough balance
+ """
+
+ portfolio = self.portfolio_service.find({"market": market})
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": symbol}
+ )
+
+ if position is None:
+ return False
+
+ return position.get_amount() >= amount
+
+ def create_limit_order(
+ self,
+ target_symbol,
+ price,
+ order_side,
+ amount=None,
+ amount_trading_symbol=None,
+ percentage=None,
+ percentage_of_portfolio=None,
+ percentage_of_position=None,
+ precision=None,
+ market=None,
+ execute=True,
+ validate=True,
+ sync=True
+ ):
+ """
+ Function to create a limit order. This function will create a limit
+ 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
+
+ Args:
+ target_symbol: The symbol of the asset to trade
+ price: The price of the asset
+ order_side: The side of the order
+ amount (optional): The amount of the asset to trade
+ amount_trading_symbol (optional): The amount of the
+ trading symbol to trade
+ percentage (optional): The percentage of the portfolio
+ to allocate to the
+ order
+ percentage_of_portfolio (optional): The percentage
+ of the portfolio to allocate to the order
+ percentage_of_position (optional): The percentage
+ of the position to allocate to
+ the order. (Only supported for SELL orders)
+ precision (optional): The precision of the amount
+ market (optional): The market to trade the asset
+ execute (optional): Default True. If set to True,
+ the order will be executed
+ validate (optional): Default True. If set to
+ True, the order will be validated
+ sync (optional): Default True. If set to True,
+ the created order will be synced with the
+ portfolio of the algorithm
+
+ Returns:
+ Order: Instance of the order created
+ """
+ portfolio = self.portfolio_service.find({"market": market})
+
+ if percentage_of_portfolio is not None:
+ if not OrderSide.BUY.equals(order_side):
+ raise OperationalException(
+ "Percentage of portfolio is only supported for BUY orders."
+ )
+
+ net_size = portfolio.get_net_size()
+ size = net_size * (percentage_of_portfolio / 100)
+ amount = size / price
+
+ elif percentage_of_position is not None:
+
+ if not OrderSide.SELL.equals(order_side):
+ raise OperationalException(
+ "Percentage of position is only supported for SELL orders."
+ )
+
+ position = self.position_service.find(
+ {
+ "symbol": target_symbol,
+ "portfolio": portfolio.id
+ }
+ )
+ amount = position.get_amount() * (percentage_of_position / 100)
+
+ elif percentage is not None:
+ net_size = portfolio.get_net_size()
+ size = net_size * (percentage / 100)
+ amount = size / price
+
+ if precision is not None:
+ amount = RoundingService.round_down(amount, precision)
+
+ if amount_trading_symbol is not None:
+ amount = amount_trading_symbol / price
+
+ if amount is None:
+ raise OperationalException(
+ "The amount parameter is required to create a limit order." +
+ "Either the amount, amount_trading_symbol, percentage, " +
+ "percentage_of_portfolio or percentage_of_position "
+ "parameter must be specified."
+ )
+
+ order_data = {
+ "target_symbol": target_symbol,
+ "price": price,
+ "amount": amount,
+ "order_type": OrderType.LIMIT.value,
+ "order_side": OrderSide.from_value(order_side).value,
+ "portfolio_id": portfolio.id,
+ "status": OrderStatus.CREATED.value,
+ "trading_symbol": portfolio.trading_symbol,
+ }
+
+ if BACKTESTING_FLAG in self.configuration_service.config \
+ and self.configuration_service.config[BACKTESTING_FLAG]:
+ order_data["created_at"] = \
+ self.configuration_service.config[BACKTESTING_INDEX_DATETIME]
+
+ return self.order_service.create(
+ order_data, execute=execute, validate=validate, sync=sync
+ )
+
+ def get_portfolio(self, market=None) -> Portfolio:
+ """
+ Function to get the portfolio of the algorithm. This function
+ will return the portfolio of the algorithm. If the market
+ parameter is specified, the portfolio of the specified market
+ will be returned.
+
+ Parameters:
+ market: The market of the portfolio
+
+ Returns:
+ Portfolio: The portfolio of the algorithm
+ """
+
+ if market is None:
+ return self.portfolio_service.get_all()[0]
+
+ return self.portfolio_service.find({{"market": market}})
+
+ def get_portfolios(self):
+ """
+ Function to get all portfolios of the algorithm. This function
+ will return all portfolios of the algorithm.
+
+ Returns:
+ List[Portfolio]: A list of all portfolios of the algorithm
+ """
+ return self.portfolio_service.get_all()
+
+ def get_unallocated(self, market=None) -> float:
+ """
+ Function to get the unallocated balance of the portfolio. This
+ function will return the unallocated balance of the portfolio.
+ If the market parameter is specified, the unallocated balance
+ of the specified market will be returned.
+
+ Args:
+ market: The market of the portfolio
+
+ Returns:
+ float: The unallocated balance of the portfolio
+ """
+
+ if market:
+ portfolio = self.portfolio_service.find({{"market": market}})
+ else:
+ portfolio = self.portfolio_service.get_all()[0]
+
+ trading_symbol = portfolio.trading_symbol
+ return self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": trading_symbol}
+ ).get_amount()
+
+ def get_total_size(self):
+ """
+ Returns the total size of the portfolio.
+
+ The total size of the portfolio is the unallocated balance and the
+ allocated balance of the portfolio.
+
+ Returns:
+ float: The total size of the portfolio
+ """
+ return self.get_unallocated() + self.get_allocated()
+
+ def get_order(
+ self,
+ reference_id=None,
+ market=None,
+ target_symbol=None,
+ trading_symbol=None,
+ order_side=None,
+ order_type=None
+ ) -> Order:
+ """
+ Function to retrieve an order.
+
+ Exception is thrown when no param has been provided.
+
+ Args:
+ reference_id [optional] (int): id given by the external
+ market or exchange.
+ market [optional] (str): the market that the order was
+ executed on.
+ target_symbol [optional] (str): the symbol of the asset
+ that the order was executed
+ """
+ query_params = {}
+
+ if reference_id:
+ query_params["reference_id"] = reference_id
+
+ if target_symbol:
+ query_params["target_symbol"] = target_symbol
+
+ if trading_symbol:
+ query_params["trading_symbol"] = trading_symbol
+
+ if order_side:
+ query_params["order_side"] = order_side
+
+ if order_type:
+ query_params["order_type"] = order_type
+
+ if market:
+ portfolio = self.portfolio_service.find({"market": market})
+ positions = self.position_service.get_all(
+ {"portfolio": portfolio.id}
+ )
+ query_params["position"] = [position.id for position in positions]
+
+ if not query_params:
+ raise OperationalException(
+ "No parameters provided to get order."
+ )
+
+ return self.order_service.find(query_params)
+
+ def get_orders(
+ self,
+ target_symbol=None,
+ status=None,
+ order_type=None,
+ order_side=None,
+ market=None
+ ) -> List[Order]:
+
+ if market is None:
+ portfolio = self.portfolio_service.get_all()[0]
+ else:
+ portfolio = self.portfolio_service.find({"market": market})
+
+ positions = self.position_service.get_all({"portfolio": portfolio.id})
+ return self.order_service.get_all(
+ {
+ "position": [position.id for position in positions],
+ "target_symbol": target_symbol,
+ "status": status,
+ "order_type": order_type,
+ "order_side": order_side
+ }
+ )
+
+ def get_positions(
+ self,
+ market=None,
+ identifier=None,
+ amount_gt=None,
+ amount_gte=None,
+ amount_lt=None,
+ amount_lte=None
+ ) -> List[Position]:
+ """
+ Function to get all positions. This function will return all
+ positions that match the specified query parameters. If the
+ market parameter is specified, the positions of the specified
+ market will be returned. If the identifier parameter is
+ specified, the positions of the specified portfolio will be
+ returned. If the amount_gt parameter is specified, the positions
+ with an amount greater than the specified amount will be returned.
+ If the amount_gte parameter is specified, the positions with an
+ amount greater than or equal to the specified amount will be
+ returned. If the amount_lt parameter is specified, the positions
+ with an amount less than the specified amount will be returned.
+ If the amount_lte parameter is specified, the positions with an
+ amount less than or equal to the specified amount will be returned.
+
+ Parameters:
+ market: The market of the portfolio where the positions are
+ identifier: The identifier of the portfolio
+ amount_gt: The amount of the asset must be greater than this
+ amount_gte: The amount of the asset must be greater than or
+ equal to this
+ amount_lt: The amount of the asset must be less than this
+ amount_lte: The amount of the asset must be less than or equal
+ to this
+
+ Returns:
+ List[Position]: A list of positions that match the query parameters
+ """
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if identifier is not None:
+ query_params["identifier"] = identifier
+
+ if amount_gt is not None:
+ query_params["amount_gt"] = amount_gt
+
+ if amount_gte is not None:
+ query_params["amount_gte"] = amount_gte
+
+ if amount_lt is not None:
+ query_params["amount_lt"] = amount_lt
+
+ if amount_lte is not None:
+ query_params["amount_lte"] = amount_lte
+
+ portfolios = self.portfolio_service.get_all(query_params)
+
+ if not portfolios:
+ raise OperationalException("No portfolio found.")
+
+ portfolio = portfolios[0]
+ return self.position_service.get_all(
+ {"portfolio": portfolio.id}
+ )
+
+ def get_position(self, symbol, market=None, identifier=None) -> Position:
+ """
+ Function to get a position. This function will return the
+ position that matches the specified query parameters. If the
+ market parameter is specified, the position of the specified
+ market will be returned. If the identifier parameter is
+ specified, the position of the specified portfolio will be
+ returned.
+
+ Parameters:
+ symbol: The symbol of the asset that represents the position
+ market: The market of the portfolio where the position is located
+ identifier: The identifier of the portfolio
+
+ Returns:
+ Position: The position that matches the query parameters
+ """
+
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if identifier is not None:
+ query_params["identifier"] = identifier
+
+ portfolios = self.portfolio_service.get_all(query_params)
+
+ if not portfolios:
+ raise OperationalException("No portfolio found.")
+
+ portfolio = portfolios[0]
+
+ try:
+ return self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": symbol}
+ )
+ except OperationalException:
+ return None
+
+ def has_position(
+ self,
+ symbol,
+ market=None,
+ identifier=None,
+ amount_gt=0,
+ amount_gte=None,
+ amount_lt=None,
+ amount_lte=None
+ ):
+ """
+ Function to check if a position exists. This function will return
+ True if a position exists, False otherwise. This function will check
+ if the amount > 0 condition by default.
+
+ Parameters:
+ param symbol: The symbol of the asset
+ param market: The market of the asset
+ param identifier: The identifier of the portfolio
+ param amount_gt: The amount of the asset must be greater than this
+ param amount_gte: The amount of the asset must be greater than
+ or equal to this
+ param amount_lt: The amount of the asset must be less than this
+ param amount_lte: The amount of the asset must be less than
+ or equal to this
+
+ Returns:
+ Boolean: True if a position exists, False otherwise
+ """
+
+ return self.position_exists(
+ symbol=symbol,
+ market=market,
+ identifier=identifier,
+ amount_gt=amount_gt,
+ amount_gte=amount_gte,
+ amount_lt=amount_lt,
+ amount_lte=amount_lte
+ )
+
+ def position_exists(
+ self,
+ symbol,
+ market=None,
+ identifier=None,
+ amount_gt=None,
+ amount_gte=None,
+ amount_lt=None,
+ amount_lte=None
+ ) -> bool:
+ """
+ Function to check if a position exists. This function will return
+ True if a position exists, False otherwise. This function will
+ not check the amount > 0 condition by default. If you want to
+ check if a position exists with an amount greater than 0, you
+ can use the amount_gt parameter. If you want to check if a
+ position exists with an amount greater than or equal to a
+ certain amount, you can use the amount_gte parameter. If you
+ want to check if a position exists with an amount less than a
+ certain amount, you can use the amount_lt parameter. If you want
+ to check if a position exists with an amount less than or equal
+ to a certain amount, you can use the amount_lte parameter.
+
+ It is not recommended to use this method directly because it can
+ have adverse effects on the algorithm. It is recommended to use
+ the has_position method instead.
+
+ param symbol: The symbol of the asset
+ param market: The market of the asset
+ param identifier: The identifier of the portfolio
+ param amount_gt: The amount of the asset must be greater than this
+ param amount_gte: The amount of the asset must be greater than
+ or equal to this
+ param amount_lt: The amount of the asset must be less than this
+ param amount_lte: The amount of the asset must be less than
+ or equal to this
+
+ return: True if a position exists, False otherwise
+ """
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if identifier is not None:
+ query_params["identifier"] = identifier
+
+ if amount_gt is not None:
+ query_params["amount_gt"] = amount_gt
+
+ if amount_gte is not None:
+ query_params["amount_gte"] = amount_gte
+
+ if amount_lt is not None:
+ query_params["amount_lt"] = amount_lt
+
+ if amount_lte is not None:
+ query_params["amount_lte"] = amount_lte
+
+ query_params["symbol"] = symbol
+ return self.position_service.exists(query_params)
+
+ def get_position_percentage_of_portfolio(
+ self, symbol, market=None, identifier=None
+ ) -> float:
+ """
+ Returns the percentage of the current total value of the portfolio
+ that is allocated to a position. This is calculated by dividing
+ the current value of the position by the total current value
+ of the portfolio.
+ """
+
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if identifier is not None:
+ query_params["identifier"] = identifier
+
+ portfolios = self.portfolio_service.get_all(query_params)
+
+ if not portfolios:
+ raise OperationalException("No portfolio found.")
+
+ portfolio = portfolios[0]
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": symbol}
+ )
+ full_symbol = f"{position.symbol.upper()}/" \
+ f"{portfolio.trading_symbol.upper()}"
+ ticker = self.market_data_source_service.get_ticker(
+ symbol=full_symbol, market=market
+ )
+ total = self.get_unallocated() + self.get_allocated()
+ return (position.amount * ticker["bid"] / total) * 100
+
+ def get_position_percentage_of_portfolio_by_net_size(
+ self, symbol, market=None, identifier=None
+ ) -> float:
+ """
+ Returns the percentage of the portfolio that is allocated to a
+ position. This is calculated by dividing the cost of the position
+ by the total net size of the portfolio.
+
+ The total net size of the portfolio is the initial balance of the
+ portfolio plus the all the net gains of your trades.
+ """
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if identifier is not None:
+ query_params["identifier"] = identifier
+
+ portfolios = self.portfolio_service.get_all(query_params)
+
+ if not portfolios:
+ raise OperationalException("No portfolio found.")
+
+ portfolio = portfolios[0]
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": symbol}
+ )
+ net_size = portfolio.get_net_size()
+ return (position.cost / net_size) * 100
+
+ def close_position(
+ self, symbol, market=None, identifier=None, precision=None
+ ):
+ """
+ Function to close a position. This function will close a position
+ by creating a market order to sell the position. If the precision
+ parameter is specified, the amount of the order will be rounded
+ down to the specified precision.
+
+ Parameters:
+ symbol: The symbol of the asset
+ market: The market of the asset
+ identifier: The identifier of the portfolio
+ precision: The precision of the amount
+
+ Returns:
+ None
+ """
+ portfolio = self.portfolio_service.find(
+ {"market": market, "identifier": identifier}
+ )
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": symbol}
+ )
+
+ if position.get_amount() == 0:
+ return
+
+ for order in self.order_service \
+ .get_all(
+ {
+ "position": position.id,
+ "status": OrderStatus.OPEN.value
+ }
+ ):
+ self.order_service.cancel_order(order)
+
+ symbol = f"{symbol.upper()}/{portfolio.trading_symbol.upper()}"
+ ticker = self.market_data_source_service.get_ticker(
+ symbol=symbol, market=market
+ )
+ self.create_limit_order(
+ target_symbol=position.symbol,
+ amount=position.get_amount(),
+ order_side=OrderSide.SELL.value,
+ price=ticker["bid"],
+ precision=precision,
+ )
+
+ def get_allocated(self, market=None, identifier=None) -> float:
+
+ if self.portfolio_configuration_service.count() > 1 \
+ and identifier is None and market is None:
+ raise OperationalException(
+ "Multiple portfolios found. Please specify a "
+ "portfolio identifier."
+ )
+
+ if market is not None and identifier is not None:
+ portfolio_configurations = self.portfolio_configuration_service \
+ .get_all()
+
+ else:
+ query_params = {"market": market, "identifier": identifier}
+ portfolio_configuration = self.portfolio_configuration_service \
+ .find(query_params)
+
+ if not portfolio_configuration:
+ raise OperationalException("No portfolio found.")
+
+ portfolio_configurations = [portfolio_configuration]
+
+ if len(portfolio_configurations) == 0:
+ raise OperationalException("No portfolio found.")
+
+ portfolios = []
+
+ for portfolio_configuration in portfolio_configurations:
+ portfolio = self.portfolio_service.find(
+ {"identifier": portfolio_configuration.identifier}
+ )
+ portfolio.configuration = portfolio_configuration
+ portfolios.append(portfolio)
+
+ allocated = 0
+
+ for portfolio in portfolios:
+ positions = self.position_service.get_all(
+ {"portfolio": portfolio.id}
+ )
+
+ for position in positions:
+ if portfolio.trading_symbol == position.symbol:
+ continue
+
+ symbol = f"{position.symbol.upper()}/" \
+ f"{portfolio.trading_symbol.upper()}"
+ ticker = self.market_data_source_service.get_ticker(
+ symbol=symbol, market=market,
+ )
+ allocated = allocated + \
+ (position.get_amount() * ticker["bid"])
+
+ return allocated
+
+ def get_unfilled(self, market=None, identifier=None) -> float:
+
+ if self.portfolio_configuration_service.count() > 1 \
+ and identifier is None and market is None:
+ raise OperationalException(
+ "Multiple portfolios found. Please specify a "
+ "portfolio identifier."
+ )
+
+ if market is not None and identifier is not None:
+ portfolio_configurations = self.portfolio_configuration_service \
+ .get_all()
+
+ else:
+ query_params = {
+ "market": market,
+ "identifier": identifier
+ }
+ portfolio_configurations = [self.portfolio_configuration_service
+ .find(query_params)]
+
+ portfolios = []
+
+ for portfolio_configuration in portfolio_configurations:
+ portfolio = self.portfolio_service.find(
+ {"identifier": portfolio_configuration.identifier}
+ )
+ portfolios.append(portfolio)
+
+ unfilled = 0
+
+ for portfolio in portfolios:
+ orders = self.order_service.get_all(
+ {"status": OrderStatus.OPEN.value, "portfolio": portfolio.id}
+ )
+ unfilled = unfilled + sum(
+ [order.get_amount() * order.get_price() for order in orders]
+ )
+
+ return unfilled
+
+ def get_portfolio_configurations(self):
+ return self.portfolio_configuration_service.get_all()
+
+ def has_open_buy_orders(self, target_symbol, identifier=None, market=None):
+ query_params = {}
+
+ if identifier is not None:
+ portfolio = self.portfolio_service.find(
+ {"identifier": identifier}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ if market is not None:
+ portfolio = self.portfolio_service.find(
+ {"market": market}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ query_params["target_symbol"] = target_symbol
+ query_params["order_side"] = OrderSide.BUY.value
+ query_params["status"] = OrderStatus.OPEN.value
+ return self.order_service.exists(query_params)
+
+ def has_open_sell_orders(self, target_symbol, identifier=None,
+ market=None):
+ query_params = {}
+
+ if identifier is not None:
+ portfolio = self.portfolio_service.find(
+ {"identifier": identifier}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ if market is not None:
+ portfolio = self.portfolio_service.find(
+ {"market": market}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ query_params["target_symbol"] = target_symbol
+ query_params["order_side"] = OrderSide.SELL.value
+ query_params["status"] = OrderStatus.OPEN.value
+ return self.order_service.exists(query_params)
+
+ def has_open_orders(
+ self, target_symbol=None, identifier=None, market=None
+ ):
+ query_params = {}
+
+ if identifier is not None:
+ portfolio = self.portfolio_service.find(
+ {"identifier": identifier}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ if market is not None:
+ portfolio = self.portfolio_service.find(
+ {"market": market}
+ )
+ query_params["portfolio"] = portfolio.id
+
+ if target_symbol is not None:
+ query_params["target_symbol"] = target_symbol
+
+ query_params["status"] = OrderStatus.OPEN.value
+ return self.order_service.exists(query_params)
+
+ def get_trade(
+ self,
+ target_symbol=None,
+ trading_symbol=None,
+ market=None,
+ portfolio=None,
+ status=None,
+ order_id=None
+ ) -> List[Trade]:
+ """
+ Function to get all trades. This function will return all trades
+ that match the specified query parameters. If the market parameter
+ is specified, the trades with the specified market will be returned.
+
+ Args:
+ market: The market of the asset
+ portfolio: The portfolio of the asset
+ status: The status of the trade
+ order_id: The order id of the trade
+ target_symbol: The symbol of the asset
+ trading_symbol: The trading symbol of the asset
+
+ Returns:
+ List[Trade]: A list of trades that match the query parameters
+ """
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if portfolio is not None:
+ query_params["portfolio"] = portfolio
+
+ if status is not None:
+ query_params["status"] = status
+
+ if order_id is not None:
+ query_params["order_id"] = order_id
+
+ if target_symbol is not None:
+ query_params["target_symbol"] = target_symbol
+
+ if trading_symbol is not None:
+ query_params["trading_symbol"] = trading_symbol
+
+ return self.trade_service.find(query_params)
+
+ def get_trades(
+ self,
+ target_symbol=None,
+ trading_symbol=None,
+ market=None,
+ portfolio=None,
+ status=None,
+ ) -> List[Trade]:
+ """
+ Function to get all trades. This function will return all trades
+ that match the specified query parameters. If the market parameter
+ is specified, the trades with the specified market will be returned.
+
+ Args:
+ market: The market of the asset
+ portfolio: The portfolio of the asset
+ status: The status of the trade
+ target_symbol: The symbol of the asset
+ trading_symbol: The trading symbol of the asset
+
+ Returns:
+ List[Trade]: A list of trades that match the query parameters
+ """
+
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if portfolio is not None:
+ query_params["portfolio"] = portfolio
+
+ if status is not None:
+ query_params["status"] = status
+
+ if target_symbol is not None:
+ query_params["target_symbol"] = target_symbol
+
+ if trading_symbol is not None:
+ query_params["trading_symbol"] = trading_symbol
+
+ return self.trade_service.get_all({"market": market})
+
+ def get_closed_trades(self) -> List[Trade]:
+ """
+ Function to get all closed trades. This function will return all
+ closed trades of the algorithm.
+
+ Returns:
+ List[Trade]: A list of closed trades
+ """
+ return self.trade_service.get_all({"status": TradeStatus.CLOSED.value})
+
+ def count_trades(
+ self,
+ target_symbol=None,
+ trading_symbol=None,
+ market=None,
+ portfolio=None
+ ) -> int:
+ """
+ Function to count trades. This function will return the number of
+ trades that match the specified query parameters.
+
+ Args:
+ target_symbol: The symbol of the asset
+ trading_symbol: The trading symbol of the asset
+ market: The market of the asset
+ portfolio: The portfolio of the asset
+
+ Returns:
+ int: The number of trades that match the query parameters
+ """
+
+ query_params = {}
+
+ if market is not None:
+ query_params["market"] = market
+
+ if portfolio is not None:
+ query_params["portfolio"] = portfolio
+
+ if target_symbol is not None:
+ query_params["target_symbol"] = target_symbol
+
+ if trading_symbol is not None:
+ query_params["trading_symbol"] = trading_symbol
+
+ return self.trade_service.count(query_params)
+
+ def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]:
+ """
+ Function to get all open trades. This function will return all
+ open trades that match the specified query parameters. If the
+ target_symbol parameter is specified, the open trades with the
+ specified target symbol will be returned. If the market parameter
+ is specified, the open trades with the specified market will be
+ returned.
+
+ Args:
+ target_symbol: The symbol of the asset
+ market: The market of the asset
+
+ Returns:
+ List[Trade]: A list of open trades that match the query parameters
+ """
+ return self.trade_service.get_all(
+ {
+ "status": TradeStatus.OPEN.value,
+ "target_symbol": target_symbol,
+ "market": market
+ }
+ )
+
+ def add_stop_loss(
+ self,
+ trade,
+ percentage: float,
+ trade_risk_type: TradeRiskType = TradeRiskType.FIXED,
+ sell_percentage: float = 100,
+ ):
+ """
+ Function to add a stop loss to a trade.
+
+ Example of fixed stop loss:
+ * You buy BTC at $40,000.
+ * You set a SL of 5% → SL level at $38,000 (40,000 - 5%).
+ * BTC price increases to $42,000 → SL level remains at $38,000.
+ * BTC price drops to $38,000 → SL level reached, trade closes.
+
+ Example of trailing stop loss:
+ * You buy BTC at $40,000.
+ * You set a TSL of 5%, setting the sell price at $38,000.
+ * BTC price increases to $42,000 → New TSL level
+ at $39,900 (42,000 - 5%).
+ * BTC price drops to $39,900 → SL level reached, trade closes.
+
+ Args:
+ trade: Trade object representing the trade
+ percentage: float representing the percentage of the open price
+ that the stop loss should be set at
+ trade_risk_type (TradeRiskType): The type of the stop loss, fixed
+ or trailing
+ sell_percentage: float representing the percentage of the trade
+ that should be sold if the stop loss is triggered
+
+ Returns:
+ None
+ """
+ self.trade_service.add_stop_loss(
+ trade,
+ percentage=percentage,
+ trade_risk_type=trade_risk_type,
+ sell_percentage=sell_percentage,
+ )
+ return self.trade_service.get(trade.id)
+
+ def add_take_profit(
+ self,
+ trade,
+ percentage: float,
+ trade_risk_type: TradeRiskType = TradeRiskType.FIXED,
+ sell_percentage: float = 100,
+ ) -> None:
+ """
+ Function to add a take profit to a trade. This function will add a
+ take profit to the specified trade. If the take profit is triggered,
+ the trade will be closed.
+
+ Example of take profit:
+ * You buy BTC at $40,000.
+ * You set a TP of 5% → TP level at $42,000 (40,000 + 5%).
+ * BTC rises to $42,000 → TP level reached, trade
+ closes, securing profit.
+
+ Example of trailing take profit:
+ * You buy BTC at $40,000
+ * You set a TTP of 5%, setting the sell price at $42,000.
+ * BTC rises to $42,000 → TTP level stays at $42,000.
+ * BTC rises to $45,000 → New TTP level at $42,750.
+ * BTC drops to $42,750 → Trade closes, securing profit.
+
+ Args:
+ trade: Trade object representing the trade
+ percentage: float representing the percentage of the open price
+ that the stop loss should be set at
+ trade_risk_type (TradeRiskType): The type of the stop loss, fixed
+ or trailing
+ sell_percentage: float representing the percentage of the trade
+ that should be sold if the stop loss is triggered
+
+ Returns:
+ None
+ """
+ self.trade_service.add_take_profit(
+ trade,
+ percentage=percentage,
+ trade_risk_type=trade_risk_type,
+ sell_percentage=sell_percentage,
+ )
+ return self.trade_service.get(trade.id)
+
+ def close_trade(self, trade, precision=None) -> None:
+ """
+ Function to close a trade. This function will close a trade by
+ creating a market order to sell the position. If the precision
+ parameter is specified, the amount of the order will be rounded
+ down to the specified precision.
+
+ Args:
+ trade: Trade - The trade to close
+ precision: int - The precision of the amount
+
+ Returns:
+ None
+ """
+
+ trade = self.trade_service.get(trade.id)
+
+ if TradeStatus.CLOSED.equals(trade.status):
+ raise OperationalException("Trade already closed.")
+
+ if trade.remaining <= 0:
+ raise OperationalException("Trade has no amount to close.")
+
+ position_id = trade.orders[0].position_id
+ portfolio = self.portfolio_service.find({"position": position_id})
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": trade.target_symbol}
+ )
+ amount = trade.remaining
+
+ if precision is not None:
+ amount = RoundingService.round_down(amount, precision)
+
+ if position.get_amount() < amount:
+ logger.warning(
+ f"Order amount {amount} is larger then amount "
+ f"of available {position.symbol} "
+ f"position: {position.get_amount()}, "
+ f"changing order amount to size of position"
+ )
+ amount = position.get_amount()
+
+ ticker = self.market_data_source_service.get_ticker(
+ symbol=trade.symbol, market=portfolio.market
+ )
+
+ self.order_service.create(
+ {
+ "portfolio_id": portfolio.id,
+ "trading_symbol": trade.trading_symbol,
+ "target_symbol": trade.target_symbol,
+ "amount": amount,
+ "order_side": OrderSide.SELL.value,
+ "order_type": OrderType.LIMIT.value,
+ "price": ticker["bid"],
+ }
+ )
+
+ def get_number_of_positions(self):
+ """
+ Returns the number of positions that have a positive amount.
+
+ Returns:
+ int: The number of positions
+ """
+ return self.position_service.count({"amount_gt": 0})
+
+ def has_trading_symbol_position_available(
+ self,
+ amount_gt=None,
+ amount_gte=None,
+ percentage_of_portfolio=None,
+ market=None
+ ):
+ """
+ Checks if there is a position available for the trading symbol of the
+ portfolio. If the amount_gt or amount_gte parameters are specified,
+ the amount of the position must be greater than the specified amount.
+ If the percentage_of_portfolio parameter is specified, the amount of
+ the position must be greater than the net_size of the
+ portfolio.
+
+ Parameters:
+ amount_gt: The amount of the position must be greater than this
+ amount.
+ :param amount_gte: The amount of the position must be greater than
+ or equal to this amount.
+ :param percentage_of_portfolio: The amount of the position must be
+ greater than the net_size of the portfolio.
+ :param market: The market of the portfolio.
+ :return: True if there is a trading symbol position available with the
+ specified parameters, False otherwise.
+ """
+ portfolio = self.portfolio_service.find({"market": market})
+ position = self.position_service.find(
+ {"portfolio": portfolio.id, "symbol": portfolio.trading_symbol}
+ )
+
+ if amount_gt is not None:
+ return position.get_amount() > amount_gt
+
+ if amount_gte is not None:
+ return position.get_amount() >= amount_gte
+
+ if percentage_of_portfolio is not None:
+ net_size = portfolio.get_net_size()
+ return position.get_amount() >= net_size \
+ * percentage_of_portfolio / 100
+
+ return position.get_amount() > 0
+
+ def get_pending_orders(
+ self, order_side=None, target_symbol=None, portfolio_id=None
+ ):
+ """
+ Function to get all pending orders of the algorithm. If the
+ portfolio_id parameter is specified, the function will return
+ all pending orders of the portfolio with the specified id.
+ """
+ query_params = {}
+
+ if portfolio_id:
+ query_params["portfolio"] = portfolio_id
+
+ if target_symbol:
+ query_params["target_symbol"] = target_symbol
+
+ if order_side:
+ query_params["order_side"] = order_side
+
+ return self.order_service.get_all({"status": OrderStatus.OPEN.value})
+
+ def get_unfilled_buy_value(self):
+ """
+ Returns the total value of all unfilled buy orders.
+ """
+ pending_orders = self.get_pending_orders(
+ order_side=OrderSide.BUY.value
+ )
+
+ return sum(
+ [order.get_amount() * order.get_price()
+ for order in pending_orders]
+ )
+
+ def get_unfilled_sell_value(self):
+ """
+ Returns the total value of all unfilled buy orders.
+ """
+ pending_orders = self.get_pending_orders(
+ order_side=OrderSide.SELL.value
+ )
+
+ return sum(
+ [order.get_amount() * order.get_price()
+ for order in pending_orders]
+ )
diff --git a/investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py b/investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py
index 1a380cbd..8f93af28 100644
--- a/investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py
+++ b/investing_algorithm_framework/app/stateless/action_handlers/run_strategy_handler.py
@@ -13,9 +13,10 @@ def handle_event(self, payload, algorithm):
tasks = algorithm.strategy_orchestrator_service.get_tasks()
for strategy in strategies:
+ context = algorithm.context
algorithm.strategy_orchestrator_service.run_strategy(
strategy=strategy,
- algorithm=algorithm,
+ context=context,
sync=True
)
diff --git a/investing_algorithm_framework/app/strategy.py b/investing_algorithm_framework/app/strategy.py
index e0c47646..ce13e129 100644
--- a/investing_algorithm_framework/app/strategy.py
+++ b/investing_algorithm_framework/app/strategy.py
@@ -6,10 +6,27 @@
from investing_algorithm_framework.domain import \
TimeUnit, StrategyProfile, Trade, ENVIRONMENT, Environment, \
BACKTESTING_INDEX_DATETIME
-from .algorithm import Algorithm
+from .context import Context
class TradingStrategy:
+ """
+ TradingStrategy is the base class for all trading strategies. A trading
+ strategy is a set of rules that defines when to buy or sell an asset.
+
+ Attributes:
+ time_unit: TimeUnit - the time unit of the strategy that defines
+ when the strategy should run e.g. HOUR, DAY, WEEK, MONTH
+ interval: int - the interval of the strategy that defines how often
+ the strategy should run within the time unit e.g. every 5 hours,
+ every 2 days, every 3 weeks, every 4 months
+ worker_id (optional): str - the id of the worker
+ strategy_id (optional): str - the id of the strategy
+ decorated (optional): function - the decorated function
+ market_data_sources (optional): list - the list of market data
+ sources to use for the strategy. This will be passed to the
+ run_strategy function.
+ """
time_unit: str = None
interval: int = None
worker_id: str = None
@@ -17,7 +34,7 @@ class TradingStrategy:
decorated = None
market_data_sources = None
traces = None
- algorithm: Algorithm = None
+ context: Context = None
def __init__(
self,
@@ -72,9 +89,9 @@ def __init__(
self._context = None
self._last_run = None
- def run_strategy(self, algorithm, market_data):
- self.algorithm = algorithm
- config = self.algorithm.get_config()
+ def run_strategy(self, context, market_data):
+ self.context = context
+ config = self.context.get_config()
if config[ENVIRONMENT] == Environment.BACKTEST.value:
self._update_trades_and_orders_for_backtest(market_data)
@@ -82,18 +99,19 @@ def run_strategy(self, algorithm, market_data):
self._update_trades_and_orders(market_data)
self._check_stop_losses()
+ self._check_take_profits()
# Run user defined strategy
- self.apply_strategy(algorithm=algorithm, market_data=market_data)
+ self.apply_strategy(context=context, market_data=market_data)
if config[ENVIRONMENT] == Environment.BACKTEST.value:
self._last_run = config[BACKTESTING_INDEX_DATETIME]
else:
self._last_run = datetime.now(tz=timezone.utc)
- def apply_strategy(self, algorithm, market_data):
+ def apply_strategy(self, context, market_data):
if self.decorated:
- self.decorated(algorithm=algorithm, market_data=market_data)
+ self.decorated(context=context, market_data=market_data)
else:
raise NotImplementedError("Apply strategy is not implemented")
@@ -107,88 +125,86 @@ def strategy_profile(self):
)
def _update_trades_and_orders(self, market_data):
- self.algorithm.order_service.check_pending_orders()
- self.algorithm.trade_service\
+ self.context.order_service.check_pending_orders()
+ self.context.trade_service\
.update_trades_with_market_data(market_data)
def _update_trades_and_orders_for_backtest(self, market_data):
- self.algorithm.order_service.check_pending_orders(market_data)
- self.algorithm.trade_service\
+ self.context.order_service.check_pending_orders(market_data)
+ self.context.trade_service\
.update_trades_with_market_data(market_data)
- def _check_pending_orders(self, market_data):
- """
- Check if there are any pending orders that need to be executed
- """
- pass
- # self.algorithm.check_pending_orders()
-
def _check_stop_losses(self):
"""
Check if there are any stop losses that result in trades being closed.
"""
- trade_service = self.algorithm.trade_service
- triggered_trades = trade_service.get_triggered_stop_losses()
+ trade_service = self.context.trade_service
- for trade in triggered_trades:
- trade_service.update(trade.id, {"stop_loss_triggered": True})
- self.algorithm.close_trade(
- trade
- )
+ stop_losses_orders_data = trade_service\
+ .get_triggered_stop_loss_orders()
- triggered_trades = trade_service.get_triggered_trailing_stop_losses()
+ order_service = self.context.order_service
- for trade in triggered_trades:
- trade_service.update(trade.id, {"stop_loss_triggered": True})
- self.algorithm.close_trade(
- trade
- )
+ for stop_loss_order in stop_losses_orders_data:
+ order_service.create(stop_loss_order)
- def on_trade_closed(self, algorithm: Algorithm, trade: Trade):
+ def _check_take_profits(self):
+ """
+ Check if there are any take profits that result in trades being closed.
+ """
+ trade_service = self.context.trade_service
+ take_profit_orders_data = trade_service.\
+ get_triggered_take_profit_orders()
+ order_service = self.context.order_service
+
+ for take_profit_order in take_profit_orders_data:
+ order_service.create(take_profit_order)
+
+ def on_trade_closed(self, context: Context, trade: Trade):
pass
- def on_trade_updated(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_updated(self, context: Context, trade: Trade):
pass
- def on_trade_created(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_created(self, context: Context, trade: Trade):
pass
- def on_trade_opened(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_opened(self, context: Context, trade: Trade):
pass
- def on_trade_stop_loss_triggered(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_stop_loss_triggered(self, context: Context, trade: Trade):
pass
def on_trade_trailing_stop_loss_triggered(
- self, algorithm: Algorithm, trade: Trade
+ self, context: Context, trade: Trade
):
pass
def on_trade_take_profit_triggered(
- self, algorithm: Algorithm, trade: Trade
+ self, context: Context, trade: Trade
):
pass
- def on_trade_stop_loss_updated(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_stop_loss_updated(self, context: Context, trade: Trade):
pass
def on_trade_trailing_stop_loss_updated(
- self, algorithm: Algorithm, trade: Trade
+ self, context: Context, trade: Trade
):
pass
- def on_trade_take_profit_updated(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_take_profit_updated(self, context: Context, trade: Trade):
pass
- def on_trade_stop_loss_created(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_stop_loss_created(self, context: Context, trade: Trade):
pass
def on_trade_trailing_stop_loss_created(
- self, algorithm: Algorithm, trade: Trade
+ self, context: Context, trade: Trade
):
pass
- def on_trade_take_profit_created(self, algorithm: Algorithm, trade: Trade):
+ def on_trade_take_profit_created(self, context: Context, trade: Trade):
pass
@property
@@ -199,14 +215,6 @@ def strategy_identifier(self):
return self.worker_id
- @property
- def context(self):
- return self._context
-
- @context.setter
- def context(self, context):
- self._context = context
-
def add_trace(
self,
symbol: str,
@@ -283,7 +291,7 @@ def has_open_orders(
Returns:
bool: True if there are open orders, False otherwise
"""
- return self.algorithm.has_open_orders(
+ return self.context.has_open_orders(
target_symbol=target_symbol, identifier=identifier, market=market
)
@@ -329,12 +337,12 @@ def create_limit_order(
validate (optional): Default True. If set to True, the order
will be validated
sync (optional): Default True. If set to True, the created
- order will be synced with the portfolio of the algorithm
+ order will be synced with the portfolio of the context
Returns:
Order: Instance of the order created
"""
- self.algorithm.create_limit_order(
+ self.context.create_limit_order(
target_symbol=target_symbol,
price=price,
order_side=order_side,
@@ -373,12 +381,12 @@ def create_market_order(
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
+ portfolio of the context
Returns:
Order: Instance of the order created
"""
- self.algorithm.create_market_order(
+ self.context.create_market_order(
target_symbol=target_symbol,
order_side=order_side,
amount=amount,
@@ -406,7 +414,7 @@ def close_position(
Returns:
None
"""
- self.algorithm.close_position(
+ self.context.close_position(
symbol=symbol,
market=market,
identifier=identifier,
@@ -450,7 +458,7 @@ def get_positions(
Returns:
List[Position]: A list of positions that match the query parameters
"""
- return self.algorithm.get_positions(
+ return self.context.get_positions(
market=market,
identifier=identifier,
amount_gt=amount_gt,
@@ -471,17 +479,17 @@ def get_trades(self, market=None) -> List[Trade]:
Returns:
List[Trade]: A list of trades that match the query parameters
"""
- return self.algorithm.get_trades(market)
+ return self.context.get_trades(market)
def get_closed_trades(self) -> List[Trade]:
"""
Function to get all closed trades. This function will return all
- closed trades of the algorithm.
+ closed trades of the context.
Returns:
List[Trade]: A list of closed trades
"""
- return self.algorithm.get_closed_trades()
+ return self.context.get_closed_trades()
def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]:
"""
@@ -499,7 +507,7 @@ def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]:
Returns:
List[Trade]: A list of open trades that match the query parameters
"""
- return self.algorithm.get_open_trades(target_symbol, market)
+ return self.context.get_open_trades(target_symbol, market)
def close_trade(self, trade, market=None, precision=None) -> None:
"""
@@ -516,7 +524,7 @@ def close_trade(self, trade, market=None, precision=None) -> None:
Returns:
None
"""
- self.algorithm.close_trade(
+ self.context.close_trade(
trade=trade, market=market, precision=precision
)
@@ -527,7 +535,7 @@ def get_number_of_positions(self):
Returns:
int: The number of positions
"""
- return self.algorithm.get_number_of_positions()
+ return self.context.get_number_of_positions()
def get_position(
self, symbol, market=None, identifier=None
@@ -548,7 +556,7 @@ def get_position(
Returns:
Position: The position that matches the query parameters
"""
- return self.algorithm.get_position(
+ return self.context.get_position(
symbol=symbol,
market=market,
identifier=identifier
@@ -583,7 +591,7 @@ def has_position(
Returns:
Boolean: True if a position exists, False otherwise
"""
- return self.algorithm.has_position(
+ return self.context.has_position(
symbol=symbol,
market=market,
identifier=identifier,
@@ -608,7 +616,7 @@ def has_balance(self, symbol, amount, market=None):
Returns:
Boolean: True if the portfolio has enough balance
"""
- return self.algorithm.has_balance(symbol, amount, market)
+ return self.context.has_balance(symbol, amount, market)
def last_run(self) -> datetime:
"""
@@ -617,4 +625,4 @@ def last_run(self) -> datetime:
Returns:
DateTime: The last run of the strategy
"""
- return self.algorithm.last_run()
+ return self.context.last_run()
diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py
index 8f34271a..ddf1cb19 100644
--- a/investing_algorithm_framework/create_app.py
+++ b/investing_algorithm_framework/create_app.py
@@ -11,7 +11,8 @@
def create_app(
config: dict = None,
state_handler=None,
- web: bool = False
+ web: bool = False,
+ name=None
) -> App:
"""
Factory method to create an app instance.
@@ -35,6 +36,7 @@ def create_app(
)
# After the container is setup, initialize the services
app.initialize_services()
+ app.name = name
if config is not None:
app.set_config_with_dict(config)
@@ -43,4 +45,5 @@ def create_app(
app.set_config("APP_MODE", AppMode.WEB.value)
logger.info("Investing algoritm framework app created")
+
return app
diff --git a/investing_algorithm_framework/dependency_container.py b/investing_algorithm_framework/dependency_container.py
index 385b0149..6f6e3262 100644
--- a/investing_algorithm_framework/dependency_container.py
+++ b/investing_algorithm_framework/dependency_container.py
@@ -1,10 +1,12 @@
from dependency_injector import containers, providers
-from investing_algorithm_framework.app.algorithm import Algorithm
+from investing_algorithm_framework.app.context import Context
from investing_algorithm_framework.infrastructure import SQLOrderRepository, \
SQLPositionRepository, SQLPortfolioRepository, \
SQLPortfolioSnapshotRepository, SQLTradeRepository, \
- SQLPositionSnapshotRepository, PerformanceService, CCXTMarketService
+ SQLPositionSnapshotRepository, PerformanceService, CCXTMarketService, \
+ SQLTradeStopLossRepository, SQLTradeTakeProfitRepository, \
+ SQLOrderMetadataRepository
from investing_algorithm_framework.services import OrderService, \
PositionService, PortfolioService, StrategyOrchestratorService, \
PortfolioConfigurationService, MarketDataSourceService, BacktestService, \
@@ -32,6 +34,7 @@ class DependencyContainer(containers.DeclarativeContainer):
MarketCredentialService
)
order_repository = providers.Factory(SQLOrderRepository)
+ order_metadata_repository = providers.Factory(SQLOrderMetadataRepository)
position_repository = providers.Factory(SQLPositionRepository)
portfolio_repository = providers.Factory(SQLPortfolioRepository)
position_snapshot_repository = providers.Factory(
@@ -41,6 +44,9 @@ class DependencyContainer(containers.DeclarativeContainer):
SQLPortfolioSnapshotRepository
)
trade_repository = providers.Factory(SQLTradeRepository)
+ trade_take_profit_repository = providers\
+ .Factory(SQLTradeTakeProfitRepository)
+ trade_stop_loss_repository = providers.Factory(SQLTradeStopLossRepository)
market_service = providers.Factory(
CCXTMarketService,
market_credential_service=market_credential_service,
@@ -71,15 +77,18 @@ class DependencyContainer(containers.DeclarativeContainer):
repository=position_repository,
market_service=market_service,
market_credential_service=market_credential_service,
- order_repository=order_repository,
)
trade_service = providers.Factory(
TradeService,
+ order_repository=order_repository,
+ trade_take_profit_repository=trade_take_profit_repository,
+ trade_stop_loss_repository=trade_stop_loss_repository,
configuration_service=configuration_service,
trade_repository=trade_repository,
portfolio_repository=portfolio_repository,
position_repository=position_repository,
market_data_source_service=market_data_source_service,
+ order_metadata_repository=order_metadata_repository,
)
order_service = providers.Factory(
OrderService,
@@ -138,14 +147,13 @@ class DependencyContainer(containers.DeclarativeContainer):
portfolio_configuration_service=portfolio_configuration_service,
strategy_orchestrator_service=strategy_orchestrator_service,
)
- algorithm = providers.Factory(
- Algorithm,
+ context = providers.Factory(
+ Context,
configuration_service=configuration_service,
portfolio_configuration_service=portfolio_configuration_service,
portfolio_service=portfolio_service,
position_service=position_service,
order_service=order_service,
- strategy_orchestrator_service=strategy_orchestrator_service,
market_credential_service=market_credential_service,
market_data_source_service=market_data_source_service,
market_service=market_service,
diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py
index 6c682cf4..0d1fbb25 100644
--- a/investing_algorithm_framework/domain/__init__.py
+++ b/investing_algorithm_framework/domain/__init__.py
@@ -19,7 +19,7 @@
BacktestReport, PortfolioSnapshot, StrategyProfile, \
BacktestPosition, Trade, MarketCredential, PositionSnapshot, \
BacktestReportsEvaluation, AppMode, BacktestDateRange, DateRange, \
- MarketDataType
+ MarketDataType, TradeRiskType, TradeTakeProfit, TradeStopLoss
from .services import TickerMarketDataSource, OrderBookMarketDataSource, \
OHLCVMarketDataSource, BacktestMarketDataSource, MarketDataSource, \
MarketService, MarketCredentialService, AbstractPortfolioSyncService, \
@@ -124,5 +124,8 @@
"DEFAULT_LOGGING_CONFIG",
"DATABASE_DIRECTORY_NAME",
"BACKTESTING_INITIAL_AMOUNT",
- "MarketDataType"
+ "MarketDataType",
+ "TradeRiskType",
+ "TradeTakeProfit",
+ "TradeStopLoss",
]
diff --git a/investing_algorithm_framework/domain/data_structures.py b/investing_algorithm_framework/domain/data_structures.py
index dddf9acb..e0a3fc6e 100644
--- a/investing_algorithm_framework/domain/data_structures.py
+++ b/investing_algorithm_framework/domain/data_structures.py
@@ -1,6 +1,6 @@
class PeekableQueue:
- def __init__(self):
- self.queue = []
+ def __init__(self, items=[]):
+ self.queue = items
self.index = 0
def enqueue(self, item):
@@ -24,6 +24,7 @@ def is_empty(self):
def __len__(self):
return len(self.queue)
+ @property
def size(self):
return len(self.queue)
diff --git a/investing_algorithm_framework/domain/models/__init__.py b/investing_algorithm_framework/domain/models/__init__.py
index 69a11f90..e67bb8c6 100644
--- a/investing_algorithm_framework/domain/models/__init__.py
+++ b/investing_algorithm_framework/domain/models/__init__.py
@@ -9,7 +9,8 @@
from .time_frame import TimeFrame
from .time_interval import TimeInterval
from .time_unit import TimeUnit
-from .trade import Trade, TradeStatus
+from .trade import Trade, TradeStatus, TradeStopLoss, TradeTakeProfit, \
+ TradeRiskType
from .trading_data_types import TradingDataType
from .trading_time_frame import TradingTimeFrame
from .date_range import DateRange
@@ -40,5 +41,8 @@
"AppMode",
"BacktestDateRange",
"DateRange",
- "MarketDataType"
+ "MarketDataType",
+ "TradeStopLoss",
+ "TradeTakeProfit",
+ "TradeRiskType",
]
diff --git a/investing_algorithm_framework/domain/models/market/market_credential.py b/investing_algorithm_framework/domain/models/market/market_credential.py
index ebb9aa3d..f97b26b3 100644
--- a/investing_algorithm_framework/domain/models/market/market_credential.py
+++ b/investing_algorithm_framework/domain/models/market/market_credential.py
@@ -35,7 +35,8 @@ def initialize(self):
if self.api_key is None:
raise OperationalException(
- "Market credential requires an api key, either"
+ f"Market credential for market {self.market}"
+ " requires an api key, either"
" as an argument or as an environment variable"
f" named as {self._market.upper()}_API_KEY"
)
@@ -52,7 +53,8 @@ def initialize(self):
if self.secret_key is None:
raise OperationalException(
- "Market credential requires a secret key, either"
+ f"Market credential for market {self.market}"
+ " requires a secret key, either"
" as an argument or as an environment variable"
f" named as {self._market.upper()}_SECRET_KEY"
)
diff --git a/investing_algorithm_framework/domain/models/order/order.py b/investing_algorithm_framework/domain/models/order/order.py
index 5bfd607a..8213a38d 100644
--- a/investing_algorithm_framework/domain/models/order/order.py
+++ b/investing_algorithm_framework/domain/models/order/order.py
@@ -249,7 +249,7 @@ def from_dict(data: dict):
if updated_at is not None:
updated_at = parse(updated_at)
- return Order(
+ order = Order(
external_id=data.get("id", None),
target_symbol=target_symbol,
trading_symbol=trading_symbol,
@@ -268,6 +268,7 @@ def from_dict(data: dict):
order_fee_rate=data.get("order_fee_rate", None),
id=data.get("id", None)
)
+ return order
@staticmethod
def from_ccxt_order(ccxt_order):
diff --git a/investing_algorithm_framework/domain/models/order/order_type.py b/investing_algorithm_framework/domain/models/order/order_type.py
index 70f9ab04..62b87df7 100644
--- a/investing_algorithm_framework/domain/models/order/order_type.py
+++ b/investing_algorithm_framework/domain/models/order/order_type.py
@@ -3,8 +3,6 @@
class OrderType(Enum):
LIMIT = 'LIMIT'
- MARKET = 'MARKET'
- STOP_LOSS_LIMIT = "STOP_LOSS_LIMIT"
@staticmethod
def from_string(value: str):
diff --git a/investing_algorithm_framework/domain/models/trade/__init__.py b/investing_algorithm_framework/domain/models/trade/__init__.py
index a884cfd2..c03ac78d 100644
--- a/investing_algorithm_framework/domain/models/trade/__init__.py
+++ b/investing_algorithm_framework/domain/models/trade/__init__.py
@@ -1,4 +1,13 @@
from .trade import Trade
from .trade_status import TradeStatus
+from .trade_stop_loss import TradeStopLoss
+from .trade_take_profit import TradeTakeProfit
+from .trade_risk_type import TradeRiskType
-__all__ = ["Trade", "TradeStatus"]
+__all__ = [
+ "Trade",
+ "TradeStatus",
+ "TradeStopLoss",
+ "TradeTakeProfit",
+ "TradeRiskType",
+]
diff --git a/investing_algorithm_framework/domain/models/trade/trade.py b/investing_algorithm_framework/domain/models/trade/trade.py
index a2d5983a..4262e077 100644
--- a/investing_algorithm_framework/domain/models/trade/trade.py
+++ b/investing_algorithm_framework/domain/models/trade/trade.py
@@ -1,9 +1,13 @@
from dateutil.parser import parse
from investing_algorithm_framework.domain.models.base_model import BaseModel
-from investing_algorithm_framework.domain.models.order import OrderSide
+from investing_algorithm_framework.domain.models.order import OrderSide, Order
from investing_algorithm_framework.domain.models.trade.trade_status import \
TradeStatus
+from investing_algorithm_framework.domain.models.trade.trade_stop_loss import \
+ TradeStopLoss
+from investing_algorithm_framework.domain.models.trade\
+ .trade_take_profit import TradeTakeProfit
class Trade(BaseModel):
@@ -36,10 +40,8 @@ class Trade(BaseModel):
created_at (datetime): the datetime when the trade was created
updated_at (datetime): the datetime when the trade was last updated
status (str): the status of the trade
- stop_loss_percentage (float): the stop loss percentage of
- the trade
- trailing_stop_loss_percentage (float): the trailing stop
- loss percentage
+ stop_losses (List[TradeStopLoss]): the stop losses of the trade
+ take_profits (List[TradeTakeProfit]): the take profits of the trade
"""
def __init__(
@@ -59,9 +61,8 @@ def __init__(
last_reported_price=None,
high_water_mark=None,
updated_at=None,
- stop_loss_percentage=None,
- trailing_stop_loss_percentage=None,
- stop_loss_triggered=False,
+ stop_losses=None,
+ take_profits=None,
):
self.id = id
self.orders = orders
@@ -78,9 +79,8 @@ def __init__(
self.high_water_mark = high_water_mark
self.status = status
self.updated_at = updated_at
- self.stop_loss_percentage = stop_loss_percentage
- self.trailing_stop_loss_percentage = trailing_stop_loss_percentage
- self.stop_loss_triggered = stop_loss_triggered
+ self.stop_losses = stop_losses
+ self.take_profits = take_profits
@property
def closed_prices(self):
@@ -107,7 +107,9 @@ def symbol(self):
@property
def duration(self):
if TradeStatus.CLOSED.equals(self.status):
- return self.closed_at - self.opened_at
+ # Get the total hours between the closed and opened datetime
+ diff = self.closed_at - self.opened_at
+ return diff.total_seconds() / 3600
if self.opened_at is None:
return None
@@ -115,7 +117,8 @@ def duration(self):
if self.updated_at is None:
return None
- return self.updated_at - self.opened_at
+ diff = self.updated_at - self.opened_at
+ return diff.total_seconds() / 3600
@property
def size(self):
@@ -228,6 +231,48 @@ def is_trailing_stop_loss_triggered(self):
return self.last_reported_price <= stop_loss_price
+ def is_take_profit_triggered(self):
+
+ if self.take_profit_percentage is None:
+ return False
+
+ if self.last_reported_price is None:
+ return False
+
+ if self.open_price is None:
+ return False
+
+ take_profit_price = self.open_price * \
+ (1 + (self.take_profit_percentage / 100))
+
+ return self.last_reported_price >= take_profit_price
+
+ def is_trailing_take_profit_triggered(self):
+ """
+ Function to check if the trailing take profit is triggered.
+ The trailing take profit is triggered when the last reported price
+ is greater than or equal to the high water mark times the trailing
+ take profit percentage.
+ """
+
+ if self.trailing_take_profit_percentage is None:
+ return False
+
+ if self.last_reported_price is None:
+ return False
+
+ if self.high_water_mark is None:
+
+ if self.open_price is not None:
+ self.high_water_mark = self.open_price
+ else:
+ return False
+
+ take_profit_price = self.high_water_mark * \
+ (1 + (self.trailing_take_profit_percentage / 100))
+
+ return self.last_reported_price >= take_profit_price
+
def to_dict(self, datetime_format=None):
if datetime_format is not None:
@@ -260,6 +305,14 @@ def to_dict(self, datetime_format=None):
"updated_at": updated_at,
"net_gain": self.net_gain,
"cost": self.cost,
+ "stop_losses": [
+ stop_loss.to_dict(datetime_format=datetime_format)
+ for stop_loss in self.stop_losses
+ ] if self.stop_losses else None,
+ "take_profits": [
+ take_profit.to_dict(datetime_format=datetime_format)
+ for take_profit in self.take_profits
+ ] if self.take_profits else None,
}
@staticmethod
@@ -267,6 +320,9 @@ def from_dict(data):
opened_at = None
closed_at = None
updated_at = None
+ stop_losses = None
+ take_profits = None
+ orders = None
if "opened_at" in data and data["opened_at"] is not None:
opened_at = parse(data["opened_at"])
@@ -277,9 +333,27 @@ def from_dict(data):
if "updated_at" in data and data["updated_at"] is not None:
updated_at = parse(data["updated_at"])
+ if "stop_losses" in data and data["stop_losses"] is not None:
+ stop_losses = [
+ TradeStopLoss.from_dict(stop_loss)
+ for stop_loss in data["stop_losses"]
+ ]
+
+ if "take_profits" in data and data["take_profits"] is not None:
+ take_profits = [
+ TradeTakeProfit.from_dict(take_profit)
+ for take_profit in data["take_profits"]
+ ]
+
+ if "orders" in data and data["orders"] is not None:
+ orders = [
+ Order.from_dict(order)
+ for order in data["orders"]
+ ]
+
return Trade(
id=data.get("id", None),
- orders=data.get("orders", None),
+ orders=orders,
target_symbol=data["target_symbol"],
trading_symbol=data["trading_symbol"],
amount=data["amount"],
@@ -292,6 +366,8 @@ def from_dict(data):
status=data["status"],
cost=data.get("cost", 0),
updated_at=updated_at,
+ stop_losses=stop_losses,
+ take_profits=take_profits,
)
def __repr__(self):
@@ -301,6 +377,7 @@ def __repr__(self):
trading_symbol=self.trading_symbol,
status=self.status,
amount=self.amount,
+ filled_amount=self.filled_amount,
remaining=self.remaining,
open_price=self.open_price,
opened_at=self.opened_at,
diff --git a/investing_algorithm_framework/domain/models/trade/trade_risk_type.py b/investing_algorithm_framework/domain/models/trade/trade_risk_type.py
new file mode 100644
index 00000000..b05055a1
--- /dev/null
+++ b/investing_algorithm_framework/domain/models/trade/trade_risk_type.py
@@ -0,0 +1,34 @@
+from enum import Enum
+
+
+class TradeRiskType(Enum):
+ FIXED = "FIXED"
+ TRAILING = "TRAILING"
+
+ @staticmethod
+ def from_string(value: str):
+
+ if isinstance(value, str):
+ for status in TradeRiskType:
+
+ if value.upper() == status.value:
+ return status
+
+ raise ValueError("Could not convert value to TradeRiskType")
+
+ @staticmethod
+ def from_value(value):
+
+ if isinstance(value, TradeRiskType):
+ for risk_type in TradeRiskType:
+
+ if value == risk_type:
+ return risk_type
+
+ elif isinstance(value, str):
+ return TradeRiskType.from_string(value)
+
+ raise ValueError("Could not convert value to TradeRiskType")
+
+ def equals(self, other):
+ return TradeRiskType.from_value(other) == self
diff --git a/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py b/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py
new file mode 100644
index 00000000..58697ded
--- /dev/null
+++ b/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py
@@ -0,0 +1,179 @@
+from investing_algorithm_framework.domain.models.base_model import BaseModel
+from investing_algorithm_framework.domain.models.trade.trade_risk_type import \
+ TradeRiskType
+
+
+class TradeStopLoss(BaseModel):
+ """
+ TradeStopLoss represents a stop loss strategy for a trade.
+
+ Attributes:
+ trade: Trade - the trade that the take profit is for
+ take_profit: float - the take profit percentage
+ trade_risk_type: TradeRiskType - the type of trade risk, either
+ trailing or fixed
+ percentage: float - the stop loss percentage
+ sell_percentage: float - the percentage of the trade to sell when the
+ take profit is hit. Default is 100% of the trade. If the
+ take profit percentage is lower than 100% a check must
+ be made that the combined sell percentage of all
+ take profits is less or equal than 100%.
+ sell_amount: float - the amount to sell when the stop loss triggers
+ sold_amount: float - the amount that has been sold
+ high_water_mark: float - the highest price of the trade
+ stop_loss_price: float - the price at which the stop loss triggers
+
+ if trade_risk_type is fixed, the stop loss price is calculated as follows:
+ You buy a stock at $100.
+ You set a 5% stop loss, meaning you will sell if
+ the price drops to $95.
+ If the price rises to $120, the stop loss is not triggered.
+ But if the price keeps falling to $95, the stop loss triggers,
+ and you exit with a $5 loss.
+
+ if trade_risk_type is trailing, the stop loss price is
+ calculated as follows:
+ You buy a stock at $100.
+ You set a 5% trailing stop loss, meaning you will sell if
+ the price drops 5% from its peak at $96
+ If the price rises to $120, the stop loss adjusts
+ to $114 (5% below $120).
+ If the price falls to $114, the position is
+ closed, securing a $14 profit.
+ But if the price keeps rising to $150, the stop
+ loss moves up to $142.50.
+ If the price drops from $150 to $142.50, the stop
+ loss triggers, and you exit with a $42.50 profit.
+ """
+
+ def __init__(
+ self,
+ trade_id: int,
+ trade_risk_type: TradeRiskType,
+ percentage: float,
+ open_price: float,
+ total_amount_trade: float,
+ sell_percentage: float = 100,
+ active: bool = True,
+ sell_prices: str = None
+ ):
+ self.trade_id = trade_id
+ self.trade_risk_type = trade_risk_type
+ self.percentage = percentage
+ self.sell_percentage = sell_percentage
+ self.high_water_mark = open_price
+ self.open_price = open_price
+ self.stop_loss_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+ self.sell_amount = total_amount_trade * (self.sell_percentage / 100)
+ self.sold_amount = 0
+ self.active = active
+ self.sell_prices = sell_prices
+
+ def update_with_last_reported_price(self, current_price: float):
+ """
+ Function to update the take profit price based on the last
+ reported price.
+ The take profit price is only updated when the trade risk
+ type is trailing.
+ The take profit price is updated based on the current price
+ and the percentage of the take profit.
+
+ Args:
+ current_price: float - the last reported price of the trade
+ """
+
+ if not self.active or self.sold_amount == self.sell_amount:
+ return
+
+ if TradeRiskType.FIXED.equals(self.trade_risk_type):
+ # Check if the current price is less than the high water mark
+ return
+ else:
+ # Check if the current price is less than the stop loss price
+ if current_price <= self.stop_loss_price:
+ return
+ elif current_price > self.high_water_mark:
+ self.high_water_mark = current_price
+ self.stop_loss_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+
+ def has_triggered(self, current_price: float) -> bool:
+ """
+ Function to check if the stop loss has triggered.
+ Function always returns False if the stop loss is not active or
+ the sold amount is equal to the sell amount.
+
+ Args:
+ current_price: float - the current price of the trade
+
+ Returns:
+ bool - True if the stop loss has triggered, False otherwise
+ """
+ if not self.active or self.sold_amount == self.sell_amount:
+ return False
+
+ if TradeRiskType.FIXED.equals(self.trade_risk_type):
+ # Check if the current price is less than the high water mark
+ return current_price <= self.stop_loss_price
+ else:
+ # Check if the current price is less than the stop loss price
+ if current_price <= self.stop_loss_price:
+ return True
+ elif current_price > self.high_water_mark:
+ self.high_water_mark = current_price
+ self.stop_loss_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+
+ return False
+
+ def get_sell_amount(self) -> float:
+ """
+ Function to calculate the amount to sell based on the
+ sell percentage and the remaining amount of the trade.
+ Keep in mind the moment the take profit triggers, the remaining
+ amount of the trade is used to calculate the sell amount.
+ If the remaining amount is smaller then the trade amount, the
+ trade stop loss stays active. The client that uses the
+ trade stop loss is responsible for setting the trade stop
+ loss to inactive.
+
+ Args:
+ trade: Trade - the trade to calculate the sell amount for
+
+ """
+
+ if not self.active:
+ return 0
+
+ return self.sell_amount - self.sold_amount
+
+ def to_dict(self, datetime_format=None):
+ return {
+ "trade_id": self.trade_id,
+ "trade_risk_type": self.trade_risk_type,
+ "percentage": self.percentage,
+ "open_price": self.open_price,
+ "sell_percentage": self.sell_percentage,
+ "high_water_mark": self.high_water_mark,
+ "stop_loss_price": self.stop_loss_price,
+ "sell_amount": self.sell_amount,
+ "sold_amount": self.sold_amount,
+ "active": self.active,
+ "sell_prices": self.sell_prices
+ }
+
+ def __repr__(self):
+ return self.repr(
+ trade_id=self.trade_id,
+ trade_risk_type=self.trade_risk_type,
+ percentage=self.percentage,
+ sell_percentage=self.sell_percentage,
+ high_water_mark=self.high_water_mark,
+ open_price=self.open_price,
+ stop_loss_price=self.stop_loss_price,
+ sell_amount=self.sell_amount,
+ sold_amount=self.sold_amount,
+ sell_prices=self.sell_prices,
+ active=self.active
+ )
diff --git a/investing_algorithm_framework/domain/models/trade/trade_take_profit.py b/investing_algorithm_framework/domain/models/trade/trade_take_profit.py
new file mode 100644
index 00000000..c9bb0f4b
--- /dev/null
+++ b/investing_algorithm_framework/domain/models/trade/trade_take_profit.py
@@ -0,0 +1,206 @@
+from investing_algorithm_framework.domain.models.base_model import BaseModel
+from investing_algorithm_framework.domain.models.trade.trade_risk_type import \
+ TradeRiskType
+
+
+class TradeTakeProfit(BaseModel):
+ """
+ TradeTakeProfit represents a take profit strategy for a trade.
+
+ Attributes:
+ trade: Trade - the trade that the take profit is for
+ take_profit: float - the take profit percentage
+ trade_risk_type: TradeRiskType - the type of trade risk, either
+ trailing or fixed
+ percentage: float - the take profit percentage
+ sell_percentage: float - the percentage of the trade to sell when the
+ take profit is hit. Default is 100% of the trade.
+ If the take profit percentage is lower than 100% a check
+ must be made that the combined sell percentage of
+ all take profits is less or equal than 100%.
+
+ if trade_risk_type is fixed, the take profit price is
+ calculated as follows:
+ You buy a stock at $100.
+ You set a 5% take profit, meaning you will sell if the price
+ rises to $105.
+ If the price rises to $120, the take profit triggers,
+ and you exit with a $20 profit.
+ But if the price keeps falling below $105, the take profit is not
+ triggered.
+
+ if trade_risk_type is trailing, the take profit price is
+ calculated as follows:
+ You buy a stock at $100.
+ You set a 5% trailing take profit, the moment the price rises
+ 5% the initial take profit mark will be set. This means you
+ will set the take_profit_price initially at none and
+ only if the price hits $105, you will set the
+ take_profit_price to $105.
+ if the price drops below $105, the take profit is triggered.
+ If the price rises to $120, the take profit adjusts to
+ $114 (5% below $120).
+ If the price falls to $114, the position is closed,
+ securing a $14 profit.
+ But if the price keeps rising to $150, the take profit
+ moves up to $142.50.
+ """
+
+ def __init__(
+ self,
+ trade_id: int,
+ trade_risk_type: TradeRiskType,
+ percentage: float,
+ open_price: float,
+ total_amount_trade: float,
+ sell_percentage: float = 100,
+ active: bool = True,
+ sell_prices: str = None
+ ):
+ self.trade_id = trade_id
+ self.trade_risk_type = trade_risk_type
+ self.percentage = percentage
+ self.sell_percentage = sell_percentage
+ self.high_water_mark = None
+ self.open_price = open_price
+ self.take_profit_price = open_price * \
+ (1 + (self.percentage / 100))
+ self.sell_amount = total_amount_trade * (self.sell_percentage / 100)
+ self.sold_amount = 0
+ self.active = active
+ self.sell_prices = sell_prices
+
+ def update_with_last_reported_price(self, current_price: float):
+ """
+ Function to update the take profit price based on
+ the last reported price.
+ The take profit price is only updated when the
+ trade risk type is trailing.
+ The take profit price is updated based on the
+ current price and the percentage of the take profit.
+
+ Args:
+ current_price: float - the last reported price of the trade
+ """
+
+ # Do nothing for fixed take profit
+ if TradeRiskType.FIXED.equals(self.trade_risk_type):
+ return
+ else:
+
+ if self.high_water_mark is None:
+
+ if current_price >= self.take_profit_price:
+ self.high_water_mark = current_price
+ new_take_profit_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+ if self.take_profit_price <= new_take_profit_price:
+ self.take_profit_price = new_take_profit_price
+
+ return
+
+ # Check if the current price is less than the take profit price
+ if current_price < self.take_profit_price:
+ return
+
+ # Increase the high water mark and take profit price
+ elif current_price > self.high_water_mark:
+ self.high_water_mark = current_price
+ new_take_profit_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+
+ # Only increase the take profit price if the new take
+ # profit price based on the new high water mark is higher
+ # then the current take profit price
+ if self.take_profit_price <= new_take_profit_price:
+ self.take_profit_price = new_take_profit_price
+
+ return
+
+ def has_triggered(self, current_price: float = None) -> bool:
+
+ if TradeRiskType.FIXED.equals(self.trade_risk_type):
+ # Check if the current price is less than the high water mark
+ return current_price >= self.take_profit_price
+ else:
+ # Always return false, when the high water mark is not set
+ # But check if we can set the high water mark
+ if self.high_water_mark is None:
+
+ if current_price >= self.take_profit_price:
+ self.high_water_mark = current_price
+ new_take_profit_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+ if self.take_profit_price <= new_take_profit_price:
+ self.take_profit_price = new_take_profit_price
+
+ return False
+
+ # Check if the current price is less than the take profit price
+ if current_price < self.take_profit_price:
+ return True
+
+ # Increase the high water mark and take profit price
+ elif current_price > self.high_water_mark:
+ self.high_water_mark = current_price
+ new_take_profit_price = self.high_water_mark * \
+ (1 - (self.percentage / 100))
+
+ # Only increase the take profit price if the new take
+ # profit price based on the new high water mark is higher
+ # then the current take profit price
+ if self.take_profit_price <= new_take_profit_price:
+ self.take_profit_price = new_take_profit_price
+
+ return False
+
+ def get_sell_amount(self) -> float:
+ """
+ Function to calculate the amount to sell based on the
+ sell percentage and the remaining amount of the trade.
+ Keep in mind the moment the take profit triggers, the remaining
+ amount of the trade is used to calculate the sell amount.
+ If the remaining amount is smaller then the trade amount, the
+ trade stop loss stays active. The client that uses the
+ trade stop loss is responsible for setting the trade stop
+ loss to inactive.
+
+ Args:
+ trade: Trade - the trade to calculate the sell amount for
+
+ """
+
+ if not self.active:
+ return 0
+
+ return self.sell_amount - self.sold_amount
+
+ def to_dict(self, datetime_format=None):
+ return {
+ "trade_id": self.trade_id,
+ "trade_risk_type": self.trade_risk_type,
+ "percentage": self.percentage,
+ "open_price": self.open_price,
+ "sell_percentage": self.sell_percentage,
+ "high_water_mark": self.high_water_mark,
+ "take_profit_price": self.take_profit_price,
+ "sell_amount": self.sell_amount,
+ "sold_amount": self.sold_amount,
+ "active": self.active,
+ "sell_prices": self.sell_prices
+ }
+
+ def __repr__(self):
+ return self.repr(
+ trade_id=self.trade_id,
+ trade_risk_type=self.trade_risk_type,
+ percentage=self.percentage,
+ open_price=self.open_price,
+ sell_percentage=self.sell_percentage,
+ high_water_mark=self.high_water_mark,
+ take_profit_price=self.take_profit_price,
+ sell_amount=self.sell_amount,
+ sold_amount=self.sold_amount,
+ active=self.active,
+ sell_prices=self.sell_prices
+ )
diff --git a/investing_algorithm_framework/domain/utils/backtesting.py b/investing_algorithm_framework/domain/utils/backtesting.py
index 5c4cd4a8..85b820e0 100644
--- a/investing_algorithm_framework/domain/utils/backtesting.py
+++ b/investing_algorithm_framework/domain/utils/backtesting.py
@@ -7,7 +7,7 @@
from tabulate import tabulate
from investing_algorithm_framework.domain import DATETIME_FORMAT, \
- BacktestDateRange, TradeStatus
+ BacktestDateRange, TradeStatus, OrderSide
from investing_algorithm_framework.domain.exceptions import \
OperationalException
from investing_algorithm_framework.domain.models.backtesting import \
@@ -89,6 +89,246 @@ def pretty_print_growth_evaluation(reports, precision=4):
tabulate(growth_table, headers="keys", tablefmt="rounded_grid")
)
+def pretty_print_stop_losses(
+ backtest_report,
+ precision=4,
+ triggered_only=False
+):
+ print(f"{COLOR_YELLOW}Stop losses overview{COLOR_RESET}")
+ stop_loss_table = {}
+ trades = backtest_report.trades
+ selection = []
+
+ def get_sold_amount(stop_loss):
+ if stop_loss["sold_amount"] > 0:
+ return float(stop_loss["sold_amount"])
+
+ return ""
+
+ def get_status(stop_loss):
+
+ if stop_loss.sold_amount == 0:
+ return "NOT TRIGGERED"
+
+ if stop_loss.sold_amount == stop_loss.sell_amount:
+ return "TRIGGERED"
+
+ if stop_loss.sold_amount < stop_loss.sell_amount:
+ return "PARTIALLY TRIGGERED"
+
+ def get_high_water_mark(stop_loss):
+ if stop_loss["high_water_mark"] is not None:
+ return float(stop_loss["high_water_mark"])
+
+ return ""
+
+ if triggered_only:
+ for trade in trades:
+
+ if trade.stop_losses is not None:
+ selection += [
+ {
+ "symbol": trade.symbol,
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "status": get_status(stop_loss),
+ "trade_id": stop_loss.trade_id,
+ "trade_risk_type": stop_loss.trade_risk_type,
+ "percentage": stop_loss.percentage,
+ "open_price": stop_loss.open_price,
+ "sell_percentage": stop_loss.sell_percentage,
+ "high_water_mark": stop_loss.high_water_mark,
+ "stop_loss_price": stop_loss.stop_loss_price,
+ "sell_amount": stop_loss.sell_amount,
+ "sold_amount": stop_loss.sold_amount,
+ "active": stop_loss.active,
+ "sell_prices": stop_loss.sell_prices
+ } for stop_loss in trade.stop_losses if stop_loss.sold_amount > 0
+ ]
+ else:
+ for trade in trades:
+
+ if trade.stop_losses is not None:
+ for stop_loss in trade.stop_losses:
+
+ selection += [
+ {
+ "symbol": trade.symbol,
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "status": get_status(stop_loss),
+ "trade_id": stop_loss.trade_id,
+ "trade_risk_type": stop_loss.trade_risk_type,
+ "percentage": stop_loss.percentage,
+ "open_price": stop_loss.open_price,
+ "sell_percentage": stop_loss.sell_percentage,
+ "high_water_mark": stop_loss.high_water_mark,
+ "stop_loss_price": stop_loss.stop_loss_price,
+ "sell_amount": stop_loss.sell_amount,
+ "sold_amount": stop_loss.sold_amount,
+ "active": stop_loss.active,
+ "sell_prices": stop_loss.sell_prices
+ } for stop_loss in trade.stop_losses
+ ]
+
+ stop_loss_table["Trade (Trade id)"] = [
+ f"{stop_loss['symbol'] + ' (' + str(stop_loss['trade_id']) + ')'}"
+ for stop_loss in selection
+ ]
+ stop_loss_table["Status"] = [
+ f"{stop_loss['status']}"
+ for stop_loss in selection
+ ]
+ stop_loss_table["Active"] = [
+ f"{stop_loss['active']}"
+ for stop_loss in selection
+ ]
+ stop_loss_table["Type"] = [
+ f"{stop_loss['trade_risk_type']}" for stop_loss in selection
+ ]
+ stop_loss_table["stop loss"] = [
+ f"{float(stop_loss['stop_loss_price']):.{precision}f}({stop_loss['percentage']}%) {stop_loss['trading_symbol']}" for stop_loss in selection
+ ]
+ stop_loss_table["Open price"] = [
+ f"{float(stop_loss['open_price']):.{precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None
+ ]
+ stop_loss_table["Sell price's"] = [
+ f"{stop_loss['sell_prices']}" for stop_loss in selection
+ ]
+ stop_loss_table["High water mark"] = [
+ get_high_water_mark(stop_loss) for stop_loss in selection
+ ]
+ stop_loss_table["Percentage"] = [
+ f"{float(stop_loss['sell_percentage'])}%" for stop_loss in selection
+ ]
+ stop_loss_table["Size"] = [
+ f"{float(stop_loss['sell_amount']):.{precision}f} {stop_loss['target_symbol']}" for stop_loss in selection
+ ]
+ stop_loss_table["Sold amount"] = [
+ get_sold_amount(stop_loss) for stop_loss in selection
+ ]
+ print(tabulate(stop_loss_table, headers="keys", tablefmt="rounded_grid"))
+
+
+def pretty_print_take_profits(
+ backtest_report, precision=4, triggered_only=False
+):
+ print(f"{COLOR_YELLOW}Take profits overview{COLOR_RESET}")
+ take_profit_table = {}
+ trades = backtest_report.trades
+ selection = []
+
+ def get_high_water_mark(take_profit):
+ if take_profit["high_water_mark"] is not None:
+ return float(take_profit["high_water_mark"])
+
+ return ""
+
+ def get_sold_amount(take_profit):
+ if take_profit["sold_amount"] > 0:
+ return float(take_profit["sold_amount"])
+
+ return ""
+
+ def get_status(take_profit):
+
+ if take_profit.sold_amount == 0:
+ return "NOT TRIGGERED"
+
+ if take_profit.sold_amount == take_profit.sell_amount:
+ return "TRIGGERED"
+
+ if take_profit.sold_amount < take_profit.sell_amount:
+ return "PARTIALLY TRIGGERED"
+
+ if triggered_only:
+ for trade in trades:
+
+ if trade.take_profits is not None:
+ selection += [
+ {
+ "symbol": trade.symbol,
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "status": get_status(take_profit),
+ "trade_id": take_profit.trade_id,
+ "trade_risk_type": take_profit.trade_risk_type,
+ "percentage": take_profit.percentage,
+ "open_price": take_profit.open_price,
+ "sell_percentage": take_profit.sell_percentage,
+ "high_water_mark": take_profit.high_water_mark,
+ "take_profit_price": take_profit.take_profit_price,
+ "sell_amount": take_profit.sell_amount,
+ "sold_amount": take_profit.sold_amount,
+ "active": take_profit.active,
+ "sell_prices": take_profit.sell_prices
+ } for take_profit in trade.take_profits if take_profit.sold_amount > 0
+ ]
+ else:
+ for trade in trades:
+
+ if trade.take_profits is not None:
+ selection += [
+ {
+ "symbol": trade.symbol,
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "status": get_status(take_profit),
+ "trade_id": take_profit.trade_id,
+ "trade_risk_type": take_profit.trade_risk_type,
+ "percentage": take_profit.percentage,
+ "open_price": take_profit.open_price,
+ "sell_percentage": take_profit.sell_percentage,
+ "high_water_mark": take_profit.high_water_mark,
+ "take_profit_price": take_profit.take_profit_price,
+ "sell_amount": take_profit.sell_amount,
+ "sold_amount": take_profit.sold_amount,
+ "active": take_profit.active,
+ "sell_prices": take_profit.sell_prices
+ } for take_profit in trade.take_profits
+ ]
+
+ take_profit_table["Trade (Trade id)"] = [
+ f"{stop_loss['symbol'] + ' (' + str(stop_loss['trade_id']) + ')'}"
+ for stop_loss in selection
+ ]
+ take_profit_table["Status"] = [
+ f"{stop_loss['status']}"
+ for stop_loss in selection
+ ]
+ take_profit_table["Active"] = [
+ f"{stop_loss['active']}"
+ for stop_loss in selection
+ ]
+ take_profit_table["Type"] = [
+ f"{stop_loss['trade_risk_type']}" for stop_loss
+ in selection
+ ]
+ take_profit_table["Take profit"] = [
+ f"{float(take_profit['take_profit_price']):.{precision}f}({take_profit['percentage']})% {take_profit['trading_symbol']}" for take_profit in selection
+ ]
+ take_profit_table["Open price"] = [
+ f"{float(stop_loss['open_price']):.{precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None
+ ]
+ take_profit_table["Sell price's"] = [
+ f"{stop_loss['sell_prices']}" for stop_loss in selection
+ ]
+ # Print nothing if high water mark is None
+ take_profit_table["High water mark"] = [
+ get_high_water_mark(stop_loss) for stop_loss in selection
+ ]
+ take_profit_table["Percentage"] = [
+ f"{float(stop_loss['sell_percentage'])}%" for stop_loss in selection
+ ]
+ take_profit_table["Size"] = [
+ f"{float(stop_loss['sell_amount']):.{precision}f} {stop_loss['target_symbol']}" for stop_loss in selection
+ ]
+ take_profit_table["Sold amount"] = [
+ get_sold_amount(stop_loss) for stop_loss in selection
+ ]
+ print(tabulate(take_profit_table, headers="keys", tablefmt="rounded_grid"))
+
+
def pretty_print_percentage_positive_trades_evaluation(
evaluation: BacktestReportsEvaluation,
backtest_date_range: BacktestDateRange,
@@ -252,10 +492,72 @@ def pretty_print_percentage_positive_trades(
def pretty_print_trades(backtest_report, precision=4):
+
+ def get_status(trade):
+ status = "OPEN"
+
+ if TradeStatus.CLOSED.equals(trade.status):
+ status = "CLOSED"
+
+ if has_triggered_stop_losses(trade):
+ status += ", SL"
+
+ if has_triggered_take_profits(trade):
+ status += ", TP"
+
+ return status
+
+ def get_close_prices(trade):
+
+ sell_orders = [
+ order for order in trade.orders
+ if OrderSide.SELL.equals(order.order_side)
+ ]
+ text = ""
+ number_of_sell_orders = 0
+
+ for sell_order in sell_orders:
+
+ if number_of_sell_orders > 0:
+ text += ", "
+
+ text += f"{sell_order.price}"
+ number_of_sell_orders += 1
+
+ return text
+
+ def has_triggered_take_profits(trade):
+
+ if trade.take_profits is None:
+ return False
+
+ triggered = [
+ take_profit for take_profit in trade.take_profits if take_profit.sold_amount != 0
+ ]
+
+ return len(triggered) > 0
+
+ def has_triggered_stop_losses(trade):
+
+ if trade.stop_losses is None:
+ return False
+
+ triggered = [
+ stop_loss for stop_loss in trade.stop_losses if stop_loss.sold_amount != 0
+ ]
+ return len(triggered) > 0
+
print(f"{COLOR_YELLOW}Trades overview{COLOR_RESET}")
trades_table = {}
- trades_table["Pair"] = [
- f"{trade.target_symbol}-{trade.trading_symbol}"
+ trades_table["Pair (Trade id)"] = [
+ f"{trade.target_symbol}/{trade.trading_symbol} ({trade.id})"
+ for trade in backtest_report.trades
+ ]
+ trades_table["Status"] = [
+ get_status(trade) for trade in backtest_report.trades
+ ]
+ trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [
+ f"{float(trade.net_gain):.{precision}f}"
for trade in backtest_report.trades
]
trades_table["Open date"] = [
@@ -264,25 +566,21 @@ def pretty_print_trades(backtest_report, precision=4):
trades_table["Close date"] = [
trade.closed_at for trade in backtest_report.trades
]
- trades_table["Duration (hours)"] = [
- trade.duration for trade in backtest_report.trades
- ]
- trades_table[f"Size ({backtest_report.trading_symbol})"] = [
- f"{float(trade.size):.{precision}f}" for trade in backtest_report.trades
+ trades_table["Duration"] = [
+ f"{trade.duration} hours" for trade in backtest_report.trades
]
+ # Add (unrealized) to the net gain if the trade is still open
trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [
- f"{float(trade.net_gain):.{precision}f}" + (" (unrealized)" if trade.closed_price is None else "")
- for trade in backtest_report.trades
- ]
- trades_table["Net gain percentage"] = [
- f"{float(trade.net_gain_percentage):.{precision}f}%" + (" (unrealized)" if trade.closed_price is None else "")
+ f"{float(trade.net_gain_absolute):.{precision}f} ({float(trade.net_gain_percentage):.{precision}f}%)" + (" (unrealized)" if not TradeStatus.CLOSED.equals(trade.status) else "")
for trade in backtest_report.trades
]
trades_table[f"Open price ({backtest_report.trading_symbol})"] = [
trade.open_price for trade in backtest_report.trades
]
- trades_table[f"Close price ({backtest_report.trading_symbol})"] = [
- trade.closed_price for trade in backtest_report.trades
+ trades_table[
+ f"Close price's ({backtest_report.trading_symbol})"
+ ] = [
+ get_close_prices(trade) for trade in backtest_report.trades
]
print(tabulate(trades_table, headers="keys", tablefmt="rounded_grid"))
@@ -361,15 +659,30 @@ def print_number_of_runs(report):
def pretty_print_backtest(
- backtest_report, show_positions=True, show_trades=True, precision=4
+ backtest_report,
+ show_positions=True,
+ show_trades=True,
+ show_stop_losses=True,
+ show_triggered_stop_losses_only=False,
+ show_take_profits=True,
+ show_triggered_take_profits_only=False,
+ precision=4
):
"""
Pretty print the backtest report to the console.
- :param backtest_report: The backtest report
- :param show_positions: Show the positions
- :param show_trades: Show the trades
- :param precision: The precision of the numbers
+ Args:
+ backtest_report: BacktestReport - the backtest report
+ show_positions: bool - show the positions
+ show_trades: bool - show the trades
+ show_stop_losses: bool - show the stop losses
+ show_triggered_stop_losses_only: bool - show only the triggered stop losses
+ show_take_profits: bool - show the take profits
+ show_triggered_take_profits_only: bool - show only the triggered take profits
+ precision: int - the precision of the floats
+
+ Returns:
+ None
"""
ascii_art = f"""
@@ -440,51 +753,32 @@ def pretty_print_backtest(
tabulate(position_table, headers="keys", tablefmt="rounded_grid")
)
- if show_trades:
- print(f"{COLOR_YELLOW}Trades overview{COLOR_RESET}")
- trades_table = {}
- trades_table["Pair"] = [
- f"{trade.target_symbol}-{trade.trading_symbol}"
- for trade in backtest_report.trades
- ]
- trades_table["Open date"] = [
- trade.opened_at for trade in backtest_report.trades
- ]
- trades_table["Close date"] = [
- trade.closed_at for trade in backtest_report.trades
- ]
- trades_table["Duration (hours)"] = [
- trade.duration for trade in backtest_report.trades
- ]
- trades_table[f"Cost ({backtest_report.trading_symbol})"] = [
- f"{float(trade.cost):.{precision}f}" for trade in backtest_report.trades
- ]
- trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [
- f"{float(trade.net_gain):.{precision}f}"
- for trade in backtest_report.trades
- ]
+ def has_triggered_stop_losses(trade):
- # Add (unrealized) to the net gain if the trade is still open
- trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [
- f"{float(trade.net_gain_absolute):.{precision}f}" + (" (unrealized)" if not TradeStatus.CLOSED.equals(trade.status) else "")
- for trade in backtest_report.trades
- ]
- trades_table["Net gain percentage"] = [
- f"{float(trade.net_gain_percentage):.{precision}f}%" + (" (unrealized)" if not TradeStatus.CLOSED.equals(trade.status) else "")
- for trade in backtest_report.trades
- ]
- trades_table[f"Open price ({backtest_report.trading_symbol})"] = [
- trade.open_price for trade in backtest_report.trades
- ]
- trades_table[
- f"Last reported price ({backtest_report.trading_symbol})"
- ] = [
- trade.last_reported_price for trade in backtest_report.trades
- ]
- trades_table["Stop loss triggered"] = [
- trade.stop_loss_triggered for trade in backtest_report.trades
+ if trade.stop_losses is None:
+ return False
+
+ triggered = [
+ stop_loss for stop_loss in trade.stop_losses if stop_loss.sold_amount != 0
]
- print(tabulate(trades_table, headers="keys", tablefmt="rounded_grid"))
+ return len(triggered) > 0
+
+ if show_trades:
+ pretty_print_trades(backtest_report, precision=precision)
+
+ if show_stop_losses:
+ pretty_print_stop_losses(
+ backtest_report=backtest_report,
+ precision=precision,
+ triggered_only=show_triggered_stop_losses_only
+ )
+
+ if show_take_profits:
+ pretty_print_take_profits(
+ backtest_report=backtest_report,
+ precision=precision,
+ triggered_only=show_triggered_take_profits_only
+ )
def load_backtest_report(file_path: str) -> BacktestReport:
diff --git a/investing_algorithm_framework/infrastructure/__init__.py b/investing_algorithm_framework/infrastructure/__init__.py
index 0c29345c..592a60b1 100644
--- a/investing_algorithm_framework/infrastructure/__init__.py
+++ b/investing_algorithm_framework/infrastructure/__init__.py
@@ -4,10 +4,13 @@
SQLPortfolioSnapshot, SQLPositionSnapshot, SQLTrade, \
CCXTOHLCVBacktestMarketDataSource, CCXTOrderBookMarketDataSource, \
CCXTTickerMarketDataSource, CCXTOHLCVMarketDataSource, \
- CSVOHLCVMarketDataSource, CSVTickerMarketDataSource
+ CSVOHLCVMarketDataSource, CSVTickerMarketDataSource, SQLTradeTakeProfit, \
+ SQLTradeStopLoss
from .repositories import SQLOrderRepository, SQLPositionRepository, \
SQLPortfolioRepository, SQLTradeRepository, \
- SQLPortfolioSnapshotRepository, SQLPositionSnapshotRepository
+ SQLPortfolioSnapshotRepository, SQLPositionSnapshotRepository, \
+ SQLTradeTakeProfitRepository, SQLTradeStopLossRepository, \
+ SQLOrderMetadataRepository
from .services import PerformanceService, CCXTMarketService, \
AzureBlobStorageStateHandler
@@ -37,5 +40,10 @@
"CCXTOHLCVBacktestMarketDataSource",
"CCXTOrderBookMarketDataSource",
"AzureBlobStorageStateHandler",
- "SQLTradeRepository"
+ "SQLTradeRepository",
+ "SQLTradeTakeProfit",
+ "SQLTradeStopLoss",
+ "SQLTradeTakeProfitRepository",
+ "SQLTradeStopLossRepository",
+ "SQLOrderMetadataRepository"
]
diff --git a/investing_algorithm_framework/infrastructure/models/__init__.py b/investing_algorithm_framework/infrastructure/models/__init__.py
index 376a802c..7509303b 100644
--- a/investing_algorithm_framework/infrastructure/models/__init__.py
+++ b/investing_algorithm_framework/infrastructure/models/__init__.py
@@ -2,10 +2,10 @@
CCXTTickerMarketDataSource, CCXTOHLCVMarketDataSource, \
CCXTOHLCVBacktestMarketDataSource, CSVOHLCVMarketDataSource, \
CSVTickerMarketDataSource
-from .order import SQLOrder
+from .order import SQLOrder, SQLOrderMetadata
from .portfolio import SQLPortfolio, SQLPortfolioSnapshot
from .position import SQLPosition, SQLPositionSnapshot
-from .trade import SQLTrade
+from .trades import SQLTrade, SQLTradeStopLoss, SQLTradeTakeProfit
__all__ = [
"SQLOrder",
@@ -19,5 +19,8 @@
"CCXTOHLCVMarketDataSource",
"CSVTickerMarketDataSource",
"CSVOHLCVMarketDataSource",
- "SQLTrade"
+ "SQLTrade",
+ "SQLTradeStopLoss",
+ "SQLTradeTakeProfit",
+ "SQLOrderMetadata",
]
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 1c379213..406e7a3e 100644
--- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py
+++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py
@@ -165,7 +165,9 @@ def _precompute_sliding_windows(self):
def load_data(self):
file_path = self._create_file_path()
self.data = polars.read_csv(
- file_path, dtypes={"Datetime": polars.Datetime}, low_memory=True
+ file_path,
+ schema_overrides={"Datetime": polars.Datetime},
+ low_memory=True
) # Faster parsing
first_row = self.data.head(1)
last_row = self.data.tail(1)
diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py
index 0c2a27ae..314ac18c 100644
--- a/investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py
+++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py
@@ -66,7 +66,9 @@ def csv_file_path(self):
def _load_data(self, file_path):
return polars.read_csv(
- file_path, dtypes={"Datetime": polars.Datetime}, low_memory=True
+ file_path,
+ schema_overrides={"Datetime": polars.Datetime},
+ low_memory=True
).with_columns(
polars.col("Datetime").cast(
polars.Datetime(time_unit="ms", time_zone="UTC")
diff --git a/investing_algorithm_framework/infrastructure/models/order/__init__.py b/investing_algorithm_framework/infrastructure/models/order/__init__.py
index b328b27b..2b5497dd 100644
--- a/investing_algorithm_framework/infrastructure/models/order/__init__.py
+++ b/investing_algorithm_framework/infrastructure/models/order/__init__.py
@@ -1,3 +1,4 @@
from .order import SQLOrder
+from .order_metadata import SQLOrderMetadata
-__all__ = ["SQLOrder"]
+__all__ = ["SQLOrder", "SQLOrderMetadata"]
diff --git a/investing_algorithm_framework/infrastructure/models/order/order.py b/investing_algorithm_framework/infrastructure/models/order/order.py
index aeda00cd..8b1fc966 100644
--- a/investing_algorithm_framework/infrastructure/models/order/order.py
+++ b/investing_algorithm_framework/infrastructure/models/order/order.py
@@ -16,6 +16,9 @@
class SQLOrder(Order, SQLBaseModel, SQLAlchemyModelExtension):
+ """
+ SQLOrder model based on the Order domain model.
+ """
__tablename__ = "orders"
id = Column(Integer, primary_key=True, unique=True)
external_id = Column(Integer)
@@ -39,27 +42,15 @@ class SQLOrder(Order, SQLBaseModel, SQLAlchemyModelExtension):
order_fee = Column(Float, default=None)
order_fee_currency = Column(String)
order_fee_rate = Column(Float, default=None)
+ sell_order_metadata_id = Column(Integer, ForeignKey('orders.id'))
+ order_metadata = relationship(
+ 'SQLOrderMetadata', back_populates='order'
+ )
def update(self, data):
- if 'amount' in data and data['amount'] is not None:
- amount = data.pop('amount')
- self.amount = amount
-
- if 'price' in data and data['price'] is not None:
- price = data.pop('price')
- self.price = price
-
- if 'remaining' in data and data['remaining'] is not None:
- remaining = data.pop('remaining')
- self.remaining = remaining
-
- if 'filled' in data and data['filled'] is not None:
- filled = data.pop('filled')
- self.filled = filled
-
if "status" in data and data["status"] is not None:
- self.status = OrderStatus.from_value(data.pop("status")).value
+ data["status"] = OrderStatus.from_value(data["status"]).value
super().update(data)
diff --git a/investing_algorithm_framework/infrastructure/models/order/order_metadata.py b/investing_algorithm_framework/infrastructure/models/order/order_metadata.py
new file mode 100644
index 00000000..fd415a72
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/models/order/order_metadata.py
@@ -0,0 +1,38 @@
+import logging
+
+from sqlalchemy import Column, Integer, ForeignKey, Float
+from sqlalchemy.orm import relationship
+
+from investing_algorithm_framework.infrastructure.database import SQLBaseModel
+from investing_algorithm_framework.infrastructure.models.model_extension \
+ import SQLAlchemyModelExtension
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
+class SQLOrderMetadata(SQLBaseModel, SQLAlchemyModelExtension):
+ __tablename__ = "sql_order_metadata"
+ id = Column(Integer, primary_key=True, unique=True)
+ order_id = Column(Integer, ForeignKey('orders.id'))
+ order = relationship('SQLOrder', back_populates='order_metadata')
+ trade_id = Column(Integer)
+ stop_loss_id = Column(Integer)
+ take_profit_id = Column(Integer)
+ amount = Column(Float)
+ amount_pending = Column(Float)
+
+ def __init__(
+ self,
+ order_id,
+ amount,
+ amount_pending,
+ trade_id=None,
+ stop_loss_id=None,
+ take_profit_id=None,
+ ):
+ self.order_id = order_id
+ self.trade_id = trade_id
+ self.stop_loss_id = stop_loss_id
+ self.take_profit_id = take_profit_id
+ self.amount = amount
+ self.amount_pending = amount_pending
diff --git a/investing_algorithm_framework/infrastructure/models/trades/__init__.py b/investing_algorithm_framework/infrastructure/models/trades/__init__.py
new file mode 100644
index 00000000..dc46cb59
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/models/trades/__init__.py
@@ -0,0 +1,9 @@
+from .trade import SQLTrade
+from .trade_stop_loss import SQLTradeStopLoss
+from .trade_take_profit import SQLTradeTakeProfit
+
+__all__ = [
+ "SQLTrade",
+ "SQLTradeStopLoss",
+ "SQLTradeTakeProfit",
+]
diff --git a/investing_algorithm_framework/infrastructure/models/trade.py b/investing_algorithm_framework/infrastructure/models/trades/trade.py
similarity index 84%
rename from investing_algorithm_framework/infrastructure/models/trade.py
rename to investing_algorithm_framework/infrastructure/models/trades/trade.py
index 0a738728..7907f304 100644
--- a/investing_algorithm_framework/infrastructure/models/trade.py
+++ b/investing_algorithm_framework/infrastructure/models/trades/trade.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean
+from sqlalchemy import Column, Integer, String, DateTime, Float
from sqlalchemy.orm import relationship
from investing_algorithm_framework.domain import Trade, TradeStatus
@@ -53,6 +53,7 @@ class SQLTrade(Trade, SQLBaseModel, SQLAlchemyModelExtension):
opened_at = Column(DateTime, default=None)
open_price = Column(Float, default=None)
amount = Column(Float, default=None)
+ filled_amount = Column(Float, default=None)
remaining = Column(Float, default=None)
net_gain = Column(Float, default=0)
cost = Column(Float, default=0)
@@ -60,9 +61,18 @@ class SQLTrade(Trade, SQLBaseModel, SQLAlchemyModelExtension):
high_water_mark = Column(Float, default=None)
updated_at = Column(DateTime, default=None)
status = Column(String, default=TradeStatus.CREATED.value)
- stop_loss_percentage = Column(Float, default=None)
- trailing_stop_loss_percentage = Column(Float, default=None)
- stop_loss_triggered = Column(Boolean, default=False)
+ # Stop losses should be actively loaded
+ stop_losses = relationship(
+ 'SQLTradeStopLoss',
+ back_populates='trade',
+ lazy='joined'
+ )
+ # Take profits should be actively loaded
+ take_profits = relationship(
+ 'SQLTradeTakeProfit',
+ back_populates='trade',
+ lazy='joined'
+ )
def __init__(
self,
@@ -71,6 +81,7 @@ def __init__(
trading_symbol,
opened_at,
amount,
+ filled_amount,
remaining,
status=TradeStatus.CREATED.value,
closed_at=None,
@@ -80,9 +91,8 @@ def __init__(
last_reported_price=None,
high_water_mark=None,
sell_orders=[],
- stop_loss_percentage=None,
- trailing_stop_loss_percentage=None,
- stop_loss_triggered=False
+ stop_losses=[],
+ take_profits=[],
):
self.orders = [buy_order]
self.open_price = buy_order.price
@@ -90,6 +100,7 @@ def __init__(
self.trading_symbol = trading_symbol
self.closed_at = closed_at
self.amount = amount
+ self.filled_amount = filled_amount
self.remaining = remaining
self.net_gain = net_gain
self.cost = cost
@@ -98,9 +109,8 @@ def __init__(
self.opened_at = opened_at
self.updated_at = updated_at
self.status = status
- self.stop_loss_percentage = stop_loss_percentage
- self.trailing_stop_loss_percentage = trailing_stop_loss_percentage
- self.stop_loss_triggered = stop_loss_triggered
+ self.stop_losses = stop_losses
+ self.take_profits = take_profits
if sell_orders is not None:
self.orders.extend(sell_orders)
diff --git a/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py b/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py
new file mode 100644
index 00000000..44e82079
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py
@@ -0,0 +1,38 @@
+from sqlalchemy import Column, Integer, String, Float, ForeignKey, Boolean
+from sqlalchemy.orm import relationship
+
+from investing_algorithm_framework.domain import TradeStopLoss
+from investing_algorithm_framework.infrastructure.database import SQLBaseModel
+from investing_algorithm_framework.infrastructure.models.model_extension \
+ import SQLAlchemyModelExtension
+
+
+class SQLTradeStopLoss(TradeStopLoss, SQLBaseModel, SQLAlchemyModelExtension):
+ """
+ SQLTradeStopLoss model
+
+ A trade stop loss is a stop loss strategy for a trade.
+
+ Attributes:
+ * trade: Trade - the trade that the take profit is for
+ * take_profit: float - the take profit percentage
+ * trade_risk_type: TradeRiskType - the type of trade risk, either
+ trailing or fixed
+ * sell_percentage: float - the percentage of the trade to sell when the
+
+ """
+
+ __tablename__ = "trade_stop_losses"
+ id = Column(Integer, primary_key=True, unique=True)
+ trade_id = Column(Integer, ForeignKey('trades.id'))
+ trade = relationship('SQLTrade', back_populates='stop_losses')
+ trade_risk_type = Column(String)
+ percentage = Column(Float)
+ sell_percentage = Column(Float)
+ open_price = Column(Float)
+ high_water_mark = Column(Float)
+ stop_loss_price = Column(Float)
+ sell_prices = Column(String)
+ sell_amount = Column(Float)
+ sold_amount = Column(Float)
+ active = Column(Boolean)
diff --git a/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py b/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py
new file mode 100644
index 00000000..d96be334
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py
@@ -0,0 +1,39 @@
+from sqlalchemy import Column, Integer, String, Float, ForeignKey, Boolean
+from sqlalchemy.orm import relationship
+
+from investing_algorithm_framework.domain import TradeTakeProfit
+from investing_algorithm_framework.infrastructure.database import SQLBaseModel
+from investing_algorithm_framework.infrastructure.models.model_extension \
+ import SQLAlchemyModelExtension
+
+
+class SQLTradeTakeProfit(
+ TradeTakeProfit, SQLBaseModel, SQLAlchemyModelExtension
+):
+ """
+ SQLTradeTakeProfit model
+
+ A trade take profit is a take profit strategy for a trade.
+
+ Attributes:
+ * trade: Trade - the trade that the take profit is for
+ * take_profit: float - the take profit percentage
+ * trade_risk_type: TradeRiskType - the type of trade risk, either
+ trailing or fixed
+ * sell_percentage: float - the percentage of the trade to sell when the
+ """
+
+ __tablename__ = "trade_take_profits"
+ id = Column(Integer, primary_key=True, unique=True)
+ trade_id = Column(Integer, ForeignKey('trades.id'))
+ trade = relationship('SQLTrade', back_populates='take_profits')
+ trade_risk_type = Column(String)
+ percentage = Column(Float)
+ sell_percentage = Column(Float)
+ open_price = Column(Float)
+ high_water_mark = Column(Float)
+ sell_prices = Column(String)
+ take_profit_price = Column(Float)
+ sell_amount = Column(Float)
+ sold_amount = Column(Float)
+ active = Column(Boolean)
diff --git a/investing_algorithm_framework/infrastructure/repositories/__init__.py b/investing_algorithm_framework/infrastructure/repositories/__init__.py
index edd3d852..9c3f1662 100644
--- a/investing_algorithm_framework/infrastructure/repositories/__init__.py
+++ b/investing_algorithm_framework/infrastructure/repositories/__init__.py
@@ -1,9 +1,12 @@
from .order_repository import SQLOrderRepository
+from .order_metadata_repository import SQLOrderMetadataRepository
from .portfolio_repository import SQLPortfolioRepository
from .portfolio_snapshot_repository import SQLPortfolioSnapshotRepository
from .position_repository import SQLPositionRepository
from .position_snapshot_repository import SQLPositionSnapshotRepository
from .trade_repository import SQLTradeRepository
+from .trade_stop_loss_repository import SQLTradeStopLossRepository
+from .trade_take_profit_repository import SQLTradeTakeProfitRepository
__all__ = [
"SQLOrderRepository",
@@ -11,5 +14,8 @@
"SQLPositionSnapshotRepository",
"SQLPortfolioRepository",
"SQLPortfolioSnapshotRepository",
- "SQLTradeRepository"
+ "SQLTradeRepository",
+ "SQLTradeTakeProfitRepository",
+ "SQLTradeStopLossRepository",
+ "SQLOrderMetadataRepository"
]
diff --git a/investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py b/investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py
new file mode 100644
index 00000000..024e01d3
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/repositories/order_metadata_repository.py
@@ -0,0 +1,17 @@
+from investing_algorithm_framework.infrastructure.models import \
+ SQLOrderMetadata
+from .repository import Repository
+
+
+class SQLOrderMetadataRepository(Repository):
+ base_class = SQLOrderMetadata
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested order metadata was not found"
+
+ def _apply_query_params(self, db, query, query_params):
+
+ if "order_id" in query_params:
+ query = query.filter(
+ SQLOrderMetadata.order_id == query_params["order_id"]
+ )
+
+ return query
diff --git a/investing_algorithm_framework/infrastructure/repositories/repository.py b/investing_algorithm_framework/infrastructure/repositories/repository.py
index c07a9ccb..1785354c 100644
--- a/investing_algorithm_framework/infrastructure/repositories/repository.py
+++ b/investing_algorithm_framework/infrastructure/repositories/repository.py
@@ -217,7 +217,7 @@ def normalize_query(self, params):
def get_query_param(self, key, params, default=None, many=False):
boolean_array = ["true", "false"]
- if params is None:
+ if params is None or key not in params:
return default
params = self.normalize_query(params)
@@ -254,8 +254,21 @@ def save(self, object):
try:
db.add(object)
db.commit()
- return self.get(object)
+ return self.get(object.id)
except SQLAlchemyError as e:
logger.error(e)
db.rollback()
raise ApiException("Error saving object")
+
+ def save_objects(self, objects):
+
+ with Session() as db:
+ try:
+ for object in objects:
+ db.add(object)
+ db.commit()
+ return objects
+ except SQLAlchemyError as e:
+ logger.error(e)
+ db.rollback()
+ raise ApiException("Error saving objects")
diff --git a/investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py b/investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py
new file mode 100644
index 00000000..f5175bd4
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py
@@ -0,0 +1,23 @@
+import logging
+
+from investing_algorithm_framework.infrastructure.models import \
+ SQLTradeStopLoss
+
+from .repository import Repository
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
+class SQLTradeStopLossRepository(Repository):
+ base_class = SQLTradeStopLoss
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested trade stop loss was not found"
+
+ def _apply_query_params(self, db, query, query_params):
+ trade_query_param = self.get_query_param("trade_id", query_params)
+
+ if trade_query_param:
+ query = query.filter(
+ SQLTradeStopLoss.trade_id == trade_query_param
+ )
+
+ return query
diff --git a/investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py b/investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py
new file mode 100644
index 00000000..efd71bda
--- /dev/null
+++ b/investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py
@@ -0,0 +1,23 @@
+import logging
+
+from investing_algorithm_framework.infrastructure.models import \
+ SQLTradeTakeProfit
+
+from .repository import Repository
+
+logger = logging.getLogger("investing_algorithm_framework")
+
+
+class SQLTradeTakeProfitRepository(Repository):
+ base_class = SQLTradeTakeProfit
+ DEFAULT_NOT_FOUND_MESSAGE = "The requested trade take profit was not found"
+
+ def _apply_query_params(self, db, query, query_params):
+ trade_query_param = self.get_query_param("trade_id", query_params)
+
+ if trade_query_param:
+ query = query.filter(
+ SQLTradeTakeProfit.trade_id == trade_query_param
+ )
+
+ return query
diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py
index 749c9605..f237096d 100644
--- a/investing_algorithm_framework/services/backtesting/backtest_service.py
+++ b/investing_algorithm_framework/services/backtesting/backtest_service.py
@@ -172,7 +172,7 @@ def run_backtest(
# index_date=index_date,
# )
self.run_backtest_v2(
- algorithm=algorithm,
+ context=algorithm.context,
strategy=algorithm.get_strategy(strategy_profile.strategy_id)
)
@@ -243,13 +243,13 @@ def run_backtest_for_profile(self, algorithm, strategy, index_date):
market_data[data_id] = \
self._market_data_source_service.get_data(data_id)
- strategy.context = algorithm.context
- strategy.run_strategy(algorithm=algorithm, market_data=market_data)
+ context = self.algorithm.context
+ strategy.run_strategy(context=context, market_data=market_data)
- def run_backtest_v2(self, strategy, algorithm):
+ def run_backtest_v2(self, strategy, context):
config = self._configuration_service.get_config()
self._strategy_orchestrator_service.run_backtest_strategy(
- algorithm=algorithm, strategy=strategy, config=config
+ context=context, strategy=strategy, config=config
)
def generate_schedule(
@@ -487,9 +487,8 @@ def create_backtest_report(
backtest_position.price = ticker["bid"]
backtest_positions.append(backtest_position)
backtest_report.positions = backtest_positions
- backtest_report.trades = algorithm.get_trades()
+ backtest_report.trades = algorithm.context.get_trades()
backtest_report.orders = orders
- backtest_report.context = algorithm.context
traces = {}
# Add traces to the backtest report
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 78a971a0..4fce0ecb 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
@@ -202,9 +202,10 @@ def get_data(self, identifier):
time_frame = market_data_source.time_frame
if time_frame is not None:
+ time_frame = TimeFrame.from_value(time_frame)
result["time_frame"] = time_frame.value
else:
- result["time_frame"] = TimeFrame.CURRENT
+ result["time_frame"] = TimeFrame.CURRENT.value
result["symbol"] = market_data_source.symbol
return result
diff --git a/investing_algorithm_framework/services/order_service/order_service.py b/investing_algorithm_framework/services/order_service/order_service.py
index edfc7400..02a50014 100644
--- a/investing_algorithm_framework/services/order_service/order_service.py
+++ b/investing_algorithm_framework/services/order_service/order_service.py
@@ -23,7 +23,7 @@ def __init__(
portfolio_configuration_service,
portfolio_snapshot_service,
market_credential_service,
- trade_service
+ trade_service,
):
super(OrderService, self).__init__(order_repository)
self.configuration_service = configuration_service
@@ -37,8 +37,100 @@ def __init__(
self.trade_service = trade_service
def create(self, data, execute=True, validate=True, sync=True) -> Order:
+ """
+ Function to create an order. The function will create the order and
+ execute it if execute is set to True. The function will also validate
+ the order if validate is set to True. The function will also sync the
+ portfolio with the order if sync is set to True.
+
+ The following only applies if the order is a sell order:
+
+ If stop_losses, or take_profits are in the data, we assume that the
+ order has been created by a stop loss or take profit. We will then
+ create for the order one or more metadata objects with the
+ amount and stop loss id or take profit id. These objects can later
+ be used to restore the stop loss or take profit to its original state
+ if the order is cancelled or rejected.
+
+ If trades are in the data, we assume that the order has
+ been created by a closing a specific trade. We will then create for
+ the order one metadata object with the amount and trade id. This
+ objects can later be used to restore the trade to its original
+ state if the order is cancelled or rejected.
+
+ If there are no trades in the data, we rely on the trade service to
+ create the metadata objects for the order.
+
+ The metadata objects are needed because for trades, stop losses and
+ take profits we need to know how much of the order has been
+ filled at any given time. If the order is cancelled or rejected we
+ need to add the pending amount back to the trade, stop loss or take
+ profit.
+
+ Args:
+ data: dict - the data to create the order with. Data should have
+ the following format:
+ {
+ "target_symbol": str,
+ "trading_symbol": str,
+ "order_side": str,
+ "order_type": str,
+ "amount": float,
+ "filled" (optional): float, // If set, trades
+ and positions are synced
+ "remaining" (optional): float, // Same as filled
+ "price": float,
+ "portfolio_id": int
+ "stop_losses" (optional): list[dict] - list of stop
+ losses with the following format:
+ {
+ "stop_loss_id": float,
+ "amount": float
+ }
+ "take_profits" (optional): list[dict] - list of
+ take profits with the following format:
+ {
+ "take_profit_id": float,
+ "amount": float
+ }
+ "trades" (optional): list[dict] - list of trades
+ with the following format:
+ {
+ "trade_id": int,
+ "amount": float
+ }
+ }
+
+ execute: bool - if True the order will be executed
+ validate: bool - if True the order will be validated
+ sync: bool - if True the portfolio will be synced with the order
+
+ Returns:
+ Order: Order object
+ """
portfolio_id = data["portfolio_id"]
portfolio = self.portfolio_repository.get(portfolio_id)
+ trades = data.get("trades", [])
+ stop_losses = data.get("stop_losses", [])
+ take_profits = data.get("take_profits", [])
+ filled = data.get("filled", 0)
+ remaining = data.get("remaining", 0)
+ amount = data.get("amount", 0)
+
+ if "filled" in data:
+ del data["filled"]
+
+ if "remaining" in data:
+ del data["remaining"]
+
+ if "trades" in data:
+ del data["trades"]
+
+ if "stop_losses" in data:
+ del data["stop_losses"]
+
+ if "take_profits" in data:
+ del data["take_profits"]
if validate:
self.validate_order(data, portfolio)
@@ -49,33 +141,91 @@ def create(self, data, execute=True, validate=True, sync=True) -> Order:
if validate:
self.validate_order(data, portfolio)
+ # Get the position
position = self._create_position_if_not_exists(symbol, portfolio)
data["position_id"] = position.id
data["remaining"] = data["amount"]
data["status"] = OrderStatus.CREATED.value
order = self.order_repository.create(data)
order_id = order.id
+ created_at = order.created_at
+ order_side = order.order_side
+
+ if OrderSide.SELL.equals(order_side):
+ # Create order metadata if their is a key in the data
+ # for trades, stop_losses or take_profits
+ self.trade_service.create_order_metadata_with_trade_context(
+ sell_order=order,
+ trades=trades,
+ stop_losses=stop_losses,
+ take_profits=take_profits
+ )
+ else:
+ self.trade_service.create_trade_from_buy_order(order)
if sync:
- if OrderSide.BUY.equals(order.get_order_side()):
+ order = self.get(order_id)
+ if OrderSide.BUY.equals(order_side):
self._sync_portfolio_with_created_buy_order(order)
else:
self._sync_portfolio_with_created_sell_order(order)
- self.create_snapshot(portfolio.id, created_at=order.created_at)
+ self.create_snapshot(portfolio.id, created_at=created_at)
+
+ if sync:
+
+ if filled or remaining:
+
+ if filled == 0:
+ filled = amount - remaining
+
+ if remaining == 0:
+ remaining = amount - filled
+
+ status = OrderStatus.OPEN.value
+
+ if filled == amount:
+ status = OrderStatus.CLOSED.value
+
+ order = self.update(
+ order_id,
+ {
+ "filled": filled,
+ "remaining": remaining,
+ "status": status
+ }
+ )
if execute:
portfolio.configuration = self.portfolio_configuration_service\
.get(portfolio.identifier)
self.execute_order(order_id, portfolio)
- # Create trade from buy order if buy order
- if OrderSide.BUY.equals(order.get_order_side()):
- self.trade_service.create_trade_from_buy_order(order)
-
+ order = self.get(order_id)
return order
def update(self, object_id, data):
+ """
+ Function to update an order. The function will update the order and
+ sync the portfolio, position and trades if the order has been filled.
+
+ If the order has been cancelled, expired or rejected the function will
+ sync the portfolio, position, trades, stop losses, and
+ take profits with the order.
+
+ Args:
+ object_id: int - the id of the order to update
+ data: dict - the data to update the order with
+ the following format:
+ {
+ "filled" (optional): float,
+ "remaining" (optional): float,
+ "status" (optional): str,
+ }
+
+ Returns:
+ Order: Order object that has been updated
+ """
previous_order = self.order_repository.get(object_id)
trading_symbol_position = self.position_repository.find(
{
@@ -91,7 +241,6 @@ def update(self, object_id, data):
- previous_order.get_filled()
if filled_difference:
-
if OrderSide.BUY.equals(new_order.get_order_side()):
self._sync_with_buy_order_filled(previous_order, new_order)
else:
@@ -100,7 +249,6 @@ def update(self, object_id, data):
if "status" in data:
if OrderStatus.CANCELED.equals(new_order.get_status()):
-
if OrderSide.BUY.equals(new_order.get_order_side()):
self._sync_with_buy_order_cancelled(new_order)
else:
@@ -373,12 +521,16 @@ def _sync_portfolio_with_created_buy_order(self, order):
def _sync_portfolio_with_created_sell_order(self, order):
"""
Function to sync the portfolio with a created sell order. The
- function will subtract the amount of the order from the position.
+ function will subtract the amount of the order from the position and
+ the trade amount.
+
The portfolio will not be updated because the sell order has not been
filled.
Args:
order: Order object representing the sell order
+ stop_loss_ids: List of stop loss order ids
+ take_profit_ids: List of take profit order ids
Returns:
None
@@ -470,11 +622,12 @@ def _sync_with_sell_order_filled(self, previous_order, current_order):
self.position_repository.update(
trading_symbol_position.id,
{
- "amount": trading_symbol_position.get_amount()
- + filled_size
+ "amount":
+ trading_symbol_position.get_amount() + filled_size
}
)
- self.trade_service.update_trade_with_sell_order_filled(
+
+ self.trade_service.update_trade_with_filled_sell_order(
filled_difference, current_order
)
@@ -520,6 +673,7 @@ def _sync_with_sell_order_cancelled(self, order):
"amount": position.get_amount() + remaining
}
)
+ self.trade_service.update_trade_with_removed_sell_order(order)
def _sync_with_buy_order_failed(self, order):
remaining = order.get_amount() - order.get_filled()
@@ -564,6 +718,8 @@ def _sync_with_sell_order_failed(self, order):
}
)
+ self.trade_service.update_trade_with_removed_sell_order(order)
+
def _sync_with_buy_order_expired(self, order):
remaining = order.get_amount() - order.get_filled()
size = remaining * order.get_price()
@@ -607,6 +763,8 @@ def _sync_with_sell_order_expired(self, order):
}
)
+ self.trade_service.update_trade_with_removed_sell_order(order)
+
def _sync_with_buy_order_rejected(self, order):
remaining = order.get_amount() - order.get_filled()
size = remaining * order.get_price()
@@ -650,6 +808,8 @@ def _sync_with_sell_order_rejected(self, order):
}
)
+ self.trade_service.update_trade_with_removed_sell_order(order)
+
def create_snapshot(self, portfolio_id, created_at=None):
if created_at is None:
diff --git a/investing_algorithm_framework/services/position_service.py b/investing_algorithm_framework/services/position_service.py
index 82d6e776..c249a59d 100644
--- a/investing_algorithm_framework/services/position_service.py
+++ b/investing_algorithm_framework/services/position_service.py
@@ -8,14 +8,12 @@ class PositionService(RepositoryService):
def __init__(
self,
repository,
- order_repository,
market_service: MarketService,
market_credential_service
):
super().__init__(repository)
self._market_service: MarketService = market_service
self._market_credentials_service = market_credential_service
- self._order_repository = order_repository
def close_position(self, position_id, portfolio):
self._market_service.market_data_credentials = \
diff --git a/investing_algorithm_framework/services/strategy_orchestrator_service.py b/investing_algorithm_framework/services/strategy_orchestrator_service.py
index 9885b610..d2d2fe26 100644
--- a/investing_algorithm_framework/services/strategy_orchestrator_service.py
+++ b/investing_algorithm_framework/services/strategy_orchestrator_service.py
@@ -57,7 +57,7 @@ def cleanup_threads(self):
stoppable.done = True
self.threads = [t for t in self.threads if not t.done]
- def run_strategy(self, strategy, algorithm, sync=False):
+ def run_strategy(self, strategy, context, sync=False):
self.cleanup_threads()
matching_thread = next(
(t for t in self.threads if t.name == strategy.worker_id),
@@ -76,7 +76,7 @@ def run_strategy(self, strategy, algorithm, sync=False):
if sync:
strategy.run_strategy(
market_data=market_data,
- algorithm=algorithm,
+ context=context,
)
else:
self.iterations += 1
@@ -84,7 +84,7 @@ def run_strategy(self, strategy, algorithm, sync=False):
target=strategy.run_strategy,
kwargs={
"market_data": market_data,
- "algorithm": algorithm,
+ "context": context,
}
)
thread.name = strategy.worker_id
@@ -93,16 +93,16 @@ def run_strategy(self, strategy, algorithm, sync=False):
self.history[strategy.worker_id] = {"last_run": datetime.utcnow()}
- def run_backtest_strategy(self, strategy, algorithm, config):
+ def run_backtest_strategy(self, strategy, context, config):
data = \
self.market_data_source_service.get_data_for_strategy(strategy)
strategy.run_strategy(
market_data=data,
- algorithm=algorithm,
+ context=context,
)
- def run_task(self, task, algorithm, sync=False):
+ def run_task(self, task, context, sync=False):
self.cleanup_threads()
matching_thread = next(
@@ -117,12 +117,12 @@ def run_task(self, task, algorithm, sync=False):
logger.info(f"Running task {task.worker_id}")
if sync:
- task.run(algorithm=algorithm)
+ task.run(context=context)
else:
self.iterations += 1
thread = StoppableThread(
target=task.run,
- kwargs={"algorithm": algorithm}
+ kwargs={"context": context}
)
thread.name = task.worker_id
thread.start()
@@ -130,7 +130,7 @@ def run_task(self, task, algorithm, sync=False):
self.history[task.worker_id] = {"last_run": datetime.utcnow()}
- def start(self, algorithm, number_of_iterations=None):
+ def start(self, context, number_of_iterations=None):
"""
Function to start and schedule the strategies and tasks
@@ -149,24 +149,24 @@ def start(self, algorithm, number_of_iterations=None):
for strategy in self.strategies:
if TimeUnit.SECOND.equals(strategy.time_unit):
schedule.every(strategy.interval)\
- .seconds.do(self.run_strategy, strategy, algorithm)
+ .seconds.do(self.run_strategy, strategy, context)
elif TimeUnit.MINUTE.equals(strategy.time_unit):
schedule.every(strategy.interval)\
- .minutes.do(self.run_strategy, strategy, algorithm)
+ .minutes.do(self.run_strategy, strategy, context)
elif TimeUnit.HOUR.equals(strategy.time_unit):
schedule.every(strategy.interval)\
- .hours.do(self.run_strategy, strategy, algorithm)
+ .hours.do(self.run_strategy, strategy, context)
for task in self.tasks:
if TimeUnit.SECOND.equals(task.time_unit):
schedule.every(task.interval)\
- .seconds.do(self.run_task, task, algorithm)
+ .seconds.do(self.run_task, task, context)
elif TimeUnit.MINUTE.equals(task.time_unit):
schedule.every(task.interval)\
- .minutes.do(self.run_task, task, algorithm)
+ .minutes.do(self.run_task, task, context)
elif TimeUnit.HOUR.equals(task.time_unit):
schedule.every(task.interval)\
- .hours.do(self.run_task, task, algorithm)
+ .hours.do(self.run_task, task, context)
def stop(self):
for thread in self.threads:
diff --git a/investing_algorithm_framework/services/trade_service/trade_service.py b/investing_algorithm_framework/services/trade_service/trade_service.py
index 2c67607b..badb3103 100644
--- a/investing_algorithm_framework/services/trade_service/trade_service.py
+++ b/investing_algorithm_framework/services/trade_service/trade_service.py
@@ -1,8 +1,9 @@
import logging
from queue import PriorityQueue
-from investing_algorithm_framework.domain import OrderStatus, \
- TradeStatus, Trade, OperationalException, MarketDataType
+from investing_algorithm_framework.domain import OrderStatus, TradeStatus, \
+ Trade, OperationalException, TradeRiskType, PeekableQueue, OrderType, \
+ OrderSide, MarketDataType
from investing_algorithm_framework.services.repository_service import \
RepositoryService
@@ -10,20 +11,34 @@
class TradeService(RepositoryService):
+ """
+ Trade service class to handle trade related operations. This class
+ is responsible for creating, updating, and deleting trades. It also
+ takes care of keeping track of all sell transactions that are
+ associated with a trade.
+ """
def __init__(
self,
trade_repository,
+ order_repository,
+ trade_stop_loss_repository,
+ trade_take_profit_repository,
position_repository,
portfolio_repository,
market_data_source_service,
- configuration_service
+ configuration_service,
+ order_metadata_repository
):
super(TradeService, self).__init__(trade_repository)
+ self.order_repository = order_repository
self.portfolio_repository = portfolio_repository
self.market_data_source_service = market_data_source_service
self.position_repository = position_repository
self.configuration_service = configuration_service
+ self.trade_stop_loss_repository = trade_stop_loss_repository
+ self.trade_take_profit_repository = trade_take_profit_repository
+ self.order_metadata_repository = order_metadata_repository
def create_trade_from_buy_order(self, buy_order) -> Trade:
"""
@@ -39,9 +54,8 @@ def create_trade_from_buy_order(self, buy_order) -> Trade:
Returns:
Trade object
"""
- status = buy_order.get_status()
- if status in \
+ if buy_order.status in \
[
OrderStatus.CANCELED.value,
OrderStatus.EXPIRED.value,
@@ -53,7 +67,8 @@ def create_trade_from_buy_order(self, buy_order) -> Trade:
"buy_order": buy_order,
"target_symbol": buy_order.target_symbol,
"trading_symbol": buy_order.trading_symbol,
- "amount": buy_order.filled,
+ "amount": buy_order.amount,
+ "filled_amount": buy_order.filled,
"remaining": buy_order.filled,
"opened_at": buy_order.created_at,
"cost": buy_order.filled * buy_order.price
@@ -65,6 +80,394 @@ def create_trade_from_buy_order(self, buy_order) -> Trade:
return self.create(data)
+ def _create_trade_metadata_with_sell_order(self, sell_order):
+ """
+ Function to create trade metadata with only a sell order.
+ This function will create all metadata objects for the trades
+ that are closed with the sell order amount.
+
+ Args:
+ sell_order: Order object representing the sell order
+
+ Returns:
+ None
+ """
+ position = self.position_repository.find({
+ "order_id": sell_order.id
+ })
+ portfolio_id = position.portfolio_id
+ matching_trades = self.get_all({
+ "status": TradeStatus.OPEN.value,
+ "target_symbol": sell_order.target_symbol,
+ "portfolio_id": portfolio_id
+ })
+ updated_at = sell_order.updated_at
+ amount_to_close = sell_order.amount
+ trade_queue = PriorityQueue()
+ total_remaining = 0
+ sell_order_id = sell_order.id
+ sell_price = sell_order.price
+
+ for trade in matching_trades:
+ if trade.remaining > 0:
+ total_remaining += trade.remaining
+ trade_queue.put(trade)
+
+ if total_remaining < amount_to_close:
+ raise OperationalException(
+ "Not enough amount to close in trades."
+ )
+
+ # Create order metadata object
+ while amount_to_close > 0 and not trade_queue.empty():
+ trade = trade_queue.get()
+ trade_id = trade.id
+ available_to_close = trade.remaining
+ cost = 0
+
+ if amount_to_close >= available_to_close:
+ amount_to_close = amount_to_close - available_to_close
+ cost = trade.open_price * available_to_close
+ net_gain = (sell_price * available_to_close) - cost
+
+ update_data = {
+ "remaining": 0,
+ "orders": trade.orders.append(sell_order),
+ "updated_at": updated_at,
+ "closed_at": updated_at,
+ "net_gain": trade.net_gain + net_gain
+ }
+
+ if trade.filled_amount == trade.amount:
+ update_data["status"] = TradeStatus.CLOSED.value
+
+ self.update(trade_id, update_data)
+ self.repository.add_order_to_trade(trade, sell_order)
+
+ # Create metadata object
+ self.order_metadata_repository.\
+ create({
+ "order_id": sell_order_id,
+ "trade_id": trade_id,
+ "amount": available_to_close,
+ "amount_pending": available_to_close,
+ })
+ else:
+ to_be_closed = amount_to_close
+ cost = trade.open_price * to_be_closed
+ net_gain = (sell_price * to_be_closed) - cost
+
+ self.update(
+ trade_id, {
+ "remaining": trade.remaining - to_be_closed,
+ "orders": trade.orders.append(sell_order),
+ "updated_at": updated_at,
+ "net_gain": trade.net_gain + net_gain
+ }
+ )
+ self.repository.add_order_to_trade(trade, sell_order)
+
+ # Create a order metadata object
+ self.order_metadata_repository.\
+ create({
+ "order_id": sell_order_id,
+ "trade_id": trade_id,
+ "amount": to_be_closed,
+ "amount_pending": to_be_closed,
+ })
+
+ amount_to_close = 0
+
+ def _create_stop_loss_metadata_with_sell_order(
+ self, sell_order_id, stop_losses
+ ):
+ """
+ """
+ sell_order = self.order_repository.get(sell_order_id)
+
+ for stop_loss_data in stop_losses:
+
+ self.order_metadata_repository.\
+ create({
+ "order_id": sell_order.id,
+ "stop_loss_id": stop_loss_data["stop_loss_id"],
+ "amount": stop_loss_data["amount"],
+ "amount_pending": stop_loss_data["amount"]
+ })
+
+ def _create_take_profit_metadata_with_sell_order(
+ self, sell_order_id, take_profits
+ ):
+ """
+ """
+ sell_order = self.order_repository.get(sell_order_id)
+
+ for take_profit_data in take_profits:
+
+ self.order_metadata_repository.\
+ create({
+ "order_id": sell_order.id,
+ "take_profit_id": take_profit_data["take_profit_id"],
+ "amount": take_profit_data["amount"],
+ "amount_pending": take_profit_data["amount"]
+ })
+
+ def update(self, trade_id, data) -> Trade:
+ """
+ Function to update a trade object. This function will update
+ the trade object with the given data.
+
+ Args:
+ trade_id: int representing the id of the trade object
+ data: dict representing the data that should be updated
+
+ Returns:
+ Trade object
+ """
+ # Update the stop losses and take profits if last reported price
+ # is updated
+ if "last_reported_price" in data:
+ trade = self.get(trade_id)
+ stop_losses = trade.stop_losses
+ to_be_saved_stop_losses = []
+ take_profits = trade.take_profits
+ to_be_saved_take_profits = []
+
+ for stop_loss in stop_losses:
+ stop_loss.update_with_last_reported_price(
+ data["last_reported_price"]
+ )
+ to_be_saved_stop_losses.append(stop_loss)
+
+ for take_profit in take_profits:
+ take_profit.update_with_last_reported_price(
+ data["last_reported_price"]
+ )
+ to_be_saved_take_profits.append(take_profit)
+
+ self.trade_stop_loss_repository\
+ .save_objects(to_be_saved_stop_losses)
+
+ self.trade_take_profit_repository\
+ .save_objects(to_be_saved_take_profits)
+
+ return super(TradeService, self).update(trade_id, data)
+
+ def _create_trade_metadata_with_sell_order_and_trades(
+ self, sell_order, trades
+ ):
+ """
+ """
+ sell_order_id = sell_order.id
+ updated_at = sell_order.updated_at
+ sell_amount = sell_order.amount
+ sell_price = sell_order.price
+
+ for trade_data in trades:
+ trade = self.get(trade_data["trade_id"])
+ trade_id = trade.id
+ open_price = trade.open_price
+ remaining = trade.remaining
+ old_net_gain = trade.net_gain
+ filled_amount = trade.filled_amount
+ amount = trade.amount
+
+ self.order_metadata_repository.\
+ create({
+ "order_id": sell_order_id,
+ "trade_id": trade_data["trade_id"],
+ "amount": trade_data["amount"],
+ "amount_pending": trade_data["amount"]
+ })
+
+ # Add the sell order to the trade
+ self.repository.add_order_to_trade(trade, sell_order)
+
+ # Update the trade
+ net_gain = (sell_price * sell_amount) - open_price * sell_amount
+ remaining = remaining - trade_data["amount"]
+ trade_updated_data = {
+ "remaining": remaining,
+ "updated_at": updated_at,
+ "net_gain": old_net_gain + net_gain
+ }
+
+ if remaining == 0 and filled_amount == amount:
+ trade_updated_data["status"] = TradeStatus.CLOSED.value
+ trade_updated_data["closed_at"] = updated_at
+ else:
+ trade_updated_data["status"] = TradeStatus.OPEN.value
+
+ # Update the trade object
+ self.update(trade_id, trade_updated_data)
+
+ def create_order_metadata_with_trade_context(
+ self, sell_order, trades=None, stop_losses=None, take_profits=None
+ ):
+ """
+ Function to create order metadata for trade related models.
+
+ If only the sell order is provided, we assume that the sell order
+ is initiated by a client of the order service. In this case we
+ create only metadata objects for the trades based on size of the
+ sell order.
+
+ If also stop losses and take profits are provided, we assume that
+ the sell order is initiated a stop loss or take profit. In this case
+ we create metadata objects for the trades, stop losses,
+ and take profits.
+
+ If the trades param is provided, we assume that the sell order is
+ based on either a stop loss or take profit or a closing of a trade.
+ In this case we create also the metadata objects for the trades,
+
+ As part of this function, we will also update the position cost.
+
+ Scenario 1: Sell order without trades, stop losses, and take profits
+ - Use the sell amount to create all trade metadata objects
+ - Update the position cost
+
+ Scenario 2: Sell order with trades
+ - We assume that the sell amount is same as the total amount
+ of the trades
+ - Use the trades to create all trade metadata objects
+ - Update trade object remaining amount
+ - Update the position cost
+
+ Scenario 3: Sell order with trades, stop losses, and take profits
+ - We assume that the sell amount is same as the total
+ amount of the trades
+ - Use the trades to create all metadata objects
+ - Update trade object remaining amount
+ - Use the stop losses to create all metadata objects
+ - Use the take profits to create all metadata objects
+ - Update the position cost
+
+ Args:
+ sell_order: Order object representing the sell order that has
+ been created
+
+ Returns:
+ None
+ """
+ sell_order_id = sell_order.id
+ sell_price = sell_order.price
+
+ if (trades is None or len(trades) == 0) \
+ and (stop_losses is None or len(stop_losses) == 0) \
+ and (take_profits is None or len(take_profits) == 0):
+ self._create_trade_metadata_with_sell_order(sell_order)
+ else:
+
+ if trades is not None:
+ self._create_trade_metadata_with_sell_order_and_trades(
+ sell_order, trades
+ )
+
+ if stop_losses is not None:
+ self._create_stop_loss_metadata_with_sell_order(
+ sell_order_id, stop_losses
+ )
+
+ if take_profits is not None:
+ self._create_take_profit_metadata_with_sell_order(
+ sell_order_id, take_profits
+ )
+
+ # Retrieve all trades metadata objects
+ order_metadatas = self.order_metadata_repository.get_all({
+ "order_id": sell_order_id
+ })
+
+ # Update the position cost
+ position = self.position_repository.find({
+ "order_id": sell_order_id
+ })
+
+ # Update position
+ cost = 0
+ net_gain = 0
+ for metadata in order_metadatas:
+ if metadata.trade_id is not None:
+ trade = self.get(metadata.trade_id)
+ cost += trade.open_price * metadata.amount
+ net_gain += (sell_price * metadata.amount) - cost
+
+ position.cost -= cost
+ self.position_repository.save(position)
+
+ # Update the net gain of the portfolio
+ portfolio = self.portfolio_repository.get(position.portfolio_id)
+ portfolio.total_net_gain += net_gain
+ self.portfolio_repository.save(portfolio)
+
+ def update_trade_with_removed_sell_order(
+ self, sell_order
+ ) -> Trade:
+ """
+ This function updates a trade with a removed sell order. It does
+ this by removing the sell transaction object from the trade object.
+
+ At time of removing, the remaining amount of the sell transaction
+ is added back to the trade object.
+ """
+ position_cost = 0
+ total_net_gain = 0
+
+ # Get all order metadata objects that are associated with
+ # the sell order
+ order_metadatas = self.order_metadata_repository.get_all({
+ "order_id": sell_order.id
+ })
+
+ for metadata in order_metadatas:
+ # If trade id is not None, update the trade object
+ if metadata.trade_id is not None:
+ trade = self.get(metadata.trade_id)
+ cost = metadata.amount_pending * trade.open_price
+ net_gain = (sell_order.price * metadata.amount_pending) - cost
+ trade.remaining += metadata.amount_pending
+ trade.status = TradeStatus.OPEN.value
+ trade.updated_at = sell_order.updated_at
+ trade.net_gain -= net_gain
+ trade.cost += cost
+ trade = self.save(trade)
+
+ # Update the position cost
+ position_cost += cost
+ total_net_gain += net_gain
+
+ if metadata.stop_loss_id is not None:
+ stop_loss = self.trade_stop_loss_repository\
+ .get(metadata.stop_loss_id)
+ stop_loss.sold_amount -= metadata.amount_pending
+
+ if stop_loss.sold_amount < stop_loss.sell_amount:
+ stop_loss.active = True
+ self.trade_stop_loss_repository.save(stop_loss)
+
+ if metadata.take_profit_id is not None:
+ take_profit = self.trade_take_profit_repository\
+ .get(metadata.take_profit_id)
+ take_profit.sold_amount -= metadata.amount_pending
+
+ if take_profit.sold_amount < take_profit.sell_amount:
+ take_profit.active = True
+
+ self.trade_take_profit_repository.save(take_profit)
+
+ # Update the position cost
+ position = self.position_repository.find({
+ "order_id": sell_order.id
+ })
+ position.cost += position_cost
+ self.position_repository.save(position)
+
+ # Update the net gain of the portfolio
+ portfolio = self.portfolio_repository.get(position.portfolio_id)
+ portfolio.total_net_gain -= total_net_gain
+ self.portfolio_repository.save(portfolio)
+
def update_trade_with_buy_order(
self, filled_difference, buy_order
) -> Trade:
@@ -105,7 +508,7 @@ def update_trade_with_buy_order(
trade = self.find({"order_id": buy_order.id})
updated_data = {
- "amount": trade.amount + filled_difference,
+ "filled_amount": trade.filled_amount + filled_difference,
"remaining": trade.remaining + filled_difference,
"cost": trade.cost + filled_difference * buy_order.price
}
@@ -116,114 +519,66 @@ def update_trade_with_buy_order(
trade = self.update(trade.id, updated_data)
return trade
- def update_trade_with_sell_order_filled(
- self, filled_amount, sell_order
+ def update_trade_with_filled_sell_order(
+ self, filled_difference, sell_order
) -> Trade:
"""
- Function to update a trade from a sell order that has been filled.
- This function checks if a trade exists for the buy order.
- If the given buy order has its status set to
- CANCLED, EXPIRED, or REJECTED, the
- trade will object will be removed. If the given buy order
- has its status set to CLOSED or OPEN, the amount and
- remaining of the trade object will be updated.
-
- Args:
- sell_order: Order object representing the sell order that has
- been filled.
- filled_amount: float representing the filled amount of the sell
- order
-
- Returns:
- Trade object
"""
-
- # Only update the trade if the sell order has been filled
- if sell_order.get_status() != OrderStatus.CLOSED.value:
- return None
-
- position = self.position_repository.find({
+ # Update all metadata objects
+ metadata_objects = self.order_metadata_repository.get_all({
"order_id": sell_order.id
})
- portfolio_id = position.portfolio_id
- matching_trades = self.get_all({
- "status": TradeStatus.OPEN.value,
- "target_symbol": sell_order.target_symbol,
- "portfolio_id": portfolio_id
- })
- target_symbol = sell_order.target_symbol
- price = sell_order.price
- updated_at = sell_order.updated_at
- amount_to_close = filled_amount
- order_queue = PriorityQueue()
- total_net_gain = 0
- total_cost = 0
- total_remaining = 0
- for trade in matching_trades:
- if trade.remaining > 0:
- total_remaining += trade.remaining
- order_queue.put(trade)
-
- if total_remaining < amount_to_close:
- raise OperationalException(
- "Not enough amount to close in trades."
- )
-
- while amount_to_close > 0 and not order_queue.empty():
- trade = order_queue.get()
- available_to_close = trade.remaining
-
- if amount_to_close >= available_to_close:
- cost = trade.buy_order.price * available_to_close
- net_gain = (price * available_to_close) - cost
- amount_to_close = amount_to_close - available_to_close
- self.update(
- trade.id, {
- "remaining": 0,
- "closed_at": updated_at,
- "net_gain": trade.net_gain + net_gain,
- "status": TradeStatus.CLOSED.value
- }
- )
- self.repository.add_order_to_trade(trade, sell_order)
- else:
- to_be_closed = amount_to_close
- cost = trade.buy_order.price * to_be_closed
- net_gain = (price * to_be_closed) - cost
- self.update(
- trade.id, {
- "remaining": trade.remaining - to_be_closed,
- "net_gain": trade.net_gain + net_gain,
- "orders": trade.orders.append(sell_order)
- }
- )
- self.repository.add_order_to_trade(trade, sell_order)
- amount_to_close = 0
-
- total_net_gain += net_gain
- total_cost += cost
-
- portfolio = self.portfolio_repository.get(portfolio_id)
- self.portfolio_repository.update(
- portfolio.id,
- {
- "total_net_gain": portfolio.total_net_gain + total_net_gain,
- "cost": portfolio.total_cost - total_cost
- }
- )
- position = self.position_repository.find(
- {
- "portfolio": portfolio.id,
- "symbol": target_symbol
- }
- )
- self.position_repository.update(
- position.id,
- {
- "cost": position.cost - total_cost,
- }
- )
+ trade_filled_difference = filled_difference
+ stop_loss_filled_difference = filled_difference
+ take_profit_filled_difference = filled_difference
+
+ for metadata_object in metadata_objects:
+
+ if metadata_object.trade_id is not None \
+ and trade_filled_difference > 0:
+
+ if metadata_object.amount_pending >= trade_filled_difference:
+ amount = trade_filled_difference
+ trade_filled_difference = 0
+ else:
+ amount = metadata_object.amount_pending
+ trade_filled_difference -= amount
+
+ metadata_object.amount_pending -= amount
+ self.order_metadata_repository.save(metadata_object)
+
+ if metadata_object.stop_loss_id is not None \
+ and stop_loss_filled_difference > 0:
+
+ if (
+ metadata_object.amount_pending >=
+ stop_loss_filled_difference
+ ):
+ amount = stop_loss_filled_difference
+ stop_loss_filled_difference = 0
+ else:
+ amount = metadata_object.amount_pending
+ stop_loss_filled_difference -= amount
+
+ metadata_object.amount_pending -= amount
+ self.order_metadata_repository.save(metadata_object)
+
+ if metadata_object.take_profit_id is not None \
+ and take_profit_filled_difference > 0:
+
+ if (
+ metadata_object.amount_pending >=
+ take_profit_filled_difference
+ ):
+ amount = take_profit_filled_difference
+ take_profit_filled_difference = 0
+ else:
+ amount = metadata_object.amount_pending
+ take_profit_filled_difference -= amount
+
+ metadata_object.amount_pending -= amount
+ self.order_metadata_repository.save(metadata_object)
def update_trades_with_market_data(self, market_data):
open_trades = self.get_all({"status": TradeStatus.OPEN.value})
@@ -249,96 +604,326 @@ def update_trades_with_market_data(self, market_data):
"last_reported_price": last_row["Close"][0],
"updated_at": last_row["Datetime"][0]
}
- price = last_row["Close"][0]
-
- if open_trade.trailing_stop_loss_percentage is not None:
-
- if open_trade.high_water_mark is None or \
- open_trade.high_water_mark < price:
- update_data["high_water_mark"] = price
-
self.update(open_trade.id, update_data)
- def add_stop_loss(self, trade, percentage):
+ def add_stop_loss(
+ self,
+ trade,
+ percentage: float,
+ trade_risk_type: TradeRiskType = TradeRiskType.FIXED,
+ sell_percentage: float = 100,
+ ):
"""
- Function to add a stop loss to a trade. The stop loss is
- represented as a percentage of the open price.
+ Function to add a stop loss to a trade.
+
+ Example of fixed stop loss:
+ * You buy BTC at $40,000.
+ * You set a SL of 5% → SL level at $38,000 (40,000 - 5%).
+ * BTC price increases to $42,000 → SL level remains at $38,000.
+ * BTC price drops to $38,000 → SL level reached, trade closes.
+
+ Example of trailing stop loss:
+ * You buy BTC at $40,000.
+ * You set a TSL of 5%, setting the sell price at $38,000.
+ * BTC price increases to $42,000 → New TSL level at
+ $39,900 (42,000 - 5%).
+ * BTC price drops to $39,900 → SL level reached, trade closes.
Args:
trade: Trade object representing the trade
percentage: float representing the percentage of the open price
that the stop loss should be set at
+ trade_risk_type (TradeRiskType): The type of the stop loss, fixed
+ or trailing
+ sell_percentage: float representing the percentage of the trade
+ that should be sold if the stop loss is triggered
Returns:
None
"""
trade = self.get(trade.id)
- updated_data = {
- "stop_loss_percentage": percentage
+
+ # Check if the sell percentage + the existing stop losses is
+ # greater than 100
+ existing_sell_percentage = 0
+ for stop_loss in trade.stop_losses:
+ existing_sell_percentage += stop_loss.sell_percentage
+
+ if existing_sell_percentage + sell_percentage > 100:
+ raise OperationalException(
+ "Combined sell percentages of stop losses belonging "
+ "to trade exceeds 100."
+ )
+
+ creation_data = {
+ "trade_id": trade.id,
+ "trade_risk_type": TradeRiskType.from_value(trade_risk_type).value,
+ "percentage": percentage,
+ "open_price": trade.open_price,
+ "total_amount_trade": trade.amount,
+ "sell_percentage": sell_percentage,
+ "active": True
}
- self.update(trade.id, updated_data)
+ return self.trade_stop_loss_repository.create(creation_data)
- def add_trailing_stop_loss(self, trade, percentage):
+ def add_take_profit(
+ self,
+ trade,
+ percentage: float,
+ trade_risk_type: TradeRiskType = TradeRiskType.FIXED,
+ sell_percentage: float = 100,
+ ) -> None:
"""
- Function to add a trailing stop loss to a trade. The trailing stop loss
- is represented as a percentage of the open price.
+ Function to add a take profit to a trade. This function will add a
+ take profit to the specified trade. If the take profit is triggered,
+ the trade will be closed.
+
+ Example of take profit:
+ * You buy BTC at $40,000.
+ * You set a TP of 5% → TP level at $42,000 (40,000 + 5%).
+ * BTC rises to $42,000 → TP level reached, trade
+ closes, securing profit.
+
+ Example of trailing take profit:
+ * You buy BTC at $40,000
+ * You set a TTP of 5%, setting the sell price at $42,000.
+ * BTC rises to $42,000 → TTP level stays at $42,000.
+ * BTC rises to $45,000 → New TTP level at $42,750.
+ * BTC drops to $42,750 → Trade closes, securing profit.
Args:
trade: Trade object representing the trade
percentage: float representing the percentage of the open price
- that the trailing stop loss should be set at
+ that the stop loss should be set at
+ trade_risk_type (TradeRiskType): The type of the stop loss, fixed
+ or trailing
+ sell_percentage: float representing the percentage of the trade
+ that should be sold if the stop loss is triggered
Returns:
None
"""
trade = self.get(trade.id)
- updated_data = {
- "trailing_stop_loss_percentage": percentage
+
+ # Check if the sell percentage + the existing stop losses is
+ # greater than 100
+ existing_sell_percentage = 0
+ for take_profit in trade.take_profits:
+ existing_sell_percentage += take_profit.sell_percentage
+
+ if existing_sell_percentage + sell_percentage > 100:
+ raise OperationalException(
+ "Combined sell percentages of stop losses belonging "
+ "to trade exceeds 100."
+ )
+
+ creation_data = {
+ "trade_id": trade.id,
+ "trade_risk_type": TradeRiskType.from_value(trade_risk_type).value,
+ "percentage": percentage,
+ "open_price": trade.open_price,
+ "total_amount_trade": trade.amount,
+ "sell_percentage": sell_percentage,
+ "active": True
}
- self.update(trade.id, updated_data)
+ return self.trade_take_profit_repository.create(creation_data)
- def get_triggered_stop_losses(self):
+ def get_triggered_stop_loss_orders(self):
"""
- Function to check if any trades have hit their stop loss. If a trade
- has hit its stop loss, the trade is added to a list of
- triggered trades. This list is then returned.
+ Function to get all triggered stop loss orders. This function will
+ return a list of trade ids that have triggered stop losses.
Returns:
- List of Trade objects
+ List of trade ids
"""
- triggered_trades = []
- query = {
- "status": TradeStatus.OPEN.value,
- "stop_loss_percentage_not_none": True
- }
+ triggered_stop_losses = {}
+ sell_orders_data = []
+ query = {"status": TradeStatus.OPEN.value}
open_trades = self.get_all(query)
+ to_be_saved_stop_loss_objects = []
- for open_trade in open_trades:
+ # Group trades by target symbol
+ stop_losses_by_target_symbol = {}
- if open_trade.is_stop_loss_triggered():
- triggered_trades.append(open_trade)
+ for open_trade in open_trades:
+ triggered_stop_losses = []
+
+ for stop_loss in open_trade.stop_losses:
+
+ if (
+ stop_loss.active
+ and stop_loss.has_triggered(open_trade.last_reported_price)
+ ):
+ triggered_stop_losses.append(stop_loss)
+
+ to_be_saved_stop_loss_objects.append(stop_loss)
+
+ if len(triggered_stop_losses) > 0:
+ stop_losses_by_target_symbol[open_trade] = \
+ triggered_stop_losses
+
+ for trade in stop_losses_by_target_symbol:
+ stop_losses = stop_losses_by_target_symbol[trade]
+ available_amount = trade.remaining
+ stop_loss_que = PeekableQueue(stop_losses)
+ order_amount = 0
+ stop_loss_metadata = []
+
+ # While there is an available amount and there are stop losses
+ # to process
+ while not stop_loss_que.is_empty() and available_amount > 0:
+ stop_loss = stop_loss_que.dequeue()
+ stop_loss_sell_amount = stop_loss.get_sell_amount()
+
+ if stop_loss_sell_amount <= available_amount:
+ available_amount = available_amount - stop_loss_sell_amount
+ stop_loss.active = False
+ stop_loss.sold_amount += stop_loss_sell_amount
+ order_amount += stop_loss_sell_amount
+ else:
+ stop_loss.sold_amount += available_amount
+ stop_loss.active = True
+ stop_loss.sold_amount += stop_loss_sell_amount
+ available_amount = 0
+ order_amount += available_amount
+
+ stop_loss_metadata.append({
+ "stop_loss_id": stop_loss.id,
+ "amount": stop_loss_sell_amount
+ })
+
+ if stop_loss.sell_prices is None:
+ stop_loss.sell_prices = trade.last_reported_price
+ else:
+ stop_loss.sell_prices = (
+ f"{stop_loss.sell_prices},"
+ f"{trade.last_reported_price}"
+ )
+
+ position = self.position_repository.find({
+ "order_id": trade.orders[0].id
+ })
+ portfolio_id = position.portfolio_id
+ sell_orders_data.append(
+ {
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "amount": order_amount,
+ "price": trade.last_reported_price,
+ "order_type": OrderType.LIMIT.value,
+ "order_side": OrderSide.SELL.value,
+ "portfolio_id": portfolio_id,
+ "stop_losses": stop_loss_metadata,
+ "trades": [{
+ "trade_id": trade.id,
+ "amount": order_amount
+ }]
+ }
+ )
- return triggered_trades
+ self.trade_stop_loss_repository\
+ .save_objects(to_be_saved_stop_loss_objects)
+ return sell_orders_data
- def get_triggered_trailing_stop_losses(self):
+ def get_triggered_take_profit_orders(self):
"""
- Function to check if any trades have hit their stop loss. If a trade
- has hit its stop loss, the trade is added to a list of
- triggered trades. This list is then returned.
+ Function to get all triggered stop loss orders. This function will
+ return a list of trade ids that have triggered stop losses.
Returns:
- List of Trade objects
+ List of trade ids
"""
- triggered_trades = []
- query = {
- "status": TradeStatus.OPEN.value,
- "trailing_stop_loss_percentage_not_none": True
- }
+ triggered_take_profits = {}
+ sell_orders_data = []
+ query = {"status": TradeStatus.OPEN.value}
open_trades = self.get_all(query)
+ to_be_saved_take_profit_objects = []
+
+ # Group trades by target symbol
+ take_profits_by_target_symbol = {}
for open_trade in open_trades:
+ triggered_take_profits = []
+ available_amount = open_trade.remaining
- if open_trade.is_trailing_stop_loss_triggered():
- triggered_trades.append(open_trade)
+ # Skip if there is no available amount
+ if available_amount == 0:
+ continue
+
+ for take_proft in open_trade.take_profits:
+
+ if (
+ take_proft.active and
+ take_proft.has_triggered(open_trade.last_reported_price)
+ ):
+ triggered_take_profits.append(take_proft)
+
+ to_be_saved_take_profit_objects.append(take_proft)
+
+ if len(triggered_take_profits) > 0:
+ take_profits_by_target_symbol[open_trade] = \
+ triggered_take_profits
+
+ for trade in take_profits_by_target_symbol:
+ take_profits = take_profits_by_target_symbol[trade]
+ available_amount = trade.remaining
+ take_profit_que = PeekableQueue(take_profits)
+ order_amount = 0
+ take_profit_metadata = []
+
+ # While there is an available amount and there are take profits
+ # to process
+ while not take_profit_que.is_empty() and available_amount > 0:
+ take_profit = take_profit_que.dequeue()
+ take_profit_sell_amount = take_profit.get_sell_amount()
+
+ if take_profit_sell_amount <= available_amount:
+ available_amount = available_amount - \
+ take_profit_sell_amount
+ take_profit.active = False
+ take_profit.sold_amount += take_profit_sell_amount
+ order_amount += take_profit_sell_amount
+ else:
+ take_profit.sold_amount += available_amount
+ take_profit.active = True
+ take_profit.sold_amount += take_profit_sell_amount
+ available_amount = 0
+ order_amount += available_amount
+
+ take_profit_metadata.append({
+ "take_profit_id": take_profit.id,
+ "amount": take_profit_sell_amount
+ })
+
+ if take_profit.sell_prices is None:
+ take_profit.sell_prices = trade.last_reported_price
+ else:
+ take_profit.sell_prices = (
+ f"{take_profit.sell_prices},"
+ f"{trade.last_reported_price}"
+ )
+
+ position = self.position_repository.find({
+ "order_id": trade.orders[0].id
+ })
+ portfolio_id = position.portfolio_id
+ sell_orders_data.append(
+ {
+ "target_symbol": trade.target_symbol,
+ "trading_symbol": trade.trading_symbol,
+ "amount": order_amount,
+ "price": trade.last_reported_price,
+ "order_type": OrderType.LIMIT.value,
+ "order_side": OrderSide.SELL.value,
+ "portfolio_id": portfolio_id,
+ "take_profits": take_profit_metadata,
+ "trades": [{
+ "trade_id": trade.id,
+ "amount": order_amount
+ }]
+ }
+ )
- return triggered_trades
+ self.trade_take_profit_repository\
+ .save_objects(to_be_saved_take_profit_objects)
+ return sell_orders_data
diff --git a/static/sponsors/finterion-dark.png b/static/sponsors/finterion-dark.png
new file mode 100644
index 00000000..bb619159
Binary files /dev/null and b/static/sponsors/finterion-dark.png differ
diff --git a/static/sponsors/finterion-light.png b/static/sponsors/finterion-light.png
new file mode 100644
index 00000000..a3438174
Binary files /dev/null and b/static/sponsors/finterion-light.png differ
diff --git a/static/sponsors/finterion.png b/static/sponsors/finterion.png
deleted file mode 100644
index ee0838e6..00000000
Binary files a/static/sponsors/finterion.png and /dev/null differ
diff --git a/static/sponsors/finterion.svg b/static/sponsors/finterion.svg
deleted file mode 100644
index bf42e7a7..00000000
--- a/static/sponsors/finterion.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/tests/app/algorithm/test_check_order_status.py b/tests/app/algorithm/test_check_order_status.py
index 9cf743c2..c44c29fe 100644
--- a/tests/app/algorithm/test_check_order_status.py
+++ b/tests/app/algorithm/test_check_order_status.py
@@ -27,7 +27,7 @@ class Test(TestBase):
def test_check_order_status(self):
order_repository = self.app.container.order_repository()
position_repository = self.app.container.position_repository()
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -35,7 +35,7 @@ def test_check_order_status(self):
)
self.assertEqual(1, order_repository.count())
self.assertEqual(2, position_repository.count())
- self.app.algorithm.order_service.check_pending_orders()
+ self.app.context.order_service.check_pending_orders()
self.assertEqual(1, order_repository.count())
self.assertEqual(2, position_repository.count())
order = order_repository.find({"target_symbol": "BTC"})
diff --git a/tests/app/algorithm/test_close_position.py b/tests/app/algorithm/test_close_position.py
index 438c9db3..e363d78f 100644
--- a/tests/app/algorithm/test_close_position.py
+++ b/tests/app/algorithm/test_close_position.py
@@ -45,24 +45,24 @@ def setUp(self) -> None:
))
def test_close_position(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, trading_symbol_position.get_amount())
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position)
self.assertEqual(0, btc_position.get_amount())
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position.get_amount())
self.assertEqual(Decimal(1), btc_position.get_amount())
self.assertNotEqual(Decimal(990), trading_symbol_position.get_amount())
- self.app.algorithm.close_position("BTC")
+ self.app.context.close_position("BTC")
self.app.run(number_of_iterations=1)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertEqual(Decimal(0), btc_position.get_amount())
diff --git a/tests/app/algorithm/test_close_trade.py b/tests/app/algorithm/test_close_trade.py
index ca1b829d..0a9d6458 100644
--- a/tests/app/algorithm/test_close_trade.py
+++ b/tests/app/algorithm/test_close_trade.py
@@ -45,58 +45,58 @@ def setUp(self) -> None:
))
def test_close_trade(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, trading_symbol_position.get_amount())
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position)
self.assertEqual(0, btc_position.get_amount())
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
+ self.assertEqual(1, len(self.app.context.get_trades()))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- trades = self.app.algorithm.get_trades()
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ trades = self.app.context.get_trades()
trade = trades[0]
self.assertIsNotNone(trade.amount)
self.assertEqual(Decimal(1), trade.amount)
- self.app.algorithm.close_trade(trade)
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
+ self.app.context.close_trade(trade)
+ self.assertEqual(1, len(self.app.context.get_trades()))
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- self.assertEqual(0, len(self.app.algorithm.get_open_trades()))
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ self.assertEqual(0, len(self.app.context.get_open_trades()))
def test_close_trade_with_already_closed_trade(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, trading_symbol_position.get_amount())
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position)
self.assertEqual(0, btc_position.get_amount())
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
+ self.assertEqual(1, len(self.app.context.get_trades()))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- trades = self.app.algorithm.get_trades()
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ trades = self.app.context.get_trades()
trade = trades[0]
self.assertIsNotNone(trade.amount)
self.assertEqual(Decimal(1), trade.amount)
- self.app.algorithm.close_trade(trade)
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
+ self.app.context.close_trade(trade)
+ self.assertEqual(1, len(self.app.context.get_trades()))
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- self.assertEqual(0, len(self.app.algorithm.get_open_trades()))
- trades = self.app.algorithm.get_trades()
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ self.assertEqual(0, len(self.app.context.get_open_trades()))
+ trades = self.app.context.get_trades()
trade = trades[0]
with self.assertRaises(OperationalException):
- self.app.algorithm.close_trade(trade)
+ self.app.context.close_trade(trade)
diff --git a/tests/app/algorithm/test_create_limit_buy_order.py b/tests/app/algorithm/test_create_limit_buy_order.py
index 42472bd8..eed00b99 100644
--- a/tests/app/algorithm/test_create_limit_buy_order.py
+++ b/tests/app/algorithm/test_create_limit_buy_order.py
@@ -31,7 +31,7 @@ def count_decimals(self, number):
def test_create_limit_buy_order(self):
self.app.run(number_of_iterations=1)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -48,7 +48,7 @@ def test_create_limit_buy_order(self):
self.assertEqual(OrderStatus.OPEN.value, order.status)
def test_create_limit_buy_order_with_percentage_of_portfolio(self):
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
@@ -64,6 +64,6 @@ def test_create_limit_buy_order_with_percentage_of_portfolio(self):
self.assertEqual(OrderStatus.OPEN.value, order.status)
self.assertEqual(20, order.get_amount())
self.assertEqual(10, order.get_price())
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertEqual(1000, portfolio.get_net_size())
self.assertEqual(800, portfolio.get_unallocated())
diff --git a/tests/app/algorithm/test_create_limit_sell_order.py b/tests/app/algorithm/test_create_limit_sell_order.py
index 414e2dfd..a32b8f36 100644
--- a/tests/app/algorithm/test_create_limit_sell_order.py
+++ b/tests/app/algorithm/test_create_limit_sell_order.py
@@ -24,7 +24,7 @@ class Test(TestBase):
def test_create_limit_sell_order(self):
self.app.run(number_of_iterations=1)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
@@ -40,13 +40,13 @@ def test_create_limit_sell_order(self):
self.assertEqual(OrderStatus.OPEN.value, order.status)
self.assertEqual(20, order.get_amount())
self.assertEqual(10, order.get_price())
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertEqual(1000, portfolio.get_net_size())
self.assertEqual(800, portfolio.get_unallocated())
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.app.algorithm.get_position("BTC")
- order = self.app.algorithm.create_limit_order(
+ self.app.context.get_position("BTC")
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
@@ -56,7 +56,7 @@ def test_create_limit_sell_order(self):
def test_create_limit_sell_order_with_percentage_position(self):
self.app.run(number_of_iterations=1)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
@@ -72,12 +72,12 @@ def test_create_limit_sell_order_with_percentage_position(self):
self.assertEqual(OrderStatus.OPEN.value, order.status)
self.assertEqual(20, order.get_amount())
self.assertEqual(10, order.get_price())
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertEqual(1000, portfolio.get_net_size())
self.assertEqual(800, portfolio.get_unallocated())
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
diff --git a/tests/app/algorithm/test_create_market_sell_order.py b/tests/app/algorithm/test_create_market_sell_order.py
deleted file mode 100644
index 52be6a98..00000000
--- a/tests/app/algorithm/test_create_market_sell_order.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from investing_algorithm_framework import PortfolioConfiguration, \
- OrderType, OrderSide, OrderStatus, MarketCredential
-from tests.resources import TestBase, MarketDataSourceServiceStub
-
-
-class Test(TestBase):
- external_balances = {
- "EUR": 1000
- }
- external_available_symbols = ["BTC/EUR"]
- portfolio_configurations = [
- PortfolioConfiguration(
- market="BITVAVO",
- trading_symbol="EUR"
- )
- ]
- market_credentials = [
- MarketCredential(
- market="bitvavo",
- api_key="api_key",
- secret_key="secret_key"
- )
- ]
- market_data_source_service = MarketDataSourceServiceStub()
-
- def test_create_market_sell_order(self):
- portfolio = self.app.algorithm.get_portfolio()
- order_service = self.app.container.order_service()
- order_service.create(
- {
- "target_symbol": "BTC",
- "price": 10,
- "amount": 1,
- "order_type": OrderType.LIMIT.value,
- "order_side": OrderSide.BUY.value,
- "portfolio_id": portfolio.id,
- "status": OrderStatus.CREATED.value,
- "trading_symbol": portfolio.trading_symbol,
- },
- )
- position_service = self.app.container.position_service()
- 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)
- trading_symbol_position = position_service.find({"symbol": "EUR"})
- self.assertEqual(990, trading_symbol_position.get_amount())
- btc_position = position_service.find({"symbol": "BTC"})
- self.assertEqual(1, btc_position.get_amount())
- self.app.algorithm.create_market_order(
- target_symbol="BTC",
- amount=1,
- order_side="SELL",
- )
- btc_position = position_service.find({"symbol": "BTC"})
- self.assertEqual(0, btc_position.get_amount())
- market_sell_order = order_service.find(
- {"target_symbol": "BTC", "order_side": "SELL"}
- )
- self.assertIsNotNone(market_sell_order)
- self.assertEqual(OrderStatus.CREATED.value, market_sell_order.status)
- self.assertEqual(1, market_sell_order.amount)
- self.assertEqual(None, market_sell_order.price)
diff --git a/tests/app/algorithm/test_get_allocated.py b/tests/app/algorithm/test_get_allocated.py
index 0cb3c01f..a275d92f 100644
--- a/tests/app/algorithm/test_get_allocated.py
+++ b/tests/app/algorithm/test_get_allocated.py
@@ -52,7 +52,7 @@ def test_get_allocated(self):
)
self.app.run(number_of_iterations=1)
order_service = self.app.container.order_service()
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -60,6 +60,6 @@ def test_get_allocated(self):
)
self.assertEqual(1, order_service.count())
order_service.check_pending_orders()
- self.assertNotEqual(0, self.app.algorithm.get_allocated())
- self.assertNotEqual(0, self.app.algorithm.get_allocated("BITVAVO"))
- self.assertNotEqual(0, self.app.algorithm.get_allocated("bitvavo"))
+ self.assertNotEqual(0, self.app.context.get_allocated())
+ self.assertNotEqual(0, self.app.context.get_allocated("BITVAVO"))
+ self.assertNotEqual(0, self.app.context.get_allocated("bitvavo"))
diff --git a/tests/app/algorithm/test_get_closed_trades.py b/tests/app/algorithm/test_get_closed_trades.py
index 3a0fa512..e49c2d00 100644
--- a/tests/app/algorithm/test_get_closed_trades.py
+++ b/tests/app/algorithm/test_get_closed_trades.py
@@ -39,28 +39,28 @@ def setUp(self) -> None:
))
def test_get_open_trades(self):
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=20
)
self.assertIsNotNone(order)
- self.assertEqual(0, len(self.app.algorithm.get_closed_trades()))
+ self.assertEqual(0, len(self.app.context.get_closed_trades()))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(0, len(self.app.algorithm.get_closed_trades()))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(0, len(self.app.context.get_closed_trades()))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(20, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
self.assertIsNone(trade.closed_at)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
amount=20
)
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_closed_trades()))
+ self.assertEqual(1, len(self.app.context.get_closed_trades()))
diff --git a/tests/app/algorithm/test_get_number_of_positions.py b/tests/app/algorithm/test_get_number_of_positions.py
index b7c1d771..5117073a 100644
--- a/tests/app/algorithm/test_get_number_of_positions.py
+++ b/tests/app/algorithm/test_get_number_of_positions.py
@@ -26,10 +26,10 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_get_number_of_positions(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
- self.assertEqual(1, self.app.algorithm.get_number_of_positions())
+ trading_symbol_position = self.app.context.get_position("EUR")
+ self.assertEqual(1, self.app.context.get_number_of_positions())
self.assertEqual(Decimal(1000), trading_symbol_position.get_amount())
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -37,19 +37,19 @@ def test_get_number_of_positions(self):
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(2, self.app.algorithm.get_number_of_positions())
- self.app.algorithm.create_limit_order(
+ self.assertEqual(2, self.app.context.get_number_of_positions())
+ self.app.context.create_limit_order(
target_symbol="DOT",
amount=1,
price=10,
order_side="BUY",
)
order_service.check_pending_orders()
- self.assertEqual(3, self.app.algorithm.get_number_of_positions())
- self.app.algorithm.create_limit_order(
+ self.assertEqual(3, self.app.context.get_number_of_positions())
+ self.app.context.create_limit_order(
target_symbol="ADA",
amount=1,
price=10,
order_side="BUY",
)
- self.assertEqual(3, self.app.algorithm.get_number_of_positions())
+ self.assertEqual(3, self.app.context.get_number_of_positions())
diff --git a/tests/app/algorithm/test_get_open_trades.py b/tests/app/algorithm/test_get_open_trades.py
index af4fb660..8afbff04 100644
--- a/tests/app/algorithm/test_get_open_trades.py
+++ b/tests/app/algorithm/test_get_open_trades.py
@@ -41,52 +41,51 @@ def setUp(self) -> None:
)
def test_get_open_trades(self):
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=20
)
self.assertIsNotNone(order)
- self.assertEqual(0, len(self.app.algorithm.get_open_trades("BTC")))
+ self.assertEqual(0, len(self.app.context.get_open_trades("BTC")))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_open_trades("BTC")))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(1, len(self.app.context.get_open_trades("BTC")))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(20, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
self.assertIsNone(trade.closed_at)
- self.app.algorithm.create_limit_order(
+ self.assertEqual(1, len(self.app.context.get_open_trades("BTC")))
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
amount=20
)
- self.assertEqual(1, len(self.app.algorithm.get_open_trades("BTC")))
- order_service.check_pending_orders()
- self.assertEqual(0, len(self.app.algorithm.get_open_trades("BTC")))
+ self.assertEqual(0, len(self.app.context.get_open_trades("BTC")))
def test_get_open_trades_with_close_trades(self):
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=5
)
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=5
)
self.assertIsNotNone(order)
- self.assertEqual(0, len(self.app.algorithm.get_open_trades("BTC")))
+ self.assertEqual(0, len(self.app.context.get_open_trades("BTC")))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(2, len(self.app.algorithm.get_open_trades("BTC")))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(2, len(self.app.context.get_open_trades("BTC")))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(5, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
@@ -94,10 +93,10 @@ def test_get_open_trades_with_close_trades(self):
self.assertIsNone(trade.closed_at)
self.assertEqual(
0,
- len(self.app.algorithm
+ len(self.app.context
.get_orders(order_side="SELL", status="OPEN"))
)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
@@ -106,29 +105,29 @@ def test_get_open_trades_with_close_trades(self):
self.assertEqual(
1,
len(
- self.app.algorithm.get_orders(order_side="SELL", status="OPEN")
+ self.app.context.get_orders(order_side="SELL", status="OPEN")
)
)
- self.assertEqual(2, len(self.app.algorithm.get_open_trades("BTC")))
- self.app.algorithm.create_limit_order(
+ self.assertEqual(1, len(self.app.context.get_open_trades("BTC")))
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
amount=5
)
self.assertEqual(2, len(
- self.app.algorithm.get_orders(order_side="SELL", status="OPEN"))
+ self.app.context.get_orders(order_side="SELL", status="OPEN"))
)
- self.assertEqual(2, len(self.app.algorithm.get_open_trades("BTC")))
+ self.assertEqual(0, len(self.app.context.get_open_trades("BTC")))
def test_get_open_trades_with_close_trades_of_partial_buy_orders(self):
- order_one = self.app.algorithm.create_limit_order(
+ order_one = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=5
)
- order_two = self.app.algorithm.create_limit_order(
+ order_two = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
@@ -138,22 +137,25 @@ def test_get_open_trades_with_close_trades_of_partial_buy_orders(self):
order_two_id = order_two.id
self.assertIsNotNone(order_one)
self.assertIsNotNone(order_two)
- self.assertEqual(0, len(self.app.algorithm.get_open_trades("BTC")))
+ self.assertEqual(0, len(self.app.context.get_open_trades("BTC")))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(2, len(self.app.algorithm.get_open_trades("BTC")))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(2, len(self.app.context.get_open_trades("BTC")))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(5, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
self.assertIsNone(trade.closed_at)
+
+ # All orders are filled
self.assertEqual(
0,
- len(self.app.algorithm.get_orders(order_side="SELL",
- status="OPEN"))
+ len(
+ self.app.context.get_orders(order_side="SELL", status="OPEN")
+ )
)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
@@ -162,35 +164,35 @@ def test_get_open_trades_with_close_trades_of_partial_buy_orders(self):
self.assertEqual(
1,
len(
- self.app.algorithm.get_orders(order_side="SELL", status="OPEN")
+ self.app.context.get_orders(order_side="SELL", status="OPEN")
)
)
- trade_one = self.app.algorithm.get_trade(order_id=order_one_id)
- trade_two = self.app.algorithm.get_trade(order_id=order_two_id)
- self.assertEqual(5, trade_one.remaining)
+ trade_one = self.app.context.get_trade(order_id=order_one_id)
+ trade_two = self.app.context.get_trade(order_id=order_two_id)
+ self.assertEqual(2.5, trade_one.remaining)
self.assertEqual(5, trade_two.remaining)
- self.app.algorithm.order_service.check_pending_orders()
- trade_one = self.app.algorithm.get_trade(order_id=order_one_id)
- trade_two = self.app.algorithm.get_trade(order_id=order_two_id)
+ self.app.context.order_service.check_pending_orders()
+ trade_one = self.app.context.get_trade(order_id=order_one_id)
+ trade_two = self.app.context.get_trade(order_id=order_two_id)
self.assertEqual(2.5, trade_one.remaining)
self.assertEqual(5, trade_two.remaining)
- self.assertEqual(2, len(self.app.algorithm.get_open_trades("BTC")))
- self.app.algorithm.create_limit_order(
+ self.assertEqual(2, len(self.app.context.get_open_trades("BTC")))
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
amount=5
)
- trades = self.app.algorithm.get_open_trades()
- self.assertEqual(2, len(trades))
- trade_one = self.app.algorithm.get_trade(order_id=order_one_id)
- trade_two = self.app.algorithm.get_trade(order_id=order_two_id)
- self.assertEqual(2.5, trade_one.remaining)
- self.assertEqual(5, trade_two.remaining)
- self.app.algorithm.order_service.check_pending_orders()
- trades = self.app.algorithm.get_open_trades()
+ trades = self.app.context.get_open_trades()
+ self.assertEqual(1, len(trades))
+ trade_one = self.app.context.get_trade(order_id=order_one_id)
+ trade_two = self.app.context.get_trade(order_id=order_two_id)
+ self.assertEqual(0, trade_one.remaining)
+ self.assertEqual(2.5, trade_two.remaining)
+ self.app.context.order_service.check_pending_orders()
+ trades = self.app.context.get_open_trades()
self.assertEqual(1, len(trades))
- trade_one = self.app.algorithm.get_trade(order_id=order_one_id)
- trade_two = self.app.algorithm.get_trade(order_id=order_two_id)
+ trade_one = self.app.context.get_trade(order_id=order_one_id)
+ trade_two = self.app.context.get_trade(order_id=order_two_id)
self.assertEqual(0, trade_one.remaining)
self.assertEqual(2.5, trade_two.remaining)
diff --git a/tests/app/algorithm/test_get_order.py b/tests/app/algorithm/test_get_order.py
index 2ca7c24e..18b13e45 100644
--- a/tests/app/algorithm/test_get_order.py
+++ b/tests/app/algorithm/test_get_order.py
@@ -26,7 +26,7 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_create_limit_buy_order_with_percentage_of_portfolio(self):
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
diff --git a/tests/app/algorithm/test_get_pending_orders.py b/tests/app/algorithm/test_get_pending_orders.py
index ec765d35..c670a2c2 100644
--- a/tests/app/algorithm/test_get_pending_orders.py
+++ b/tests/app/algorithm/test_get_pending_orders.py
@@ -137,10 +137,10 @@ def test_get_pending_orders(self):
)
self.assertEqual(400, eur_position.amount)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(2, len(pending_orders))
- pending_order = self.app.algorithm\
+ pending_order = self.app.context\
.get_pending_orders(target_symbol="ETH")[0]
order_service = self.app.container.order_service()
@@ -153,5 +153,5 @@ def test_get_pending_orders(self):
}
)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(1, len(pending_orders))
diff --git a/tests/app/algorithm/test_get_portfolio.py b/tests/app/algorithm/test_get_portfolio.py
index 6043646b..81a5ca20 100644
--- a/tests/app/algorithm/test_get_portfolio.py
+++ b/tests/app/algorithm/test_get_portfolio.py
@@ -26,5 +26,5 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_get_portfolio(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertEqual(Decimal(1000), portfolio.get_unallocated())
diff --git a/tests/app/algorithm/test_get_position.py b/tests/app/algorithm/test_get_position.py
index ff03272b..9e1a94da 100644
--- a/tests/app/algorithm/test_get_position.py
+++ b/tests/app/algorithm/test_get_position.py
@@ -26,20 +26,20 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_get_position(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(Decimal(1000), trading_symbol_position.get_amount())
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position)
self.assertEqual(Decimal(0), btc_position.get_amount())
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position.get_amount())
self.assertEqual(Decimal(1), btc_position.get_amount())
self.assertNotEqual(Decimal(990), trading_symbol_position.get_amount())
diff --git a/tests/app/algorithm/test_get_trades.py b/tests/app/algorithm/test_get_trades.py
index 358b80d3..e2afcfee 100644
--- a/tests/app/algorithm/test_get_trades.py
+++ b/tests/app/algorithm/test_get_trades.py
@@ -39,32 +39,32 @@ def setUp(self) -> None:
))
def test_get_trades(self):
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="BUY",
amount=20
)
self.assertIsNotNone(order)
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
+ self.assertEqual(1, len(self.app.context.get_trades()))
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(20, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
self.assertIsNone(trade.closed_at)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
price=10,
order_side="SELL",
amount=20
)
order_service.check_pending_orders()
- self.assertEqual(1, len(self.app.algorithm.get_trades()))
- trade = self.app.algorithm.get_trades()[0]
+ self.assertEqual(1, len(self.app.context.get_trades()))
+ trade = self.app.context.get_trades()[0]
self.assertEqual(10, trade.open_price)
self.assertEqual(20, trade.amount)
self.assertEqual("BTC", trade.target_symbol)
diff --git a/tests/app/algorithm/test_get_unallocated.py b/tests/app/algorithm/test_get_unallocated.py
index cd74279b..6b63248c 100644
--- a/tests/app/algorithm/test_get_unallocated.py
+++ b/tests/app/algorithm/test_get_unallocated.py
@@ -26,5 +26,5 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_create_limit_buy_order_with_percentage_of_portfolio(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.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 a03fda54..f906a278 100644
--- a/tests/app/algorithm/test_get_unfilled_buy_value.py
+++ b/tests/app/algorithm/test_get_unfilled_buy_value.py
@@ -148,14 +148,14 @@ def test_get_unfilled_buy_value(self):
)
self.assertEqual(700, eur_position.amount)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(2, len(pending_orders))
# Check the unfilled buy value
- unfilled_buy_value = self.app.algorithm.get_unfilled_buy_value()
+ unfilled_buy_value = self.app.context.get_unfilled_buy_value()
self.assertEqual(200, unfilled_buy_value)
- pending_order = self.app.algorithm\
+ pending_order = self.app.context\
.get_pending_orders(target_symbol="ETH")[0]
order_service = self.app.container.order_service()
@@ -168,9 +168,9 @@ def test_get_unfilled_buy_value(self):
}
)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(1, len(pending_orders))
# Check the unfilled buy value
- unfilled_buy_value = self.app.algorithm.get_unfilled_buy_value()
+ unfilled_buy_value = self.app.context.get_unfilled_buy_value()
self.assertEqual(100, unfilled_buy_value)
diff --git a/tests/app/algorithm/test_get_unfilled_sell_value.py b/tests/app/algorithm/test_get_unfilled_sell_value.py
index 29e4256b..da4aede5 100644
--- a/tests/app/algorithm/test_get_unfilled_sell_value.py
+++ b/tests/app/algorithm/test_get_unfilled_sell_value.py
@@ -160,7 +160,7 @@ def test_get_unfilled_sell_value(self):
trade_service = self.app.container.trade_service()
self.assertEqual(3, trade_service.count())
self.assertEqual(
- 2, trade_service.count(
+ 0, trade_service.count(
{"portfolio_id": portfolio.id, "status": "OPEN"}
)
)
@@ -193,14 +193,14 @@ def test_get_unfilled_sell_value(self):
)
self.assertEqual(900, eur_position.amount)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(2, len(pending_orders))
# Check the unfilled sell value
- unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value()
+ unfilled_sell_value = self.app.context.get_unfilled_sell_value()
self.assertEqual(200, unfilled_sell_value)
- pending_order = self.app.algorithm\
+ pending_order = self.app.context\
.get_pending_orders(target_symbol="ETH")[0]
order_service = self.app.container.order_service()
@@ -213,11 +213,11 @@ def test_get_unfilled_sell_value(self):
}
)
- pending_orders = self.app.algorithm.get_pending_orders()
+ pending_orders = self.app.context.get_pending_orders()
self.assertEqual(1, len(pending_orders))
# Check the unfilled buy value
- unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value()
+ unfilled_sell_value = self.app.context.get_unfilled_sell_value()
self.assertEqual(100, unfilled_sell_value)
# Check if eur position exists
diff --git a/tests/app/algorithm/test_has_open_buy_orders.py b/tests/app/algorithm/test_has_open_buy_orders.py
index 7c7051c5..2814913c 100644
--- a/tests/app/algorithm/test_has_open_buy_orders.py
+++ b/tests/app/algorithm/test_has_open_buy_orders.py
@@ -27,9 +27,9 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_has_open_buy_orders(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(Decimal(1000), trading_symbol_position.get_amount())
- order = self.app.algorithm.create_limit_order(
+ order = self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -39,6 +39,6 @@ def test_has_open_buy_orders(self):
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"))
+ self.assertTrue(self.app.context.has_open_buy_orders("BTC"))
order_service.check_pending_orders()
- self.assertFalse(self.app.algorithm.has_open_buy_orders("BTC"))
+ self.assertFalse(self.app.context.has_open_buy_orders("BTC"))
diff --git a/tests/app/algorithm/test_has_open_sell_orders.py b/tests/app/algorithm/test_has_open_sell_orders.py
index 94429f2f..94a34b89 100644
--- a/tests/app/algorithm/test_has_open_sell_orders.py
+++ b/tests/app/algorithm/test_has_open_sell_orders.py
@@ -25,10 +25,10 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_has_open_sell_orders(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, trading_symbol_position.get_amount())
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
@@ -36,12 +36,12 @@ def test_has_open_sell_orders(self):
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="SELL",
)
- self.assertTrue(self.app.algorithm.has_open_sell_orders("BTC"))
+ self.assertTrue(self.app.context.has_open_sell_orders("BTC"))
order_service.check_pending_orders()
- self.assertFalse(self.app.algorithm.has_open_sell_orders("BTC"))
+ self.assertFalse(self.app.context.has_open_sell_orders("BTC"))
diff --git a/tests/app/algorithm/test_has_position.py b/tests/app/algorithm/test_has_position.py
index da00d7a9..7373bd4c 100644
--- a/tests/app/algorithm/test_has_position.py
+++ b/tests/app/algorithm/test_has_position.py
@@ -25,106 +25,106 @@ class Test(TestBase):
market_data_source_service = MarketDataSourceServiceStub()
def test_has_position(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
- self.assertTrue(self.app.algorithm.has_position("EUR"))
- self.assertFalse(self.app.algorithm.has_position("BTC"))
+ trading_symbol_position = self.app.context.get_position("EUR")
+ self.assertTrue(self.app.context.has_position("EUR"))
+ self.assertFalse(self.app.context.has_position("BTC"))
self.assertEqual(1000, trading_symbol_position.get_amount())
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position)
- self.assertTrue(self.app.algorithm.position_exists("BTC"))
- self.assertFalse(self.app.algorithm.has_position("BTC"))
+ self.assertTrue(self.app.context.position_exists("BTC"))
+ self.assertFalse(self.app.context.has_position("BTC"))
self.assertEqual(0, btc_position.get_amount())
order_service = self.app.container.order_service()
order_service.check_pending_orders()
- btc_position = self.app.algorithm.get_position("BTC")
+ btc_position = self.app.context.get_position("BTC")
self.assertIsNotNone(btc_position.get_amount())
self.assertEqual(1, btc_position.get_amount())
self.assertNotEqual(990, trading_symbol_position.amount)
- self.assertTrue(self.app.algorithm.has_position("BTC"))
+ self.assertTrue(self.app.context.has_position("BTC"))
def test_position_exists_with_amount_gt(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, int(trading_symbol_position.get_amount()))
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- self.assertTrue(self.app.algorithm.position_exists("BTC"))
+ self.assertTrue(self.app.context.position_exists("BTC"))
self.assertFalse(
- self.app.algorithm.position_exists("BTC", amount_gt=0)
+ self.app.context.position_exists("BTC", amount_gt=0)
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_gt=0)
+ self.app.context.position_exists("BTC", amount_gt=0)
)
def test_position_exists_with_amount_gte(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, int(trading_symbol_position.get_amount()))
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- self.assertTrue(self.app.algorithm.position_exists("BTC"))
+ self.assertTrue(self.app.context.position_exists("BTC"))
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_gte=0)
+ self.app.context.position_exists("BTC", amount_gte=0)
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_gte=0)
+ self.app.context.position_exists("BTC", amount_gte=0)
)
def test_position_exists_with_amount_lt(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, int(trading_symbol_position.get_amount()))
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- self.assertTrue(self.app.algorithm.position_exists("BTC"))
+ self.assertTrue(self.app.context.position_exists("BTC"))
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_lt=1)
+ self.app.context.position_exists("BTC", amount_lt=1)
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
self.assertFalse(
- self.app.algorithm.position_exists("BTC", amount_lt=1)
+ self.app.context.position_exists("BTC", amount_lt=1)
)
def test_position_exists_with_amount_lte(self):
- trading_symbol_position = self.app.algorithm.get_position("EUR")
+ trading_symbol_position = self.app.context.get_position("EUR")
self.assertEqual(1000, int(trading_symbol_position.get_amount()))
- self.assertFalse(self.app.algorithm.position_exists(symbol="BTC"))
- self.app.algorithm.create_limit_order(
+ self.assertFalse(self.app.context.position_exists(symbol="BTC"))
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=10,
order_side="BUY",
)
- self.assertTrue(self.app.algorithm.position_exists("BTC"))
+ self.assertTrue(self.app.context.position_exists("BTC"))
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_lte=1)
+ self.app.context.position_exists("BTC", amount_lte=1)
)
order_service = self.app.container.order_service()
order_service.check_pending_orders()
self.assertTrue(
- self.app.algorithm.position_exists("BTC", amount_lte=1)
+ self.app.context.position_exists("BTC", amount_lte=1)
)
diff --git a/tests/app/algorithm/test_has_trading_symbol_position_available.py b/tests/app/algorithm/test_has_trading_symbol_position_available.py
index dc7397c6..af3c2a89 100644
--- a/tests/app/algorithm/test_has_trading_symbol_position_available.py
+++ b/tests/app/algorithm/test_has_trading_symbol_position_available.py
@@ -26,60 +26,60 @@ class Test(TestBase):
def test_has_trading_symbol_available(self):
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available()
+ self.app.context.has_trading_symbol_position_available()
)
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
percentage_of_portfolio=100
)
)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="BTC",
amount=1,
price=250,
order_side="BUY",
)
self.assertFalse(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
percentage_of_portfolio=100
)
)
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available()
+ self.app.context.has_trading_symbol_position_available()
)
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
percentage_of_portfolio=75
)
)
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
amount_gt=700
)
)
self.assertTrue(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
amount_gte=750
)
)
- self.app.algorithm.create_limit_order(
+ self.app.context.create_limit_order(
target_symbol="DOT",
amount=1,
price=250,
order_side="BUY",
)
self.assertFalse(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
percentage_of_portfolio=75
)
)
self.assertFalse(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
amount_gt=700
)
)
self.assertFalse(
- self.app.algorithm.has_trading_symbol_position_available(
+ self.app.context.has_trading_symbol_position_available(
amount_gte=750
)
)
diff --git a/tests/app/algorithm/test_run_strategy.py b/tests/app/algorithm/test_run_strategy.py
index 658737c1..6cafef02 100644
--- a/tests/app/algorithm/test_run_strategy.py
+++ b/tests/app/algorithm/test_run_strategy.py
@@ -12,7 +12,7 @@ class StrategyOne(TradingStrategy):
time_unit = TimeUnit.SECOND
interval = 2
- def apply_strategy(self, algorithm, market_data):
+ def apply_strategy(self, context, market_data):
pass
@@ -20,7 +20,7 @@ class StrategyTwo(TradingStrategy):
time_unit = TimeUnit.SECOND
interval = 2
- def apply_strategy(self, algorithm, market_data):
+ def apply_strategy(self, context, market_data):
pass
diff --git a/tests/app/algorithm/test_trade_price_update.py b/tests/app/algorithm/test_trade_price_update.py
index 074df5d0..56e6b105 100644
--- a/tests/app/algorithm/test_trade_price_update.py
+++ b/tests/app/algorithm/test_trade_price_update.py
@@ -6,9 +6,7 @@
TimeUnit, PortfolioConfiguration, RESOURCE_DIRECTORY, \
Algorithm, MarketCredential, CSVOHLCVMarketDataSource, \
CSVTickerMarketDataSource
-from tests.resources import random_string, MarketServiceStub, \
- MarketDataSourceServiceStub
-
+from tests.resources import random_string, MarketServiceStub
class StrategyOne(TradingStrategy):
time_unit = TimeUnit.SECOND
@@ -112,7 +110,7 @@ def test_trade_recent_price_update(self):
)
app.initialize_config()
app.initialize()
- app.algorithm.create_limit_order(
+ app.context.create_limit_order(
target_symbol="btc",
amount=20,
price=20,
@@ -125,6 +123,6 @@ def test_trade_recent_price_update(self):
self.assertTrue(strategy_orchestration_service.has_run("StrategyTwo"))
# Check that the last reported price is updated
- trade = app.algorithm.get_trades()[0]
+ trade = app.context.get_trades()[0]
self.assertIsNotNone(trade)
self.assertIsNotNone(trade.last_reported_price)
diff --git a/tests/app/backtesting/test_backtest_report.py b/tests/app/backtesting/test_backtest_report.py
index 63e85877..a96f0917 100644
--- a/tests/app/backtesting/test_backtest_report.py
+++ b/tests/app/backtesting/test_backtest_report.py
@@ -13,7 +13,7 @@ class TestStrategy(TradingStrategy):
time_unit = TimeUnit.MINUTE
interval = 1
- def run_strategy(self, algorithm, market_data):
+ def run_strategy(self, context, market_data):
pass
@@ -92,7 +92,7 @@ def test_report_json_creation_with_multiple_strategies_with_id(self):
algorithm = Algorithm()
@algorithm.strategy()
- def run_strategy(algorithm, market_data):
+ def run_strategy(context, market_data):
pass
algorithm.add_strategy(TestStrategy)
diff --git a/tests/app/backtesting/test_run_backtest.py b/tests/app/backtesting/test_run_backtest.py
index 17703492..428be6c9 100644
--- a/tests/app/backtesting/test_run_backtest.py
+++ b/tests/app/backtesting/test_run_backtest.py
@@ -13,7 +13,7 @@ class TestStrategy(TradingStrategy):
time_unit = TimeUnit.MINUTE
interval = 1
- def run_strategy(self, algorithm, market_data):
+ def run_strategy(self, context, market_data):
pass
@@ -128,7 +128,7 @@ def test_report_csv_creation_with_multiple_strategies(self):
algorithm.add_strategy(strategy)
@algorithm.strategy()
- def run_strategy(algorithm, market_data):
+ def run_strategy(context, market_data):
pass
app.add_portfolio_configuration(
@@ -164,7 +164,7 @@ def test_report_csv_creation_with_multiple_strategies_with_id(self):
algorithm = Algorithm()
@algorithm.strategy()
- def run_strategy(algorithm, market_data):
+ def run_strategy(context, market_data):
pass
algorithm.add_strategy(TestStrategy)
diff --git a/tests/app/backtesting/test_run_backtests.py b/tests/app/backtesting/test_run_backtests.py
index 41b46d3b..f4a35e1a 100644
--- a/tests/app/backtesting/test_run_backtests.py
+++ b/tests/app/backtesting/test_run_backtests.py
@@ -12,7 +12,7 @@ class TestStrategy(TradingStrategy):
time_unit = TimeUnit.MINUTE
interval = 1
- def run_strategy(self, algorithm, market_data):
+ def run_strategy(self, context, market_data):
pass
diff --git a/tests/app/test_add_portfolio_configuration.py b/tests/app/test_add_portfolio_configuration.py
index d9bb7cbc..8af23904 100644
--- a/tests/app/test_add_portfolio_configuration.py
+++ b/tests/app/test_add_portfolio_configuration.py
@@ -22,10 +22,10 @@ class Test(TestBase):
}
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())
+ self.assertEqual(1, self.app.context.portfolio_service.count())
+ self.assertEqual(1, self.app.context.position_service.count())
+ self.assertEqual(1000, self.app.context.get_unallocated())
# Make sure that the portfolio is initialized
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertTrue(portfolio.initialized)
diff --git a/tests/app/test_backtesting.py b/tests/app/test_backtesting.py
index 1b943118..68c5fecd 100644
--- a/tests/app/test_backtesting.py
+++ b/tests/app/test_backtesting.py
@@ -3,7 +3,7 @@
from investing_algorithm_framework import TradingStrategy, Algorithm, \
create_app, RESOURCE_DIRECTORY, PortfolioConfiguration, \
- BacktestDateRange, BacktestReport
+ BacktestDateRange, BacktestReport, Context
from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI
@@ -11,7 +11,7 @@ class SimpleTradingStrategy(TradingStrategy):
interval = 2
time_unit = "hour"
- def apply_strategy(self, algorithm: Algorithm, market_data):
+ def apply_strategy(self, context: Context, market_data):
pass
class Test(TestCase):
@@ -49,7 +49,7 @@ def test_backtest_with_initial_amount(self):
backtest_date_range=date_range,
initial_amount=1000
)
- portfolios = app.algorithm.get_portfolios()
+ portfolios = app.context.get_portfolios()
self.assertEqual(report.get_initial_unallocated(), 1000)
self.assertEqual(len(portfolios), 0)
self.assertEqual(report.get_growth(), 0)
@@ -81,7 +81,7 @@ def test_backtest_with_initial_balance(self):
report: BacktestReport = app.run_backtest(
backtest_date_range=date_range,
)
- portfolios = app.algorithm.get_portfolios()
+ portfolios = app.context.get_portfolios()
self.assertEqual(len(portfolios), 0)
self.assertEqual(report.get_initial_unallocated(), 500)
self.assertEqual(report.get_growth(), 0)
diff --git a/tests/app/test_config.py b/tests/app/test_config.py
index 249f6dec..2ce69da6 100644
--- a/tests/app/test_config.py
+++ b/tests/app/test_config.py
@@ -75,7 +75,7 @@ def test_config_backtest(self):
)
@self.app.algorithm.strategy(interval=1, time_unit="SECOND")
- def test_strategy(algorithm, market_data):
+ def test_strategy(context, market_data):
pass
self.app.run_backtest(
diff --git a/tests/app/test_start.py b/tests/app/test_start.py
index 114e6a59..23b34bc9 100644
--- a/tests/app/test_start.py
+++ b/tests/app/test_start.py
@@ -4,7 +4,7 @@
from investing_algorithm_framework import create_app, TradingStrategy, \
TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \
MarketCredential
-from tests.resources import TestBase, MarketServiceStub
+from tests.resources import MarketServiceStub
class StrategyOne(TradingStrategy):
@@ -33,7 +33,7 @@ def __init__(
def apply_strategy(
self,
- algorithm,
+ context,
market_date=None,
**kwargs
):
@@ -66,7 +66,7 @@ def __init__(
def apply_strategy(
self,
- algorithm,
+ context,
market_date=None,
**kwargs
):
diff --git a/tests/app/test_start_with_new_external_orders.py b/tests/app/test_start_with_new_external_orders.py
index 32038d03..b5f3c6e8 100644
--- a/tests/app/test_start_with_new_external_orders.py
+++ b/tests/app/test_start_with_new_external_orders.py
@@ -108,25 +108,25 @@ def test_start_with_new_external_positions(self):
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.assertTrue(self.app.context.has_position("BTC"))
+ btc_position = self.app.context.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.assertTrue(self.app.context.has_position("DOT"))
+ dot_position = self.app.context.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.assertFalse(self.app.context.has_position("ETH"))
+ eth_position = self.app.context.get_position("ETH")
self.assertEqual(0, eth_position.get_amount())
- self.assertFalse(self.app.algorithm.has_position("KSM"))
+ self.assertFalse(self.app.context.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.assertTrue(self.app.context.has_position("BTC"))
+ btc_position = self.app.context.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.assertTrue(self.app.context.has_position("DOT"))
+ dot_position = self.app.context.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.assertTrue(self.app.context.has_position("ETH"))
+ eth_position = self.app.context.get_position("ETH")
self.assertEqual(10, eth_position.get_amount())
- self.assertFalse(self.app.algorithm.has_position("KSM"))
+ self.assertFalse(self.app.context.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
index 32038d03..b5f3c6e8 100644
--- a/tests/app/test_start_with_new_external_positions.py
+++ b/tests/app/test_start_with_new_external_positions.py
@@ -108,25 +108,25 @@ def test_start_with_new_external_positions(self):
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.assertTrue(self.app.context.has_position("BTC"))
+ btc_position = self.app.context.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.assertTrue(self.app.context.has_position("DOT"))
+ dot_position = self.app.context.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.assertFalse(self.app.context.has_position("ETH"))
+ eth_position = self.app.context.get_position("ETH")
self.assertEqual(0, eth_position.get_amount())
- self.assertFalse(self.app.algorithm.has_position("KSM"))
+ self.assertFalse(self.app.context.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.assertTrue(self.app.context.has_position("BTC"))
+ btc_position = self.app.context.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.assertTrue(self.app.context.has_position("DOT"))
+ dot_position = self.app.context.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.assertTrue(self.app.context.has_position("ETH"))
+ eth_position = self.app.context.get_position("ETH")
self.assertEqual(10, eth_position.get_amount())
- self.assertFalse(self.app.algorithm.has_position("KSM"))
+ self.assertFalse(self.app.context.has_position("KSM"))
diff --git a/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py b/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
index 5bf96afd..c67da174 100644
--- a/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
+++ b/tests/app/web/controllers/portfolio_controller/test_list_portfolio.py
@@ -24,7 +24,7 @@ class Test(FlaskTestBase):
}
def test_list_portfolios(self):
- self.iaf_app.algorithm.create_limit_order(
+ self.iaf_app.context.create_limit_order(
target_symbol="KSM",
price=10,
amount=10,
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 c601d699..e52e42b8 100644
--- a/tests/app/web/controllers/position_controller/test_list_positions.py
+++ b/tests/app/web/controllers/position_controller/test_list_positions.py
@@ -24,7 +24,7 @@ class Test(FlaskTestBase):
}
def test_list_portfolios(self):
- self.iaf_app.algorithm.create_limit_order(
+ self.iaf_app.context.create_limit_order(
amount=10,
target_symbol="KSM",
price=10,
diff --git a/tests/domain/models/trades/__init__.py b/tests/domain/models/trades/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/domain/models/trades/test_trade_stop_loss.py b/tests/domain/models/trades/test_trade_stop_loss.py
new file mode 100644
index 00000000..7f622fb4
--- /dev/null
+++ b/tests/domain/models/trades/test_trade_stop_loss.py
@@ -0,0 +1,57 @@
+from unittest import TestCase
+from investing_algorithm_framework.domain import TradeStopLoss
+
+class TestTradeStopLoss(TestCase):
+
+ def test_model_creation(self):
+ stop_loss = TradeStopLoss(
+ trade_id=1,
+ trade_risk_type="fixed",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+ self.assertEqual(stop_loss.trade_id, 1)
+ self.assertEqual(stop_loss.trade_risk_type, "fixed")
+ self.assertEqual(stop_loss.percentage, 10)
+ self.assertEqual(stop_loss.sell_percentage, 50)
+ self.assertEqual(stop_loss.high_water_mark, 20)
+
+ def test_is_triggered_default(self):
+ stop_loss = TradeStopLoss(
+ trade_id=1,
+ trade_risk_type="fixed",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+
+ self.assertFalse(stop_loss.has_triggered(20))
+ self.assertFalse(stop_loss.has_triggered(19))
+ self.assertTrue(stop_loss.has_triggered(18))
+ self.assertTrue(stop_loss.has_triggered(17))
+
+ def test_is_triggered_trailing(self):
+ stop_loss = TradeStopLoss(
+ trade_id=1,
+ trade_risk_type="trailing",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+
+ self.assertEqual(stop_loss.stop_loss_price, 18)
+ self.assertFalse(stop_loss.has_triggered(20))
+ self.assertFalse(stop_loss.has_triggered(19))
+ self.assertTrue(stop_loss.has_triggered(18))
+ self.assertTrue(stop_loss.has_triggered(17))
+
+ # Increase the high water mark, setting the stop loss price to 22.5
+ self.assertFalse(stop_loss.has_triggered(25))
+ self.assertEqual(stop_loss.stop_loss_price, 22.5)
+ self.assertFalse(stop_loss.has_triggered(24))
+ self.assertFalse(stop_loss.has_triggered(23))
+ self.assertTrue(stop_loss.has_triggered(22))
diff --git a/tests/domain/models/trades/test_trade_take_profit.py b/tests/domain/models/trades/test_trade_take_profit.py
new file mode 100644
index 00000000..c2191f0c
--- /dev/null
+++ b/tests/domain/models/trades/test_trade_take_profit.py
@@ -0,0 +1,86 @@
+from unittest import TestCase
+from investing_algorithm_framework.domain import TradeTakeProfit
+
+class TestTradeStopLoss(TestCase):
+
+ def test_model_creation(self):
+ stop_loss = TradeTakeProfit(
+ trade_id=1,
+ trade_risk_type="fixed",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+ self.assertEqual(stop_loss.trade_id, 1)
+ self.assertEqual(stop_loss.trade_risk_type, "fixed")
+ self.assertEqual(stop_loss.percentage, 10)
+ self.assertEqual(stop_loss.sell_percentage, 50)
+ self.assertEqual(stop_loss.high_water_mark, None)
+ self.assertEqual(stop_loss.take_profit_price, 22)
+
+ stop_loss = TradeTakeProfit(
+ trade_id=1,
+ trade_risk_type="trailing",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+ self.assertEqual(stop_loss.trade_id, 1)
+ self.assertEqual(stop_loss.trade_risk_type, "trailing")
+ self.assertEqual(stop_loss.percentage, 10)
+ self.assertEqual(stop_loss.sell_percentage, 50)
+ self.assertEqual(stop_loss.high_water_mark, None)
+ self.assertEqual(stop_loss.take_profit_price, 22)
+
+ def test_is_triggered_default(self):
+ take_profit = TradeTakeProfit(
+ trade_id=1,
+ trade_risk_type="fixed",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+
+ self.assertEqual(take_profit.take_profit_price, 22)
+ self.assertFalse(take_profit.has_triggered(20))
+ self.assertFalse(take_profit.has_triggered(19))
+ self.assertFalse(take_profit.has_triggered(18))
+ self.assertFalse(take_profit.has_triggered(17))
+ self.assertTrue(take_profit.has_triggered(22))
+
+ def test_is_triggered_trailing(self):
+ take_profit = TradeTakeProfit(
+ trade_id=1,
+ trade_risk_type="trailing",
+ percentage=10,
+ open_price=20,
+ sell_percentage=50,
+ total_amount_trade=100
+ )
+
+ self.assertEqual(take_profit.take_profit_price, 22)
+ self.assertEqual(take_profit.high_water_mark, None)
+ self.assertFalse(take_profit.has_triggered(20))
+ self.assertFalse(take_profit.has_triggered(19))
+ self.assertFalse(take_profit.has_triggered(18))
+ self.assertFalse(take_profit.has_triggered(17))
+
+ # Increase the high water mark, setting the stop loss price to 22.5
+ self.assertFalse(take_profit.has_triggered(25))
+ self.assertEqual(take_profit.high_water_mark, 25)
+ self.assertAlmostEqual(take_profit.take_profit_price, 22.5, 2)
+ self.assertFalse(take_profit.has_triggered(24))
+ self.assertFalse(take_profit.has_triggered(23))
+ self.assertTrue(take_profit.has_triggered(22))
+
+ # Increase the high water mark, setting the stop loss price
+ # to 27.0
+ self.assertFalse(take_profit.has_triggered(30))
+ self.assertAlmostEqual(take_profit.take_profit_price, 27.0, 2)
+ self.assertEqual(take_profit.high_water_mark, 30)
+ self.assertFalse(take_profit.has_triggered(29))
+ self.assertFalse(take_profit.has_triggered(28))
+ self.assertTrue(take_profit.has_triggered(25))
diff --git a/tests/infrastructure/models/test_portfolio.py b/tests/infrastructure/models/test_portfolio.py
index fb4748f8..8471bf7d 100644
--- a/tests/infrastructure/models/test_portfolio.py
+++ b/tests/infrastructure/models/test_portfolio.py
@@ -45,7 +45,7 @@ def test_default(self):
self.assertIsNone(portfolio.get_updated_at())
def test_created_by_app(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
self.assertEqual("BINANCE", portfolio.get_market())
self.assertEqual("USDT", portfolio.get_trading_symbol())
self.assertEqual(10000, portfolio.get_unallocated())
diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py
index b8b61335..0776984c 100644
--- a/tests/resources/test_base.py
+++ b/tests/resources/test_base.py
@@ -32,7 +32,6 @@ def run_strategy(self, algorithm, market_data):
class TestBase(TestCase):
portfolio_configurations = []
config = {}
- algorithm = Algorithm()
external_balances = {}
external_orders = []
initial_orders = []
@@ -63,8 +62,6 @@ def setUp(self) -> None:
portfolio_configuration
)
- self.app.add_algorithm(self.algorithm)
-
# Add all market credentials
if len(self.market_credentials) > 0:
for market_credential in self.market_credentials:
@@ -77,7 +74,7 @@ def setUp(self) -> None:
if self.initial_orders is not None:
for order in self.initial_orders:
- created_order = self.app.algorithm.create_order(
+ created_order = self.app.context.create_order(
target_symbol=order.get_target_symbol(),
amount=order.get_amount(),
price=order.get_price(),
@@ -142,7 +139,6 @@ class FlaskTestBase(FlaskTestCase):
market_credentials = []
iaf_app = None
config = {}
- algorithm = Algorithm()
external_balances = {}
initial_orders = []
external_orders = []
@@ -168,8 +164,6 @@ def create_app(self):
portfolio_configuration
)
- self.iaf_app.add_algorithm(self.algorithm)
-
# Add all market credentials
if len(self.market_credentials) > 0:
for market_credential in self.market_credentials:
@@ -181,7 +175,7 @@ def create_app(self):
if self.initial_orders is not None:
for order in self.initial_orders:
- created_order = self.app.algorithm.create_order(
+ created_order = self.app.context.create_order(
target_symbol=order.get_target_symbol(),
amount=order.get_amount(),
price=order.get_price(),
diff --git a/tests/services/test_order_backtest_service.py b/tests/services/test_order_backtest_service.py
index 1462e193..f812addd 100644
--- a/tests/services/test_order_backtest_service.py
+++ b/tests/services/test_order_backtest_service.py
@@ -246,7 +246,9 @@ def test_create_limit_sell_order(self):
self.assertEqual("EUR", order.get_trading_symbol())
self.assertEqual("SELL", order.get_order_side())
self.assertEqual("LIMIT", order.get_order_type())
- self.assertEqual("CREATED", order.get_status())
+
+ # Order is synced so is OPEN
+ self.assertEqual("OPEN", order.get_status())
def test_update_buy_order_with_successful_order(self):
pass
diff --git a/tests/services/test_order_service.py b/tests/services/test_order_service.py
index f5c5248d..d62c0d95 100644
--- a/tests/services/test_order_service.py
+++ b/tests/services/test_order_service.py
@@ -158,7 +158,9 @@ def test_create_limit_sell_order(self):
self.assertEqual("EUR", order.get_trading_symbol())
self.assertEqual("SELL", order.get_order_side())
self.assertEqual("LIMIT", order.get_order_type())
- self.assertEqual("CREATED", order.get_status())
+
+ # Because its synced
+ self.assertEqual("OPEN", order.get_status())
def test_update_buy_order_with_successful_order(self):
pass
diff --git a/tests/services/test_trade_service.py b/tests/services/test_trade_service.py
index 8cf1920a..e4abd90b 100644
--- a/tests/services/test_trade_service.py
+++ b/tests/services/test_trade_service.py
@@ -1,5 +1,5 @@
from investing_algorithm_framework import PortfolioConfiguration, \
- MarketCredential, OrderStatus, TradeStatus
+ MarketCredential, OrderStatus, TradeStatus, TradeRiskType
from tests.resources import TestBase
@@ -21,14 +21,14 @@ class TestTradeService(TestBase):
"EUR": 1000
}
- def test_create_trade_from_buy_order(self):
+ def test_create_trade_from_buy_order_with_created_status(self):
order_repository = self.app.container.order_repository()
buy_order = order_repository.create(
{
"target_symbol": "ADA",
"trading_symbol": "EUR",
"amount": 2004,
- "filled": 2004,
+ "filled": 0,
"order_side": "BUY",
"price": 0.24262,
"order_type": "LIMIT",
@@ -43,7 +43,76 @@ def test_create_trade_from_buy_order(self):
self.assertEqual(0.24262, trade.open_price)
self.assertIsNotNone(trade.opened_at)
self.assertIsNone(trade.closed_at)
+ self.assertEqual(0, trade.remaining)
+ self.assertEqual(TradeStatus.CREATED.value, trade.status)
+
+ def test_create_trade_from_buy_order_with_open_status(self):
+ order_repository = self.app.container.order_repository()
+ buy_order = order_repository.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 2004,
+ "filled": 1000,
+ "order_side": "BUY",
+ "price": 0.24262,
+ "order_type": "LIMIT",
+ "status": OrderStatus.OPEN.value,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = trade_service.create_trade_from_buy_order(buy_order)
+ self.assertEqual("ADA", trade.target_symbol)
+ self.assertEqual("EUR", trade.trading_symbol)
+ self.assertEqual(2004, trade.amount)
+ self.assertEqual(0.24262, trade.open_price)
+ self.assertIsNotNone(trade.opened_at)
+ self.assertIsNone(trade.closed_at)
+ self.assertEqual(1000, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
+
+ def test_create_trade_from_buy_order_with_closed_status(self):
+ order_repository = self.app.container.order_repository()
+ buy_order = order_repository.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 2004,
+ "filled": 2004,
+ "order_side": "BUY",
+ "price": 0.24262,
+ "order_type": "LIMIT",
+ "status": "OPEN",
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = trade_service.create_trade_from_buy_order(buy_order)
+ self.assertEqual("ADA", trade.target_symbol)
+ self.assertEqual("EUR", trade.trading_symbol)
+ self.assertEqual(2004, trade.amount)
+ self.assertEqual(0.24262, trade.open_price)
+ self.assertIsNotNone(trade.opened_at)
+ self.assertIsNone(trade.closed_at)
self.assertEqual(2004, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
+
+ def test_create_trade_from_buy_order_with_rejected_status(self):
+ order_repository = self.app.container.order_repository()
+ buy_order = order_repository.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 2004,
+ "filled": 2004,
+ "order_side": "BUY",
+ "price": 0.24262,
+ "order_type": "LIMIT",
+ "status": OrderStatus.REJECTED.value,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = trade_service.create_trade_from_buy_order(buy_order)
+ self.assertIsNone(trade)
def test_create_trade_from_buy_order_with_rejected_buy_order(self):
order_repository = self.app.container.order_repository()
@@ -99,7 +168,7 @@ def test_create_trade_from_buy_order_with_expired_buy_order(self):
trade = trade_service.create_trade_from_buy_order(buy_order)
self.assertIsNone(trade)
- def test_update_trade(self):
+ def test_update_trade_with_filled_buy_order(self):
order_repository = self.app.container.order_repository()
buy_order = order_repository.create(
{
@@ -118,8 +187,9 @@ def test_update_trade(self):
trade = trade_service.create_trade_from_buy_order(buy_order)
self.assertEqual("ADA", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
- self.assertEqual(0, trade.amount)
+ self.assertEqual(2004, trade.amount)
self.assertEqual(0.24262, trade.open_price)
+ self.assertEqual(TradeStatus.CREATED.value, trade.status)
self.assertIsNotNone(trade.opened_at)
self.assertIsNone(trade.closed_at)
self.assertEqual(0, trade.remaining)
@@ -138,6 +208,7 @@ def test_update_trade(self):
self.assertEqual(0.24262, trade.open_price)
self.assertIsNone(trade.closed_at)
self.assertEqual(2004, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
def test_update_trade_with_existing_buy_order(self):
order_repository = self.app.container.order_repository()
@@ -158,7 +229,7 @@ def test_update_trade_with_existing_buy_order(self):
trade = trade_service.create_trade_from_buy_order(buy_order)
self.assertEqual("ADA", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
- self.assertEqual(1000, trade.amount)
+ self.assertEqual(2004, trade.amount)
self.assertEqual(0.24262, trade.open_price)
self.assertIsNotNone(trade.opened_at)
self.assertIsNone(trade.closed_at)
@@ -198,31 +269,32 @@ def test_update_trade_with_existing_buy_order_and_partily_closed(self):
trade = trade_service.create_trade_from_buy_order(buy_order)
self.assertEqual("ADA", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
- self.assertEqual(1000, trade.amount)
+ self.assertEqual(2004, trade.amount)
self.assertEqual(0.24262, trade.open_price)
self.assertIsNotNone(trade.opened_at)
self.assertIsNone(trade.closed_at)
self.assertEqual(1000, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
buy_order = order_repository.get(order_id)
buy_order = order_repository.update(
buy_order.id,
{
- "status": OrderStatus.CLOSED.value,
- "filled": 2004,
+ "status": OrderStatus.OPEN.value,
+ "filled": 500,
}
)
- trade = trade_service.update_trade_with_buy_order(1004, buy_order)
+ trade = trade_service.update_trade_with_buy_order(500, buy_order)
self.assertEqual("ADA", trade.target_symbol)
self.assertEqual("EUR", trade.trading_symbol)
self.assertEqual(2004, trade.amount)
self.assertEqual(0.24262, trade.open_price)
self.assertIsNone(trade.closed_at)
- self.assertEqual(2004, trade.remaining)
+ self.assertEqual(1500, trade.remaining)
def test_close_trades(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
order_service = self.app.container.order_service()
- order_service.create(
+ order = order_service.create(
{
"target_symbol": "ADA",
"trading_symbol": "EUR",
@@ -235,18 +307,12 @@ def test_close_trades(self):
"portfolio_id": portfolio.id,
}
)
+ order_id = order.id
trade_service = self.app.container.trade_service()
- order = order_service.find({
- "target_symbol": "ADA",
- "trading_symbol": "EUR",
- "portfolio_id": portfolio.id
- })
-
- # Update the buy order to closed
- order_service = self.app.container.order_service()
+ # Fill the buy order
order_service.update(
- order.id,
+ order_id,
{
"status": OrderStatus.CLOSED.value,
"filled": 2004,
@@ -257,13 +323,15 @@ def test_close_trades(self):
trade = trade_service.find(
{"target_symbol": "ADA", "trading_symbol": "EUR"}
)
+ trade_id = trade.id
self.assertEqual(2004, trade.amount)
self.assertEqual(2004, trade.remaining)
self.assertEqual(TradeStatus.OPEN.value, trade.status)
self.assertEqual(0.2, trade.open_price)
self.assertEqual(1, len(trade.orders))
- order_service.create(
+ # Create a sell order
+ sell_order = order_service.create(
{
"target_symbol": "ADA",
"trading_symbol": "EUR",
@@ -276,48 +344,118 @@ def test_close_trades(self):
"portfolio_id": portfolio.id,
}
)
- trade_service = self.app.container.trade_service()
- # Check that the trade was updated
- trade = trade_service.find(
- {"target_symbol": "ADA", "trading_symbol": "EUR"}
- )
+ sell_order_id = sell_order.id
+
+ # Check that the trade was updated and that the trade has
+ # nothing remaining
+ trade = trade_service.get(trade_id)
self.assertEqual(2004, trade.amount)
- self.assertEqual(2004, trade.remaining)
- self.assertEqual(TradeStatus.OPEN.value, trade.status)
+ self.assertEqual(0, trade.remaining)
+ self.assertEqual(TradeStatus.CLOSED.value, trade.status)
self.assertEqual(0.2, trade.open_price)
- # Amount of orders should be 1, because the sell order has not
- # been filled
- self.assertEqual(1, len(trade.orders))
+ self.assertEqual(2, len(trade.orders))
- # Update the sell order to closed
- order = order_service.find({
- "target_symbol": "ADA",
- "trading_symbol": "EUR",
- "order_side": "SELL",
- "portfolio_id": portfolio.id
- })
+ # Fill the sell order
order_service.update(
- order.id,
+ sell_order_id,
{
"status": OrderStatus.CLOSED.value,
"filled": 2004,
}
)
+ # Check that the trade was updated
+ trade = trade_service.get(trade_id)
+ self.assertEqual(2004, trade.amount)
+ self.assertEqual(0, trade.remaining)
+ self.assertEqual(TradeStatus.CLOSED.value, trade.status)
+ self.assertEqual(0.2, trade.open_price)
+ self.assertAlmostEqual(2004 * 0.3 - 2004 * 0.2, trade.net_gain)
+ self.assertEqual(2, len(trade.orders))
+
+ def test_active_trade_after_canceling_close_trades(self):
+ portfolio = self.app.context.get_portfolio()
+ order_service = self.app.container.order_service()
+ order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 2004,
+ "filled": 0,
+ "order_side": "BUY",
+ "price": 0.2,
+ "order_type": "LIMIT",
+ "status": "CREATED",
+ "portfolio_id": portfolio.id,
+ }
+ )
+ order_id = order.id
trade_service = self.app.container.trade_service()
- # Check that the trade was updated
+
+ # Fill the buy order
+ order_service.update(
+ order_id,
+ {
+ "status": OrderStatus.CLOSED.value,
+ "filled": 2004,
+ }
+ )
+
+ # Check that the trade was updated
trade = trade_service.find(
{"target_symbol": "ADA", "trading_symbol": "EUR"}
)
+ trade_id = trade.id
+ self.assertEqual(2004, trade.amount)
+ self.assertEqual(2004, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
+ self.assertEqual(0.2, trade.open_price)
+ self.assertEqual(1, len(trade.orders))
+
+ # Create a sell order
+ sell_order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 2004,
+ "filled": 0,
+ "order_side": "SELL",
+ "price": 0.3,
+ "order_type": "LIMIT",
+ "status": "CREATED",
+ "portfolio_id": portfolio.id,
+ }
+ )
+ sell_order_id = sell_order.id
+
+ # Check that the trade was updated and that the trade has
+ # nothing remaining
+ trade = trade_service.get(trade_id)
self.assertEqual(2004, trade.amount)
self.assertEqual(0, trade.remaining)
self.assertEqual(TradeStatus.CLOSED.value, trade.status)
self.assertEqual(0.2, trade.open_price)
- self.assertAlmostEqual(2004 * 0.3 - 2004 * 0.2, trade.net_gain)
+ self.assertEqual(2, len(trade.orders))
+
+ # Cancel the sell order
+ order_service.update(
+ sell_order_id,
+ {
+ "status": OrderStatus.CANCELED.value,
+ }
+ )
+
+ # Check that the trade was updated
+ trade = trade_service.get(trade_id)
+ self.assertEqual(2004, trade.amount)
+ self.assertEqual(2004, trade.remaining)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
+ self.assertEqual(0.2, trade.open_price)
+ self.assertAlmostEqual(0, trade.net_gain)
self.assertEqual(2, len(trade.orders))
def test_close_trades_with_no_open_trades(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
order_service = self.app.container.order_service()
order_service.create(
{
@@ -354,7 +492,7 @@ def test_close_trades_with_no_open_trades(self):
)
def test_close_trades_with_multiple_trades(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
order_service = self.app.container.order_service()
buy_order = order_service.create(
{
@@ -370,7 +508,6 @@ def test_close_trades_with_multiple_trades(self):
}
)
order_one_id = buy_order.id
-
buy_order = order_service.create(
{
"target_symbol": "ADA",
@@ -417,7 +554,7 @@ def test_close_trades_with_multiple_trades(self):
self.assertEqual(TradeStatus.OPEN.value, t.status)
self.assertEqual(1, len(t.orders))
- order_service.create(
+ sell_order = order_service.create(
{
"target_symbol": "ADA",
"trading_symbol": "EUR",
@@ -430,6 +567,7 @@ def test_close_trades_with_multiple_trades(self):
"portfolio_id": portfolio.id,
}
)
+ sell_order_id = sell_order.id
trade_service = self.app.container.trade_service()
self.assertEqual(2, len(trade_service.get_all()))
trades = trade_service.get_all(
@@ -438,23 +576,15 @@ def test_close_trades_with_multiple_trades(self):
for t in trades:
self.assertNotEqual(0, t.amount)
- self.assertEqual(t.amount, t.remaining)
- self.assertEqual(TradeStatus.OPEN.value, t.status)
- self.assertEqual(1, len(t.orders))
-
+ self.assertEqual(0, t.remaining)
+ self.assertEqual(TradeStatus.CLOSED.value, t.status)
+ self.assertEqual(2, len(t.orders))
- # Update the sell order to closed
- order = order_service.find({
- "target_symbol": "ADA",
- "trading_symbol": "EUR",
- "order_side": "SELL",
- "portfolio_id": portfolio.id
- })
order_service.update(
- order.id,
+ sell_order_id,
{
"status": OrderStatus.CLOSED.value,
- "filled": order.amount,
+ "filled": sell_order.amount,
}
)
@@ -464,6 +594,8 @@ def test_close_trades_with_multiple_trades(self):
{"target_symbol": "ADA", "trading_symbol": "EUR"}
)
+ self.assertEqual(2, len(trades))
+
for t in trades:
self.assertNotEqual(0, t.amount)
self.assertNotEqual(t.amount, t.remaining)
@@ -478,7 +610,7 @@ def test_close_trades_with_multiple_trades(self):
self.assertEqual(50, trade.net_gain)
def test_close_trades_with_partailly_filled_buy_order(self):
- portfolio = self.app.algorithm.get_portfolio()
+ portfolio = self.app.context.get_portfolio()
order_service = self.app.container.order_service()
order = order_service.create(
{
@@ -506,7 +638,7 @@ def test_close_trades_with_partailly_filled_buy_order(self):
trade = trade_service.find(
{"order_id": order_id}
)
- self.assertEqual(1000, trade.amount)
+ self.assertEqual(2000, trade.amount)
self.assertEqual(1000, trade.remaining)
self.assertEqual(TradeStatus.OPEN.value, trade.status)
self.assertEqual(0.2, trade.open_price)
@@ -533,9 +665,10 @@ def test_close_trades_with_partailly_filled_buy_order(self):
}
)
trade = trade_service.find({"order_id": order_id})
- self.assertEqual(1000, trade.amount)
+ self.assertEqual(2000, trade.amount)
self.assertEqual(0, trade.remaining)
- self.assertEqual(TradeStatus.CLOSED.value, trade.status)
+ self.assertEqual(1000, trade.filled_amount)
+ self.assertEqual(TradeStatus.OPEN.value, trade.status)
self.assertEqual(0.2, trade.open_price)
self.assertAlmostEqual(1000 * 0.3 - 1000 * 0.2, trade.net_gain)
self.assertEqual(2, len(trade.orders))
@@ -566,6 +699,14 @@ def test_trade_closing_winning_trade(self):
self.assertEqual(updated_buy_order.filled, 1000)
self.assertEqual(updated_buy_order.remaining, 0)
+ trade = self.app.container.trade_service().find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(trade.status, "OPEN")
+ self.assertEqual(trade.amount, 1000)
+ self.assertEqual(trade.remaining, 1000)
+ self.assertEqual(trade.open_price, 0.2)
+
# Create a sell order with a higher price
sell_order = order_service.create(
{
@@ -597,64 +738,1786 @@ def test_trade_closing_winning_trade(self):
trade = self.app.container.trade_service().find(
{"order_id": buy_order.id}
)
+
+ self.assertEqual(100.0, trade.net_gain)
self.assertEqual(trade.status, "CLOSED")
self.assertIsNotNone(trade.closed_at)
- self.assertIsNotNone(trade.net_gain)
-
- # def test_trade_closing_losing_trade(self):
- # order_service = self.app.container.order_service()
- # buy_order = order_service.create(
- # {
- # "target_symbol": "ADA",
- # "trading_symbol": "EUR",
- # "amount": 1000,
- # "order_side": "BUY",
- # "price": 0.2,
- # "order_type": "LIMIT",
- # "portfolio_id": 1,
- # "status": "CREATED",
- # }
- # )
- # updated_buy_order = order_service.update(
- # buy_order.id,
- # {
- # "status": "CLOSED",
- # "filled": 1000,
- # "remaining": 0,
- # }
- # )
- # self.assertEqual(updated_buy_order.amount, 1000)
- # self.assertEqual(updated_buy_order.filled, 1000)
- # self.assertEqual(updated_buy_order.remaining, 0)
-
- # # Create a sell order with a higher price
- # sell_order = order_service.create(
- # {
- # "target_symbol": "ADA",
- # "trading_symbol": "EUR",
- # "amount": 1000,
- # "order_side": "SELL",
- # "price": 0.1,
- # "order_type": "LIMIT",
- # "portfolio_id": 1,
- # "status": "CREATED",
- # }
- # )
- # self.assertEqual(0.1, sell_order.get_price())
- # updated_sell_order = order_service.update(
- # sell_order.id,
- # {
- # "status": "CLOSED",
- # "filled": 1000,
- # "remaining": 0,
- # }
- # )
- # self.assertEqual(0.1, updated_sell_order.get_price())
- # self.assertEqual(updated_sell_order.amount, 1000)
- # self.assertEqual(updated_sell_order.filled, 1000)
- # self.assertEqual(updated_sell_order.remaining, 0)
- # buy_order = order_service.get(buy_order.id)
- # self.assertEqual(buy_order.status, "CLOSED")
- # self.assertIsNotNone(buy_order.get_trade_closed_at())
- # self.assertIsNotNone(buy_order.get_trade_closed_price())
- # self.assertEqual(-100, buy_order.get_net_gain())
\ No newline at end of file
+
+ def test_add_stop_loss_to_trade(self):
+ order_service = self.app.container.order_service()
+ buy_order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 1000,
+ "order_side": "BUY",
+ "price": 0.2,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ order_service.update(
+ buy_order.id,
+ {
+ "status": "CLOSED",
+ "filled": 1000,
+ "remaining": 0,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = self.app.container.trade_service().find(
+ {"order_id": buy_order.id}
+ )
+ trade_service.add_stop_loss(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(1, len(trade.stop_losses))
+ trade_service.add_stop_loss(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(2, len(trade.stop_losses))
+
+ def test_add_stop_loss_to_trade_with_already_reached_sell_percentage(self):
+ order_service = self.app.container.order_service()
+ buy_order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 1000,
+ "order_side": "BUY",
+ "price": 0.2,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ order_service.update(
+ buy_order.id,
+ {
+ "status": "CLOSED",
+ "filled": 1000,
+ "remaining": 0,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = self.app.container.trade_service().find(
+ {"order_id": buy_order.id}
+ )
+ trade_service.add_stop_loss(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(1, len(trade.stop_losses))
+ trade_service.add_stop_loss(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(2, len(trade.stop_losses))
+
+ with self.assertRaises(Exception) as context:
+ trade_service.add_stop_loss(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+
+ self.assertEqual(
+ "Combined sell percentages of stop losses belonging "
+ "to trade exceeds 100.",
+ str(context.exception)
+ )
+ self.assertEqual(2, len(trade.stop_losses))
+
+ def test_add_take_profit_to_trade(self):
+ order_service = self.app.container.order_service()
+ buy_order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 1000,
+ "order_side": "BUY",
+ "price": 0.2,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ order_service.update(
+ buy_order.id,
+ {
+ "status": "CLOSED",
+ "filled": 1000,
+ "remaining": 0,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = self.app.container.trade_service().find(
+ {"order_id": buy_order.id}
+ )
+ trade_service.add_take_profit(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(1, len(trade.take_profits))
+ trade_service.add_take_profit(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(2, len(trade.take_profits))
+
+ def test_add_take_profit_to_trade_with_already_reached_percentage(self):
+ order_service = self.app.container.order_service()
+ buy_order = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 1000,
+ "order_side": "BUY",
+ "price": 0.2,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ order_service.update(
+ buy_order.id,
+ {
+ "status": "CLOSED",
+ "filled": 1000,
+ "remaining": 0,
+ }
+ )
+ trade_service = self.app.container.trade_service()
+ trade = self.app.container.trade_service().find(
+ {"order_id": buy_order.id}
+ )
+ trade_service.add_take_profit(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(1, len(trade.take_profits))
+ trade_service.add_take_profit(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ trade = trade_service.find(
+ {"order_id": buy_order.id}
+ )
+ self.assertEqual(2, len(trade.take_profits))
+
+ with self.assertRaises(Exception) as context:
+ trade_service.add_take_profit(
+ trade,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+
+ self.assertEqual(
+ "Combined sell percentages of stop losses belonging "
+ "to trade exceeds 100.",
+ str(context.exception)
+ )
+ self.assertEqual(2, len(trade.take_profits))
+
+ def test_get_triggered_stop_loss_orders(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR
+ 3. Create a stop loss with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 18 EUR
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 7. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 8. Check that the triggered stop loss orders are correct
+ """
+ order_service = self.app.container.order_service()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ stop_loss_one = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(18, stop_loss_one.stop_loss_price)
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(18, stop_loss_two.stop_loss_price)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.stop_losses))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(1, len(trade_two.stop_losses))
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 17,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 7,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_stop_loss_orders()
+ self.assertEqual(2, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(7, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ else:
+ self.assertEqual(17, order_data["price"])
+ self.assertEqual(15, order_data["amount"])
+
+ def test_get_triggered_stop_loss_orders_with_unfilled_order(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR. This is order does not get filled.
+ 3. Create a stop loss with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 18 EUR
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 7. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 8. Check that the triggered stop loss orders are correct. Only 1
+ order should be created for ADA, given that the dot order was
+ not filled.
+ """
+ order_service = self.app.container.order_service()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ stop_loss_one = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(18, stop_loss_one.stop_loss_price)
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(18, stop_loss_two.stop_loss_price)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.stop_losses))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 0,
+ "remaining": 20,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(1, len(trade_two.stop_losses))
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 17,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 7,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_stop_loss_orders()
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+ self.assertEqual(17, order_data["price"])
+ self.assertEqual(15, order_data["amount"])
+
+ def test_get_triggered_stop_loss_orders_with_cancelled_order(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR
+ 3. Create a stop loss with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 18 EUR
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 7. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 8. Check that the triggered stop loss orders are correct
+ 9. Cancel the the dot order
+ 10. Cancel the ada order after partially filling it with amount 5
+ 11. Check that the stop losses are active again and partially filled
+ or entirely filled back.
+ """
+ order_service = self.app.container.order_service()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ # Check that the position costs are correctly updated
+ ada_position = self.app.container.position_service().find(
+ {"symbol": "ADA", "portfolio_id": 1}
+ )
+ # 20 * 20 = 400
+ self.assertEqual(400, ada_position.cost)
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ stop_loss_one = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(18, stop_loss_one.stop_loss_price)
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(18, stop_loss_two.stop_loss_price)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.stop_losses))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ dot_position = self.app.container.position_service().find(
+ {"symbol": "DOT", "portfolio_id": 1}
+ )
+ # 20 * 10 = 200
+ self.assertEqual(200, dot_position.cost)
+
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(1, len(trade_two.stop_losses))
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 17,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 7,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_stop_loss_orders()
+ self.assertEqual(2, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(7, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ else:
+ self.assertEqual(17, order_data["price"])
+ self.assertEqual(15, order_data["amount"])
+
+ for order_data in sell_order_data:
+ order_service.create(order_data)
+
+ ada_sell_order = order_service.find({
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ dot_sell_order = order_service.find({
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ dot_trade = trade_service.find(
+ {"order_id": buy_order_two.id}
+ )
+ ada_trade = trade_service.find(
+ {"order_id": buy_order_one.id}
+ )
+
+ self.assertEqual(2, len(ada_trade.orders))
+ self.assertEqual(5, ada_trade.remaining)
+ self.assertEqual(20, ada_trade.amount)
+
+ self.assertEqual(2, len(dot_trade.orders))
+ self.assertEqual(15, dot_trade.remaining)
+ self.assertEqual(20, dot_trade.amount)
+
+ # Update the ada order to be partially filled
+ order_service.update(
+ object_id=ada_sell_order.id,
+ data={
+ "filled": 5,
+ "remaining": 10,
+ "status": OrderStatus.CANCELED.value,
+ }
+ )
+
+ # Cancel the dot order
+ order_service.update(
+ object_id=dot_sell_order.id,
+ data={
+ "status": OrderStatus.CANCELED.value,
+ }
+ )
+
+ # Check that the positions are correctly updated with amount and cost
+ dot_position = self.app.container.position_service().find(
+ {"symbol": "DOT", "portfolio_id": 1}
+ )
+ # Position cost should be 200, since the order was not filled
+ self.assertEqual(200, dot_position.cost)
+ self.assertEqual(20, dot_position.amount)
+
+ ada_position = self.app.container.position_service().find(
+ {"symbol": "ADA", "portfolio_id": 1}
+ )
+ # Position cost should be 300, since the order was partially filled
+ # with amount 5, 20 * 15 = 300
+ self.assertEqual(15, ada_position.amount)
+ self.assertEqual(300, ada_position.cost)
+
+ # Check that the dot trade is open, with amount of 20, and net gain
+ # of 0, and remaining of 20
+ dot_trade = trade_service.find(
+ {"order_id": buy_order_two.id}
+ )
+ self.assertEqual(2, len(ada_trade.orders))
+ self.assertEqual(20, dot_trade.amount)
+ self.assertEqual(0, dot_trade.net_gain)
+ self.assertEqual(20, dot_trade.remaining)
+
+ ada_trade = trade_service.find(
+ {"order_id": buy_order_one.id}
+ )
+ # Check that the ada trade is open, with amount of 15, and net gain
+ # of 5
+ self.assertEqual(2, len(ada_trade.orders))
+ self.assertEqual(15, ada_trade.remaining)
+ self.assertEqual(20, ada_trade.amount)
+
+ # Check that all stop losses are active again and filled back to
+ # correct amount
+ stop_losses = ada_trade.stop_losses
+
+ for stop_loss in stop_losses:
+ self.assertTrue(stop_loss.active)
+
+ if stop_loss.trade_risk_type == "FIXED":
+ self.assertEqual(10, stop_loss.sell_amount)
+ self.assertEqual(5, stop_loss.sold_amount)
+ else:
+ self.assertEqual(5, stop_loss.sell_amount)
+ self.assertEqual(0, stop_loss.sold_amount)
+
+ stop_losses = dot_trade.stop_losses
+
+ for stop_loss in stop_losses:
+ self.assertTrue(stop_loss.active)
+ self.assertEqual(5, stop_loss.sell_amount)
+ self.assertEqual(0, stop_loss.sold_amount)
+
+ def test_get_triggered_stop_loss_orders_with_partially_filled_orders(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR
+ 3. Create a stop loss with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 18 EUR
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Fill the ada order with amount 10
+ 7. Fill the dot order with amount 20
+ 8. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 9. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 10. Check that the triggered stop loss orders are correct
+ # 9. Cancel the the dot order
+ # 10. Cancel the ada order after partially filling it with amount 5
+ # 11. Check that the stop losses are active again and partially filled
+ # or entirely filled back.
+ """
+ order_service = self.app.container.order_service()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 10,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "OPEN",
+ }
+ )
+
+ # Check that the position costs are correctly updated
+ ada_position = self.app.container.position_service().find(
+ {"symbol": "ADA", "portfolio_id": 1}
+ )
+ # 10 * 20 = 200
+ self.assertEqual(200, ada_position.cost)
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ stop_loss_one = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(18, stop_loss_one.stop_loss_price)
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(18, stop_loss_two.stop_loss_price)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.stop_losses))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ dot_position = self.app.container.position_service().find(
+ {"symbol": "DOT", "portfolio_id": 1}
+ )
+ # 20 * 10 = 200
+ self.assertEqual(200, dot_position.cost)
+
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ stop_loss_two = trade_service.add_stop_loss(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(1, len(trade_two.stop_losses))
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 17,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 7,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_stop_loss_orders()
+ self.assertEqual(2, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(7, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+ else:
+ self.assertEqual(17, order_data["price"])
+ self.assertEqual(10, order_data["amount"])
+
+ for order_data in sell_order_data:
+ order_service.create(order_data)
+
+ ada_sell_order = order_service.find({
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ dot_sell_order = order_service.find({
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ dot_trade = trade_service.find(
+ {"order_id": buy_order_two.id}
+ )
+ ada_trade = trade_service.find(
+ {"order_id": buy_order_one.id}
+ )
+
+ self.assertEqual(2, len(ada_trade.orders))
+ self.assertEqual(0, ada_trade.remaining)
+ self.assertEqual(20, ada_trade.amount)
+ self.assertEqual("OPEN", ada_trade.status)
+
+ self.assertEqual(2, len(dot_trade.orders))
+ self.assertEqual(15, dot_trade.remaining)
+ self.assertEqual(20, dot_trade.amount)
+
+ def test_get_triggered_take_profits_orders(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR
+ 3. Create a take profit with a trailing percentage of 10 and
+ sell percentage 25 for the trade.
+
+ The first take profit will trigger at 22 EUR, and the second
+ take profit will set its high water mark and take profit price at 22 EUR, and only trigger if the price goes down from take profit price.
+
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 7. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 8. Check that the triggered stop loss orders are correct
+ """
+ order_service = self.app.container.order_service()
+ trade_take_profit_repository = self.app.container.\
+ trade_take_profit_repository()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ take_profit_one = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(22, take_profit_one.take_profit_price)
+ take_profit_two = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(22, take_profit_two.take_profit_price)
+ self.assertEqual(None, take_profit_two.high_water_mark)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.take_profits))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ take_profit_three = trade_service.add_take_profit(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(11, take_profit_three.take_profit_price)
+ self.assertEqual(1, len(trade_two.take_profits))
+
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 11,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+
+ # Only the ada order should be triggered, because its fixed
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+ self.assertEqual(22, order_data["price"])
+ self.assertEqual(10, order_data["amount"])
+ self.assertEqual("ADA", order_data["target_symbol"])
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 25,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 14,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ # Take profit 2
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(0, take_profit_two.sold_amount)
+
+ # Take profit 3
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(0, take_profit_three.sold_amount)
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22.4,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 12.5,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(2, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(12.5, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ else:
+ self.assertEqual(22.4, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ def test_get_triggered_take_profits_with_unfilled_order(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a stop loss with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a stop loss price
+ of 18 EUR. This is order does not get filled.
+ 3. Create a stop loss with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 18 EUR
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing stop loss with percentage of 10 and
+ sell percentage 25 for the trade. This is a stop loss price
+ initially set at 7 EUR
+ 6. Update the last reported price of ada to 17 EUR, triggering 2
+ stop loss orders
+ 7. Update the last reported price of dot to 7 EUR, triggering 1
+ stop loss order
+ 8. Check that the triggered stop loss orders are correct. Only 1
+ order should be created for ADA, given that the dot order was
+ not filled.
+ """
+ order_service = self.app.container.order_service()
+ trade_take_profit_repository = self.app.container.\
+ trade_take_profit_repository()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 10,
+ "remaining": 10,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "OPEN",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ self.assertEqual(10, trade_one.remaining)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ trade_one_id = trade_one.id
+ take_profit_one = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(22, take_profit_one.take_profit_price)
+ take_profit_two = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(22, take_profit_two.take_profit_price)
+ self.assertEqual(None, take_profit_two.high_water_mark)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.take_profits))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ take_profit_three = trade_service.add_take_profit(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(11, take_profit_three.take_profit_price)
+ self.assertEqual(1, len(trade_two.take_profits))
+
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 11,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+
+ # Only the ada order should be triggered, because its fixed
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+ self.assertEqual(22, order_data["price"])
+ self.assertEqual(10, order_data["amount"])
+ self.assertEqual("ADA", order_data["target_symbol"])
+
+ # Execute the sell orders
+ for data in sell_order_data:
+ order_service.create(data)
+
+ # Take profit 2
+ take_profit_one = trade_take_profit_repository.get(
+ take_profit_one.id
+ )
+ self.assertEqual(22, take_profit_one.take_profit_price)
+ self.assertEqual(None, take_profit_one.high_water_mark)
+ self.assertEqual(10, take_profit_one.sold_amount)
+
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ self.assertEqual(0, trade_one.remaining)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 25,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 14,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ # Take profit 2
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(0, take_profit_two.sold_amount)
+
+ # Take profit 3
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(0, take_profit_three.sold_amount)
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22.4,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 12.5,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ # Only one, because ada trade has nothing remaining
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(12.5, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ def test_get_triggered_take_profits_orders_with_cancelled_order(self):
+ """
+ Test for triggered stop loss orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR (filled)
+ 2. Create a take profit with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a take profit price
+ of 22 EUR
+ 3. Create a take profit with trailing percentage of 10 and
+ sell percentage 25 for the trade.
+ 4. Create a buy order for DOT with amount 20 at 10 EUR (filled)
+ 5. Create a trailing take profit with percentage of 10 and
+ sell percentage 25 for the trade. This is a take profit price
+ initially set at 11 EUR
+ 6. Update the last reported price of ada to 22 EUR, triggering 1
+ stop loss orders
+ 7. Update the last reported price of dot to 14 EUR, triggering 0
+ stop loss order
+ 8. Check the stop loss orders are correct
+ 9. Update the last reported price of ada to 25 EUR, triggering 0
+ stop loss orders
+ 10. Update the last reported price of dot to 14 EUR, triggering 0
+ stop loss order
+ 11. Check the stop loss orders are correct (0 orders)
+ 12. Update the last reported price of ada to 22.4 EUR, triggering 1
+ stop loss orders
+ 13. Update the last reported price of dot to 12.5 EUR, triggering 1
+ stop loss order
+ 14. Check the stop loss orders are correct
+ 15. Fill the ada order with amount 2.5
+ 16. Cancel the dot order and ada order
+ 17. Check that the stop losses are active again and partially filled
+ """
+ order_service = self.app.container.order_service()
+ trade_take_profit_repository = self.app.container.\
+ trade_take_profit_repository()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ take_profit_one = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(22, take_profit_one.take_profit_price)
+ take_profit_two = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(22, take_profit_two.take_profit_price)
+ self.assertEqual(None, take_profit_two.high_water_mark)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.take_profits))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ take_profit_three = trade_service.add_take_profit(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(11, take_profit_three.take_profit_price)
+ self.assertEqual(1, len(trade_two.take_profits))
+
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 11,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+
+ # Only the ada order should be triggered, because its fixed
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+ self.assertEqual(22, order_data["price"])
+ self.assertEqual(10, order_data["amount"])
+ self.assertEqual("ADA", order_data["target_symbol"])
+
+ order = order_service.create(sell_order_data[0])
+ order_service.update(
+ order.id,
+ {
+ "filled": 10,
+ "remaining": 0,
+ "status": OrderStatus.CLOSED.value,
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = trade_service.get(trade_one_id)
+ trade_two = trade_service.get(trade_two_id)
+
+ self.assertEqual(10, trade_one.remaining)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ self.assertEqual(20, trade_two.remaining)
+ self.assertEqual(20, trade_two.amount)
+ self.assertEqual("OPEN", trade_two.status)
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 25,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 14,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ # Take profit 2
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(0, take_profit_two.sold_amount)
+
+ # Take profit 3
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(0, take_profit_three.sold_amount)
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22.4,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 12.5,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(2, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+
+ if "DOT" == order_data["target_symbol"]:
+ self.assertEqual(12.5, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ else:
+ self.assertEqual(22.4, order_data["price"])
+ self.assertEqual(5, order_data["amount"])
+
+ for order_data in sell_order_data:
+ order_service.create(order_data)
+
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(5, take_profit_two.sold_amount)
+
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(5, take_profit_three.sold_amount)
+
+ trade_service = self.app.container.trade_service()
+ trade_one = trade_service.get(trade_one_id)
+ trade_two = trade_service.get(trade_two_id)
+
+ self.assertEqual(5, trade_one.remaining)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ self.assertEqual(15, trade_two.remaining)
+ self.assertEqual(20, trade_two.amount)
+ self.assertEqual("OPEN", trade_two.status)
+
+ # Fill the ada sell order with amount 2.5
+ ada_sell_order = order_service.find({
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+ order_service.update(
+ ada_sell_order.id,
+ {
+ "filled": 2.5,
+ "remaining": 2.5,
+ "status": OrderStatus.OPEN.value,
+ }
+ )
+
+ # Cancel the dot order
+ dot_order = order_service.find({
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ order_service.update(
+ dot_order.id,
+ {
+ "status": OrderStatus.CANCELED.value,
+ }
+ )
+
+ # Cancel the ada order
+ ada_sell_order = order_service.find({
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "order_side": "SELL",
+ })
+
+ order_service.update(
+ ada_sell_order.id,
+ {
+ "status": OrderStatus.CANCELED.value,
+ }
+ )
+
+ trade_one = trade_service.get(trade_one_id)
+ trade_two = trade_service.get(trade_two_id)
+
+ self.assertEqual(7.5, trade_one.remaining)
+ self.assertEqual("ADA", trade_one.target_symbol)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ self.assertEqual("DOT", trade_two.target_symbol)
+ self.assertEqual(20, trade_two.remaining)
+ self.assertEqual(20, trade_two.amount)
+ self.assertEqual("OPEN", trade_two.status)
+
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(2.5, take_profit_two.sold_amount)
+
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(0, take_profit_three.sold_amount)
+
+ ada_trade = trade_service.find(
+ {"order_id": buy_order_one.id}
+ )
+ dot_trade = trade_service.find(
+ {"order_id": buy_order_two.id}
+ )
+ dot_position = self.app.container.position_service().find(
+ {"symbol": "DOT", "portfolio_id": 1}
+ )
+
+ # 20 * 10 = 200
+ self.assertEqual(200, dot_position.cost)
+
+ ada_position = self.app.container.position_service().find(
+ {"symbol": "ADA", "portfolio_id": 1}
+ )
+ # 20 * 7.5 = 150
+ self.assertEqual(150, ada_position.cost)
+
+ # Check net gain, trade two should have a net gain of 5
+ # Dot trade net gain should be 0
+ self.assertEqual(0, dot_trade.net_gain)
+
+ # Ada trade net gain should be:
+ # First trade (10 * 22) - (10 * 20) = 20
+ # Second trade (2.5 * 22.4) - (2.5 * 20) = 6
+ # Total = 26
+ self.assertEqual(26, ada_trade.net_gain)
+
+ def test_get_triggered_tp_orders_with_partially_filled_orders(self):
+ """
+ Test for triggered take profit orders:
+
+ 1. Create a buy order for ADA with amount 20 at 20 EUR
+ 2. Create a tp with fixed percentage of 10 and
+ sell percentage 50 for the trade. This is a tp price
+ of 22 EUR
+ 3. Create a tp with trailing percentage of 10 and
+ sell percentage 25 for the trade. This is a tp price
+ that will be active at 22
+ 4. Create a buy order for DOT with amount 20 at 10 EUR
+ 5. Create a trailing tp with percentage of 10 and
+ sell percentage 25 for the trade. This is a tp price
+ that will be active at 11 EUR
+ 6. Fill the ada order with amount 10
+ 7. Fill the dot order with amount 20
+ 8. Update the last reported price of ada to 22 EUR, triggering 1
+ tp orders
+ 9. Update the last reported price of dot to 11 EUR, triggering 0
+ tp order
+ 10. Check that the triggered tp orders are correct
+ 11. Fill the ada sell order with amount 10
+ 12. Update the last reported price of ada to 25 EUR, triggering 0
+ tp orders
+ 13. Update the last reported price of ada to 22 EUR, triggering 1
+ tp orders
+ 14. Check that no tp orders are triggered, because there is no amount
+ """
+ order_service = self.app.container.order_service()
+ trade_take_profit_repository = self.app.container.\
+ trade_take_profit_repository()
+ buy_order_one = order_service.create(
+ {
+ "target_symbol": "ADA",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 10,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 20,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CLOSED",
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = self.app.container.trade_service().find(
+ {"order_id": buy_order_one.id}
+ )
+ trade_one_id = trade_one.id
+ take_profit_one = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "fixed",
+ sell_percentage=50,
+ )
+ self.assertEqual(22, take_profit_one.take_profit_price)
+ take_profit_two = trade_service.add_take_profit(
+ trade_one,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ self.assertEqual(22, take_profit_two.take_profit_price)
+ self.assertEqual(None, take_profit_two.high_water_mark)
+ trade_one = trade_service.get(trade_one_id)
+ self.assertEqual(2, len(trade_one.take_profits))
+
+ buy_order_two = order_service.create(
+ {
+ "target_symbol": "DOT",
+ "trading_symbol": "EUR",
+ "amount": 20,
+ "filled": 20,
+ "remaining": 0,
+ "order_side": "BUY",
+ "price": 10,
+ "order_type": "LIMIT",
+ "portfolio_id": 1,
+ "status": "CREATED",
+ }
+ )
+ trade_two = self.app.container.trade_service().find(
+ {"order_id": buy_order_two.id}
+ )
+ trade_two_id = trade_two.id
+ take_profit_three = trade_service.add_take_profit(
+ trade_two,
+ 10,
+ "trailing",
+ sell_percentage=25,
+ )
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+ trade_two = trade_service.get(trade_two_id)
+ self.assertEqual(11, take_profit_three.take_profit_price)
+ self.assertEqual(1, len(trade_two.take_profits))
+
+ trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22,
+ }
+ )
+ trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 11,
+ }
+ )
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+
+ # Only the ada order should be triggered, because its fixed
+ self.assertEqual(1, len(sell_order_data))
+
+ for order_data in sell_order_data:
+ self.assertEqual("SELL", order_data["order_side"])
+ self.assertEqual("EUR", order_data["trading_symbol"])
+ self.assertEqual(1, order_data["portfolio_id"])
+ self.assertEqual("LIMIT", order_data["order_type"])
+ self.assertEqual(22, order_data["price"])
+ self.assertEqual(10, order_data["amount"])
+ self.assertEqual("ADA", order_data["target_symbol"])
+
+ order = order_service.create(sell_order_data[0])
+ order_service.update(
+ order.id,
+ {
+ "filled": 10,
+ "remaining": 0,
+ "status": OrderStatus.CLOSED.value,
+ }
+ )
+
+ trade_service = self.app.container.trade_service()
+ trade_one = trade_service.get(trade_one_id)
+ trade_two = trade_service.get(trade_two_id)
+
+ self.assertEqual(0, trade_one.remaining)
+ self.assertEqual(20, trade_one.amount)
+ self.assertEqual("OPEN", trade_one.status)
+
+ self.assertEqual(20, trade_two.remaining)
+ self.assertEqual(20, trade_two.amount)
+ self.assertEqual("OPEN", trade_two.status)
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 25,
+ }
+ )
+ trade_two = trade_service.update(
+ trade_two_id,
+ {
+ "last_reported_price": 14,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ # Take profit 2
+ take_profit_two = trade_take_profit_repository.get(
+ take_profit_two.id
+ )
+ self.assertEqual(22.5, take_profit_two.take_profit_price)
+ self.assertEqual(25, take_profit_two.high_water_mark)
+ self.assertEqual(0, take_profit_two.sold_amount)
+
+ # Take profit 3
+ take_profit_three = trade_take_profit_repository.get(
+ take_profit_three.id
+ )
+
+ self.assertEqual(12.6, take_profit_three.take_profit_price)
+ self.assertEqual(14, take_profit_three.high_water_mark)
+ self.assertEqual(0, take_profit_three.sold_amount)
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
+
+ trade_one = trade_service.update(
+ trade_one_id,
+ {
+ "last_reported_price": 22.4,
+ }
+ )
+
+ sell_order_data = trade_service.get_triggered_take_profit_orders()
+ self.assertEqual(0, len(sell_order_data))
diff --git a/tests/test_create_app.py b/tests/test_create_app.py
index 378a3b42..3e661e63 100644
--- a/tests/test_create_app.py
+++ b/tests/test_create_app.py
@@ -26,7 +26,7 @@ def test_create_app(self):
self.assertIsNotNone(app)
self.assertIsNone(app._flask_app)
self.assertIsNotNone(app.container)
- self.assertIsNone(app.algorithm)
+ self.assertIsNotNone(app.algorithm)
def test_create_app_with_config(self):
app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir})
@@ -34,14 +34,12 @@ def test_create_app_with_config(self):
self.assertIsNotNone(app.config)
self.assertIsNone(app._flask_app)
self.assertIsNotNone(app.container)
- self.assertIsNone(app.algorithm)
+ self.assertIsNotNone(app.algorithm)
def test_create_app_web(self):
app = create_app(
web=True, config={RESOURCE_DIRECTORY: self.resource_dir}
)
- algorithm = Algorithm()
- app.add_algorithm(algorithm)
app.add_market_credential(
MarketCredential(
market="BINANCE",