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
|
||||
"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(),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__)
|
||||
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:
|
||||
|
|
Loading…
Reference in New Issue