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 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):

View File

@ -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,7 +165,8 @@ class Account(models.Model):
"""
Override the save function to update supported symbols.
"""
self.update_info(save=False)
if self.exchange != "fake":
self.update_info(save=False)
super().save(*args, **kwargs)
def get_client(self):

View File

@ -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",
end_day=1, # Monday
end_time="16:00",
start_day=1, # Monday
start_time=self.time_8,
end_day=1, # Monday
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"))

View File

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

View File

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

View File

@ -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",
)
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}")
within_trends = checks.within_trends(strategy, symbol, direction)
if not within_trends:
return
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))

View File

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