Continue implementing live risk checks
This commit is contained in:
parent
93be9e6ffe
commit
e55f903f42
|
@ -1,6 +1,10 @@
|
||||||
from decimal import Decimal as D
|
from decimal import Decimal as D
|
||||||
|
|
||||||
|
from core.exchanges import GenericAPIError
|
||||||
from core.lib.elastic import store_msg
|
from core.lib.elastic import store_msg
|
||||||
|
from core.util import logs
|
||||||
|
|
||||||
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_balance_hook(user_id, user_name, account_id, account_name, balance):
|
def get_balance_hook(user_id, user_name, account_id, account_name, balance):
|
||||||
|
@ -20,144 +24,73 @@ def get_balance_hook(user_id, user_name, account_id, account_name, balance):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised_pl):
|
def get_pair(account, base, quote, invert=False):
|
||||||
"""
|
"""
|
||||||
Determine the percent change of the TP price from the initial price.
|
Get the pair for the given account and currencies.
|
||||||
Positive values indicate a profit, negative values indicate a loss.
|
:param account: Account object
|
||||||
|
:param base: Base currency
|
||||||
|
:param quote: Quote currency
|
||||||
|
:param invert: Invert the pair
|
||||||
|
:return: currency symbol, e.g. BTC_USD, BTC/USD, etc.
|
||||||
"""
|
"""
|
||||||
pl_per_unit = D(unrealised_pl) / D(current_units)
|
# Currently we only have two exchanges with different pair separators
|
||||||
if side == "long":
|
if account.exchange == "alpaca":
|
||||||
initial_price = D(current_price) - pl_per_unit
|
separator = "/"
|
||||||
|
elif account.exchange == "oanda":
|
||||||
|
separator = "_"
|
||||||
|
|
||||||
|
# Flip the pair if needed
|
||||||
|
if invert:
|
||||||
|
symbol = f"{quote.upper()}{separator}{base.upper()}"
|
||||||
else:
|
else:
|
||||||
initial_price = D(current_price) + pl_per_unit
|
symbol = f"{base.upper()}{separator}{quote.upper()}"
|
||||||
|
# Check it exists
|
||||||
# Get the percent change of the TP price from the initial price.
|
if symbol not in account.supported_symbols:
|
||||||
change_percent = ((initial_price - D(tp_price)) / initial_price) * 100
|
return False
|
||||||
|
return symbol
|
||||||
if side == "long":
|
|
||||||
if D(tp_price) < initial_price:
|
|
||||||
loss = True
|
|
||||||
else:
|
|
||||||
loss = False
|
|
||||||
else:
|
|
||||||
if D(tp_price) > initial_price:
|
|
||||||
loss = True
|
|
||||||
else:
|
|
||||||
loss = False
|
|
||||||
|
|
||||||
# if we are in loss on the short side, we want to show a negative
|
|
||||||
if loss:
|
|
||||||
change_percent = 0 - abs(change_percent)
|
|
||||||
else:
|
|
||||||
change_percent = abs(change_percent)
|
|
||||||
|
|
||||||
return round(change_percent, 5)
|
|
||||||
|
|
||||||
|
|
||||||
def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised_pl):
|
def to_currency(direction, account, amount, from_currency, to_currency):
|
||||||
"""
|
"""
|
||||||
Determine the percent change of the SL price from the initial price.
|
Convert an amount from one currency to another.
|
||||||
Positive values indicate a loss, negative values indicate a profit.
|
:param direction: Direction of the trade
|
||||||
This may seem backwards, but it is important to note that by default,
|
:param account: Account object
|
||||||
SL indicates a loss, and positive values should be expected.
|
:param amount: Amount to convert
|
||||||
Negative values indicate a negative loss, so a profit.
|
:param from_currency: Currency to convert from
|
||||||
|
:param to_currency: Currency to convert to
|
||||||
|
:return: Converted amount
|
||||||
"""
|
"""
|
||||||
pl_per_unit = D(unrealised_pl) / D(current_units)
|
# If we're converting to the same currency, just return the amount
|
||||||
if side == "long":
|
if from_currency == to_currency:
|
||||||
initial_price = D(current_price) - pl_per_unit
|
return amount
|
||||||
else:
|
inverted = False
|
||||||
initial_price = D(current_price) + pl_per_unit
|
|
||||||
|
|
||||||
# initial_price = D(current_price) - pl_per_unit
|
# This is needed because OANDA has different values for bid and ask
|
||||||
|
if direction == "buy":
|
||||||
|
price_index = "bids"
|
||||||
|
elif direction == "sell":
|
||||||
|
price_index = "asks"
|
||||||
|
symbol = get_pair(account, from_currency, to_currency)
|
||||||
|
if not symbol:
|
||||||
|
symbol = get_pair(account, from_currency, to_currency, invert=True)
|
||||||
|
inverted = True
|
||||||
|
|
||||||
# Get the percent change of the SL price from the initial price.
|
# Bit of a hack but it works
|
||||||
change_percent = ((initial_price - D(sl_price)) / initial_price) * 100
|
if not symbol:
|
||||||
|
log.error(f"Could not find symbol for {from_currency} -> {to_currency}")
|
||||||
|
raise Exception("Could not find symbol")
|
||||||
|
try:
|
||||||
|
prices = account.client.get_currencies([symbol])
|
||||||
|
except GenericAPIError as e:
|
||||||
|
log.error(f"Error getting currencies and inverted currencies: {e}")
|
||||||
|
return None
|
||||||
|
price = D(prices["prices"][0][price_index][0]["price"])
|
||||||
|
|
||||||
# If the trade is long, the SL price will be higher than the initial price.
|
# If we had to flip base and quote, we need to use the reciprocal of the price
|
||||||
# if side == "long":
|
if inverted:
|
||||||
# change_percent *= -1
|
price = D(1.0) / price
|
||||||
|
|
||||||
if side == "long":
|
# Convert the amount to the destination currency
|
||||||
if D(sl_price) > initial_price:
|
converted = D(amount) * price
|
||||||
profit = True
|
|
||||||
else:
|
|
||||||
profit = False
|
|
||||||
else:
|
|
||||||
if D(sl_price) < initial_price:
|
|
||||||
profit = True
|
|
||||||
else:
|
|
||||||
profit = False
|
|
||||||
|
|
||||||
if profit:
|
return converted
|
||||||
change_percent = 0 - abs(change_percent)
|
|
||||||
else:
|
|
||||||
change_percent = abs(change_percent)
|
|
||||||
|
|
||||||
return round(change_percent, 5)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_open_trades(open_trades):
|
|
||||||
"""
|
|
||||||
Convert a list of open trades into a list of Trade-like objects.
|
|
||||||
"""
|
|
||||||
trades = []
|
|
||||||
for trade in open_trades:
|
|
||||||
current_price = trade["price"]
|
|
||||||
current_units = trade["currentUnits"]
|
|
||||||
unrealised_pl = trade["unrealizedPL"]
|
|
||||||
side = trade["side"]
|
|
||||||
cast = {
|
|
||||||
"id": trade["id"],
|
|
||||||
"symbol": trade["symbol"],
|
|
||||||
"amount": current_units,
|
|
||||||
"side": side,
|
|
||||||
"state": trade["state"],
|
|
||||||
"price": current_price,
|
|
||||||
"pl": unrealised_pl,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add some extra fields, sometimes we have already looked up the
|
|
||||||
# prices and don't need to call convert_trades_to_usd
|
|
||||||
if "take_profit_usd" in trade:
|
|
||||||
cast["take_profit_usd"] = trade["take_profit_usd"]
|
|
||||||
if "stop_loss_usd" in trade:
|
|
||||||
cast["stop_loss_usd"] = trade["stop_loss_usd"]
|
|
||||||
if "trailing_stop_loss_usd" in trade:
|
|
||||||
cast["trailing_stop_loss_usd"] = trade["trailing_stop_loss_usd"]
|
|
||||||
|
|
||||||
if "takeProfitOrder" in trade:
|
|
||||||
if trade["takeProfitOrder"]:
|
|
||||||
take_profit = trade["takeProfitOrder"]["price"]
|
|
||||||
take_profit_percent = tp_price_to_percent(
|
|
||||||
take_profit, side, current_price, current_units, unrealised_pl
|
|
||||||
)
|
|
||||||
|
|
||||||
cast["take_profit"] = take_profit
|
|
||||||
cast["take_profit_percent"] = take_profit_percent
|
|
||||||
|
|
||||||
if "stopLossOrder" in trade:
|
|
||||||
if trade["stopLossOrder"]:
|
|
||||||
stop_loss = trade["stopLossOrder"]["price"]
|
|
||||||
stop_loss_percent = sl_price_to_percent(
|
|
||||||
stop_loss, side, current_price, current_units, unrealised_pl
|
|
||||||
)
|
|
||||||
|
|
||||||
cast["stop_loss"] = stop_loss
|
|
||||||
cast["stop_loss_percent"] = stop_loss_percent
|
|
||||||
|
|
||||||
if "trailingStopLossOrder" in trade:
|
|
||||||
if trade["trailingStopLossOrder"]:
|
|
||||||
trailing_stop_loss = trade["trailingStopLossOrder"]["price"]
|
|
||||||
trailing_stop_loss_percent = sl_price_to_percent(
|
|
||||||
trailing_stop_loss,
|
|
||||||
side,
|
|
||||||
current_price,
|
|
||||||
current_units,
|
|
||||||
unrealised_pl,
|
|
||||||
)
|
|
||||||
|
|
||||||
cast["trailing_stop_loss"] = trailing_stop_loss
|
|
||||||
cast["trailing_stop_loss_percent"] = trailing_stop_loss_percent
|
|
||||||
|
|
||||||
trades.append(cast)
|
|
||||||
return trades
|
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
from decimal import Decimal as D
|
||||||
|
|
||||||
|
from core.models import Account
|
||||||
|
from core.trading.market import direction_to_side, get_price, side_to_direction
|
||||||
|
|
||||||
|
# Separate module to prevent circular import from
|
||||||
|
# models -> exchanges -> common -> models
|
||||||
|
# Since we need Account here to look up missing prices
|
||||||
|
|
||||||
|
|
||||||
|
def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised_pl):
|
||||||
|
"""
|
||||||
|
Determine the percent change of the TP price from the initial price.
|
||||||
|
Positive values indicate a profit, negative values indicate a loss.
|
||||||
|
"""
|
||||||
|
pl_per_unit = D(unrealised_pl) / D(current_units)
|
||||||
|
if side == "long":
|
||||||
|
initial_price = D(current_price) - pl_per_unit
|
||||||
|
else:
|
||||||
|
initial_price = D(current_price) + pl_per_unit
|
||||||
|
|
||||||
|
# Get the percent change of the TP price from the initial price.
|
||||||
|
change_percent = ((initial_price - D(tp_price)) / initial_price) * 100
|
||||||
|
|
||||||
|
if side == "long":
|
||||||
|
if D(tp_price) < initial_price:
|
||||||
|
loss = True
|
||||||
|
else:
|
||||||
|
loss = False
|
||||||
|
else:
|
||||||
|
if D(tp_price) > initial_price:
|
||||||
|
loss = True
|
||||||
|
else:
|
||||||
|
loss = False
|
||||||
|
|
||||||
|
# if we are in loss on the short side, we want to show a negative
|
||||||
|
if loss:
|
||||||
|
change_percent = 0 - abs(change_percent)
|
||||||
|
else:
|
||||||
|
change_percent = abs(change_percent)
|
||||||
|
|
||||||
|
return round(change_percent, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised_pl):
|
||||||
|
"""
|
||||||
|
Determine the percent change of the SL price from the initial price.
|
||||||
|
Positive values indicate a loss, negative values indicate a profit.
|
||||||
|
This may seem backwards, but it is important to note that by default,
|
||||||
|
SL indicates a loss, and positive values should be expected.
|
||||||
|
Negative values indicate a negative loss, so a profit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pl_per_unit = D(unrealised_pl) / D(current_units)
|
||||||
|
if side == "long":
|
||||||
|
initial_price = D(current_price) - pl_per_unit
|
||||||
|
else:
|
||||||
|
initial_price = D(current_price) + pl_per_unit
|
||||||
|
|
||||||
|
# initial_price = D(current_price) - pl_per_unit
|
||||||
|
|
||||||
|
# Get the percent change of the SL price from the initial price.
|
||||||
|
change_percent = ((initial_price - D(sl_price)) / initial_price) * 100
|
||||||
|
|
||||||
|
# If the trade is long, the SL price will be higher than the initial price.
|
||||||
|
# if side == "long":
|
||||||
|
# change_percent *= -1
|
||||||
|
|
||||||
|
if side == "long":
|
||||||
|
if D(sl_price) > initial_price:
|
||||||
|
profit = True
|
||||||
|
else:
|
||||||
|
profit = False
|
||||||
|
else:
|
||||||
|
if D(sl_price) < initial_price:
|
||||||
|
profit = True
|
||||||
|
else:
|
||||||
|
profit = False
|
||||||
|
|
||||||
|
if profit:
|
||||||
|
change_percent = 0 - abs(change_percent)
|
||||||
|
else:
|
||||||
|
change_percent = abs(change_percent)
|
||||||
|
|
||||||
|
return round(change_percent, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def annotate_trade_tp_sl_percent(trade):
|
||||||
|
"""
|
||||||
|
Annotate the trade with the TP and SL percent.
|
||||||
|
This works on Trade objects, which will require an additional market
|
||||||
|
lookup to get the current price.
|
||||||
|
"""
|
||||||
|
if "current_price" in trade:
|
||||||
|
current_price = trade["current_price"]
|
||||||
|
else:
|
||||||
|
account_id = trade["account_id"]
|
||||||
|
account = Account.get_by_id_no_user_check(account_id)
|
||||||
|
|
||||||
|
current_price = get_price(account, trade["direction"], trade["symbol"])
|
||||||
|
trade["current_price"] = current_price
|
||||||
|
|
||||||
|
current_units = trade["amount"]
|
||||||
|
if "pl" in trade:
|
||||||
|
unrealised_pl = trade["pl"]
|
||||||
|
else:
|
||||||
|
unrealised_pl = 0
|
||||||
|
|
||||||
|
if "side" in trade:
|
||||||
|
side = trade["side"]
|
||||||
|
direction = side_to_direction(side)
|
||||||
|
trade["direction"] = direction
|
||||||
|
else:
|
||||||
|
direction = trade["direction"]
|
||||||
|
side = direction_to_side(direction)
|
||||||
|
trade["side"] = side
|
||||||
|
|
||||||
|
if "take_profit" in trade:
|
||||||
|
if trade["take_profit"]:
|
||||||
|
take_profit = trade["take_profit"]
|
||||||
|
take_profit_percent = tp_price_to_percent(
|
||||||
|
take_profit, trade["side"], current_price, current_units, unrealised_pl
|
||||||
|
)
|
||||||
|
|
||||||
|
trade["take_profit_percent"] = take_profit_percent
|
||||||
|
|
||||||
|
if "stop_loss" in trade:
|
||||||
|
if trade["stop_loss"]:
|
||||||
|
stop_loss = trade["stop_loss"]
|
||||||
|
stop_loss_percent = sl_price_to_percent(
|
||||||
|
stop_loss, side, current_price, current_units, unrealised_pl
|
||||||
|
)
|
||||||
|
|
||||||
|
trade["stop_loss_percent"] = stop_loss_percent
|
||||||
|
|
||||||
|
if "trailing_stop_loss" in trade:
|
||||||
|
if trade["trailing_stop_loss"]:
|
||||||
|
trailing_stop_loss = trade["trailing_stop_loss"]
|
||||||
|
trailing_stop_loss_percent = sl_price_to_percent(
|
||||||
|
trailing_stop_loss,
|
||||||
|
trade["side"],
|
||||||
|
current_price,
|
||||||
|
current_units,
|
||||||
|
unrealised_pl,
|
||||||
|
)
|
||||||
|
|
||||||
|
trade["trailing_stop_loss_percent"] = trailing_stop_loss_percent
|
||||||
|
|
||||||
|
return trade
|
||||||
|
|
||||||
|
|
||||||
|
def open_trade_to_unified_format(trade):
|
||||||
|
"""
|
||||||
|
Convert an open trade to a Trade-like format.
|
||||||
|
"""
|
||||||
|
current_price = trade["price"]
|
||||||
|
current_units = trade["currentUnits"]
|
||||||
|
unrealised_pl = trade["unrealizedPL"]
|
||||||
|
side = trade["side"]
|
||||||
|
cast = {
|
||||||
|
"id": trade["id"],
|
||||||
|
"symbol": trade["symbol"],
|
||||||
|
"amount": current_units,
|
||||||
|
"side": side,
|
||||||
|
"direction": side_to_direction(side),
|
||||||
|
"state": trade["state"],
|
||||||
|
"current_price": current_price,
|
||||||
|
"pl": unrealised_pl,
|
||||||
|
}
|
||||||
|
# Add some extra fields, sometimes we have already looked up the
|
||||||
|
# prices and don't need to call convert_trades_to_usd
|
||||||
|
# This is mostly for tests, but it can be useful in other places.
|
||||||
|
if "take_profit_usd" in trade:
|
||||||
|
cast["take_profit_usd"] = trade["take_profit_usd"]
|
||||||
|
if "stop_loss_usd" in trade:
|
||||||
|
cast["stop_loss_usd"] = trade["stop_loss_usd"]
|
||||||
|
if "trailing_stop_loss_usd" in trade:
|
||||||
|
cast["trailing_stop_loss_usd"] = trade["trailing_stop_loss_usd"]
|
||||||
|
if "takeProfitOrder" in trade:
|
||||||
|
if trade["takeProfitOrder"]:
|
||||||
|
take_profit = trade["takeProfitOrder"]["price"]
|
||||||
|
cast["take_profit"] = take_profit
|
||||||
|
|
||||||
|
if "stopLossOrder" in trade:
|
||||||
|
if trade["stopLossOrder"]:
|
||||||
|
stop_loss = trade["stopLossOrder"]["price"]
|
||||||
|
cast["stop_loss"] = stop_loss
|
||||||
|
|
||||||
|
if "trailingStopLossOrder" in trade:
|
||||||
|
if trade["trailingStopLossOrder"]:
|
||||||
|
trailing_stop_loss = trade["trailingStopLossOrder"]["price"]
|
||||||
|
cast["trailing_stop_loss"] = trailing_stop_loss
|
||||||
|
|
||||||
|
return cast
|
||||||
|
|
||||||
|
|
||||||
|
def convert_trades(open_trades):
|
||||||
|
"""
|
||||||
|
Convert a list of open trades into a list of Trade-like objects.
|
||||||
|
"""
|
||||||
|
trades = []
|
||||||
|
for trade in open_trades:
|
||||||
|
if "currentUnits" in trade: # Open trade
|
||||||
|
cast = open_trade_to_unified_format(trade)
|
||||||
|
cast = annotate_trade_tp_sl_percent(cast)
|
||||||
|
trades.append(cast)
|
||||||
|
else:
|
||||||
|
cast = annotate_trade_tp_sl_percent(trade)
|
||||||
|
trades.append(cast)
|
||||||
|
|
||||||
|
return trades
|
|
@ -36,18 +36,25 @@ class OANDAExchange(BaseExchange):
|
||||||
response = self.get_instruments()
|
response = self.get_instruments()
|
||||||
return [x["name"] for x in response["itemlist"]]
|
return [x["name"] for x in response["itemlist"]]
|
||||||
|
|
||||||
def get_balance(self):
|
def get_balance(self, return_usd=False):
|
||||||
r = accounts.AccountSummary(self.account_id)
|
r = accounts.AccountSummary(self.account_id)
|
||||||
response = self.call(r)
|
response = self.call(r)
|
||||||
|
print("RESPONSE", response)
|
||||||
balance = float(response["balance"])
|
balance = float(response["balance"])
|
||||||
|
currency = response["currency"]
|
||||||
|
balance_usd = common.to_currency("sell", self.account, balance, currency, "USD")
|
||||||
|
print("BALANCE", balance)
|
||||||
|
print("BALANCE USD", balance_usd)
|
||||||
|
|
||||||
common.get_balance_hook(
|
common.get_balance_hook(
|
||||||
self.account.user.id,
|
self.account.user.id,
|
||||||
self.account.user.username,
|
self.account.user.username,
|
||||||
self.account.id,
|
self.account.id,
|
||||||
self.account.name,
|
self.account.name,
|
||||||
balance,
|
balance_usd,
|
||||||
)
|
)
|
||||||
|
if return_usd:
|
||||||
|
return balance_usd
|
||||||
return balance
|
return balance
|
||||||
|
|
||||||
def get_market_value(self, symbol):
|
def get_market_value(self, symbol):
|
||||||
|
@ -138,7 +145,7 @@ class OANDAExchange(BaseExchange):
|
||||||
|
|
||||||
def get_all_open_trades(self):
|
def get_all_open_trades(self):
|
||||||
r = trades.OpenTrades(accountID=self.account_id)
|
r = trades.OpenTrades(accountID=self.account_id)
|
||||||
return self.call(r)
|
return self.call(r)["itemlist"]
|
||||||
|
|
||||||
def close_position(self, side, symbol):
|
def close_position(self, side, symbol):
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -119,6 +119,7 @@ class AccountForm(RestrictedFormMixin, ModelForm):
|
||||||
"sandbox",
|
"sandbox",
|
||||||
"enabled",
|
"enabled",
|
||||||
"risk_model",
|
"risk_model",
|
||||||
|
"initial_balance",
|
||||||
)
|
)
|
||||||
help_texts = {
|
help_texts = {
|
||||||
"name": "Name of the account. Informational only.",
|
"name": "Name of the account. Informational only.",
|
||||||
|
@ -128,6 +129,7 @@ class AccountForm(RestrictedFormMixin, ModelForm):
|
||||||
"sandbox": "Whether to use the sandbox/demo or not.",
|
"sandbox": "Whether to use the sandbox/demo or not.",
|
||||||
"enabled": "Whether the account is enabled.",
|
"enabled": "Whether the account is enabled.",
|
||||||
"risk_model": "The risk model to use for this account.",
|
"risk_model": "The risk model to use for this account.",
|
||||||
|
"initial_balance": "The initial balance of the account.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.4 on 2023-01-11 17:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0050_account_enabled'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='account',
|
||||||
|
name='initial_balance',
|
||||||
|
field=models.FloatField(default=0),
|
||||||
|
),
|
||||||
|
]
|
|
@ -116,6 +116,7 @@ class Account(models.Model):
|
||||||
risk_model = models.ForeignKey(
|
risk_model = models.ForeignKey(
|
||||||
"core.RiskModel", on_delete=models.SET_NULL, null=True, blank=True
|
"core.RiskModel", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
initial_balance = models.FloatField(default=0)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
name = f"{self.name} ({self.exchange})"
|
name = f"{self.name} ({self.exchange})"
|
||||||
|
@ -167,6 +168,10 @@ class Account(models.Model):
|
||||||
def get_by_id(cls, account_id, user):
|
def get_by_id(cls, account_id, user):
|
||||||
return cls.objects.get(id=account_id, user=user)
|
return cls.objects.get(id=account_id, user=user)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id_no_user_check(cls, account_id):
|
||||||
|
return cls.objects.get(id=account_id)
|
||||||
|
|
||||||
|
|
||||||
class Session(models.Model):
|
class Session(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.exchanges.common import sl_price_to_percent, tp_price_to_percent
|
from core.exchanges.convert import sl_price_to_percent, tp_price_to_percent
|
||||||
|
|
||||||
|
|
||||||
class CommonTestCase(TestCase):
|
class CommonTestCase(TestCase):
|
|
@ -86,6 +86,7 @@ If you have done this, please see the following line for more information:
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
api_secret=api_secret,
|
api_secret=api_secret,
|
||||||
|
initial_balance=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from decimal import Decimal as D
|
from decimal import Decimal as D
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.exchanges.common import convert_open_trades
|
from core.exchanges.convert import convert_trades
|
||||||
from core.models import Trade
|
from core.models import RiskModel, Trade
|
||||||
from core.tests.helpers import ElasticMock, LiveBase
|
from core.tests.helpers import ElasticMock, LiveBase
|
||||||
from core.trading.market import get_precision, get_price, get_sl, get_tp
|
from core.trading import market, risk
|
||||||
|
|
||||||
|
|
||||||
class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
|
@ -21,6 +22,14 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
direction="buy",
|
direction="buy",
|
||||||
)
|
)
|
||||||
self.commission = 0.025
|
self.commission = 0.025
|
||||||
|
self.risk_model = RiskModel.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test Risk Model",
|
||||||
|
max_loss_percent=4,
|
||||||
|
max_risk_percent=2,
|
||||||
|
max_open_trades=3,
|
||||||
|
max_open_trades_per_symbol=2,
|
||||||
|
)
|
||||||
|
|
||||||
def test_account_functional(self):
|
def test_account_functional(self):
|
||||||
"""
|
"""
|
||||||
|
@ -34,11 +43,12 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
if trade:
|
if trade:
|
||||||
posted = trade.post()
|
posted = trade.post()
|
||||||
else:
|
else:
|
||||||
|
trade = self.trade
|
||||||
posted = self.trade.post()
|
posted = self.trade.post()
|
||||||
# Check the opened trade
|
# Check the opened trade
|
||||||
self.assertEqual(posted["type"], "MARKET_ORDER")
|
self.assertEqual(posted["type"], "MARKET_ORDER")
|
||||||
self.assertEqual(posted["symbol"], "EUR_USD")
|
self.assertEqual(posted["symbol"], trade.symbol)
|
||||||
self.assertEqual(posted["units"], "10")
|
self.assertEqual(posted["units"], str(trade.amount))
|
||||||
self.assertEqual(posted["timeInForce"], "FOK")
|
self.assertEqual(posted["timeInForce"], "FOK")
|
||||||
|
|
||||||
return posted
|
return posted
|
||||||
|
@ -48,14 +58,17 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
trade.refresh_from_db()
|
trade.refresh_from_db()
|
||||||
closed = self.account.client.close_trade(trade.order_id)
|
closed = self.account.client.close_trade(trade.order_id)
|
||||||
else:
|
else:
|
||||||
|
trade = self.trade
|
||||||
# refresh the trade to get the trade id
|
# refresh the trade to get the trade id
|
||||||
self.trade.refresh_from_db()
|
self.trade.refresh_from_db()
|
||||||
closed = self.account.client.close_trade(self.trade.order_id)
|
closed = self.account.client.close_trade(self.trade.order_id)
|
||||||
|
|
||||||
# Check the feedback from closing the trade
|
# Check the feedback from closing the trade
|
||||||
|
print("CLOSED", closed)
|
||||||
|
print("TRADE AMOUNT", trade.amount)
|
||||||
self.assertEqual(closed["type"], "MARKET_ORDER")
|
self.assertEqual(closed["type"], "MARKET_ORDER")
|
||||||
self.assertEqual(closed["symbol"], "EUR_USD")
|
self.assertEqual(closed["symbol"], trade.symbol)
|
||||||
self.assertEqual(closed["units"], "-10")
|
self.assertEqual(closed["units"], str(0 - int(trade.amount)))
|
||||||
self.assertEqual(closed["timeInForce"], "FOK")
|
self.assertEqual(closed["timeInForce"], "FOK")
|
||||||
self.assertEqual(closed["reason"], "TRADE_CLOSE")
|
self.assertEqual(closed["reason"], "TRADE_CLOSE")
|
||||||
|
|
||||||
|
@ -78,7 +91,7 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
trades = self.account.client.get_all_open_trades()
|
trades = self.account.client.get_all_open_trades()
|
||||||
self.trade.refresh_from_db()
|
self.trade.refresh_from_db()
|
||||||
found = False
|
found = False
|
||||||
for trade in trades["itemlist"]:
|
for trade in trades:
|
||||||
if trade["id"] == self.trade.order_id:
|
if trade["id"] == self.trade.order_id:
|
||||||
self.assertEqual(trade["symbol"], "EUR_USD")
|
self.assertEqual(trade["symbol"], "EUR_USD")
|
||||||
self.assertEqual(trade["currentUnits"], "10")
|
self.assertEqual(trade["currentUnits"], "10")
|
||||||
|
@ -91,14 +104,11 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
if not found:
|
if not found:
|
||||||
self.fail("Could not find the trade in the list of open trades")
|
self.fail("Could not find the trade in the list of open trades")
|
||||||
|
|
||||||
def test_convert_open_trades(self):
|
def create_complex_trade(self, direction, amount, symbol, tp_percent, sl_percent):
|
||||||
"""
|
eur_usd_price = market.get_price(self.account, direction, symbol)
|
||||||
Test converting open trades response to Trade-like format.
|
trade_tp = market.get_tp(direction, tp_percent, eur_usd_price)
|
||||||
"""
|
trade_sl = market.get_sl(direction, sl_percent, eur_usd_price)
|
||||||
eur_usd_price = get_price(self.account, "buy", "EUR_USD")
|
# trade_tsl = market.get_sl("buy", 1, eur_usd_price, return_var=True)
|
||||||
trade_tp = get_tp("buy", 1, eur_usd_price)
|
|
||||||
trade_sl = get_sl("buy", 2, eur_usd_price)
|
|
||||||
# trade_tsl = get_sl("buy", 1, eur_usd_price, return_var=True)
|
|
||||||
# # TP 1% profit
|
# # TP 1% profit
|
||||||
# trade_tp = eur_usd_price * D(1.01)
|
# trade_tp = eur_usd_price * D(1.01)
|
||||||
# # SL 2% loss
|
# # SL 2% loss
|
||||||
|
@ -106,7 +116,7 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
# # TSL 1% loss
|
# # TSL 1% loss
|
||||||
# trade_tsl = eur_usd_price * D(0.99)
|
# trade_tsl = eur_usd_price * D(0.99)
|
||||||
|
|
||||||
trade_precision, display_precision = get_precision(self.account, "EUR_USD")
|
trade_precision, display_precision = market.get_precision(self.account, symbol)
|
||||||
# Round everything to the display precision
|
# Round everything to the display precision
|
||||||
|
|
||||||
trade_tp = round(trade_tp, display_precision)
|
trade_tp = round(trade_tp, display_precision)
|
||||||
|
@ -116,19 +126,79 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
complex_trade = Trade.objects.create(
|
complex_trade = Trade.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
account=self.account,
|
account=self.account,
|
||||||
symbol="EUR_USD",
|
symbol=symbol,
|
||||||
time_in_force="FOK",
|
time_in_force="FOK",
|
||||||
type="market",
|
type="market",
|
||||||
amount=10,
|
amount=amount,
|
||||||
direction="buy",
|
direction=direction,
|
||||||
take_profit=trade_tp,
|
take_profit=trade_tp,
|
||||||
stop_loss=trade_sl,
|
stop_loss=trade_sl,
|
||||||
# trailing_stop_loss=trade_tsl,
|
# trailing_stop_loss=trade_tsl,
|
||||||
)
|
)
|
||||||
|
return complex_trade
|
||||||
|
|
||||||
|
@patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=100)
|
||||||
|
def test_check_risk_max_risk_pass(self, mock_balance):
|
||||||
|
# SL of 19% on a 10 trade on a 100 account is 1.8 loss
|
||||||
|
# Should be comfortably under 2% risk
|
||||||
|
trade = self.create_complex_trade("buy", 10, "EUR_USD", 1, 18)
|
||||||
|
allowed = risk.check_risk(self.risk_model, self.account, trade)
|
||||||
|
self.assertTrue(allowed["allowed"])
|
||||||
|
|
||||||
|
@patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=100)
|
||||||
|
def test_check_risk_max_risk_fail(self, mock_balance):
|
||||||
|
# SL of 21% on a 10 trade on a 100 account is 2.2 loss
|
||||||
|
# Should be over 2% risk
|
||||||
|
trade = self.create_complex_trade("buy", 10, "EUR_USD", 1, 22)
|
||||||
|
allowed = risk.check_risk(self.risk_model, self.account, trade)
|
||||||
|
print("ALLOWED", allowed)
|
||||||
|
self.assertFalse(allowed["allowed"])
|
||||||
|
self.assertEqual(allowed["reason"], "Maximum risk exceeded.")
|
||||||
|
|
||||||
|
@patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=94)
|
||||||
|
# We have lost 6% of our account
|
||||||
|
def test_check_risk_max_loss_fail(self, mock_balance):
|
||||||
|
# Doesn't matter, shouldn't get as far as the trade
|
||||||
|
trade = self.create_complex_trade("buy", 1, "EUR_USD", 1, 1)
|
||||||
|
allowed = risk.check_risk(self.risk_model, self.account, trade)
|
||||||
|
self.assertFalse(allowed["allowed"])
|
||||||
|
self.assertEqual(allowed["reason"], "Maximum loss exceeded.")
|
||||||
|
|
||||||
|
@patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=100)
|
||||||
|
def test_check_risk_max_open_trades_fail(self, mock_balance):
|
||||||
|
# The maximum open trades is 3. Let's open 2 trades
|
||||||
|
trade1 = self.create_complex_trade("buy", 1, "EUR_USD", 1, 1)
|
||||||
|
self.open_trade(trade1)
|
||||||
|
|
||||||
|
trade2 = self.create_complex_trade("buy", 1, "EUR_USD", 1, 1)
|
||||||
|
self.open_trade(trade2)
|
||||||
|
|
||||||
|
trade3 = self.create_complex_trade("buy", 1, "EUR_USD", 1, 1)
|
||||||
|
|
||||||
|
allowed = risk.check_risk(self.risk_model, self.account, trade3)
|
||||||
|
self.assertFalse(allowed["allowed"])
|
||||||
|
self.assertEqual(allowed["reason"], "Maximum open trades exceeded.")
|
||||||
|
|
||||||
|
self.close_trade(trade1)
|
||||||
|
self.close_trade(trade2)
|
||||||
|
|
||||||
|
@patch("core.exchanges.oanda.OANDAExchange.get_balance", return_value=100)
|
||||||
|
def test_check_risk_max_open_trades_per_symbol_fail(self, mock_balance):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_convert_trades(self):
|
||||||
|
"""
|
||||||
|
Test converting open trades response to Trade-like format.
|
||||||
|
"""
|
||||||
|
complex_trade = self.create_complex_trade("buy", 10, "EUR_USD", 1, 2)
|
||||||
|
|
||||||
self.open_trade(complex_trade)
|
self.open_trade(complex_trade)
|
||||||
|
|
||||||
|
# Get and annotate the trades
|
||||||
trades = self.account.client.get_all_open_trades()
|
trades = self.account.client.get_all_open_trades()
|
||||||
trades_converted = convert_open_trades(trades["itemlist"])
|
trades_converted = convert_trades(trades)
|
||||||
|
|
||||||
|
# Check the converted trades
|
||||||
self.assertEqual(len(trades_converted), 1)
|
self.assertEqual(len(trades_converted), 1)
|
||||||
expected_tp_percent = D(1 - self.commission)
|
expected_tp_percent = D(1 - self.commission)
|
||||||
expected_sl_percent = D(2 - self.commission)
|
expected_sl_percent = D(2 - self.commission)
|
||||||
|
@ -141,4 +211,24 @@ class LiveTradingTestCase(ElasticMock, LiveBase, TestCase):
|
||||||
|
|
||||||
self.assertLess(tp_percent_difference, max_difference)
|
self.assertLess(tp_percent_difference, max_difference)
|
||||||
self.assertLess(sl_percent_difference, max_difference)
|
self.assertLess(sl_percent_difference, max_difference)
|
||||||
|
|
||||||
|
# Convert the trades to USD
|
||||||
|
trades_usd = market.convert_trades_to_usd(self.account, trades_converted)
|
||||||
|
|
||||||
|
# Convert the trade to USD ourselves
|
||||||
|
trade_in_usd = D(trades_usd[0]["amount"]) * D(trades_usd[0]["current_price"])
|
||||||
|
|
||||||
|
# It will never be perfect, but let's check it's at least close
|
||||||
|
trade_usd_conversion_difference = (
|
||||||
|
trades_usd[0]["trade_amount_usd"] - trade_in_usd
|
||||||
|
)
|
||||||
|
self.assertLess(trade_usd_conversion_difference, D(0.01))
|
||||||
|
|
||||||
|
# Check the converted TP and SL values
|
||||||
|
trade_usd_tp_difference = trades_usd[0]["take_profit_usd"] - D(0.1)
|
||||||
|
trade_usd_sl_difference = trades_usd[0]["stop_loss_usd"] - D(0.2)
|
||||||
|
|
||||||
|
self.assertLess(trade_usd_tp_difference, D(0.01))
|
||||||
|
self.assertLess(trade_usd_sl_difference, D(0.02))
|
||||||
|
|
||||||
self.close_trade(complex_trade)
|
self.close_trade(complex_trade)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.exchanges import common
|
from core.exchanges import convert
|
||||||
from core.models import RiskModel, User
|
from core.models import RiskModel, User
|
||||||
from core.trading import risk
|
from core.trading import risk
|
||||||
|
|
||||||
|
@ -228,7 +228,7 @@ class RiskModelTestCase(TestCase):
|
||||||
# Hardcoded prices to avoid calling market API here
|
# Hardcoded prices to avoid calling market API here
|
||||||
"stop_loss_usd": 50, # 5% of $1000
|
"stop_loss_usd": 50, # 5% of $1000
|
||||||
}
|
}
|
||||||
converted = common.convert_open_trades([trade])
|
converted = convert.convert_trades([trade])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
||||||
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
||||||
|
@ -253,7 +253,7 @@ class RiskModelTestCase(TestCase):
|
||||||
# Hardcoded prices to avoid calling market API here
|
# Hardcoded prices to avoid calling market API here
|
||||||
"stop_loss_usd": 40, # 4% of $1000
|
"stop_loss_usd": 40, # 4% of $1000
|
||||||
}
|
}
|
||||||
converted = common.convert_open_trades([trade, trade])
|
converted = convert.convert_trades([trade, trade])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 4)
|
self.assertEqual(converted[0]["stop_loss_percent"], 4)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 40)
|
self.assertEqual(converted[0]["stop_loss_usd"], 40)
|
||||||
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
||||||
|
@ -277,7 +277,7 @@ class RiskModelTestCase(TestCase):
|
||||||
# Hardcoded prices to avoid calling market API here
|
# Hardcoded prices to avoid calling market API here
|
||||||
"stop_loss_usd": 100, # 10% of $1000
|
"stop_loss_usd": 100, # 10% of $1000
|
||||||
}
|
}
|
||||||
converted = common.convert_open_trades([trade])
|
converted = convert.convert_trades([trade])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 10)
|
self.assertEqual(converted[0]["stop_loss_percent"], 10)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 100)
|
self.assertEqual(converted[0]["stop_loss_usd"], 100)
|
||||||
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
max_risk_check = risk.check_max_risk(self.risk_model, 1000, converted)
|
||||||
|
@ -302,7 +302,7 @@ class RiskModelTestCase(TestCase):
|
||||||
# Hardcoded prices to avoid calling market API here
|
# Hardcoded prices to avoid calling market API here
|
||||||
"stop_loss_usd": 50, # 5% of $1000
|
"stop_loss_usd": 50, # 5% of $1000
|
||||||
}
|
}
|
||||||
converted = common.convert_open_trades([trade, trade])
|
converted = convert.convert_trades([trade, trade])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
||||||
self.assertEqual(converted[1]["stop_loss_percent"], 5)
|
self.assertEqual(converted[1]["stop_loss_percent"], 5)
|
||||||
|
@ -334,7 +334,7 @@ class RiskModelTestCase(TestCase):
|
||||||
trade2["trailingStopLossOrder"] = {"price": 0.95}
|
trade2["trailingStopLossOrder"] = {"price": 0.95}
|
||||||
del trade2["stopLossOrder"]
|
del trade2["stopLossOrder"]
|
||||||
|
|
||||||
converted = common.convert_open_trades([trade, trade2])
|
converted = convert.convert_trades([trade, trade2])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
||||||
self.assertEqual(converted[1]["trailing_stop_loss_percent"], 5)
|
self.assertEqual(converted[1]["trailing_stop_loss_percent"], 5)
|
||||||
|
@ -365,7 +365,7 @@ class RiskModelTestCase(TestCase):
|
||||||
trade2 = trade.copy()
|
trade2 = trade.copy()
|
||||||
trade2["trailingStopLossOrder"] = {"price": 0.951}
|
trade2["trailingStopLossOrder"] = {"price": 0.951}
|
||||||
|
|
||||||
converted = common.convert_open_trades([trade, trade2])
|
converted = convert.convert_trades([trade, trade2])
|
||||||
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
self.assertEqual(converted[0]["stop_loss_percent"], 5)
|
||||||
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
self.assertEqual(converted[0]["stop_loss_usd"], 50)
|
||||||
self.assertEqual(float(converted[1]["trailing_stop_loss_percent"]), 4.9)
|
self.assertEqual(float(converted[1]["trailing_stop_loss_percent"]), 4.9)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal as D
|
from decimal import Decimal as D
|
||||||
|
|
||||||
from core.exchanges import GenericAPIError
|
from core.exchanges import GenericAPIError, common
|
||||||
from core.lib.notify import sendmsg
|
from core.lib.notify import sendmsg
|
||||||
from core.models import Account, Strategy, Trade
|
from core.models import Account, Strategy, Trade
|
||||||
from core.trading.crossfilter import crossfilter
|
from core.trading.crossfilter import crossfilter
|
||||||
|
@ -10,21 +10,50 @@ from core.util import logs
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def side_to_direction(side):
|
def side_to_direction(side, flip_direction=False):
|
||||||
"""
|
"""
|
||||||
Convert a side to a direction.
|
Convert a side to a direction.
|
||||||
|
:param side: Side, e.g. long, short
|
||||||
|
:param flip_direction: Flip the direction
|
||||||
|
:return: Direction, e.g. buy, sell
|
||||||
"""
|
"""
|
||||||
if side == "long":
|
if side == "long":
|
||||||
|
if flip_direction:
|
||||||
|
return "sell"
|
||||||
return "buy"
|
return "buy"
|
||||||
elif side == "short":
|
elif side == "short":
|
||||||
|
if flip_direction:
|
||||||
|
return "buy"
|
||||||
return "sell"
|
return "sell"
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def direction_to_side(direction, flip_side=False):
|
||||||
|
"""
|
||||||
|
Convert a direction to a side.
|
||||||
|
:param direction: Direction, e.g. buy, sell
|
||||||
|
:param flip_side: Flip the side
|
||||||
|
:return: Side, e.g. long, short
|
||||||
|
"""
|
||||||
|
if direction == "buy":
|
||||||
|
if flip_side:
|
||||||
|
return "short"
|
||||||
|
return "long"
|
||||||
|
elif direction == "sell":
|
||||||
|
if flip_side:
|
||||||
|
return "long"
|
||||||
|
return "short"
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_to_usd(account, trades):
|
def convert_trades_to_usd(account, trades):
|
||||||
"""
|
"""
|
||||||
Convert a list of trades to USD.
|
Convert a list of trades to USD. Input will also be mutated.
|
||||||
|
:param account: Account object
|
||||||
|
:param trades: List of trades
|
||||||
|
:return: List of trades, with amount_usd added
|
||||||
"""
|
"""
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
amount = trade["amount"]
|
amount = trade["amount"]
|
||||||
|
@ -35,34 +64,19 @@ def convert_trades_to_usd(account, trades):
|
||||||
print("BASE", base)
|
print("BASE", base)
|
||||||
print("QUOTE", quote)
|
print("QUOTE", quote)
|
||||||
print("AMOUNT", amount)
|
print("AMOUNT", amount)
|
||||||
amount_usd = to_currency(direction, account, amount, quote, "USD")
|
amount_usd = common.to_currency(direction, account, amount, base, "USD")
|
||||||
print("AMOUNT USD", amount_usd)
|
print("TRADE AMOUNT USD", amount_usd)
|
||||||
|
trade["trade_amount_usd"] = amount_usd
|
||||||
|
if "stop_loss_percent" in trade:
|
||||||
|
trade["stop_loss_usd"] = (trade["stop_loss_percent"] / 100) * amount_usd
|
||||||
|
if "take_profit_percent" in trade:
|
||||||
|
trade["take_profit_usd"] = (trade["take_profit_percent"] / 100) * amount_usd
|
||||||
|
if "trailing_stop_loss_percent" in trade:
|
||||||
|
trade["trailing_stop_loss_usd"] = (
|
||||||
|
trade["trailing_stop_loss_percent"] / 100
|
||||||
|
) * amount_usd
|
||||||
|
|
||||||
|
return trades
|
||||||
def get_pair(account, base, quote, invert=False):
|
|
||||||
"""
|
|
||||||
Get the pair for the given account and currencies.
|
|
||||||
:param account: Account object
|
|
||||||
:param base: Base currency
|
|
||||||
:param quote: Quote currency
|
|
||||||
:param invert: Invert the pair
|
|
||||||
:return: currency symbol, e.g. BTC_USD, BTC/USD, etc.
|
|
||||||
"""
|
|
||||||
# Currently we only have two exchanges with different pair separators
|
|
||||||
if account.exchange == "alpaca":
|
|
||||||
separator = "/"
|
|
||||||
elif account.exchange == "oanda":
|
|
||||||
separator = "_"
|
|
||||||
|
|
||||||
# Flip the pair if needed
|
|
||||||
if invert:
|
|
||||||
symbol = f"{quote.upper()}{separator}{base.upper()}"
|
|
||||||
else:
|
|
||||||
symbol = f"{base.upper()}{separator}{quote.upper()}"
|
|
||||||
# Check it exists
|
|
||||||
if symbol not in account.supported_symbols:
|
|
||||||
return False
|
|
||||||
return symbol
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_quote(exchange, symbol):
|
def get_base_quote(exchange, symbol):
|
||||||
|
@ -80,49 +94,6 @@ def get_base_quote(exchange, symbol):
|
||||||
return (base, quote)
|
return (base, quote)
|
||||||
|
|
||||||
|
|
||||||
def to_currency(direction, account, amount, from_currency, to_currency):
|
|
||||||
"""
|
|
||||||
Convert an amount from one currency to another.
|
|
||||||
:param direction: Direction of the trade
|
|
||||||
:param account: Account object
|
|
||||||
:param amount: Amount to convert
|
|
||||||
:param from_currency: Currency to convert from
|
|
||||||
:param to_currency: Currency to convert to
|
|
||||||
:return: Converted amount
|
|
||||||
"""
|
|
||||||
inverted = False
|
|
||||||
|
|
||||||
# This is needed because OANDA has different values for bid and ask
|
|
||||||
if direction == "buy":
|
|
||||||
price_index = "bids"
|
|
||||||
elif direction == "sell":
|
|
||||||
price_index = "asks"
|
|
||||||
symbol = get_pair(account, from_currency, to_currency)
|
|
||||||
if not symbol:
|
|
||||||
symbol = get_pair(account, from_currency, to_currency, invert=True)
|
|
||||||
inverted = True
|
|
||||||
|
|
||||||
# Bit of a hack but it works
|
|
||||||
if not symbol:
|
|
||||||
log.error(f"Could not find symbol for {from_currency} -> {to_currency}")
|
|
||||||
raise Exception("Could not find symbol")
|
|
||||||
try:
|
|
||||||
prices = account.client.get_currencies([symbol])
|
|
||||||
except GenericAPIError as e:
|
|
||||||
log.error(f"Error getting currencies and inverted currencies: {e}")
|
|
||||||
return None
|
|
||||||
price = D(prices["prices"][0][price_index][0]["price"])
|
|
||||||
|
|
||||||
# If we had to flip base and quote, we need to use the reciprocal of the price
|
|
||||||
if inverted:
|
|
||||||
price = D(1.0) / price
|
|
||||||
|
|
||||||
# Convert the amount to the destination currency
|
|
||||||
converted = D(amount) * price
|
|
||||||
|
|
||||||
return converted
|
|
||||||
|
|
||||||
|
|
||||||
def get_price(account, direction, symbol):
|
def get_price(account, direction, symbol):
|
||||||
"""
|
"""
|
||||||
Get the price for a given symbol.
|
Get the price for a given symbol.
|
||||||
|
@ -168,7 +139,7 @@ def get_trade_size_in_base(direction, account, strategy, cash_balance, base):
|
||||||
if account.currency.lower() == base.lower():
|
if account.currency.lower() == base.lower():
|
||||||
trade_size_in_base = amount_fiat
|
trade_size_in_base = amount_fiat
|
||||||
else:
|
else:
|
||||||
trade_size_in_base = to_currency(
|
trade_size_in_base = common.to_currency(
|
||||||
direction, account, amount_fiat, account.currency, base
|
direction, account, amount_fiat, account.currency, base
|
||||||
)
|
)
|
||||||
log.debug(f"Trade size in base: {trade_size_in_base}")
|
log.debug(f"Trade size in base: {trade_size_in_base}")
|
||||||
|
@ -381,7 +352,7 @@ def execute_strategy(callback, strategy, func):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the pair we are trading
|
# Get the pair we are trading
|
||||||
symbol = get_pair(account, base, quote)
|
symbol = common.get_pair(account, base, quote)
|
||||||
if not symbol:
|
if not symbol:
|
||||||
sendmsg(user, f"Symbol not supported by account: {symbol}", title="Error")
|
sendmsg(user, f"Symbol not supported by account: {symbol}", title="Error")
|
||||||
log.error(f"Symbol not supported by account: {symbol}")
|
log.error(f"Symbol not supported by account: {symbol}")
|
||||||
|
|
|
@ -1,19 +1,41 @@
|
||||||
|
from decimal import Decimal as D
|
||||||
|
|
||||||
|
from core.exchanges import convert
|
||||||
|
from core.models import Trade
|
||||||
|
from core.trading import market
|
||||||
|
|
||||||
|
|
||||||
def check_max_loss(risk_model, initial_balance, account_balance):
|
def check_max_loss(risk_model, initial_balance, account_balance):
|
||||||
"""
|
"""
|
||||||
Check that the account balance is within the max loss limit.
|
Check that the account balance is within the max loss limit.
|
||||||
"""
|
"""
|
||||||
|
# print("Max loss percent", risk_model.max_loss_percent)
|
||||||
|
# print("Initial balance", initial_balance)
|
||||||
|
# print("Account balance", account_balance)
|
||||||
|
# max_loss_percent = risk_model.max_loss_percent
|
||||||
|
# print("Max loss ratio", (max_loss_percent / 100))
|
||||||
|
# max_loss = initial_balance * (max_loss_percent / 100)
|
||||||
|
# print("Max loss", max_loss)
|
||||||
|
# return account_balance > max_loss
|
||||||
|
|
||||||
max_loss_percent = risk_model.max_loss_percent
|
max_loss_percent = risk_model.max_loss_percent
|
||||||
max_loss = initial_balance * (max_loss_percent / 100)
|
|
||||||
return account_balance > max_loss
|
# calculate the inverse of the max loss percent as a ratio
|
||||||
|
inverse_loss_multiplier = 1 - max_loss_percent / 100
|
||||||
|
minimum_balance = initial_balance * inverse_loss_multiplier
|
||||||
|
|
||||||
|
return account_balance > minimum_balance
|
||||||
|
|
||||||
|
|
||||||
def check_max_risk(risk_model, account_balance_usd, account_trades):
|
def check_max_risk(risk_model, account_balance_usd, account_trades):
|
||||||
"""
|
"""
|
||||||
Check that all of the trades in the account are within the max risk limit.
|
Check that all of the trades in the account are within the max risk limit.
|
||||||
"""
|
"""
|
||||||
max_risk_percent = risk_model.max_risk_percent
|
max_risk_percent = D(risk_model.max_risk_percent)
|
||||||
|
print("Max risk percent", max_risk_percent)
|
||||||
# Calculate the max risk of the account in USD
|
# Calculate the max risk of the account in USD
|
||||||
max_risk_usd = account_balance_usd * (max_risk_percent / 100)
|
max_risk_usd = account_balance_usd * (max_risk_percent / D(100))
|
||||||
|
print("Max risk USD", max_risk_usd)
|
||||||
total_risk = 0
|
total_risk = 0
|
||||||
for trade in account_trades:
|
for trade in account_trades:
|
||||||
max_tmp = []
|
max_tmp = []
|
||||||
|
@ -24,8 +46,13 @@ def check_max_risk(risk_model, account_balance_usd, account_trades):
|
||||||
max_tmp.append(trade["stop_loss_usd"])
|
max_tmp.append(trade["stop_loss_usd"])
|
||||||
if "trailing_stop_loss_usd" in trade:
|
if "trailing_stop_loss_usd" in trade:
|
||||||
max_tmp.append(trade["trailing_stop_loss_usd"])
|
max_tmp.append(trade["trailing_stop_loss_usd"])
|
||||||
total_risk += max(max_tmp)
|
if max_tmp:
|
||||||
|
print("MAX TMP", max_tmp)
|
||||||
|
total_risk += max(max_tmp)
|
||||||
|
|
||||||
|
print("total risk", total_risk)
|
||||||
allowed = total_risk < max_risk_usd
|
allowed = total_risk < max_risk_usd
|
||||||
|
print("check amx risk allowed", allowed)
|
||||||
return allowed
|
return allowed
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +60,7 @@ def check_max_open_trades(risk_model, account_trades):
|
||||||
"""
|
"""
|
||||||
Check that the number of trades in the account is within the max open trades limit.
|
Check that the number of trades in the account is within the max open trades limit.
|
||||||
"""
|
"""
|
||||||
|
print("LEN ACCOUNT TRADES", len(account_trades))
|
||||||
return len(account_trades) < risk_model.max_open_trades
|
return len(account_trades) < risk_model.max_open_trades
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,14 +88,25 @@ def check_risk(risk_model, account, proposed_trade):
|
||||||
max_loss_check = check_max_loss(
|
max_loss_check = check_max_loss(
|
||||||
risk_model, account.initial_balance, account.client.get_balance()
|
risk_model, account.initial_balance, account.client.get_balance()
|
||||||
)
|
)
|
||||||
|
print("Max loss check", max_loss_check)
|
||||||
if not max_loss_check:
|
if not max_loss_check:
|
||||||
return {"allowed": False, "reason": "Maximum loss exceeded."}
|
return {"allowed": False, "reason": "Maximum loss exceeded."}
|
||||||
|
|
||||||
# Check that the account max trades is not exceeded
|
# Check that the account max trades is not exceeded
|
||||||
account_trades = account.client.get_all_open_trades() # TODO
|
account_trades = account.client.get_all_open_trades()
|
||||||
account_trades.append(proposed_trade) # TODO
|
print("Account trades: ", account_trades)
|
||||||
|
if isinstance(proposed_trade, Trade):
|
||||||
|
proposed_trade = proposed_trade.__dict__
|
||||||
|
account_trades.append(proposed_trade)
|
||||||
|
print("After append", account_trades)
|
||||||
|
|
||||||
|
account_trades = convert.convert_trades(account_trades)
|
||||||
|
print("After convert", account_trades)
|
||||||
|
account_trades = market.convert_trades_to_usd(account, account_trades)
|
||||||
|
print("After convert to USD", account_trades)
|
||||||
|
|
||||||
max_open_trades_check = check_max_open_trades(risk_model, account_trades)
|
max_open_trades_check = check_max_open_trades(risk_model, account_trades)
|
||||||
|
print("Max open trades check: ", max_open_trades_check)
|
||||||
if not max_open_trades_check:
|
if not max_open_trades_check:
|
||||||
return {"allowed": False, "reason": "Maximum open trades exceeded."}
|
return {"allowed": False, "reason": "Maximum open trades exceeded."}
|
||||||
|
|
||||||
|
@ -75,10 +114,16 @@ def check_risk(risk_model, account, proposed_trade):
|
||||||
max_open_trades_per_symbol_check = check_max_open_trades_per_symbol(
|
max_open_trades_per_symbol_check = check_max_open_trades_per_symbol(
|
||||||
risk_model, account_trades
|
risk_model, account_trades
|
||||||
)
|
)
|
||||||
|
print("Max open trades per symbol check: ", max_open_trades_per_symbol_check)
|
||||||
if not max_open_trades_per_symbol_check:
|
if not max_open_trades_per_symbol_check:
|
||||||
return {"allowed": False, "reason": "Maximum open trades per symbol exceeded."}
|
return {"allowed": False, "reason": "Maximum open trades per symbol exceeded."}
|
||||||
|
|
||||||
# Check that the max risk is not exceeded
|
# Check that the max risk is not exceeded
|
||||||
max_risk_check = check_max_risk(risk_model, account_trades)
|
account_balance_usd = account.client.get_balance(return_usd=True)
|
||||||
|
print("Account balance USD (not)", account_balance_usd)
|
||||||
|
max_risk_check = check_max_risk(risk_model, account_balance_usd, account_trades)
|
||||||
|
print("Max risk check: ", max_risk_check)
|
||||||
if not max_risk_check:
|
if not max_risk_check:
|
||||||
return {"allowed": False, "reason": "Maximum risk exceeded."}
|
return {"allowed": False, "reason": "Maximum risk exceeded."}
|
||||||
|
|
||||||
|
return {"allowed": True}
|
||||||
|
|
Loading…
Reference in New Issue