Write crossfilter, asset groups and max open trades implementation and tests

This commit is contained in:
Mark Veidemanis 2023-02-17 22:11:46 +00:00
parent 67117f0978
commit d262f208b5
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 587 additions and 82 deletions

View File

@ -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 initial_price = D(current_price) - pl_per_unit
else: else:
initial_price = D(current_price) + pl_per_unit initial_price = D(current_price) + pl_per_unit
# Get the percent change of the TP price from the initial price. # Get the percent change of the TP price from the initial price.
change_percent = ((initial_price - D(tp_price)) / initial_price) * 100 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): 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. 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) pl_per_unit = D(unrealised_pl) / D(current_units)
if side == "long": 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 change_percent = D(tp_percent) / 100
# Get the price of the TP percent from the initial price. # 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": 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: 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) 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 change_percent = D(sl_percent) / 100
# Get the price of the SL percent from the initial price. # 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": 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: 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) return round(sl_price, 5)
@ -270,6 +300,8 @@ def open_trade_to_unified_format(trade):
"id": trade["id"], "id": trade["id"],
"symbol": trade["symbol"], "symbol": trade["symbol"],
"amount": current_units, "amount": current_units,
# For crossfilter
"units": current_units,
"side": side, "side": side,
"direction": side_to_direction(side), "direction": side_to_direction(side),
"state": trade["state"], "state": trade["state"],

View File

@ -16,69 +16,104 @@ class CommonTestCase(TestCase):
""" """
Test that the TP price to percent conversion works for long trades. 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_price = 1.0
current_units = 1 current_units = 1
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_initial_short(self):
""" """
Test that the TP price to percent conversion works for short trades. 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_price = 1.0
current_units = 1 current_units = 1
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "short", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_long(self):
""" """
Test that the TP price to percent conversion works for long trades Test that the TP price to percent conversion works for long trades
when the price has changed. when the price has changed.
""" """
tp_price = 1.2 # 20% tp_price = D("1.2") # 20%
current_price = 1.1 # + 10% current_price = 1.1 # + 10%
current_units = 1 current_units = 1
unrealised_pl = 0.1 # 10% unrealised_pl = 0.1 # 10%
expected_percent = 20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_long_loss(self):
""" """
Test that the TP price to percent conversion works for long trades Test that the TP price to percent conversion works for long trades
when the price has changed and the TP is at a loss. 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_price = 0.9 # - 10%
current_units = 1 current_units = 1
unrealised_pl = -0.1 # -10% unrealised_pl = -0.1 # -10%
expected_percent = -20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_short_loss(self):
""" """
Test that the TP price to percent conversion works for short trades Test that the TP price to percent conversion works for short trades
when the price has changed and the TP is at a loss. 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_price = 1.1 # - 10%
current_units = 1 current_units = 1
unrealised_pl = -0.1 # -10% unrealised_pl = -0.1 # -10%
expected_percent = -20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "short", current_price, current_units, unrealised_pl 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 # For multiple units
def test_tp_price_to_percent_initial_long_multi(self): 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 Test that the TP price to percent conversion works for long trades
with multiple units. with multiple units.
""" """
tp_price = 1.1 # 10% tp_price = D("1.1") # 10%
current_price = 1.0 current_price = 1.0
current_units = 10 current_units = 10
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_initial_short_multi(self):
""" """
Test that the TP price to percent conversion works for short trades Test that the TP price to percent conversion works for short trades
with multiple units. with multiple units.
""" """
tp_price = 0.9 # 10% tp_price = D("0.9") # 10%
current_price = 1.0 current_price = 1.0
current_units = 10 current_units = 10
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "short", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_long_multi(self):
""" """
Test that the TP price to percent conversion works for long trades Test that the TP price to percent conversion works for long trades
when the price has changed, with multiple units. 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_price = 1.1 # +10%
current_units = 10 current_units = 10
unrealised_pl = 1 # 10% unrealised_pl = 1 # 10%
expected_percent = 20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_short_multi(self):
""" """
Test that the TP price to percent conversion works for short trades Test that the TP price to percent conversion works for short trades
when the price has changed, with multiple units. 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_price = 0.9 # -10%
current_units = 10 current_units = 10
unrealised_pl = 1 # 10% unrealised_pl = 1 # 10%
expected_percent = 20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "short", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_long_multi_loss(self):
""" """
Test that the TP price to percent conversion works for long trades 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. 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_price = 0.9 # -10%
current_units = 10 current_units = 10
unrealised_pl = -1 # -10% unrealised_pl = -1 # -10%
expected_percent = -20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "long", current_price, current_units, unrealised_pl 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): def test_tp_price_to_percent_change_short_multi_loss(self):
""" """
Test that the TP price to percent conversion works for short trades 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. 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_price = 1.1 # -10%
current_units = 10 current_units = 10
unrealised_pl = -1 # 10% unrealised_pl = -1 # 10%
expected_percent = -20
percent = tp_price_to_percent( percent = tp_price_to_percent(
tp_price, "short", current_price, current_units, unrealised_pl 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 # 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. 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_price = 1.0
current_units = 1 current_units = 1
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "long", current_price, current_units, unrealised_pl 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): def test_sl_price_to_percent_initial_short(self):
""" """
Test that the SL price to percent conversion works for short trades. 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_price = 1.0
current_units = 1 current_units = 1
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "short", current_price, current_units, unrealised_pl 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): def test_sl_price_to_percent_change_long_profit(self):
""" """
Test that the SL price to percent conversion works for long trades Test that the SL price to percent conversion works for long trades
when the price has changed and the SL is at a profit. 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_price = 1.1 # +10%
current_units = 1 current_units = 1
unrealised_pl = 0.1 # +10% unrealised_pl = 0.1 # +10%
expected_percent = -20
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "long", current_price, current_units, unrealised_pl 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): def test_sl_price_to_percent_change_short_profit(self):
""" """
Test that the SL price to percent conversion works for short trades Test that the SL price to percent conversion works for short trades
when the price has changed and the SL is at a profit. 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_price = 0.9 # +10%
current_units = 1 current_units = 1
unrealised_pl = 0.1 # +10% unrealised_pl = 0.1 # +10%
expected_percent = -20
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "short", current_price, current_units, unrealised_pl 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 # For multiple units
def test_sl_price_to_percent_initial_long_multi(self): 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 Test that the SL price to percent conversion works for long trades
with multiple units. with multiple units.
""" """
sl_price = 0.9 # -10% sl_price = D("0.9") # -10%
current_price = 1.0 current_price = 1.0
current_units = 10 current_units = 10
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 10
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "long", current_price, current_units, unrealised_pl 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): def test_sl_price_to_percent_initial_short_multi(self):
""" """
Test that the SL price to percent conversion works for short trades Test that the SL price to percent conversion works for short trades
with multiple units. with multiple units.
""" """
sl_price = 1.2 # -20% sl_price = D("1.2") # -20%
current_price = 1.0 current_price = 1.0
current_units = 10 current_units = 10
unrealised_pl = 0 unrealised_pl = 0
expected_percent = 20
percent = sl_price_to_percent( percent = sl_price_to_percent(
sl_price, "short", current_price, current_units, unrealised_pl 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): def test_sl_price_to_percent_change_long_multi_profit(self):
""" """
Test that the SL price to percent conversion works for long trades 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. 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_price = 1.1 # +10%
current_units = 10 current_units = 10
unrealised_pl = 1 # +10% unrealised_pl = 1 # +10%
@ -264,7 +383,7 @@ class CommonTestCase(TestCase):
) )
self.assertEqual(percent, expected_percent) self.assertEqual(percent, expected_percent)
self.assertEqual( self.assertEqual(
tp_percent_to_price( sl_percent_to_price(
expected_percent, "long", current_price, current_units, unrealised_pl expected_percent, "long", current_price, current_units, unrealised_pl
), ),
sl_price, sl_price,
@ -275,7 +394,7 @@ class CommonTestCase(TestCase):
Test that the SL price to percent conversion works for short trades 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. 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_price = 0.9 # +10%
current_units = 10 current_units = 10
unrealised_pl = 1 # +10% unrealised_pl = 1 # +10%
@ -285,7 +404,7 @@ class CommonTestCase(TestCase):
) )
self.assertEqual(percent, expected_percent) self.assertEqual(percent, expected_percent)
self.assertEqual( self.assertEqual(
tp_percent_to_price( sl_percent_to_price(
expected_percent, "short", current_price, current_units, unrealised_pl expected_percent, "short", current_price, current_units, unrealised_pl
), ),
sl_price, sl_price,

View File

@ -1,10 +1,19 @@
from decimal import Decimal as D
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from django.test import TestCase from django.test import TestCase
from core.exchanges.convert import convert_trades from core.exchanges.convert import convert_trades
from core.lib.schemas.oanda_s import parse_time 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.tests.helpers import StrategyMixin, SymbolPriceMock
from core.trading.active_management import ActiveManagement from core.trading.active_management import ActiveManagement
@ -56,17 +65,17 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase):
"dividendAdjustment": "0.0000", "dividendAdjustment": "0.0000",
"unrealizedPL": "-0.0008", "unrealizedPL": "-0.0008",
"marginUsed": "0.2966", "marginUsed": "0.2966",
"takeProfitOrder": {"price": "1.06331"}, "takeProfitOrder": {"price": "1.07934"},
"stopLossOrder": {"price": "1.06331"}, "stopLossOrder": {"price": "1.05276"},
"trailingStopLossOrder": {"price": "1.06331"}, "trailingStopLossOrder": None,
"trailingStopValue": None, "trailingStopValue": None,
"side": "long", "side": "long",
}, },
{ {
"id": "20083", "id": "20084",
"symbol": "EUR_USD", "symbol": "EUR_USD",
"price": "1.06331", "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", "initialUnits": "10",
"initialMarginRequired": "0.2966", "initialMarginRequired": "0.2966",
"state": "OPEN", "state": "OPEN",
@ -76,9 +85,9 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase):
"dividendAdjustment": "0.0000", "dividendAdjustment": "0.0000",
"unrealizedPL": "-0.0008", "unrealizedPL": "-0.0008",
"marginUsed": "0.2966", "marginUsed": "0.2966",
"takeProfitOrder": {"price": "1.06331"}, "takeProfitOrder": {"price": "1.07934"},
"stopLossOrder": {"price": "1.06331"}, "stopLossOrder": {"price": "1.05276"},
"trailingStopLossOrder": {"price": "1.06331"}, "trailingStopLossOrder": None,
"trailingStopValue": None, "trailingStopValue": None,
"side": "long", "side": "long",
}, },
@ -90,6 +99,30 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase):
self.ams.get_balance = self.fake_get_balance self.ams.get_balance = self.fake_get_balance
# self.ams.trades = self.trades # 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): def fake_get_trades(self):
self.ams.trades = self.trades self.ams.trades = self.trades
return self.trades return self.trades
@ -211,14 +244,154 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase):
{"size": 50}, {"size": 50},
) )
def test_protection_violated(self): @patch("core.trading.active_management.ActiveManagement.handle_violation")
pass 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): expected_args = {
pass "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): @patch("core.trading.active_management.ActiveManagement.handle_violation")
pass 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): def test_max_open_trades_per_symbol_violated(self):
pass pass
@ -228,6 +401,3 @@ class ActiveManagementTestCase(StrategyMixin, SymbolPriceMock, TestCase):
def test_max_risk_violated(self): def test_max_risk_violated(self):
pass pass
def test_crossfilter_violated(self):
pass

View File

@ -1,8 +1,15 @@
from datetime import datetime from datetime import datetime
from decimal import Decimal as D from decimal import Decimal as D
from core.exchanges.convert import convert_trades, side_to_direction import core.trading.market # to avoid messy circular import
from core.trading import checks 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 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): def check_protection(self, trade):
print("CHECK PROTECTION", trade)
deviation = D(0.05) # 5% deviation = D(0.05) # 5%
matches = { matches = {
@ -89,22 +95,187 @@ class ActiveManagement(object):
violations = {} violations = {}
for key, expected in matches.items(): for key, expected in matches.items():
if expected == 0:
continue
if key in trade: if key in trade:
actual = D(trade[key]) actual = D(trade[key])
if expected is None:
continue
expected = D(expected) expected = D(expected)
min_val = expected - (deviation * expected) min_val = expected - (deviation * expected)
max_val = expected + (deviation * expected) max_val = expected + (deviation * expected)
within_deviation = min_val <= actual <= max_val within_deviation = min_val <= actual <= max_val
if not within_deviation: else:
violations[key] = expected 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: if violations:
self.handle_violation( self.handle_violation(
"protection", self.policy.when_protection_violated, trade, violations "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): def run_checks(self):
converted_trades = convert_trades(self.get_trades()) converted_trades = convert_trades(self.get_trades())
for trade in converted_trades: for trade in converted_trades:
@ -112,6 +283,13 @@ class ActiveManagement(object):
self.check_trends(trade) self.check_trends(trade)
self.check_position_size(trade) self.check_position_size(trade)
self.check_protection(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 # Trading Time
# Max loss # Max loss

View File

@ -1,14 +1,14 @@
from core.models import AssetRule 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. Determine whether the trade is allowed according to the group.
See tests for examples. The logic requires trading knowledge. See tests for examples. The logic requires trading knowledge.
:param group: The group to check :param group: The group to check
:param base: The base currency :param base: The base currency
:param quote: The quote 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 # 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 # Always deny
return False return False
elif mapped_status == 3: elif mapped_status == 3:
if direction == "long": if side == "long":
return False return False
elif mapped_status == 2: elif mapped_status == 2:
if direction == "short": if side == "short":
return False return False
# If our quote has allowed == False, we can only long it, or short the base # 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() quote_rule = AssetRule.objects.filter(group=group, asset=quote).first()
@ -38,10 +38,10 @@ def get_allowed(group, base, quote, direction):
# Always deny # Always deny
return False return False
elif mapped_status == 3: elif mapped_status == 3:
if direction == "short": if side == "short":
return False return False
elif mapped_status == 2: elif mapped_status == 2:
if direction == "long": if side == "long":
return False return False
if not base_rule and not quote_rule: if not base_rule and not quote_rule:

View File

@ -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. Determine if we are betting against ourselves.
Checks open positions for the account, rejecting the trade if there is one 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: try:
# Only get the data we need # Only get the data we need
if func == "entry": if func == "entry":
all_positions = account.client.get_all_positions() if all_positions is None:
all_positions = account.client.get_all_positions()
else: 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: except GenericAPIError as e:
if "No position exists for the specified instrument" in str(e): if "No position exists for the specified instrument" in str(e):
log.debug("No position exists for this symbol") log.debug("No position exists for this symbol")

View File

@ -49,7 +49,7 @@ def check_max_open_trades(risk_model, account_trades):
return len(account_trades) < risk_model.max_open_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. 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(): for symbol, count in symbol_map.items():
if count >= risk_model.max_open_trades_per_symbol: if count >= risk_model.max_open_trades_per_symbol:
return False if yield_symbol:
return True yield symbol
else:
return False
if not yield_symbol:
return True
def check_risk(risk_model, account, proposed_trade): def check_risk(risk_model, account, proposed_trade):