diff --git a/core/exchanges/convert.py b/core/exchanges/convert.py index 010f9d9..1280841 100644 --- a/core/exchanges/convert.py +++ b/core/exchanges/convert.py @@ -79,7 +79,6 @@ def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised initial_price = D(current_price) - pl_per_unit else: initial_price = D(current_price) + pl_per_unit - # Get the percent change of the TP price from the initial price. change_percent = ((initial_price - D(tp_price)) / initial_price) * 100 @@ -106,6 +105,7 @@ def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised def tp_percent_to_price(tp_percent, side, current_price, current_units, unrealised_pl): """ Determine the price of the TP percent from the initial price. + Negative values for tp_percent indicate a loss. """ pl_per_unit = D(unrealised_pl) / D(current_units) if side == "long": @@ -117,12 +117,31 @@ def tp_percent_to_price(tp_percent, side, current_price, current_units, unrealis change_percent = D(tp_percent) / 100 # Get the price of the TP percent from the initial price. - change_price = initial_price * change_percent + change_price = initial_price * abs(change_percent) + # loss is true if tp_percent is: + # - below initial_price for long + # - above initial_price for short + + if D(tp_percent) < D(0): + loss = True + else: + loss = False if side == "long": - tp_price = initial_price - change_price + if loss: + tp_price = D(initial_price) - change_price + else: + tp_price = D(initial_price) + change_price else: - tp_price = initial_price + change_price + if loss: + tp_price = D(initial_price) + change_price + else: + tp_price = D(initial_price) - change_price + + # if side == "long": + # tp_price = initial_price - change_price + # else: + # tp_price = initial_price + change_price return round(tp_price, 5) @@ -184,12 +203,23 @@ def sl_percent_to_price(sl_percent, side, current_price, current_units, unrealis change_percent = D(sl_percent) / 100 # Get the price of the SL percent from the initial price. - change_price = initial_price * change_percent + change_price = initial_price * abs(change_percent) + + if D(sl_percent) < D(0): + profit = True + else: + profit = False if side == "long": - sl_price = initial_price - change_price + if profit: + sl_price = D(initial_price) + change_price + else: + sl_price = D(initial_price) - change_price else: - sl_price = initial_price + change_price + if profit: + sl_price = D(initial_price) - change_price + else: + sl_price = D(initial_price) + change_price return round(sl_price, 5) @@ -270,6 +300,8 @@ def open_trade_to_unified_format(trade): "id": trade["id"], "symbol": trade["symbol"], "amount": current_units, + # For crossfilter + "units": current_units, "side": side, "direction": side_to_direction(side), "state": trade["state"], diff --git a/core/tests/exchanges/test_convert.py b/core/tests/exchanges/test_convert.py index 41557cb..80e230f 100644 --- a/core/tests/exchanges/test_convert.py +++ b/core/tests/exchanges/test_convert.py @@ -16,69 +16,104 @@ class CommonTestCase(TestCase): """ Test that the TP price to percent conversion works for long trades. """ - tp_price = 1.1 # 10% + tp_price = D("1.1") # 10% current_price = 1.0 current_units = 1 unrealised_pl = 0 + expected_percent = 10 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_initial_short(self): """ Test that the TP price to percent conversion works for short trades. """ - tp_price = 0.9 # 10% + tp_price = D("0.9") # 10% current_price = 1.0 current_units = 1 unrealised_pl = 0 + expected_percent = 10 percent = tp_price_to_percent( tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_long(self): """ Test that the TP price to percent conversion works for long trades when the price has changed. """ - tp_price = 1.2 # 20% + tp_price = D("1.2") # 20% current_price = 1.1 # + 10% current_units = 1 unrealised_pl = 0.1 # 10% + expected_percent = 20 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_long_loss(self): """ Test that the TP price to percent conversion works for long trades when the price has changed and the TP is at a loss. """ - tp_price = 0.8 # -20% + tp_price = D("0.8") # -20% current_price = 0.9 # - 10% current_units = 1 unrealised_pl = -0.1 # -10% + expected_percent = -20 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_short_loss(self): """ Test that the TP price to percent conversion works for short trades when the price has changed and the TP is at a loss. """ - tp_price = 1.2 # -20% + tp_price = D("1.2") # -20% current_price = 1.1 # - 10% current_units = 1 unrealised_pl = -0.1 # -10% + expected_percent = -20 percent = tp_price_to_percent( tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + tp_price, + ) # For multiple units def test_tp_price_to_percent_initial_long_multi(self): @@ -86,139 +121,209 @@ class CommonTestCase(TestCase): Test that the TP price to percent conversion works for long trades with multiple units. """ - tp_price = 1.1 # 10% + tp_price = D("1.1") # 10% current_price = 1.0 current_units = 10 unrealised_pl = 0 + expected_percent = 10 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_initial_short_multi(self): """ Test that the TP price to percent conversion works for short trades with multiple units. """ - tp_price = 0.9 # 10% + tp_price = D("0.9") # 10% current_price = 1.0 current_units = 10 unrealised_pl = 0 + expected_percent = 10 percent = tp_price_to_percent( tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_long_multi(self): """ Test that the TP price to percent conversion works for long trades when the price has changed, with multiple units. """ - tp_price = 1.2 # +20% + tp_price = D("1.2") # +20% current_price = 1.1 # +10% current_units = 10 unrealised_pl = 1 # 10% + expected_percent = 20 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_short_multi(self): """ Test that the TP price to percent conversion works for short trades when the price has changed, with multiple units. """ - tp_price = 0.8 # -20% + tp_price = D("0.8") # -20% current_price = 0.9 # -10% current_units = 10 unrealised_pl = 1 # 10% + expected_percent = 20 percent = tp_price_to_percent( tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_long_multi_loss(self): """ Test that the TP price to percent conversion works for long trades when the price has changed, with multiple units, and the TP is at a loss. """ - tp_price = 0.8 # -20% + tp_price = D("0.8") # -20% current_price = 0.9 # -10% current_units = 10 unrealised_pl = -1 # -10% + expected_percent = -20 percent = tp_price_to_percent( tp_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + tp_price, + ) def test_tp_price_to_percent_change_short_multi_loss(self): """ Test that the TP price to percent conversion works for short trades when the price has changed, with multiple units, and the TP is at a loss. """ - tp_price = 1.2 # -20% + tp_price = D("1.2") # -20% current_price = 1.1 # -10% current_units = 10 unrealised_pl = -1 # 10% + expected_percent = -20 percent = tp_price_to_percent( tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + tp_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + tp_price, + ) # SL def test_sl_price_to_percent_initial_long(self): """ Test that the SL price to percent conversion works for long trades. """ - sl_price = 0.9 # 10% + sl_price = D("0.9") # 10% current_price = 1.0 current_units = 1 unrealised_pl = 0 + expected_percent = 10 percent = sl_price_to_percent( sl_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + sl_price, + ) def test_sl_price_to_percent_initial_short(self): """ Test that the SL price to percent conversion works for short trades. """ - sl_price = 1.1 # 10% + sl_price = D("1.1") # 10% current_price = 1.0 current_units = 1 unrealised_pl = 0 + expected_percent = 10 percent = sl_price_to_percent( sl_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + sl_price, + ) def test_sl_price_to_percent_change_long_profit(self): """ Test that the SL price to percent conversion works for long trades when the price has changed and the SL is at a profit. """ - sl_price = 1.2 # +20% + sl_price = D("1.2") # +20% current_price = 1.1 # +10% current_units = 1 unrealised_pl = 0.1 # +10% + expected_percent = -20 percent = sl_price_to_percent( sl_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + sl_price, + ) def test_sl_price_to_percent_change_short_profit(self): """ Test that the SL price to percent conversion works for short trades when the price has changed and the SL is at a profit. """ - sl_price = 0.8 # +20% + sl_price = D("0.8") # +20% current_price = 0.9 # +10% current_units = 1 unrealised_pl = 0.1 # +10% + expected_percent = -20 percent = sl_price_to_percent( sl_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, -20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + sl_price, + ) # For multiple units def test_sl_price_to_percent_initial_long_multi(self): @@ -226,35 +331,49 @@ class CommonTestCase(TestCase): Test that the SL price to percent conversion works for long trades with multiple units. """ - sl_price = 0.9 # -10% + sl_price = D("0.9") # -10% current_price = 1.0 current_units = 10 unrealised_pl = 0 + expected_percent = 10 percent = sl_price_to_percent( sl_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 10) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "long", current_price, current_units, unrealised_pl + ), + sl_price, + ) def test_sl_price_to_percent_initial_short_multi(self): """ Test that the SL price to percent conversion works for short trades with multiple units. """ - sl_price = 1.2 # -20% + sl_price = D("1.2") # -20% current_price = 1.0 current_units = 10 unrealised_pl = 0 + expected_percent = 20 percent = sl_price_to_percent( sl_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, expected_percent) + self.assertEqual( + sl_percent_to_price( + expected_percent, "short", current_price, current_units, unrealised_pl + ), + sl_price, + ) def test_sl_price_to_percent_change_long_multi_profit(self): """ Test that the SL price to percent conversion works for long trades when the price has changed, with multiple units, and the SL is at a profit. """ - sl_price = D(1.2) # +20% + sl_price = D("1.2") # +20% current_price = 1.1 # +10% current_units = 10 unrealised_pl = 1 # +10% @@ -264,7 +383,7 @@ class CommonTestCase(TestCase): ) self.assertEqual(percent, expected_percent) self.assertEqual( - tp_percent_to_price( + sl_percent_to_price( expected_percent, "long", current_price, current_units, unrealised_pl ), sl_price, @@ -275,7 +394,7 @@ class CommonTestCase(TestCase): Test that the SL price to percent conversion works for short trades when the price has changed, with multiple units, and the SL is at a profit. """ - sl_price = D(0.8) # -20% + sl_price = D("0.8") # -20% current_price = 0.9 # +10% current_units = 10 unrealised_pl = 1 # +10% @@ -285,7 +404,7 @@ class CommonTestCase(TestCase): ) self.assertEqual(percent, expected_percent) self.assertEqual( - tp_percent_to_price( + sl_percent_to_price( expected_percent, "short", current_price, current_units, unrealised_pl ), sl_price, diff --git a/core/tests/trading/test_active_management.py b/core/tests/trading/test_active_management.py index cac985d..32508ab 100644 --- a/core/tests/trading/test_active_management.py +++ b/core/tests/trading/test_active_management.py @@ -1,10 +1,19 @@ +from decimal import Decimal as D from unittest.mock import Mock, patch from django.test import TestCase from core.exchanges.convert import convert_trades from core.lib.schemas.oanda_s import parse_time -from core.models import Account, ActiveManagementPolicy, Hook, Signal, User +from core.models import ( + Account, + ActiveManagementPolicy, + AssetGroup, + AssetRule, + Hook, + Signal, + User, +) from core.tests.helpers import StrategyMixin, SymbolPriceMock from core.trading.active_management import ActiveManagement @@ -56,17 +65,17 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase): "dividendAdjustment": "0.0000", "unrealizedPL": "-0.0008", "marginUsed": "0.2966", - "takeProfitOrder": {"price": "1.06331"}, - "stopLossOrder": {"price": "1.06331"}, - "trailingStopLossOrder": {"price": "1.06331"}, + "takeProfitOrder": {"price": "1.07934"}, + "stopLossOrder": {"price": "1.05276"}, + "trailingStopLossOrder": None, "trailingStopValue": None, "side": "long", }, { - "id": "20083", + "id": "20084", "symbol": "EUR_USD", "price": "1.06331", - "openTime": "2023-02-13T11:38:06.302917985Z", # Monday at 11:38 + "openTime": "2023-02-13T11:39:06.302917985Z", # Monday at 11:38 "initialUnits": "10", "initialMarginRequired": "0.2966", "state": "OPEN", @@ -76,9 +85,9 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase): "dividendAdjustment": "0.0000", "unrealizedPL": "-0.0008", "marginUsed": "0.2966", - "takeProfitOrder": {"price": "1.06331"}, - "stopLossOrder": {"price": "1.06331"}, - "trailingStopLossOrder": {"price": "1.06331"}, + "takeProfitOrder": {"price": "1.07934"}, + "stopLossOrder": {"price": "1.05276"}, + "trailingStopLossOrder": None, "trailingStopValue": None, "side": "long", }, @@ -90,6 +99,30 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase): self.ams.get_balance = self.fake_get_balance # self.ams.trades = self.trades + def add_trade(self, id, symbol, side, open_time): + trade = { + "id": id, + "symbol": symbol, + "price": "1.06331", + "openTime": open_time, + "initialUnits": "10", + "initialMarginRequired": "0.2966", + "state": "OPEN", + "currentUnits": "10", + "realizedPL": "0.0000", + "financing": "0.0000", + "dividendAdjustment": "0.0000", + "unrealizedPL": "-0.0008", + "marginUsed": "0.2966", + "takeProfitOrder": {"price": "1.07934"}, + "stopLossOrder": {"price": "1.05276"}, + "trailingStopLossOrder": None, + "trailingStopValue": None, + "side": side, + } + trade["openTime"] = parse_time(trade) + self.trades.append(trade) + def fake_get_trades(self): self.ams.trades = self.trades return self.trades @@ -211,14 +244,154 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase): {"size": 50}, ) - def test_protection_violated(self): - pass + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_protection_violated_absent(self, handle_violation): + self.trades[0]["takeProfitOrder"] = None + self.trades[0]["stopLossOrder"] = None + self.ams.run_checks() - def test_asset_groups_violated(self): - pass + expected_args = { + "take_profit_price": D("1.07934"), + "stop_loss_price": D("1.05276"), + } + self.check_violation( + "protection", + handle_violation.call_args_list, + "close", + [self.trades[0]], + expected_args, + ) - def test_max_open_trades_violated(self): - pass + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_protection_violated_absent_not_required(self, handle_violation): + self.strategy.order_settings.take_profit_percent = 0 + self.strategy.order_settings.stop_loss_percent = 0 + self.strategy.order_settings.save() + self.trades[0]["takeProfitOrder"] = None + self.trades[0]["stopLossOrder"] = None + self.ams.run_checks() + print("CALLS", handle_violation.call_args_list) + + self.assertEqual(handle_violation.call_count, 0) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_asset_groups_violated(self, handle_violation): + asset_group = AssetGroup.objects.create( + user=self.user, + name="Test Asset Group", + ) + AssetRule.objects.create( + user=self.user, + asset="USD", + group=asset_group, + status=2, # Bullish + ) + self.strategy.asset_group = asset_group + self.strategy.save() + self.ams.run_checks() + + self.check_violation( + "asset_group", + handle_violation.call_args_list, + "close", + self.trades, # All trades should be closed, since all are USD quote + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_asset_groups_violated_invert(self, handle_violation): + self.trades[0]["side"] = "short" + self.trades[1]["side"] = "short" + asset_group = AssetGroup.objects.create( + user=self.user, + name="Test Asset Group", + ) + AssetRule.objects.create( + user=self.user, + asset="USD", + group=asset_group, + status=3, # Bullish + ) + self.strategy.asset_group = asset_group + self.strategy.save() + self.ams.run_checks() + + self.check_violation( + "asset_group", + handle_violation.call_args_list, + "close", + self.trades, # All trades should be closed, since all are USD quote + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_crossfilter_violated_side(self, handle_violation): + self.trades[1]["side"] = "short" + self.ams.run_checks() + + self.check_violation( + "crossfilter", + handle_violation.call_args_list, + "close", + [self.trades[1]], # Only close newer trade + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_crossfilter_violated_side_multiple(self, handle_violation): + self.add_trade("20085", "EUR_USD", "short", "2023-02-13T12:39:06.302917985Z") + self.add_trade("20086", "EUR_USD", "short", "2023-02-14T12:39:06.302917985Z") + self.add_trade("20087", "EUR_USD", "short", "2023-02-10T12:39:06.302917985Z") + self.ams.run_checks() + + self.check_violation( + "crossfilter", + handle_violation.call_args_list, + "close", + self.trades[0:4], # Only close newer trades + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_crossfilter_violated_symbol(self, handle_violation): + # Change symbol to conflict with long on EUR_USD + self.trades[1]["symbol"] = "USD_EUR" + self.ams.run_checks() + + self.check_violation( + "crossfilter", + handle_violation.call_args_list, + "close", + [self.trades[1]], # Only close newer trade + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_crossfilter_violated_symbol_multiple(self, handle_violation): + self.add_trade("20085", "USD_EUR", "long", "2023-02-13T12:39:06.302917985Z") + self.add_trade("20086", "USD_EUR", "long", "2023-02-14T12:39:06.302917985Z") + self.add_trade("20087", "USD_EUR", "long", "2023-02-10T12:39:06.302917985Z") + self.ams.run_checks() + + self.check_violation( + "crossfilter", + handle_violation.call_args_list, + "close", + self.trades[0:4], # Only close newer trades + ) + + @patch("core.trading.active_management.ActiveManagement.handle_violation") + def test_max_open_trades_violated(self, handle_violation): + for x in range(9): + self.add_trade( + str(x), + "EUR_USD", + "long", + f"2023-02-13T12:39:1{x}.302917985Z", + ) + + self.ams.run_checks() + self.check_violation( + "max_open_trades", + handle_violation.call_args_list, + "close", + self.trades[10:], # Only close newer trades + ) def test_max_open_trades_per_symbol_violated(self): pass @@ -228,6 +401,3 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase): def test_max_risk_violated(self): pass - - def test_crossfilter_violated(self): - pass diff --git a/core/trading/active_management.py b/core/trading/active_management.py index a58f45b..9dafa99 100644 --- a/core/trading/active_management.py +++ b/core/trading/active_management.py @@ -1,8 +1,15 @@ from datetime import datetime from decimal import Decimal as D -from core.exchanges.convert import convert_trades, side_to_direction -from core.trading import checks +import core.trading.market # to avoid messy circular import +from core.exchanges.convert import ( + convert_trades, + side_to_direction, + sl_percent_to_price, + tp_percent_to_price, +) +from core.trading import assetfilter, checks, risk +from core.trading.crossfilter import crossfilter from core.trading.market import get_base_quote, get_trade_size_in_base @@ -77,7 +84,6 @@ class ActiveManagement(object): ) def check_protection(self, trade): - print("CHECK PROTECTION", trade) deviation = D(0.05) # 5% matches = { @@ -89,22 +95,187 @@ class ActiveManagement(object): violations = {} for key, expected in matches.items(): + if expected == 0: + continue if key in trade: actual = D(trade[key]) - if expected is None: - continue expected = D(expected) min_val = expected - (deviation * expected) max_val = expected + (deviation * expected) within_deviation = min_val <= actual <= max_val - if not within_deviation: - violations[key] = expected + else: + within_deviation = False + + if not within_deviation: + # violations[key] = expected + if key == "take_profit_percent": + tp_price = tp_percent_to_price( + expected, + trade["side"], + trade["current_price"], + trade["amount"], + trade["pl"], + ) + violations["take_profit_price"] = tp_price + elif key == "stop_loss_percent": + sl_price = sl_percent_to_price( + expected, + trade["side"], + trade["current_price"], + trade["amount"], + trade["pl"], + ) + violations["stop_loss_price"] = sl_price + elif key == "trailing_stop_loss_percent": + tsl_price = sl_percent_to_price( + expected, + trade["side"], + trade["current_price"], + trade["amount"], + trade["pl"], + ) + violations["trailing_stop_loss_price"] = tsl_price if violations: self.handle_violation( "protection", self.policy.when_protection_violated, trade, violations ) + def check_asset_groups(self, trade): + if self.strategy.asset_group is not None: + base, quote = get_base_quote( + self.strategy.account.exchange, trade["symbol"] + ) + allowed = assetfilter.get_allowed( + self.strategy.asset_group, base, quote, trade["side"] + ) + if not allowed: + self.handle_violation( + "asset_group", self.policy.when_asset_groups_violated, trade + ) + + def get_sorted_trades_copy(self, trades, reverse=True): + trades_copy = trades.copy() + # sort by open time, newest first + trades_copy.sort( + key=lambda x: datetime.strptime(x["open_time"], "%Y-%m-%dT%H:%M:%S.%fZ"), + reverse=reverse, + ) + return trades_copy + + def check_crossfilter(self, trades): + close_trades = [] + + trades_copy = self.get_sorted_trades_copy(trades) + + iterations = 0 + finished = [] + # Recursively run crossfilter on the newest-first list until we have no more conflicts + while not len(finished) == len(trades): + iterations += 1 + if iterations > 10000: + raise Exception("Too many iterations") + # For each trade + for trade in trades_copy: + # Abort if we've already checked this trade + if trade in close_trades: + continue + # Calculate trades excluding this one + # Also remove if we have already checked this + others = [ + t + for t in trades_copy + if t["id"] != trade["id"] and t not in close_trades + ] + symbol = trade["symbol"] + direction = trade["direction"] + func = "entry" + + # Check if this trade is filtered, pretending we are opening it + # And passing the remaining trades as the other trades in the account + filtered = crossfilter( + self.strategy.account, symbol, direction, func, all_positions=others + ) + if not filtered: + # This trade is fine, add it to finished + finished.append(trade) + continue + if filtered["action"] == "rejected": + # It's rejected, add it to the close trades list + # And don't check it again + finished.append(trade) + close_trades.append(trade) + if not close_trades: + return + # For each conflicting symbol, identify the oldest trades + # removed_trades = [] + # for symbol in conflict: + # newest_trade = max(conflict, key=lambda x: datetime.strptime(x["open_time"], "%Y-%m-%dT%H:%M:%S.%fZ")) + # removed_trades.append(newest_trade) + # print("KEEP TRADES", keep_trade_ids) + # close_trades = [] + # for x in keep_trade_ids: + # for position in conflict[x]: + # if position["id"] not in keep_trade_ids[x]: + # close_trades.append(position) + if close_trades: + for trade in close_trades: + self.handle_violation( + "crossfilter", self.policy.when_crossfilter_violated, trade + ) + + def check_max_open_trades(self, trades): + if self.strategy.risk_model is not None: + max_open_pass = risk.check_max_open_trades(self.strategy.risk_model, trades) + if not max_open_pass: + trades_copy = self.get_sorted_trades_copy(trades, reverse=False) + print("TRADES COPY", [x["id"] for x in trades_copy]) + print("MAX", self.strategy.risk_model.max_open_trades) + trades_over_limit = trades_copy[ + self.strategy.risk_model.max_open_trades : + ] + for trade in trades_over_limit: + self.handle_violation( + "max_open_trades", + self.policy.when_max_open_trades_violated, + trade, + ) + print("TRADES OVER LIMNIT", trades_over_limit) + + def check_max_open_trades_per_symbol(self, trades): + if self.strategy.risk_model is not None: + max_open_pass = risk.check_max_open_trades_per_symbol( + self.strategy.risk_model, trades + ) + max_open_pass = list(max_open_pass) + print("MAX OPEN PASS", max_open_pass) + if max_open_pass: + trades_copy = self.get_sorted_trades_copy(trades, reverse=False) + trades_over_limit = [] + for symbol in max_open_pass: + print("SYMBOL", symbol) + print("TRADES", trades) + symbol_trades = [x for x in trades_copy if x["symbol"] == symbol] + exceeding_limit = symbol_trades[ + self.strategy.risk_model.max_open_trades_per_symbol : + ] + for x in exceeding_limit: + trades_over_limit.append(x) + + for trade in trades_over_limit: + self.handle_violation( + "max_open_trades_per_symbol", + self.policy.when_max_open_trades_violated, + trade, + ) + print("TRADES OVER LIMNIT", trades_over_limit) + + def check_max_loss(self): + pass + + def check_max_risk(self, trades): + pass + def run_checks(self): converted_trades = convert_trades(self.get_trades()) for trade in converted_trades: @@ -112,6 +283,13 @@ class ActiveManagement(object): self.check_trends(trade) self.check_position_size(trade) self.check_protection(trade) + self.check_asset_groups(trade) + + self.check_crossfilter(converted_trades) + self.check_max_open_trades(converted_trades) + self.check_max_open_trades_per_symbol(converted_trades) + self.check_max_loss() + self.check_max_risk(converted_trades) # Trading Time # Max loss diff --git a/core/trading/assetfilter.py b/core/trading/assetfilter.py index 86b79b7..639f1d3 100644 --- a/core/trading/assetfilter.py +++ b/core/trading/assetfilter.py @@ -1,14 +1,14 @@ from core.models import AssetRule -def get_allowed(group, base, quote, direction): +def get_allowed(group, base, quote, side): """ Determine whether the trade is allowed according to the group. See tests for examples. The logic requires trading knowledge. :param group: The group to check :param base: The base currency :param quote: The quote currency - :param direction: The direction of the trade + :param side: The direction of the trade """ # If our base has allowed == False, we can only short it, or long the quote @@ -22,10 +22,10 @@ def get_allowed(group, base, quote, direction): # Always deny return False elif mapped_status == 3: - if direction == "long": + if side == "long": return False elif mapped_status == 2: - if direction == "short": + if side == "short": return False # If our quote has allowed == False, we can only long it, or short the base quote_rule = AssetRule.objects.filter(group=group, asset=quote).first() @@ -38,10 +38,10 @@ def get_allowed(group, base, quote, direction): # Always deny return False elif mapped_status == 3: - if direction == "short": + if side == "short": return False elif mapped_status == 2: - if direction == "long": + if side == "long": return False if not base_rule and not quote_rule: diff --git a/core/trading/crossfilter.py b/core/trading/crossfilter.py index 12428bd..1377631 100644 --- a/core/trading/crossfilter.py +++ b/core/trading/crossfilter.py @@ -95,7 +95,7 @@ def check_conflicting_position( } -def crossfilter(account, new_symbol, new_direction, func): +def crossfilter(account, new_symbol, new_direction, func, all_positions=None): """ Determine if we are betting against ourselves. Checks open positions for the account, rejecting the trade if there is one @@ -109,9 +109,11 @@ def crossfilter(account, new_symbol, new_direction, func): try: # Only get the data we need if func == "entry": - all_positions = account.client.get_all_positions() + if all_positions is None: + all_positions = account.client.get_all_positions() else: - all_positions = [account.client.get_position_info(new_symbol)] + if all_positions is None: + all_positions = [account.client.get_position_info(new_symbol)] except GenericAPIError as e: if "No position exists for the specified instrument" in str(e): log.debug("No position exists for this symbol") diff --git a/core/trading/risk.py b/core/trading/risk.py index ed16cc0..204bf47 100644 --- a/core/trading/risk.py +++ b/core/trading/risk.py @@ -49,7 +49,7 @@ def check_max_open_trades(risk_model, account_trades): return len(account_trades) < risk_model.max_open_trades -def check_max_open_trades_per_symbol(risk_model, account_trades): +def check_max_open_trades_per_symbol(risk_model, account_trades, yield_symbol=False): """ Check we cannot open more trades per symbol than permissible. """ @@ -62,8 +62,12 @@ def check_max_open_trades_per_symbol(risk_model, account_trades): for symbol, count in symbol_map.items(): if count >= risk_model.max_open_trades_per_symbol: - return False - return True + if yield_symbol: + yield symbol + else: + return False + if not yield_symbol: + return True def check_risk(risk_model, account, proposed_trade):