From d7e81dedb27d37e88dfedde5d74b5540e30e0db7 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Tue, 15 Nov 2022 07:20:17 +0000 Subject: [PATCH] Implement trailing stop loss --- core/exchanges/oanda.py | 5 + core/forms.py | 2 + core/lib/market.py | 112 +++++++++++------- ...egy_trailing_stop_loss_percent_and_more.py | 23 ++++ core/models.py | 2 + 5 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 core/migrations/0027_strategy_trailing_stop_loss_percent_and_more.py diff --git a/core/exchanges/oanda.py b/core/exchanges/oanda.py index a18dd2b..9a53b02 100644 --- a/core/exchanges/oanda.py +++ b/core/exchanges/oanda.py @@ -66,6 +66,11 @@ class OANDAExchange(BaseExchange): data["order"]["price"] = str(trade.price) elif trade.type == "market": data["order"]["priceBound"] = str(trade.price) + if trade.trailing_stop_loss is not None: + data["order"]["trailingStopLossOnFill"] = { + "distance": str(trade.trailing_stop_loss), + "timeInForce": "GTC", + } r = orders.OrderCreate(self.account_id, data=data) response = self.call(r) trade.response = response diff --git a/core/forms.py b/core/forms.py index d5b2db0..9316c78 100644 --- a/core/forms.py +++ b/core/forms.py @@ -70,6 +70,7 @@ class StrategyForm(ModelForm): "enabled", "take_profit_percent", "stop_loss_percent", + "trailing_stop_loss_percent", "price_slippage_percent", "callback_price_deviation_percent", "trade_size_percent", @@ -91,6 +92,7 @@ class TradeForm(ModelForm): "amount", "price", "stop_loss", + "trailing_stop_loss", "take_profit", "direction", ) diff --git a/core/lib/market.py b/core/lib/market.py index 7c07b54..bafc4a2 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -138,6 +138,56 @@ def get_trade_size_in_base(direction, account, strategy, cash_balance, base): return trade_size_in_base +def get_tp(direction, take_profit_percent, price): + """ + Get the take profit price. + :param direction: Direction of the trade + :param strategy: Strategy object + :param price: Entry price + """ + # Convert to ratio + take_profit_as_ratio = D(take_profit_percent) / D(100) + log.debug(f"Take profit as ratio: {take_profit_as_ratio}") + + take_profit_var = D(price) * D(take_profit_as_ratio) + log.debug(f"Take profit var: {take_profit_var}") + + if direction == "buy": + take_profit = D(price) + D(take_profit_var) + elif direction == "sell": + take_profit = D(price) - D(take_profit_var) + + log.debug(f"Take profit: {take_profit}") + return take_profit + + +def get_sl(direction, stop_loss_percent, price, return_var=False): + """ + Get the stop loss price. + Also used for trailing stop loss. + :param direction: Direction of the trade + :param strategy: Strategy object + :param price: Entry price + """ + # Convert to ratio + stop_loss_as_ratio = D(stop_loss_percent) / D(100) + log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}") + + stop_loss_var = D(price) * D(stop_loss_as_ratio) + log.debug(f"Stop loss var: {stop_loss_var}") + + if return_var: + return stop_loss_var + + if direction == "buy": + stop_loss = D(price) - D(stop_loss_var) + elif direction == "sell": + stop_loss = D(price) + D(stop_loss_var) + + log.debug(f"Stop loss: {stop_loss}") + return stop_loss + + def get_tp_sl(direction, strategy, price): """ Get the take profit and stop loss prices. @@ -146,38 +196,18 @@ def get_tp_sl(direction, strategy, price): :param price: Price of the trade :return: Take profit and stop loss prices """ + take_profit = get_tp(direction, strategy.take_profit_percent, price) + stop_loss = get_sl(direction, strategy.stop_loss_percent, price) + cast = {"tp": take_profit, "sl": stop_loss} - # Convert TP and SL to ratios - stop_loss_as_ratio = D(strategy.stop_loss_percent) / D(100) - take_profit_as_ratio = D(strategy.take_profit_percent) / D(100) - log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}") - log.debug(f"Take profit as ratio: {take_profit_as_ratio}") + # Look up the TSL if required by the strategy + if strategy.trailing_stop_loss_percent: + trailing_stop_loss = get_sl( + direction, strategy.trailing_stop_loss_percent, price, return_var=True + ) + cast["tsl"] = trailing_stop_loss - # Calculate the TP and SL prices by multiplying with the price - stop_loss_var = D(price) * D(stop_loss_as_ratio) - take_profit_var = D(price) * D(take_profit_as_ratio) - log.debug(f"Stop loss var: {stop_loss_var}") - log.debug(f"Take profit var: {take_profit_var}") - - # Flip addition operators for inverse trade directions - # * We need to subtract the SL for buys, since we are losing money if - # the price goes down - # * We need to add the TP for buys, since we are gaining money if - # the price goes up - # * We need to add the SL for sells, since we are losing money if - # the price goes up - # * We need to subtract the TP for sells, since we are gaining money if - # the price goes down - if direction == "buy": - stop_loss = D(price) - D(stop_loss_var) - take_profit = D(price) + D(take_profit_var) - elif direction == "sell": - stop_loss = D(price) + D(stop_loss_var) - take_profit = D(price) - D(take_profit_var) - log.debug(f"Stop loss: {stop_loss}") - log.debug(f"Take profit: {take_profit}") - - return (stop_loss, take_profit) + return cast def get_price_bound(direction, strategy, price, current_price): @@ -306,8 +336,13 @@ def execute_strategy(callback, strategy): direction, account, strategy, cash_balance, base ) - # Calculate TP/SL - stop_loss, take_profit = get_tp_sl(direction, strategy, price) + # Calculate TP/SL/TSL + protection = get_tp_sl(direction, strategy, current_price) + stop_loss = protection["sl"] + take_profit = protection["tp"] + trailing_stop_loss = None + if "tsl" in protection: + trailing_stop_loss = protection["tsl"] # Calculate price bound and round to the display precision price_bound = get_price_bound(direction, strategy, price, current_price) @@ -315,14 +350,6 @@ def execute_strategy(callback, strategy): return price_bound = round(price_bound, display_precision) - # # Use the price reported by the callback for limit orders - # if type == "limit": - # price_for_trade = price - - # # Use the price bound for market orders - # elif type == "market": - # price_for_trade = price_bound - # Create object, note that the amount is rounded to the trade precision new_trade = Trade.objects.create( user=user, @@ -339,6 +366,11 @@ def execute_strategy(callback, strategy): take_profit=float(round(take_profit, display_precision)), direction=direction, ) + # Add TSL if applicable + if trailing_stop_loss: + new_trade.trailing_stop_loss = float( + round(trailing_stop_loss, display_precision) + ) new_trade.save() info = new_trade.post() log.debug(f"Posted trade: {info}") diff --git a/core/migrations/0027_strategy_trailing_stop_loss_percent_and_more.py b/core/migrations/0027_strategy_trailing_stop_loss_percent_and_more.py new file mode 100644 index 0000000..ce78040 --- /dev/null +++ b/core/migrations/0027_strategy_trailing_stop_loss_percent_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-15 15:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0026_trade_time_in_force'), + ] + + operations = [ + migrations.AddField( + model_name='strategy', + name='trailing_stop_loss_percent', + field=models.FloatField(blank=True, default=1.0, null=True), + ), + migrations.AddField( + model_name='trade', + name='trailing_stop_loss', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index bea1a86..c110993 100644 --- a/core/models.py +++ b/core/models.py @@ -175,6 +175,7 @@ class Trade(models.Model): amount_usd = models.FloatField(null=True, blank=True) price = models.FloatField(null=True, blank=True) stop_loss = models.FloatField(null=True, blank=True) + trailing_stop_loss = models.FloatField(null=True, blank=True) take_profit = models.FloatField(null=True, blank=True) status = models.CharField(max_length=255, null=True, blank=True) direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255) @@ -224,6 +225,7 @@ class Strategy(models.Model): enabled = models.BooleanField(default=False) take_profit_percent = models.FloatField(default=1.5) stop_loss_percent = models.FloatField(default=1.0) + trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True) price_slippage_percent = models.FloatField(default=2.5) callback_price_deviation_percent = models.FloatField(default=0.5) trade_size_percent = models.FloatField(default=0.5)