Calculate price slippage more reliably and allow specifying order type and time in force
This commit is contained in:
parent
c8f776e2a8
commit
5c68191e5b
|
@ -54,7 +54,7 @@ class OANDAExchange(BaseExchange):
|
||||||
# "price": "1.5000", - added later
|
# "price": "1.5000", - added later
|
||||||
"stopLossOnFill": {"timeInForce": "GTC", "price": str(trade.stop_loss)},
|
"stopLossOnFill": {"timeInForce": "GTC", "price": str(trade.stop_loss)},
|
||||||
"takeProfitOnFill": {"price": str(trade.take_profit)},
|
"takeProfitOnFill": {"price": str(trade.take_profit)},
|
||||||
"timeInForce": "GTC",
|
"timeInForce": trade.time_in_force.upper(),
|
||||||
"instrument": trade.symbol,
|
"instrument": trade.symbol,
|
||||||
"units": str(amount),
|
"units": str(amount),
|
||||||
"type": trade.type.upper(),
|
"type": trade.type.upper(),
|
||||||
|
|
|
@ -64,11 +64,14 @@ class StrategyForm(ModelForm):
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"account",
|
"account",
|
||||||
|
"order_type",
|
||||||
|
"time_in_force",
|
||||||
"hooks",
|
"hooks",
|
||||||
"enabled",
|
"enabled",
|
||||||
"take_profit_percent",
|
"take_profit_percent",
|
||||||
"stop_loss_percent",
|
"stop_loss_percent",
|
||||||
"price_slippage_percent",
|
"price_slippage_percent",
|
||||||
|
"callback_price_deviation_percent",
|
||||||
"trade_size_percent",
|
"trade_size_percent",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,6 +87,7 @@ class TradeForm(ModelForm):
|
||||||
"account",
|
"account",
|
||||||
"symbol",
|
"symbol",
|
||||||
"type",
|
"type",
|
||||||
|
"time_in_force",
|
||||||
"amount",
|
"amount",
|
||||||
"price",
|
"price",
|
||||||
"stop_loss",
|
"stop_loss",
|
||||||
|
|
|
@ -85,6 +85,27 @@ def to_currency(direction, account, amount, from_currency, to_currency):
|
||||||
return converted
|
return converted
|
||||||
|
|
||||||
|
|
||||||
|
def get_price(account, direction, symbol):
|
||||||
|
"""
|
||||||
|
Get the price for a given symbol.
|
||||||
|
:param account: Account object
|
||||||
|
:param direction: direction of the trade
|
||||||
|
:param symbol: symbol
|
||||||
|
:return: price of bid for buys, price of ask for sells
|
||||||
|
"""
|
||||||
|
if direction == "buy":
|
||||||
|
price_index = "bids"
|
||||||
|
elif direction == "sell":
|
||||||
|
price_index = "asks"
|
||||||
|
try:
|
||||||
|
prices = account.client.get_currencies([symbol])
|
||||||
|
except GenericAPIError as e:
|
||||||
|
log.error(f"Error getting currencies: {e}")
|
||||||
|
return None
|
||||||
|
price = D(prices["prices"][0][price_index][0]["price"])
|
||||||
|
return price
|
||||||
|
|
||||||
|
|
||||||
def get_trade_size_in_base(direction, account, strategy, cash_balance, base):
|
def get_trade_size_in_base(direction, account, strategy, cash_balance, base):
|
||||||
"""
|
"""
|
||||||
Get the trade size in the base currency.
|
Get the trade size in the base currency.
|
||||||
|
@ -159,31 +180,57 @@ def get_tp_sl(direction, strategy, price):
|
||||||
return (stop_loss, take_profit)
|
return (stop_loss, take_profit)
|
||||||
|
|
||||||
|
|
||||||
def get_price_bound(direction, strategy, price):
|
def get_price_bound(direction, strategy, price, current_price):
|
||||||
"""
|
"""
|
||||||
Get the price bound for a given price using the slippage from the strategy.
|
Get the price bound for a given price using the slippage from the strategy.
|
||||||
|
* Check that the price of the callback is within the callback price deviation of the
|
||||||
|
current price
|
||||||
|
* Calculate the price bounds such that the maximum slippage should be within the
|
||||||
|
price slippage relative to the current price.
|
||||||
|
Note that the maximum actual slippage may be as high as the sum of these two values.
|
||||||
:param direction: Direction of the trade
|
:param direction: Direction of the trade
|
||||||
:param strategy: Strategy object
|
:param strategy: Strategy object
|
||||||
:param price: Price of the trade
|
:param price: Price of the trade
|
||||||
|
:param current_price: current price from the exchange
|
||||||
:return: Price bound
|
:return: Price bound
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Convert the slippage to a ratio
|
# Convert the callback price deviation to a ratio
|
||||||
|
callback_price_deviation_as_ratio = D(
|
||||||
|
strategy.callback_price_deviation_percent
|
||||||
|
) / D(100)
|
||||||
|
log.debug(f"Callback price deviation as ratio: {callback_price_deviation_as_ratio}")
|
||||||
|
|
||||||
|
maximum_price_deviation = D(current_price) * D(callback_price_deviation_as_ratio)
|
||||||
|
|
||||||
|
# Ensure the current price is within price_slippage_as_ratio of the callback price
|
||||||
|
if abs(current_price - price) <= maximum_price_deviation:
|
||||||
|
log.debug("Current price is within price deviation of callback price")
|
||||||
|
else:
|
||||||
|
log.error("Current price is not within price deviation of callback price")
|
||||||
|
log.debug(f"Difference: {abs(current_price - price)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Convert the maximum price slippage to a ratio
|
||||||
price_slippage_as_ratio = D(strategy.price_slippage_percent) / D(100)
|
price_slippage_as_ratio = D(strategy.price_slippage_percent) / D(100)
|
||||||
log.debug(f"Price slippage as ratio: {price_slippage_as_ratio}")
|
log.debug(f"Maximum price slippage as ratio: {price_slippage_as_ratio}")
|
||||||
|
|
||||||
# Calculate the price bound by multiplying with the price
|
# Calculate the price bound by multiplying with the price
|
||||||
# The price bound is the worst price we are willing to pay for the trade
|
# The price bound is the worst price we are willing to pay for the trade
|
||||||
price_slippage = D(price) * D(price_slippage_as_ratio)
|
price_slippage = D(current_price) * D(price_slippage_as_ratio)
|
||||||
log.debug(f"Price slippage: {price_slippage}")
|
log.debug(f"Maximum deviation from callback price: {price_slippage}")
|
||||||
|
|
||||||
|
current_price_slippage = D(current_price) * D(price_slippage_as_ratio)
|
||||||
|
log.debug(f"Maximum deviation from current price: {current_price_slippage}")
|
||||||
|
|
||||||
# Subtract slippage for buys, since we lose money if the price goes down
|
# Subtract slippage for buys, since we lose money if the price goes down
|
||||||
if direction == "buy":
|
if direction == "buy":
|
||||||
price_bound = D(price) - D(price_slippage)
|
price_bound = D(current_price) - D(price_slippage)
|
||||||
|
|
||||||
# Add slippage for sells, since we lose money if the price goes up
|
# Add slippage for sells, since we lose money if the price goes up
|
||||||
elif direction == "sell":
|
elif direction == "sell":
|
||||||
price_bound = D(price) + D(price_slippage)
|
price_bound = D(current_price) + D(price_slippage)
|
||||||
|
|
||||||
log.debug(f"Price bound: {price_bound}")
|
log.debug(f"Price bound: {price_bound}")
|
||||||
return price_bound
|
return price_bound
|
||||||
|
|
||||||
|
@ -247,22 +294,11 @@ def execute_strategy(callback, strategy):
|
||||||
price = round(D(callback.price), display_precision)
|
price = round(D(callback.price), display_precision)
|
||||||
log.debug(f"Extracted price of quote: {price}")
|
log.debug(f"Extracted price of quote: {price}")
|
||||||
|
|
||||||
# market_from_alpaca = get_market_value(account, symbol)
|
type = strategy.order_type
|
||||||
# change_percent = abs(((float(market_from_alpaca)-price)/price)*100)
|
|
||||||
# if change_percent > strategy.price_slippage_percent:
|
|
||||||
# log.error(f"Price slippage too high: {change_percent}")
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# type = "limit"
|
current_price = get_price(account, direction, symbol)
|
||||||
|
log.debug(f"Callback price: {price}")
|
||||||
# Only using market orders for now, but with price bounds, so it's a similar
|
log.debug(f"Current price: {current_price}")
|
||||||
# amount of protection from market fluctuations
|
|
||||||
# type = "market"
|
|
||||||
|
|
||||||
# For OANDA we can use the price since it should match exactly
|
|
||||||
# Not yet sure how to use both limit and market orders
|
|
||||||
# type = "limit"
|
|
||||||
type = "market"
|
|
||||||
|
|
||||||
# Convert the trade size, which is currently in the account's base currency,
|
# Convert the trade size, which is currently in the account's base currency,
|
||||||
# to the base currency of the pair we are trading
|
# to the base currency of the pair we are trading
|
||||||
|
@ -274,15 +310,18 @@ def execute_strategy(callback, strategy):
|
||||||
stop_loss, take_profit = get_tp_sl(direction, strategy, price)
|
stop_loss, take_profit = get_tp_sl(direction, strategy, price)
|
||||||
|
|
||||||
# Calculate price bound and round to the display precision
|
# Calculate price bound and round to the display precision
|
||||||
price_bound = round(get_price_bound(direction, strategy, price), display_precision)
|
price_bound = get_price_bound(direction, strategy, price, current_price)
|
||||||
|
if not price_bound:
|
||||||
|
return
|
||||||
|
price_bound = round(price_bound, display_precision)
|
||||||
|
|
||||||
# Use the price reported by the callback for limit orders
|
# # Use the price reported by the callback for limit orders
|
||||||
if type == "limit":
|
# if type == "limit":
|
||||||
price_for_trade = price
|
# price_for_trade = price
|
||||||
|
|
||||||
# Use the price bound for market orders
|
# # Use the price bound for market orders
|
||||||
elif type == "market":
|
# elif type == "market":
|
||||||
price_for_trade = price_bound
|
# 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(
|
||||||
|
@ -291,10 +330,11 @@ def execute_strategy(callback, strategy):
|
||||||
hook=hook,
|
hook=hook,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
type=type,
|
type=type,
|
||||||
|
time_in_force=strategy.time_in_force,
|
||||||
# amount_fiat=amount_fiat,
|
# amount_fiat=amount_fiat,
|
||||||
amount=float(round(trade_size_in_base, trade_precision)),
|
amount=float(round(trade_size_in_base, trade_precision)),
|
||||||
# price=price_bound,
|
# price=price_bound,
|
||||||
price=price_for_trade,
|
price=price_bound,
|
||||||
stop_loss=float(round(stop_loss, display_precision)),
|
stop_loss=float(round(stop_loss, display_precision)),
|
||||||
take_profit=float(round(take_profit, display_precision)),
|
take_profit=float(round(take_profit, display_precision)),
|
||||||
direction=direction,
|
direction=direction,
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-15 15:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0022_account_currency'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='strategy',
|
||||||
|
options={'verbose_name_plural': 'strategies'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='strategy',
|
||||||
|
name='callback_price_deviation_percent',
|
||||||
|
field=models.FloatField(default=0.5),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='strategy',
|
||||||
|
name='order_type',
|
||||||
|
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-15 15:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0023_alter_strategy_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='strategy',
|
||||||
|
name='time_in_force',
|
||||||
|
field=models.CharField(choices=[('gtc', 'Good Til Cancelled'), ('gfd', 'Good For Day'), ('fok', 'Fill Or Kill'), ('ioc', 'Immediate Or Cancel')], default='gtc', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-15 15:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0024_strategy_time_in_force'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='strategy',
|
||||||
|
name='time_in_force',
|
||||||
|
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-15 15:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0025_alter_strategy_time_in_force'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trade',
|
||||||
|
name='time_in_force',
|
||||||
|
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,6 +10,20 @@ from core.util import logs
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange}
|
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange}
|
||||||
|
TYPE_CHOICES = (
|
||||||
|
("market", "Market"),
|
||||||
|
("limit", "Limit"),
|
||||||
|
)
|
||||||
|
DIRECTION_CHOICES = (
|
||||||
|
("buy", "Buy"),
|
||||||
|
("sell", "Sell"),
|
||||||
|
)
|
||||||
|
TIF_CHOICES = (
|
||||||
|
("gtc", "GTC (Good Til Cancelled)"),
|
||||||
|
("gfd", "GFD (Good For Day)"),
|
||||||
|
("fok", "FOK (Fill Or Kill)"),
|
||||||
|
("ioc", "IOC (Immediate Or Cancel)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Plan(models.Model):
|
class Plan(models.Model):
|
||||||
|
@ -140,10 +154,6 @@ class Session(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Hook(models.Model):
|
class Hook(models.Model):
|
||||||
DIRECTION_CHOICES = (
|
|
||||||
("buy", "Buy"),
|
|
||||||
("sell", "Sell"),
|
|
||||||
)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
|
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
|
||||||
hook = models.CharField(max_length=255, unique=True)
|
hook = models.CharField(max_length=255, unique=True)
|
||||||
|
@ -155,18 +165,11 @@ class Hook(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Trade(models.Model):
|
class Trade(models.Model):
|
||||||
TYPE_CHOICES = (
|
|
||||||
("market", "Market"),
|
|
||||||
("limit", "Limit"),
|
|
||||||
)
|
|
||||||
DIRECTION_CHOICES = (
|
|
||||||
("buy", "Buy"),
|
|
||||||
("sell", "Sell"),
|
|
||||||
)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||||
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
|
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
symbol = models.CharField(max_length=255)
|
symbol = models.CharField(max_length=255)
|
||||||
|
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||||
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
|
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
|
||||||
amount = models.FloatField(null=True, blank=True)
|
amount = models.FloatField(null=True, blank=True)
|
||||||
amount_usd = models.FloatField(null=True, blank=True)
|
amount_usd = models.FloatField(null=True, blank=True)
|
||||||
|
@ -213,11 +216,16 @@ class Strategy(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||||
|
order_type = models.CharField(
|
||||||
|
choices=TYPE_CHOICES, max_length=255, default="market"
|
||||||
|
)
|
||||||
|
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||||
hooks = models.ManyToManyField(Hook)
|
hooks = models.ManyToManyField(Hook)
|
||||||
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)
|
||||||
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)
|
||||||
trade_size_percent = models.FloatField(default=0.5)
|
trade_size_percent = models.FloatField(default=0.5)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
Loading…
Reference in New Issue