Move more checks from market into checks library

This commit is contained in:
Mark Veidemanis 2023-02-17 07:20:28 +00:00
parent da67177a18
commit dd3b3521d9
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
8 changed files with 274 additions and 87 deletions

57
core/exchanges/fake.py Normal file
View File

@ -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

View File

@ -5,8 +5,8 @@ from asgiref.sync import sync_to_async
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from core.models import Strategy from core.models import Strategy
from core.util import logs
from core.trading import active_management from core.trading import active_management
from core.util import logs
log = logs.get_logger("scheduling") log = logs.get_logger("scheduling")
@ -24,8 +24,7 @@ async def job():
log.debug(f"Found {len(strategies)} strategies") log.debug(f"Found {len(strategies)} strategies")
for strategy in strategies: for strategy in strategies:
log.debug(f"Running strategy {strategy.name}") log.debug(f"Running strategy {strategy.name}")
ams = active_management.ActiveManagement(strategy) ams = active_management.ActiveManagement(strategy) # noqa
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -7,12 +7,13 @@ from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from core.exchanges.alpaca import AlpacaExchange from core.exchanges.alpaca import AlpacaExchange
from core.exchanges.fake import FakeExchange
from core.exchanges.oanda import OANDAExchange from core.exchanges.oanda import OANDAExchange
from core.lib.customers import get_or_create, update_customer_fields from core.lib.customers import get_or_create, update_customer_fields
from core.util import logs from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange} EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange, "fake": FakeExchange}
TYPE_CHOICES = ( TYPE_CHOICES = (
("market", "Market"), ("market", "Market"),
("limit", "Limit"), ("limit", "Limit"),
@ -125,7 +126,7 @@ class User(AbstractUser):
class Account(models.Model): 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) user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
exchange = models.CharField(choices=EXCHANGE_CHOICES, 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. Override the save function to update supported symbols.
""" """
if self.exchange != "fake":
self.update_info(save=False) self.update_info(save=False)
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -1,26 +1,106 @@
from datetime import time
import freezegun
from django.test import TestCase from django.test import TestCase
from core.models import (
Account,
Hook,
OrderSettings,
RiskModel,
Signal,
Strategy,
TradingTime,
User,
)
from core.trading import checks from core.trading import checks
from core.models import TradingTime, Strategy, OrderSettings, User
class ChecksTestCase(TestCase): class ChecksTestCase(TestCase):
def setUp(self): def setUp(self):
self.time_8 = time(8, 0, 0)
self.time_16 = time(16, 0, 0)
self.user = User.objects.create_user( self.user = User.objects.create_user(
username="testuser", email="test@example.com", password="test" 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( self.trading_time_now = TradingTime.objects.create(
user=self.user, user=self.user,
name="Test Trading Time", name="Test Trading Time",
start_day=1, # Monday start_day=1, # Monday
start_time="08:00", start_time=self.time_8,
end_day=1, # Monday 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): @freezegun.freeze_time("2023-02-13T17:00:00") # Monday at 17:00
pass 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"))

View File

@ -1,3 +1,16 @@
class ActiveManagement(object): class ActiveManagement(object):
def __init__(self, strategy): def __init__(self, strategy):
self.strategy = 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

View File

@ -1,9 +1,16 @@
from datetime import datetime from datetime import datetime
from decimal import Decimal as D
from core.lib.notify import sendmsg
from core.util import logs from core.util import logs
log = logs.get_logger("checks") log = logs.get_logger("checks")
def run_checks(strategy, x):
pass
def within_trading_times(strategy, ts=None): def within_trading_times(strategy, ts=None):
if not ts: if not ts:
ts = datetime.utcnow() ts = datetime.utcnow()
@ -17,3 +24,85 @@ def within_trading_times(strategy, ts=None):
log.debug("Not within trading time range") log.debug("Not within trading time range")
return False return False
return True 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

View File

@ -1,4 +1,3 @@
from datetime import datetime
from decimal import Decimal as D from decimal import Decimal as D
from core.exchanges import common from core.exchanges import common
@ -172,7 +171,7 @@ def get_tp_sl(direction, strategy, price, round_to=None):
return cast 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. 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 * 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 :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 # Convert the maximum price slippage to a ratio
if strategy.risk_model is not None: if strategy.risk_model is not None:
price_slippage_as_ratio = D(strategy.risk_model.price_slippage_percent) / D(100) 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. # Only check times for entries. We can always exit trades and set trends.
if func == "entry": if func == "entry":
# Check if we can trade now! within_trading_times = checks.within_trading_times(strategy)
now_utc = datetime.utcnow() if not within_trading_times:
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")
return return
# Don't touch the account if it's disabled. # 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"Callback price: {price}")
log.debug(f"Current price: {current_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 # 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: if not price_bound:
return return
price_bound = round(price_bound, display_precision) price_bound = round(price_bound, display_precision)
@ -390,34 +372,9 @@ def execute_strategy(callback, strategy, func):
return return
# Check if we are trading against the trend # Check if we are trading against the trend
if strategy.trend_signals.exists(): within_trends = checks.within_trends(strategy, symbol, direction)
if strategy.trends is None: if not within_trends:
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",
)
return 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 type = strategy.order_settings.order_type
@ -435,18 +392,6 @@ def execute_strategy(callback, strategy, func):
protection = get_tp_sl( protection = get_tp_sl(
direction, strategy, current_price, round_to=display_precision 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 # Create object, note that the amount is rounded to the trade precision
amount_rounded = float(round(trade_size_in_base, trade_precision)) amount_rounded = float(round(trade_size_in_base, trade_precision))

View File

@ -22,6 +22,8 @@ oandapyV20
glom glom
elasticsearch elasticsearch
apscheduler apscheduler
# For trading time checks
freezegun
git+https://git.zm.is/XF/django-crud-mixins git+https://git.zm.is/XF/django-crud-mixins
# pyroscope-io # pyroscope-io
# For caching # For caching