Implement trailing stop loss

This commit is contained in:
Mark Veidemanis 2022-11-15 07:20:17 +00:00
parent 5c68191e5b
commit d7e81dedb2
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
5 changed files with 104 additions and 40 deletions

View File

@ -66,6 +66,11 @@ class OANDAExchange(BaseExchange):
data["order"]["price"] = str(trade.price) data["order"]["price"] = str(trade.price)
elif trade.type == "market": elif trade.type == "market":
data["order"]["priceBound"] = str(trade.price) 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) r = orders.OrderCreate(self.account_id, data=data)
response = self.call(r) response = self.call(r)
trade.response = response trade.response = response

View File

@ -70,6 +70,7 @@ class StrategyForm(ModelForm):
"enabled", "enabled",
"take_profit_percent", "take_profit_percent",
"stop_loss_percent", "stop_loss_percent",
"trailing_stop_loss_percent",
"price_slippage_percent", "price_slippage_percent",
"callback_price_deviation_percent", "callback_price_deviation_percent",
"trade_size_percent", "trade_size_percent",
@ -91,6 +92,7 @@ class TradeForm(ModelForm):
"amount", "amount",
"price", "price",
"stop_loss", "stop_loss",
"trailing_stop_loss",
"take_profit", "take_profit",
"direction", "direction",
) )

View File

@ -138,6 +138,56 @@ def get_trade_size_in_base(direction, account, strategy, cash_balance, base):
return trade_size_in_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): def get_tp_sl(direction, strategy, price):
""" """
Get the take profit and stop loss prices. 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 :param price: Price of the trade
:return: Take profit and stop loss prices :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 # Look up the TSL if required by the strategy
stop_loss_as_ratio = D(strategy.stop_loss_percent) / D(100) if strategy.trailing_stop_loss_percent:
take_profit_as_ratio = D(strategy.take_profit_percent) / D(100) trailing_stop_loss = get_sl(
log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}") direction, strategy.trailing_stop_loss_percent, price, return_var=True
log.debug(f"Take profit as ratio: {take_profit_as_ratio}") )
cast["tsl"] = trailing_stop_loss
# Calculate the TP and SL prices by multiplying with the price return cast
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)
def get_price_bound(direction, strategy, price, current_price): def get_price_bound(direction, strategy, price, current_price):
@ -306,8 +336,13 @@ def execute_strategy(callback, strategy):
direction, account, strategy, cash_balance, base direction, account, strategy, cash_balance, base
) )
# Calculate TP/SL # Calculate TP/SL/TSL
stop_loss, take_profit = get_tp_sl(direction, strategy, price) 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 # Calculate price bound and round to the display precision
price_bound = get_price_bound(direction, strategy, price, current_price) price_bound = get_price_bound(direction, strategy, price, current_price)
@ -315,14 +350,6 @@ def execute_strategy(callback, strategy):
return return
price_bound = round(price_bound, display_precision) 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 # Create object, note that the amount is rounded to the trade precision
new_trade = Trade.objects.create( new_trade = Trade.objects.create(
user=user, user=user,
@ -339,6 +366,11 @@ def execute_strategy(callback, strategy):
take_profit=float(round(take_profit, display_precision)), take_profit=float(round(take_profit, display_precision)),
direction=direction, 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() new_trade.save()
info = new_trade.post() info = new_trade.post()
log.debug(f"Posted trade: {info}") log.debug(f"Posted trade: {info}")

View File

@ -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),
),
]

View File

@ -175,6 +175,7 @@ class Trade(models.Model):
amount_usd = models.FloatField(null=True, blank=True) amount_usd = models.FloatField(null=True, blank=True)
price = models.FloatField(null=True, blank=True) price = models.FloatField(null=True, blank=True)
stop_loss = 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) take_profit = models.FloatField(null=True, blank=True)
status = models.CharField(max_length=255, null=True, blank=True) status = models.CharField(max_length=255, null=True, blank=True)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255) direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
@ -224,6 +225,7 @@ class Strategy(models.Model):
enabled = models.BooleanField(default=False) enabled = models.BooleanField(default=False)
take_profit_percent = models.FloatField(default=1.5) take_profit_percent = models.FloatField(default=1.5)
stop_loss_percent = models.FloatField(default=1.0) 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) price_slippage_percent = models.FloatField(default=2.5)
callback_price_deviation_percent = models.FloatField(default=0.5) callback_price_deviation_percent = models.FloatField(default=0.5)
trade_size_percent = models.FloatField(default=0.5) trade_size_percent = models.FloatField(default=0.5)