diff --git a/core/tests/helpers.py b/core/tests/helpers.py index 521b752..b9527f4 100644 --- a/core/tests/helpers.py +++ b/core/tests/helpers.py @@ -3,7 +3,16 @@ from decimal import Decimal as D from os import getenv from unittest.mock import Mock, patch -from core.models import Account, OrderSettings, RiskModel, Strategy, TradingTime, User +from core.models import ( + Account, + OrderSettings, + RiskModel, + Strategy, + Trade, + TradingTime, + User, +) +from core.trading import market # Create patch mixin to mock out the Elastic client @@ -110,6 +119,75 @@ If you have done this, please see the following line for more information: if self.fail: self.skipTest("Live tests aborted") + def open_trade(self, trade=None): + if trade: + posted = trade.post() + else: + trade = self.trade + posted = self.trade.post() + # Check the opened trade + self.assertEqual(posted["type"], "MARKET_ORDER") + self.assertEqual(posted["symbol"], trade.symbol) + if trade.direction == "sell": + self.assertEqual(posted["units"], str(0 - trade.amount)) + else: + self.assertEqual(posted["units"], str(trade.amount)) + self.assertEqual(posted["timeInForce"], "FOK") + + return posted + + def close_trade(self, trade=None): + if trade: + trade.refresh_from_db() + closed = self.account.client.close_trade(trade.order_id) + else: + trade = self.trade + # refresh the trade to get the trade id + self.trade.refresh_from_db() + closed = self.account.client.close_trade(self.trade.order_id) + + # Check the feedback from closing the trade + self.assertEqual(closed["type"], "MARKET_ORDER") + self.assertEqual(closed["symbol"], trade.symbol) + self.assertEqual(closed["units"], str(0 - int(trade.amount))) + self.assertEqual(closed["timeInForce"], "FOK") + self.assertEqual(closed["reason"], "TRADE_CLOSE") + + return closed + + def create_complex_trade(self, direction, amount, symbol, tp_percent, sl_percent): + eur_usd_price = market.get_price(self.account, direction, symbol) + trade_tp = market.get_tp(direction, tp_percent, eur_usd_price) + trade_sl = market.get_sl(direction, sl_percent, eur_usd_price) + # trade_tsl = market.get_sl("buy", 1, eur_usd_price, return_var=True) + # # TP 1% profit + # trade_tp = eur_usd_price * D(1.01) + # # SL 2% loss + # trade_sl = eur_usd_price * D(0.98) + # # TSL 1% loss + # trade_tsl = eur_usd_price * D(0.99) + + trade_precision, display_precision = market.get_precision(self.account, symbol) + # Round everything to the display precision + + trade_tp = round(trade_tp, display_precision) + trade_sl = round(trade_sl, display_precision) + # trade_tsl = round(trade_tsl, display_precision) + + complex_trade = Trade.objects.create( + user=self.user, + account=self.account, + symbol=symbol, + time_in_force="FOK", + type="market", + amount=amount, + direction=direction, + take_profit=trade_tp, + stop_loss=trade_sl, + # trailing_stop_loss=trade_tsl, + ) + return complex_trade + class StrategyMixin: def setUp(self): diff --git a/core/tests/trading/test_live.py b/core/tests/trading/test_live.py index 26ba225..682ef84 100644 --- a/core/tests/trading/test_live.py +++ b/core/tests/trading/test_live.py @@ -20,9 +20,27 @@ from core.trading import market, risk from core.trading.active_management import ActiveManagement -class ActiveManagementMixinTestCase(StrategyMixin): +class ActiveManagementLiveTestCase(ElasticMock, StrategyMixin, LiveBase, TestCase): def setUp(self): - super().setUp() + super(ActiveManagementLiveTestCase, self).setUp() + self.trade = Trade.objects.create( + user=self.user, + account=self.account, + symbol="EUR_USD", + time_in_force="FOK", + type="market", + amount=10, + direction="buy", + ) + self.commission = 0.025 + self.risk_model = RiskModel.objects.create( + user=self.user, + name="Test Risk Model", + max_loss_percent=4, + max_risk_percent=2, + max_open_trades=3, + max_open_trades_per_symbol=2, + ) self.trading_time_all = TradingTime.objects.create( user=self.user, name="All", @@ -51,6 +69,9 @@ class ActiveManagementMixinTestCase(StrategyMixin): self.strategy.save() self.ams = ActiveManagement(self.strategy) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) + def test_ams_success(self): complex_trade = self.create_complex_trade("buy", 10, "EUR_USD", 1.5, 1.0) self.open_trade(complex_trade) @@ -179,6 +200,8 @@ class ActiveManagementMixinTestCase(StrategyMixin): complex_trade.save() self.close_trade(complex_trade) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) def test_ams_protection_violated(self): self.active_management_policy.when_protection_violated = "close" @@ -226,6 +249,8 @@ class ActiveManagementMixinTestCase(StrategyMixin): self.assertEqual(D(trades[0]["stopLossOrder"]["price"]), expected_sl) self.close_trade(complex_trade) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) def test_ams_asset_groups_violated(self): asset_group = AssetGroup.objects.create( @@ -288,6 +313,8 @@ class ActiveManagementMixinTestCase(StrategyMixin): self.assertEqual(len(trades), 1) self.close_trade(complex_trade1) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) @patch( "core.trading.active_management.ActiveManagement.check_trends", @@ -338,6 +365,8 @@ class ActiveManagementMixinTestCase(StrategyMixin): for x in [trade1, trade2]: self.close_trade(x) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) @patch( "core.trading.active_management.ActiveManagement.check_trends", @@ -384,6 +413,8 @@ class ActiveManagementMixinTestCase(StrategyMixin): }, ] } + print("ACTIONS", self.ams.actions) + print("EXP", expected) self.assertEqual(self.ams.actions, expected) @@ -401,6 +432,14 @@ class ActiveManagementMixinTestCase(StrategyMixin): self.assertIn(trade5.order_id, trade_ids) self.assertNotIn(trade6.order_id, trade_ids) + for x in [trade1, trade2, trade4, trade5]: + self.close_trade(x) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) + + self.strategy.risk_model.max_open_trades_per_symbol = 5 + self.strategy.risk_model.save() + def test_ams_max_loss_violated(self): trade1 = self.create_complex_trade("buy", 10, "EUR_USD", 1.5, 1.0) self.open_trade(trade1) @@ -427,6 +466,9 @@ class ActiveManagementMixinTestCase(StrategyMixin): trades = self.account.client.get_all_open_trades() self.assertEqual(len(trades), 0) + self.account.initial_balance = 100000 + self.account.save() + @patch( "core.trading.active_management.ActiveManagement.check_position_size", return_value=None, @@ -468,11 +510,11 @@ class ActiveManagementMixinTestCase(StrategyMixin): self.assertNotIn(trade2.order_id, trade_ids) self.close_trade(trade1) + trades = self.account.client.get_all_open_trades() + self.assertEqual(len(trades), 0) -class LiveTradingTestCase( - ElasticMock, ActiveManagementMixinTestCase, LiveBase, TestCase -): +class LiveTradingTestCase(ElasticMock, LiveBase, TestCase): def setUp(self): super(LiveTradingTestCase, self).setUp() self.trade = Trade.objects.create( @@ -502,42 +544,6 @@ class LiveTradingTestCase( # We need some money to place trades self.assertTrue(balance > 1000) - def open_trade(self, trade=None): - if trade: - posted = trade.post() - else: - trade = self.trade - posted = self.trade.post() - # Check the opened trade - self.assertEqual(posted["type"], "MARKET_ORDER") - self.assertEqual(posted["symbol"], trade.symbol) - if trade.direction == "sell": - self.assertEqual(posted["units"], str(0 - trade.amount)) - else: - self.assertEqual(posted["units"], str(trade.amount)) - self.assertEqual(posted["timeInForce"], "FOK") - - return posted - - def close_trade(self, trade=None): - if trade: - trade.refresh_from_db() - closed = self.account.client.close_trade(trade.order_id) - else: - trade = self.trade - # refresh the trade to get the trade id - self.trade.refresh_from_db() - closed = self.account.client.close_trade(self.trade.order_id) - - # Check the feedback from closing the trade - self.assertEqual(closed["type"], "MARKET_ORDER") - self.assertEqual(closed["symbol"], trade.symbol) - self.assertEqual(closed["units"], str(0 - int(trade.amount))) - self.assertEqual(closed["timeInForce"], "FOK") - self.assertEqual(closed["reason"], "TRADE_CLOSE") - - return closed - def test_place_close_trade(self): """ Test placing a trade. @@ -568,39 +574,6 @@ class LiveTradingTestCase( if not found: self.fail("Could not find the trade in the list of open trades") - def create_complex_trade(self, direction, amount, symbol, tp_percent, sl_percent): - eur_usd_price = market.get_price(self.account, direction, symbol) - trade_tp = market.get_tp(direction, tp_percent, eur_usd_price) - trade_sl = market.get_sl(direction, sl_percent, eur_usd_price) - # trade_tsl = market.get_sl("buy", 1, eur_usd_price, return_var=True) - # # TP 1% profit - # trade_tp = eur_usd_price * D(1.01) - # # SL 2% loss - # trade_sl = eur_usd_price * D(0.98) - # # TSL 1% loss - # trade_tsl = eur_usd_price * D(0.99) - - trade_precision, display_precision = market.get_precision(self.account, symbol) - # Round everything to the display precision - - trade_tp = round(trade_tp, display_precision) - trade_sl = round(trade_sl, display_precision) - # trade_tsl = round(trade_tsl, display_precision) - - complex_trade = Trade.objects.create( - user=self.user, - account=self.account, - symbol=symbol, - time_in_force="FOK", - type="market", - amount=amount, - direction=direction, - take_profit=trade_tp, - stop_loss=trade_sl, - # trailing_stop_loss=trade_tsl, - ) - return complex_trade - @patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=100000) def test_check_risk_max_risk_pass(self, mock_balance): # SL of 19% on a 10000 trade on a 100000 account is 1.8 loss diff --git a/core/trading/checks.py b/core/trading/checks.py index 41fecb8..2b0fc80 100644 --- a/core/trading/checks.py +++ b/core/trading/checks.py @@ -46,34 +46,34 @@ def within_callback_price_deviation(strategy, price, current_price): def within_trends(strategy, symbol, direction): if strategy.trend_signals.exists(): - if len(strategy.trend_signals.all()) > 0: - if strategy.trends is None: - log.debug("Refusing to trade with no trend signals received") + if strategy.trends is None: + log.debug("Refusing to trade with no trend signals received") + sendmsg( + strategy.user, + f"Refusing to trade {symbol} with no trend signals received", + title="Trend not ready", + ) + return None + if symbol not in strategy.trends: + log.debug("Refusing to trade asset without established trend") + sendmsg( + strategy.user, + f"Refusing to trade {symbol} without established trend", + title="Trend not ready", + ) + return None + else: + if strategy.trends[symbol] != direction: + log.debug("Refusing to trade against the trend") sendmsg( strategy.user, - f"Refusing to trade {symbol} with no trend signals received", - title="Trend not ready", + f"Refusing to trade {symbol} against the trend", + title="Trend rejection", ) - return None - if symbol not in strategy.trends: - log.debug("Refusing to trade asset without established trend") - sendmsg( - strategy.user, - f"Refusing to trade {symbol} without established trend", - title="Trend not ready", - ) - return None + return False else: - if strategy.trends[symbol] != direction: - log.debug("Refusing to trade against the trend") - sendmsg( - strategy.user, - f"Refusing to trade {symbol} against the trend", - title="Trend rejection", - ) - return False - else: - log.debug(f"Trend check passed for {symbol} - {direction}") - return True - log.debug("No trend signals configured") - return True + log.debug(f"Trend check passed for {symbol} - {direction}") + return True + else: + log.debug("No trend signals configured") + return True