Move more checks from market into checks library
This commit is contained in:
parent
da67177a18
commit
dd3b3521d9
|
@ -0,0 +1,57 @@
|
|||
from core.exchanges import BaseExchange
|
||||
|
||||
|
||||
class FakeExchange(BaseExchange):
|
||||
def call_method(self, request):
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
pass
|
||||
|
||||
def get_account(self):
|
||||
pass
|
||||
|
||||
def get_instruments(self):
|
||||
pass
|
||||
|
||||
def get_currencies(self, currencies):
|
||||
pass
|
||||
|
||||
def get_supported_assets(self, response=None):
|
||||
pass
|
||||
|
||||
def get_balance(self, return_usd=False):
|
||||
pass
|
||||
|
||||
def get_market_value(self, symbol):
|
||||
pass
|
||||
|
||||
def post_trade(self, trade):
|
||||
pass
|
||||
|
||||
def close_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def update_trade(self, trade):
|
||||
pass
|
||||
|
||||
def cancel_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_position_info(self, symbol):
|
||||
pass
|
||||
|
||||
def get_all_positions(self):
|
||||
pass
|
||||
|
||||
def get_all_open_trades(self):
|
||||
pass
|
||||
|
||||
def close_position(self, side, symbol):
|
||||
pass
|
||||
|
||||
def close_all_positions(self):
|
||||
pass
|
|
@ -5,8 +5,8 @@ from asgiref.sync import sync_to_async
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import Strategy
|
||||
from core.util import logs
|
||||
from core.trading import active_management
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
||||
|
@ -24,8 +24,7 @@ async def job():
|
|||
log.debug(f"Found {len(strategies)} strategies")
|
||||
for strategy in strategies:
|
||||
log.debug(f"Running strategy {strategy.name}")
|
||||
ams = active_management.ActiveManagement(strategy)
|
||||
|
||||
ams = active_management.ActiveManagement(strategy) # noqa
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
|
@ -7,12 +7,13 @@ from django.contrib.auth.models import AbstractUser
|
|||
from django.db import models
|
||||
|
||||
from core.exchanges.alpaca import AlpacaExchange
|
||||
from core.exchanges.fake import FakeExchange
|
||||
from core.exchanges.oanda import OANDAExchange
|
||||
from core.lib.customers import get_or_create, update_customer_fields
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange}
|
||||
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange, "fake": FakeExchange}
|
||||
TYPE_CHOICES = (
|
||||
("market", "Market"),
|
||||
("limit", "Limit"),
|
||||
|
@ -125,7 +126,7 @@ class User(AbstractUser):
|
|||
|
||||
|
||||
class Account(models.Model):
|
||||
EXCHANGE_CHOICES = (("alpaca", "Alpaca"), ("oanda", "OANDA"))
|
||||
EXCHANGE_CHOICES = (("alpaca", "Alpaca"), ("oanda", "OANDA"), ("fake", "Fake"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
exchange = models.CharField(choices=EXCHANGE_CHOICES, max_length=255)
|
||||
|
@ -164,6 +165,7 @@ class Account(models.Model):
|
|||
"""
|
||||
Override the save function to update supported symbols.
|
||||
"""
|
||||
if self.exchange != "fake":
|
||||
self.update_info(save=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -1,26 +1,106 @@
|
|||
from datetime import time
|
||||
|
||||
import freezegun
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import (
|
||||
Account,
|
||||
Hook,
|
||||
OrderSettings,
|
||||
RiskModel,
|
||||
Signal,
|
||||
Strategy,
|
||||
TradingTime,
|
||||
User,
|
||||
)
|
||||
from core.trading import checks
|
||||
|
||||
from core.models import TradingTime, Strategy, OrderSettings, User
|
||||
|
||||
class ChecksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.time_8 = time(8, 0, 0)
|
||||
self.time_16 = time(16, 0, 0)
|
||||
self.user = User.objects.create_user(
|
||||
username="testuser", email="test@example.com", password="test"
|
||||
)
|
||||
self.order_settings = OrderSettings.objects.create(user=self.user, name="Default")
|
||||
self.account = Account.objects.create(
|
||||
user=self.user,
|
||||
name="Test Account",
|
||||
exchange="fake",
|
||||
)
|
||||
self.order_settings = OrderSettings.objects.create(
|
||||
user=self.user, name="Default"
|
||||
)
|
||||
self.trading_time_now = TradingTime.objects.create(
|
||||
user=self.user,
|
||||
name="Test Trading Time",
|
||||
start_day=1, # Monday
|
||||
start_time="08:00",
|
||||
start_time=self.time_8,
|
||||
end_day=1, # Monday
|
||||
end_time="16:00",
|
||||
end_time=self.time_16,
|
||||
)
|
||||
self.risk_model = RiskModel.objects.create(
|
||||
user=self.user,
|
||||
name="Test Risk Model",
|
||||
max_loss_percent=50,
|
||||
max_risk_percent=10,
|
||||
max_open_trades=10,
|
||||
max_open_trades_per_symbol=5,
|
||||
)
|
||||
|
||||
self.strategy = Strategy.objects.create(
|
||||
user=self.user,
|
||||
name="Test Strategy",
|
||||
account=self.account,
|
||||
order_settings=self.order_settings,
|
||||
risk_model=self.risk_model,
|
||||
active_management_enabled=True,
|
||||
)
|
||||
self.strategy.trading_times.set([self.trading_time_now])
|
||||
self.strategy.save()
|
||||
|
||||
self.strategy = Strategy.objects.create(user=self.user, name="Test Strategy", )
|
||||
@freezegun.freeze_time("2023-02-13T09:00:00") # Monday at 09:00
|
||||
def test_within_trading_times_pass(self):
|
||||
self.assertTrue(checks.within_trading_times(self.strategy))
|
||||
|
||||
def test_within_trading_times(self):
|
||||
pass
|
||||
@freezegun.freeze_time("2023-02-13T17:00:00") # Monday at 17:00
|
||||
def test_within_trading_times_fail(self):
|
||||
self.assertFalse(checks.within_trading_times(self.strategy))
|
||||
|
||||
def test_within_callback_price_deviation_fail(self):
|
||||
price_callback = 100
|
||||
current_price = 200
|
||||
self.assertFalse(
|
||||
checks.within_callback_price_deviation(
|
||||
self.strategy, price_callback, current_price
|
||||
)
|
||||
)
|
||||
|
||||
def test_within_callback_price_deviation_pass(self):
|
||||
price_callback = 100
|
||||
current_price = 100.5
|
||||
self.assertTrue(
|
||||
checks.within_callback_price_deviation(
|
||||
self.strategy, price_callback, current_price
|
||||
)
|
||||
)
|
||||
|
||||
def test_within_trends(self):
|
||||
hook = Hook.objects.create(
|
||||
user=self.user,
|
||||
name="Test Hook",
|
||||
)
|
||||
signal = Signal.objects.create(
|
||||
user=self.user,
|
||||
name="Test Signal",
|
||||
hook=hook,
|
||||
type="trend",
|
||||
)
|
||||
self.strategy.trend_signals.set([signal])
|
||||
self.strategy.trends = {"EUR_USD": "buy"}
|
||||
self.strategy.save()
|
||||
self.assertTrue(checks.within_trends(self.strategy, "EUR_USD", "buy"))
|
||||
self.assertFalse(checks.within_trends(self.strategy, "EUR_USD", "sell"))
|
||||
|
||||
self.assertIsNone(checks.within_trends(self.strategy, "EUR_XXX", "buy"))
|
||||
self.assertIsNone(checks.within_trends(self.strategy, "EUR_XXX", "sell"))
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
class ActiveManagement(object):
|
||||
def __init__(self, strategy):
|
||||
self.strategy = strategy
|
||||
|
||||
def run_checks(self):
|
||||
pass
|
||||
# Trading Time
|
||||
# Max loss
|
||||
# Trends
|
||||
# Asset Groups
|
||||
# Position Size
|
||||
# Protection
|
||||
# Max open positions
|
||||
# Max open positions per asset
|
||||
# Max risk
|
||||
# Crossfilter
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
from datetime import datetime
|
||||
from decimal import Decimal as D
|
||||
|
||||
from core.lib.notify import sendmsg
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("checks")
|
||||
|
||||
|
||||
def run_checks(strategy, x):
|
||||
pass
|
||||
|
||||
|
||||
def within_trading_times(strategy, ts=None):
|
||||
if not ts:
|
||||
ts = datetime.utcnow()
|
||||
|
@ -17,3 +24,85 @@ def within_trading_times(strategy, ts=None):
|
|||
log.debug("Not within trading time range")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def within_callback_price_deviation(strategy, price, current_price):
|
||||
# Convert the callback price deviation to a ratio
|
||||
if strategy.risk_model is not None:
|
||||
callback_price_deviation_as_ratio = D(
|
||||
strategy.risk_model.callback_price_deviation_percent
|
||||
) / D(100)
|
||||
else:
|
||||
callback_price_deviation_as_ratio = D(0.5) / 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")
|
||||
return True
|
||||
else:
|
||||
log.error("Current price is not within price deviation of callback price")
|
||||
log.debug(f"Difference: {abs(current_price - price)}")
|
||||
return False
|
||||
|
||||
|
||||
def within_max_loss(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_trends(strategy, symbol, direction):
|
||||
if strategy.trend_signals.exists():
|
||||
if strategy.trends is None:
|
||||
log.debug("Refusing to trade with no trend signals received")
|
||||
sendmsg(
|
||||
strategy.user,
|
||||
f"Refusing to trade {symbol} with no trend signals received",
|
||||
title="Trend not ready",
|
||||
)
|
||||
return None
|
||||
if symbol not in strategy.trends:
|
||||
log.debug("Refusing to trade asset without established trend")
|
||||
sendmsg(
|
||||
strategy.user,
|
||||
f"Refusing to trade {symbol} without established trend",
|
||||
title="Trend not ready",
|
||||
)
|
||||
return None
|
||||
else:
|
||||
if strategy.trends[symbol] != direction:
|
||||
log.debug("Refusing to trade against the trend")
|
||||
sendmsg(
|
||||
strategy.user,
|
||||
f"Refusing to trade {symbol} against the trend",
|
||||
title="Trend rejection",
|
||||
)
|
||||
return False
|
||||
else:
|
||||
log.debug(f"Trend check passed for {symbol} - {direction}")
|
||||
return True
|
||||
|
||||
|
||||
def within_position_size(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_protection(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_max_open_trades(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_max_open_trades_per_asset(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_max_risk(strategy):
|
||||
pass
|
||||
|
||||
|
||||
def within_crossfilter(strategy):
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from datetime import datetime
|
||||
from decimal import Decimal as D
|
||||
|
||||
from core.exchanges import common
|
||||
|
@ -172,7 +171,7 @@ def get_tp_sl(direction, strategy, price, round_to=None):
|
|||
return cast
|
||||
|
||||
|
||||
def get_price_bound(direction, strategy, price, current_price):
|
||||
def get_price_bound(direction, strategy, 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
|
||||
|
@ -187,25 +186,6 @@ def get_price_bound(direction, strategy, price, current_price):
|
|||
:return: Price bound
|
||||
"""
|
||||
|
||||
# Convert the callback price deviation to a ratio
|
||||
if strategy.risk_model is not None:
|
||||
callback_price_deviation_as_ratio = D(
|
||||
strategy.risk_model.callback_price_deviation_percent
|
||||
) / D(100)
|
||||
else:
|
||||
callback_price_deviation_as_ratio = D(0.5) / 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
|
||||
if strategy.risk_model is not None:
|
||||
price_slippage_as_ratio = D(strategy.risk_model.price_slippage_percent) / D(100)
|
||||
|
@ -277,15 +257,8 @@ def execute_strategy(callback, strategy, func):
|
|||
|
||||
# Only check times for entries. We can always exit trades and set trends.
|
||||
if func == "entry":
|
||||
# Check if we can trade now!
|
||||
now_utc = datetime.utcnow()
|
||||
trading_times = strategy.trading_times.all()
|
||||
if not trading_times:
|
||||
log.error("No trading times set for strategy")
|
||||
return
|
||||
matches = [x.within_range(now_utc) for x in trading_times]
|
||||
if not any(matches):
|
||||
log.debug("Not within trading time range")
|
||||
within_trading_times = checks.within_trading_times(strategy)
|
||||
if not within_trading_times:
|
||||
return
|
||||
|
||||
# Don't touch the account if it's disabled.
|
||||
|
@ -338,8 +311,17 @@ def execute_strategy(callback, strategy, func):
|
|||
log.debug(f"Callback price: {price}")
|
||||
log.debug(f"Current price: {current_price}")
|
||||
|
||||
# Check callback price deviation
|
||||
within_callback_price_deviation = checks.within_callback_price_deviation(
|
||||
strategy, price, current_price
|
||||
)
|
||||
if not within_callback_price_deviation:
|
||||
log.debug("Not within callback price deviation")
|
||||
return
|
||||
|
||||
# Calculate price bound and round to the display precision
|
||||
price_bound = get_price_bound(direction, strategy, price, current_price)
|
||||
# Also enforces max price slippage
|
||||
price_bound = get_price_bound(direction, strategy, current_price)
|
||||
if not price_bound:
|
||||
return
|
||||
price_bound = round(price_bound, display_precision)
|
||||
|
@ -390,34 +372,9 @@ def execute_strategy(callback, strategy, func):
|
|||
return
|
||||
|
||||
# Check if we are trading against the trend
|
||||
if strategy.trend_signals.exists():
|
||||
if strategy.trends is None:
|
||||
log.debug("Refusing to trade with no trend signals received")
|
||||
sendmsg(
|
||||
user,
|
||||
f"Refusing to trade {symbol} with no trend signals received",
|
||||
title="Trend not ready",
|
||||
)
|
||||
within_trends = checks.within_trends(strategy, symbol, direction)
|
||||
if not within_trends:
|
||||
return
|
||||
if symbol not in strategy.trends:
|
||||
log.debug("Refusing to trade asset without established trend")
|
||||
sendmsg(
|
||||
user,
|
||||
f"Refusing to trade {symbol} without established trend",
|
||||
title="Trend not ready",
|
||||
)
|
||||
return
|
||||
else:
|
||||
if strategy.trends[symbol] != direction:
|
||||
log.debug("Refusing to trade against the trend")
|
||||
sendmsg(
|
||||
user,
|
||||
f"Refusing to trade {symbol} against the trend",
|
||||
title="Trend rejection",
|
||||
)
|
||||
return
|
||||
else:
|
||||
log.debug(f"Trend check passed for {symbol} - {direction}")
|
||||
|
||||
type = strategy.order_settings.order_type
|
||||
|
||||
|
@ -435,18 +392,6 @@ def execute_strategy(callback, strategy, func):
|
|||
protection = get_tp_sl(
|
||||
direction, strategy, current_price, round_to=display_precision
|
||||
)
|
||||
# protection_cast = {}
|
||||
# if "sl" in protection:
|
||||
# protection_cast["stop_loss"] = float(round(protect
|
||||
# ion["sl"], display_precision))
|
||||
# if "tp" in protection:
|
||||
# protection_cast["take_profit"] = float(
|
||||
# round(protection["tp"], display_precision)
|
||||
# )
|
||||
# if "tsl" in protection:
|
||||
# protection_cast["trailing_stop_loss"] = float(
|
||||
# round(protection["tsl"], display_precision)
|
||||
# )
|
||||
|
||||
# Create object, note that the amount is rounded to the trade precision
|
||||
amount_rounded = float(round(trade_size_in_base, trade_precision))
|
||||
|
|
|
@ -22,6 +22,8 @@ oandapyV20
|
|||
glom
|
||||
elasticsearch
|
||||
apscheduler
|
||||
# For trading time checks
|
||||
freezegun
|
||||
git+https://git.zm.is/XF/django-crud-mixins
|
||||
# pyroscope-io
|
||||
# For caching
|
||||
|
|
Loading…
Reference in New Issue