From b818e7e3f5312457a6735f8e73e8304cbeab09db Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 13 Dec 2022 07:20:49 +0000 Subject: [PATCH] Write risk checking helpers and tests --- core/tests/trading/__init__.py | 0 core/tests/trading/test_risk.py | 191 ++++++++++++++++++++++++++++++++ core/trading/risk.py | 46 ++++++++ 3 files changed, 237 insertions(+) create mode 100644 core/tests/trading/__init__.py create mode 100644 core/tests/trading/test_risk.py create mode 100644 core/trading/risk.py diff --git a/core/tests/trading/__init__.py b/core/tests/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/tests/trading/test_risk.py b/core/tests/trading/test_risk.py new file mode 100644 index 0000000..3974e58 --- /dev/null +++ b/core/tests/trading/test_risk.py @@ -0,0 +1,191 @@ +from django.test import TestCase + +from core.models import RiskModel, User +from core.trading import risk + + +class RiskModelTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username="testuser", email="test@example.com", password="test" + ) + + self.risk_model = RiskModel.objects.create( + user=self.user, + name="Test Risk Model", + max_loss_percent=50, + max_risk_percent=10, + max_open_trades=3, + max_open_trades_per_symbol=2, + ) + self.account_initial_balance = 100 + self.trade = { + "symbol": "XXXYYY", + "side": "BUY", + # We already calculated the TP percent loss relative to the account size + "tp_percent": 9, + "sl_percent": 9, + "tsl_percent": 9, + } + + def test_check_max_loss(self): + """ + Check that we can open a trade within the max loss limit. + """ + account_balance = 100 # We have lost no money + allowed = risk.check_max_loss( + self.risk_model, self.account_initial_balance, account_balance + ) + self.assertTrue(allowed) + + def test_check_max_loss_fail_exact(self): + """ + Check that we cannot open a trade outside the max loss limit where the amount + lost is exactly the max loss limit. + """ + account_balance = 50 # We have lost 50% exactly + allowed = risk.check_max_loss( + self.risk_model, self.account_initial_balance, account_balance + ) + self.assertFalse(allowed) + + def test_check_max_loss_fail(self): + """ + Check that we cannot open a trade outside the max loss limit. + """ + account_balance = 49 # We have lost 51% + allowed = risk.check_max_loss( + self.risk_model, self.account_initial_balance, account_balance + ) + self.assertFalse(allowed) + + def test_check_max_risk(self): + """ + Check that we can open a trade within the max risk limit. + """ + account_trades = [self.trade] + allowed = risk.check_max_risk(self.risk_model, account_trades) + self.assertTrue(allowed) + + def test_check_max_risk_multiple_trades(self): + """ + Check that we can open a trade within the max risk limit where there are + multiple trades. + """ + trade = self.trade.copy() + trade["sl_percent"] = 1 + trade["tsl_percent"] = 1 + account_trades = [trade] * 9 + allowed = risk.check_max_risk(self.risk_model, account_trades) + self.assertTrue(allowed) + + def test_check_max_risk_fail_exact(self): + """ + Check that we cannot open a trade outside the max risk limit where the amount + risked is exactly the max risk limit. + """ + trade = self.trade.copy() + trade["sl_percent"] = 10 + account_trades = [trade] + allowed = risk.check_max_risk(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_risk_fail_exact_multiple_trades(self): + """ + Check that we cannot open a trade outside the max risk limit where the amount + risked is exactly the max risk limit with multiple trades. + """ + trade = self.trade.copy() + trade["sl_percent"] = 1 + trade["tsl_percent"] = 1 + account_trades = [trade] * 10 + allowed = risk.check_max_risk(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades(self): + """ + Check that we can open a trade within the max open trades limit. + """ + account_trades = [self.trade] * 2 + allowed = risk.check_max_open_trades(self.risk_model, account_trades) + self.assertTrue(allowed) + + def test_check_max_open_trades_fail_exact(self): + """ + Check that we cannot open a trade at the max open trades limit. + """ + account_trades = [self.trade] * 3 + allowed = risk.check_max_open_trades(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades_fail(self): + """ + Check that we cannot open a trade outside the max open trades limit. + """ + account_trades = [self.trade] * 4 + allowed = risk.check_max_open_trades(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades_per_symbol(self): + """ + Check that we can open a trade within the max open trades per symbol limit. + """ + account_trades = [self.trade] + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertTrue(allowed) + + def test_check_max_open_trades_per_symbol_fail_exact(self): + """ + Check that we cannot open a trade at the max open trades per symbol limit. + """ + account_trades = [self.trade] * 2 + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades_per_symbol_fail(self): + """ + Check that we cannot open a trade outside the max open trades per symbol limit. + """ + account_trades = [self.trade] * 3 + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades_per_symbol_different_symbols(self): + """ + Check that we can open a trade within the max open trades per symbol limit with + different symbols. + """ + trade1 = self.trade.copy() + trade2 = self.trade.copy() + trade1["symbol"] = "ONE" + trade2["symbol"] = "TWO" + account_trades = [trade1, trade2] + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertTrue(allowed) + + def test_check_max_open_trades_per_symbol_fail_exact_different_symbols(self): + """ + Check that we cannot open a trade at the max open trades per symbol limit with + different symbols. + """ + trade1 = self.trade.copy() + trade2 = self.trade.copy() + trade1["symbol"] = "ONE" + trade2["symbol"] = "TWO" + account_trades = [trade1, trade2, trade1, trade2] + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertFalse(allowed) + + def test_check_max_open_trades_per_symbol_fail_different_symbols(self): + """ + Check that we cannot open a trade outside the max open trades per symbol limit + with different symbols. + """ + trade1 = self.trade.copy() + trade2 = self.trade.copy() + trade1["symbol"] = "ONE" + trade2["symbol"] = "TWO" + # Each one 3 times + account_trades = [trade1, trade2, trade1, trade2, trade1, trade2] + allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) + self.assertFalse(allowed) diff --git a/core/trading/risk.py b/core/trading/risk.py new file mode 100644 index 0000000..59874b2 --- /dev/null +++ b/core/trading/risk.py @@ -0,0 +1,46 @@ +def check_max_loss(risk_model, initial_balance, account_balance): + """ + Check that the account balance is within the max loss limit. + """ + max_loss_percent = risk_model.max_loss_percent + max_loss = initial_balance * (max_loss_percent / 100) + return account_balance > max_loss + + +def check_max_risk(risk_model, account_trades): + """ + Check that all of the trades in the account are within the max risk limit. + """ + max_risk_percent = risk_model.max_risk_percent + total_risk = 0 + for trade in account_trades: + max_tmp = [] + if "sl_percent" in trade: + max_tmp.append(trade["sl_percent"]) + if "tsl_percent" in trade: + max_tmp.append(trade["tsl_percent"]) + total_risk += max(max_tmp) + return total_risk < max_risk_percent + + +def check_max_open_trades(risk_model, account_trades): + """ + Check that the number of trades in the account is within the max open trades limit. + """ + return len(account_trades) < risk_model.max_open_trades + + +def check_max_open_trades_per_symbol(risk_model, account_trades): + """ + Check we cannot open more trades per symbol than permissible. + """ + symbol_map = {} + for trade in account_trades: + symbol = trade["symbol"] + if symbol not in symbol_map: + symbol_map[symbol] = 0 + symbol_map[symbol] += 1 + for symbol, count in symbol_map.items(): + if count >= risk_model.max_open_trades_per_symbol: + return False + return True