From db870c39c69019926d2a7ef23fa7b27ee350e107 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Thu, 5 Jan 2023 23:37:50 +0000 Subject: [PATCH] Fix TP/SL calculation and make more tests for profit/loss --- core/exchanges/common.py | 80 ++++++++++----- core/tests/exchanges/test_common.py | 148 ++++++++++++++++++---------- core/tests/trading/test_live.py | 23 +++-- 3 files changed, 170 insertions(+), 81 deletions(-) diff --git a/core/exchanges/common.py b/core/exchanges/common.py index 9086da0..e86dc40 100644 --- a/core/exchanges/common.py +++ b/core/exchanges/common.py @@ -20,34 +20,65 @@ def get_balance_hook(user_id, user_name, account_id, account_name, balance): ) -def tp_price_to_percent(tp_price, current_price, current_units, unrealised_pl): - # Is this right? +def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised_pl): + """ + Determine the percent change of the TP price from the initial price. + Positive values indicate a profit, negative values indicate a loss. + """ pl_per_unit = D(unrealised_pl) / D(current_units) - print("pl_per_unit: ", pl_per_unit) - initial_price = D(current_price) - pl_per_unit - print("initial_price: ", initial_price) + if side == "long": + 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 = ((D(tp_price) - initial_price) / initial_price) * 100 - print("change_percent: ", change_percent) + change_percent = ((initial_price - D(tp_price)) / initial_price) * 100 - # Doesn't check direction - return abs(round(change_percent, 5)) + # If the trade is long, the TP price will be lower than the initial price. + if side == "long": + change_percent *= -1 + + return round(change_percent, 5) -def sl_price_to_percent(sl_price, current_price, current_units, unrealised_pl): - # Is this right? +def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised_pl): + """ + Determine the percent change of the SL price from the initial price. + Positive values indicate a loss, negative values indicate a profit. + This may seem backwards, but it is important to note that by default, + SL indicates a loss, and positive values should be expected. + Negative values indicate a negative loss, so a profit. + """ pl_per_unit = D(unrealised_pl) / D(current_units) - print("pl_per_unit: ", pl_per_unit) - initial_price = D(current_price) - pl_per_unit - print("initial_price: ", initial_price) + if side == "long": + initial_price = D(current_price) - pl_per_unit + else: + initial_price = D(current_price) + pl_per_unit + + # initial_price = D(current_price) - pl_per_unit # Get the percent change of the SL price from the initial price. - change_percent = ((D(sl_price) - initial_price) / initial_price) * 100 - print("change_percent: ", change_percent) + change_percent = ((initial_price - D(sl_price)) / initial_price) * 100 - # Doesn't check direction - return abs(round(change_percent, 5)) + # If the trade is long, the SL price will be higher than the initial price. + if side == "long": + change_percent *= -1 + + if side == "long": + if current_price > initial_price: + profit = True + else: + profit = False + else: + if current_price < initial_price: + profit = True + else: + profit = False + + if profit: + change_percent *= -1 + + return round(change_percent, 5) def convert_open_trades(open_trades): @@ -59,11 +90,12 @@ def convert_open_trades(open_trades): current_price = trade["price"] current_units = trade["currentUnits"] unrealised_pl = trade["unrealizedPL"] + side = trade["side"] cast = { "id": trade["id"], "symbol": trade["symbol"], "amount": current_units, - "side": trade["side"], + "side": side, "state": trade["state"], "price": current_price, "pl": unrealised_pl, @@ -73,7 +105,7 @@ def convert_open_trades(open_trades): if trade["takeProfitOrder"]: take_profit = trade["takeProfitOrder"]["price"] take_profit_percent = tp_price_to_percent( - take_profit, current_price, current_units, unrealised_pl + take_profit, side, current_price, current_units, unrealised_pl ) cast["take_profit"] = take_profit @@ -83,7 +115,7 @@ def convert_open_trades(open_trades): if trade["stopLossOrder"]: stop_loss = trade["stopLossOrder"]["price"] stop_loss_percent = sl_price_to_percent( - stop_loss, current_price, current_units, unrealised_pl + stop_loss, side, current_price, current_units, unrealised_pl ) cast["stop_loss"] = stop_loss @@ -93,7 +125,11 @@ def convert_open_trades(open_trades): if trade["trailingStopLossOrder"]: trailing_stop_loss = trade["trailingStopLossOrder"]["price"] trailing_stop_loss_percent = sl_price_to_percent( - trailing_stop_loss, current_price, current_units, unrealised_pl + trailing_stop_loss, + side, + current_price, + current_units, + unrealised_pl, ) cast["trailing_stop_loss"] = trailing_stop_loss diff --git a/core/tests/exchanges/test_common.py b/core/tests/exchanges/test_common.py index 1ab2d7d..8ce583e 100644 --- a/core/tests/exchanges/test_common.py +++ b/core/tests/exchanges/test_common.py @@ -14,7 +14,7 @@ class CommonTestCase(TestCase): current_units = 1 unrealised_pl = 0 percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -27,7 +27,7 @@ class CommonTestCase(TestCase): current_units = 1 unrealised_pl = 0 percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "short", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -41,23 +41,37 @@ class CommonTestCase(TestCase): current_units = 1 unrealised_pl = 0.1 # 10% percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 20) - def test_tp_price_to_percent_change_short(self): + 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% + current_price = 0.9 # - 10% + current_units = 1 + unrealised_pl = -0.1 # -10% + percent = tp_price_to_percent( + tp_price, "long", current_price, current_units, unrealised_pl + ) + self.assertEqual(percent, -20) + + 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. + when the price has changed and the TP is at a loss. """ - tp_price = 0.8 # 20% - current_price = 1.1 # + 10% + tp_price = 1.2 # -20% + current_price = 1.1 # - 10% current_units = 1 - unrealised_pl = 0.1 # 10% + unrealised_pl = -0.1 # -10% percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, -20) # For multiple units def test_tp_price_to_percent_initial_long_multi(self): @@ -70,7 +84,7 @@ class CommonTestCase(TestCase): current_units = 10 unrealised_pl = 0 percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -84,7 +98,7 @@ class CommonTestCase(TestCase): current_units = 10 unrealised_pl = 0 percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "short", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -93,12 +107,12 @@ class CommonTestCase(TestCase): 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% - current_price = 1.1 # + 10% + tp_price = 1.2 # +20% + current_price = 1.1 # +10% current_units = 10 unrealised_pl = 1 # 10% percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 20) @@ -107,17 +121,45 @@ class CommonTestCase(TestCase): 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% - current_price = 1.1 # + 10% + tp_price = 0.8 # -20% + current_price = 0.9 # -10% current_units = 10 unrealised_pl = 1 # 10% percent = tp_price_to_percent( - tp_price, current_price, current_units, unrealised_pl + tp_price, "short", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 20) + 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% + current_price = 0.9 # -10% + current_units = 10 + unrealised_pl = -1 # -10% + percent = tp_price_to_percent( + tp_price, "long", current_price, current_units, unrealised_pl + ) + self.assertEqual(percent, -20) + + 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% + current_price = 1.1 # -10% + current_units = 10 + unrealised_pl = -1 # 10% + percent = tp_price_to_percent( + tp_price, "short", current_price, current_units, unrealised_pl + ) + self.assertEqual(percent, -20) + # SL - def test_SL_price_to_percent_initial_long(self): + def test_sl_price_to_percent_initial_long(self): """ Test that the SL price to percent conversion works for long trades. """ @@ -126,7 +168,7 @@ class CommonTestCase(TestCase): current_units = 1 unrealised_pl = 0 percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -139,37 +181,37 @@ class CommonTestCase(TestCase): current_units = 1 unrealised_pl = 0 percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "short", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) - def test_sl_price_to_percent_change_long(self): + 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. + when the price has changed and the SL is at a profit. """ - sl_price = 1.2 # 20% - current_price = 1.1 # + 10% + sl_price = 1.2 # +20% + current_price = 1.1 # +10% current_units = 1 - unrealised_pl = 0.1 # 10% + unrealised_pl = 0.1 # +10% percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, -20) - def test_sl_price_to_percent_change_short(self): + 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. + when the price has changed and the SL is at a profit. """ - sl_price = 0.8 # 20% - current_price = 1.1 # + 10% + sl_price = 0.8 # +20% + current_price = 0.9 # +10% current_units = 1 - unrealised_pl = 0.1 # 10% + unrealised_pl = 0.1 # +10% percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, -20) # For multiple units def test_sl_price_to_percent_initial_long_multi(self): @@ -177,12 +219,12 @@ class CommonTestCase(TestCase): Test that the SL price to percent conversion works for long trades with multiple units. """ - sl_price = 1.1 # 10% + sl_price = 1.1 # -10% current_price = 1.0 current_units = 10 unrealised_pl = 0 percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "long", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) @@ -191,39 +233,39 @@ class CommonTestCase(TestCase): Test that the SL price to percent conversion works for short trades with multiple units. """ - sl_price = 0.9 # 10% + sl_price = 0.9 # -10% current_price = 1.0 current_units = 10 unrealised_pl = 0 percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "short", current_price, current_units, unrealised_pl ) self.assertEqual(percent, 10) - def test_sl_price_to_percent_change_long_multi(self): + 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. + when the price has changed, with multiple units, and the SL is at a profit. """ - sl_price = 1.2 # 20% - current_price = 1.1 # + 10% + sl_price = 1.2 # +20% + current_price = 1.1 # +10% current_units = 10 - unrealised_pl = 1 # 10% + unrealised_pl = 1 # +10% percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "long", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, -20) - def test_sl_price_to_percent_change_short_multi(self): + def test_sl_price_to_percent_change_short_multi_profit(self): """ Test that the SL price to percent conversion works for short trades - when the price has changed, with multiple units. + when the price has changed, with multiple units, and the SL is at a profit. """ - sl_price = 0.8 # 20% - current_price = 1.1 # + 10% + sl_price = 0.8 # -20% + current_price = 0.9 # +10% current_units = 10 - unrealised_pl = 1 # 10% + unrealised_pl = 1 # +10% percent = sl_price_to_percent( - sl_price, current_price, current_units, unrealised_pl + sl_price, "short", current_price, current_units, unrealised_pl ) - self.assertEqual(percent, 20) + self.assertEqual(percent, -20) diff --git a/core/tests/trading/test_live.py b/core/tests/trading/test_live.py index 6a4b905..714b67d 100644 --- a/core/tests/trading/test_live.py +++ b/core/tests/trading/test_live.py @@ -1,3 +1,5 @@ +from decimal import Decimal as D + from django.test import TestCase from core.exchanges.common import convert_open_trades @@ -18,6 +20,7 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase): amount=10, direction="buy", ) + self.commission = 0.025 def test_account_functional(self): """ @@ -123,11 +126,19 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase): # trailing_stop_loss=trade_tsl, ) - posted = self.open_trade(complex_trade) - print("OPENED", posted) + self.open_trade(complex_trade) trades = self.account.client.get_all_open_trades() - print("TRADES", trades) trades_converted = convert_open_trades(trades["itemlist"]) - print("TRADES CONVERTED", trades_converted) - closed = self.close_trade(complex_trade) - print("CLOSED", closed) + self.assertEqual(len(trades_converted), 1) + expected_tp_percent = D(1 - self.commission) + expected_sl_percent = D(2 - self.commission) + actual_tp_percent = trades_converted[0]["take_profit_percent"] + actual_sl_percent = trades_converted[0]["stop_loss_percent"] + + tp_percent_difference = abs(expected_tp_percent - actual_tp_percent) + sl_percent_difference = abs(expected_sl_percent - actual_sl_percent) + max_difference = D(0.08) # depends on market conditions + + self.assertLess(tp_percent_difference, max_difference) + self.assertLess(sl_percent_difference, max_difference) + self.close_trade(complex_trade)