From 93be9e6ffe580b75b44fc15e5d00a0df4c93940d Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Fri, 6 Jan 2023 07:20:55 +0000 Subject: [PATCH] Check the max risk relative to the account balance --- core/exchanges/common.py | 19 ++++--- core/tests/trading/test_risk.py | 95 +++++++++++++++++++++++++-------- core/trading/market.py | 44 +++++++++++++++ core/trading/risk.py | 22 ++++---- 4 files changed, 136 insertions(+), 44 deletions(-) diff --git a/core/exchanges/common.py b/core/exchanges/common.py index a25bf95..8d2126b 100644 --- a/core/exchanges/common.py +++ b/core/exchanges/common.py @@ -1,7 +1,6 @@ from decimal import Decimal as D from core.lib.elastic import store_msg -from core.trading.market import to_currency def get_balance_hook(user_id, user_name, account_id, account_name, balance): @@ -89,9 +88,6 @@ def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised else: profit = False - print("CHANGE PERCENT: ", change_percent) - print("PROFIT", profit) - if profit: change_percent = 0 - abs(change_percent) else: @@ -120,6 +116,15 @@ def convert_open_trades(open_trades): "pl": unrealised_pl, } + # Add some extra fields, sometimes we have already looked up the + # prices and don't need to call convert_trades_to_usd + if "take_profit_usd" in trade: + cast["take_profit_usd"] = trade["take_profit_usd"] + if "stop_loss_usd" in trade: + cast["stop_loss_usd"] = trade["stop_loss_usd"] + if "trailing_stop_loss_usd" in trade: + cast["trailing_stop_loss_usd"] = trade["trailing_stop_loss_usd"] + if "takeProfitOrder" in trade: if trade["takeProfitOrder"]: take_profit = trade["takeProfitOrder"]["price"] @@ -156,9 +161,3 @@ def convert_open_trades(open_trades): trades.append(cast) return trades - - -def convert_trades_to_usd(account, trades): - """ - Convert a list of trades to USD. - """ diff --git a/core/tests/trading/test_risk.py b/core/tests/trading/test_risk.py index 6f23d1b..170e71c 100644 --- a/core/tests/trading/test_risk.py +++ b/core/tests/trading/test_risk.py @@ -1,6 +1,6 @@ from django.test import TestCase -from core.exchanges.common import convert_open_trades +from core.exchanges import common from core.models import RiskModel, User from core.trading import risk @@ -25,8 +25,11 @@ class RiskModelTestCase(TestCase): "side": "BUY", # We already calculated the TP percent loss relative to the account size "take_profit_percent": 9, + "take_profit_usd": 9, "stop_loss_percent": 9, + "stop_loss_usd": 9, "trailing_stop_loss_percent": 9, + "trailing_stop_loss_usd": 9, } def test_check_max_loss(self): @@ -65,7 +68,9 @@ class RiskModelTestCase(TestCase): 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) + allowed = risk.check_max_risk( + self.risk_model, self.account_initial_balance, account_trades + ) self.assertTrue(allowed) def test_check_max_risk_multiple_trades(self): @@ -76,8 +81,12 @@ class RiskModelTestCase(TestCase): trade = self.trade.copy() trade["stop_loss_percent"] = 1 trade["trailing_stop_loss_percent"] = 1 + trade["stop_loss_usd"] = 1 + trade["trailing_stop_loss_usd"] = 1 account_trades = [trade] * 9 - allowed = risk.check_max_risk(self.risk_model, account_trades) + allowed = risk.check_max_risk( + self.risk_model, self.account_initial_balance, account_trades + ) self.assertTrue(allowed) def test_check_max_risk_fail_exact(self): @@ -87,8 +96,13 @@ class RiskModelTestCase(TestCase): """ trade = self.trade.copy() trade["stop_loss_percent"] = 10 + trade["trailing_stop_loss_percent"] = 10 + trade["stop_loss_usd"] = 10 + trade["trailing_stop_loss_usd"] = 10 account_trades = [trade] - allowed = risk.check_max_risk(self.risk_model, account_trades) + allowed = risk.check_max_risk( + self.risk_model, self.account_initial_balance, account_trades + ) self.assertFalse(allowed) def test_check_max_risk_fail_exact_multiple_trades(self): @@ -99,8 +113,12 @@ class RiskModelTestCase(TestCase): trade = self.trade.copy() trade["stop_loss_percent"] = 1 trade["trailing_stop_loss_percent"] = 1 + trade["stop_loss_usd"] = 1 + trade["trailing_stop_loss_usd"] = 1 account_trades = [trade] * 10 - allowed = risk.check_max_risk(self.risk_model, account_trades) + allowed = risk.check_max_risk( + self.risk_model, self.account_initial_balance, account_trades + ) self.assertFalse(allowed) def test_check_max_open_trades(self): @@ -191,7 +209,8 @@ class RiskModelTestCase(TestCase): allowed = risk.check_max_open_trades_per_symbol(self.risk_model, account_trades) self.assertFalse(allowed) - def check_max_risk_market_data(self): + # Market data tests, account size: $1000 + def test_check_max_risk_market_data(self): """ Check that we can open a trade within the max risk limit with market data. """ @@ -206,13 +225,16 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.95, # down by 5%, 5% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 50, # 5% of $1000 } - converted = convert_open_trades([trade]) + converted = common.convert_open_trades([trade]) self.assertEqual(converted[0]["stop_loss_percent"], 5) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + self.assertEqual(converted[0]["stop_loss_usd"], 50) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertTrue(max_risk_check) # 5% risk is fine - def check_max_risk_market_data_multiple(self): + def test_check_max_risk_market_data_multiple(self): """ Check that we can open a trade within the max risk limit with market data and multiple trades. @@ -228,12 +250,16 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.96, # down by 4%, 4% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 40, # 4% of $1000 } - converted = convert_open_trades([trade, trade]) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + converted = common.convert_open_trades([trade, trade]) + self.assertEqual(converted[0]["stop_loss_percent"], 4) + self.assertEqual(converted[0]["stop_loss_usd"], 40) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertTrue(max_risk_check) # 8% risk is fine - def check_max_risk_market_data_fail(self): + def test_check_max_risk_market_data_fail(self): """ Check that we can not open a trade outside the max risk limit with market data. """ @@ -248,13 +274,16 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.9, # down by 10%, 10% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 100, # 10% of $1000 } - converted = convert_open_trades([trade]) + converted = common.convert_open_trades([trade]) self.assertEqual(converted[0]["stop_loss_percent"], 10) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + self.assertEqual(converted[0]["stop_loss_usd"], 100) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertFalse(max_risk_check) # 10% risk is too much - def check_max_risk_market_data_fail_multiple(self): + def test_check_max_risk_market_data_fail_multiple(self): """ Check that we can not open a trade outside the max risk limit with market data and multiple trades. @@ -270,12 +299,18 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.95, # down by 5%, 5% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 50, # 5% of $1000 } - converted = convert_open_trades([trade, trade]) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + converted = common.convert_open_trades([trade, trade]) + self.assertEqual(converted[0]["stop_loss_percent"], 5) + self.assertEqual(converted[0]["stop_loss_usd"], 50) + self.assertEqual(converted[1]["stop_loss_percent"], 5) + self.assertEqual(converted[1]["stop_loss_usd"], 50) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertFalse(max_risk_check) # 10% risk is too much - def check_max_risk_market_data_fail_multiple_mixed(self): + def test_check_max_risk_market_data_fail_multiple_mixed(self): """ Check that we can not open a trade outside the max risk limit with market data and multiple trades, mixing SL and TSL. @@ -291,16 +326,23 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.95, # down by 5%, 5% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 50, # 5% of $1000 + "trailing_stop_loss_usd": 50, } trade2 = trade.copy() trade2["trailingStopLossOrder"] = {"price": 0.95} del trade2["stopLossOrder"] - converted = convert_open_trades([trade, trade2]) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + converted = common.convert_open_trades([trade, trade2]) + self.assertEqual(converted[0]["stop_loss_percent"], 5) + self.assertEqual(converted[0]["stop_loss_usd"], 50) + self.assertEqual(converted[1]["trailing_stop_loss_percent"], 5) + self.assertEqual(converted[1]["trailing_stop_loss_usd"], 50) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertFalse(max_risk_check) # 10% risk is too much - def check_max_risk_market_data_fail_multiple_mixed_both(self): + def test_check_max_risk_market_data_fail_multiple_mixed_both(self): """ Check that we can not open a trade outside the max risk limit with market data and multiple trades, mixing SL and TSL, where both are set. @@ -316,10 +358,17 @@ class RiskModelTestCase(TestCase): "stopLossOrder": { "price": 0.95, # down by 5%, 5% risk }, + # Hardcoded prices to avoid calling market API here + "stop_loss_usd": 50, # 5% of $1000 + "trailing_stop_loss_usd": 49, } trade2 = trade.copy() trade2["trailingStopLossOrder"] = {"price": 0.951} - converted = convert_open_trades([trade, trade2]) - max_risk_check = risk.check_max_risk(self.risk_model, converted) + converted = common.convert_open_trades([trade, trade2]) + self.assertEqual(converted[0]["stop_loss_percent"], 5) + self.assertEqual(converted[0]["stop_loss_usd"], 50) + self.assertEqual(float(converted[1]["trailing_stop_loss_percent"]), 4.9) + self.assertEqual(converted[1]["trailing_stop_loss_usd"], 49) + max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted) self.assertFalse(max_risk_check) # 10% risk is too much diff --git a/core/trading/market.py b/core/trading/market.py index e579178..1f28f0f 100644 --- a/core/trading/market.py +++ b/core/trading/market.py @@ -10,6 +10,35 @@ from core.util import logs log = logs.get_logger(__name__) +def side_to_direction(side): + """ + Convert a side to a direction. + """ + if side == "long": + return "buy" + elif side == "short": + return "sell" + else: + return False + + +def convert_trades_to_usd(account, trades): + """ + Convert a list of trades to USD. + """ + for trade in trades: + amount = trade["amount"] + symbol = trade["symbol"] + side = trade["side"] + direction = side_to_direction(side) + base, quote = get_base_quote(account.exchange, symbol) + print("BASE", base) + print("QUOTE", quote) + print("AMOUNT", amount) + amount_usd = to_currency(direction, account, amount, quote, "USD") + print("AMOUNT USD", amount_usd) + + def get_pair(account, base, quote, invert=False): """ Get the pair for the given account and currencies. @@ -36,6 +65,21 @@ def get_pair(account, base, quote, invert=False): return symbol +def get_base_quote(exchange, symbol): + """ + Get the base and quote currencies from a symbol. + :param exchange: Exchange name + :param symbol: Symbol + :return: Tuple of base and quote currencies + """ + if exchange == "alpaca": + separator = "/" + elif exchange == "oanda": + separator = "_" + base, quote = symbol.split(separator) + return (base, quote) + + def to_currency(direction, account, amount, from_currency, to_currency): """ Convert an amount from one currency to another. diff --git a/core/trading/risk.py b/core/trading/risk.py index 16c81d2..2cbf002 100644 --- a/core/trading/risk.py +++ b/core/trading/risk.py @@ -1,6 +1,3 @@ -from core.trading.market import to_currency - - def check_max_loss(risk_model, initial_balance, account_balance): """ Check that the account balance is within the max loss limit. @@ -10,23 +7,26 @@ def check_max_loss(risk_model, initial_balance, account_balance): return account_balance > max_loss -def check_max_risk(risk_model, account_trades): +def check_max_risk(risk_model, account_balance_usd, 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 + # Calculate the max risk of the account in USD + max_risk_usd = account_balance_usd * (max_risk_percent / 100) total_risk = 0 for trade in account_trades: max_tmp = [] # Need to calculate the max risk in base account currency - # Percentages relate to the price movement, without accounting the size of the trade - if "stop_loss_percent" in trade: - max_tmp.append(trade["stop_loss_percent"]) - if "trailing_stop_loss_percent" in trade: - max_tmp.append(trade["trailing_stop_loss_percent"]) + # Percentages relate to the price movement, without accounting the + # size of the trade + if "stop_loss_usd" in trade: + max_tmp.append(trade["stop_loss_usd"]) + if "trailing_stop_loss_usd" in trade: + max_tmp.append(trade["trailing_stop_loss_usd"]) total_risk += max(max_tmp) - print("Total risk: ", total_risk) - return total_risk < max_risk_percent + allowed = total_risk < max_risk_usd + return allowed def check_max_open_trades(risk_model, account_trades):