Calculate price slippage more reliably and allow specifying order type and time in force

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

View File

@ -54,7 +54,7 @@ class OANDAExchange(BaseExchange):
# "price": "1.5000", - added later
"stopLossOnFill": {"timeInForce": "GTC", "price": str(trade.stop_loss)},
"takeProfitOnFill": {"price": str(trade.take_profit)},
"timeInForce": "GTC",
"timeInForce": trade.time_in_force.upper(),
"instrument": trade.symbol,
"units": str(amount),
"type": trade.type.upper(),

View File

@ -64,11 +64,14 @@ class StrategyForm(ModelForm):
"name",
"description",
"account",
"order_type",
"time_in_force",
"hooks",
"enabled",
"take_profit_percent",
"stop_loss_percent",
"price_slippage_percent",
"callback_price_deviation_percent",
"trade_size_percent",
)
@ -84,6 +87,7 @@ class TradeForm(ModelForm):
"account",
"symbol",
"type",
"time_in_force",
"amount",
"price",
"stop_loss",

View File

@ -85,6 +85,27 @@ def to_currency(direction, account, amount, from_currency, to_currency):
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):
"""
Get the trade size in the base currency.
@ -159,31 +180,57 @@ def get_tp_sl(direction, strategy, price):
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.
* 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 strategy: Strategy object
:param price: Price of the trade
:param current_price: current price from the exchange
: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)
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
# The price bound is the worst price we are willing to pay for the trade
price_slippage = D(price) * D(price_slippage_as_ratio)
log.debug(f"Price slippage: {price_slippage}")
price_slippage = D(current_price) * D(price_slippage_as_ratio)
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
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
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}")
return price_bound
@ -247,22 +294,11 @@ def execute_strategy(callback, strategy):
price = round(D(callback.price), display_precision)
log.debug(f"Extracted price of quote: {price}")
# market_from_alpaca = get_market_value(account, symbol)
# 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 = strategy.order_type
# type = "limit"
# Only using market orders for now, but with price bounds, so it's a similar
# 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"
current_price = get_price(account, direction, symbol)
log.debug(f"Callback price: {price}")
log.debug(f"Current price: {current_price}")
# Convert the trade size, which is currently in the account's base currency,
# 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)
# 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
if type == "limit":
price_for_trade = price
# # 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
# # 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(
@ -291,10 +330,11 @@ def execute_strategy(callback, strategy):
hook=hook,
symbol=symbol,
type=type,
time_in_force=strategy.time_in_force,
# amount_fiat=amount_fiat,
amount=float(round(trade_size_in_base, trade_precision)),
# price=price_bound,
price=price_for_trade,
price=price_bound,
stop_loss=float(round(stop_loss, display_precision)),
take_profit=float(round(take_profit, display_precision)),
direction=direction,

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,20 @@ from core.util import logs
log = logs.get_logger(__name__)
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):
@ -140,10 +154,6 @@ class Session(models.Model):
class Hook(models.Model):
DIRECTION_CHOICES = (
("buy", "Buy"),
("sell", "Sell"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
hook = models.CharField(max_length=255, unique=True)
@ -155,18 +165,11 @@ class Hook(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)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
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)
amount = 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)
description = models.TextField(null=True, blank=True)
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)
enabled = models.BooleanField(default=False)
take_profit_percent = models.FloatField(default=1.5)
stop_loss_percent = models.FloatField(default=1.0)
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)
class Meta: