From 4973582bdf19ee38094b091ddbe272f4f7a02031 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Fri, 25 Nov 2022 19:28:21 +0000 Subject: [PATCH] Implement trading time limits --- Makefile | 3 + core/lib/market.py | 21 +++-- ...0032_alter_tradingtime_end_day_and_more.py | 23 +++++ ...0033_alter_tradingtime_end_day_and_more.py | 23 +++++ .../0034_alter_strategy_trading_times.py | 18 ++++ ...0035_alter_tradingtime_end_day_and_more.py | 23 +++++ core/models.py | 74 ++++++++++++--- .../templates/partials/trading-time-list.html | 4 +- core/tests.py | 3 - core/tests/__init__.py | 0 core/tests/lib/test_market.py | 90 +++++++++++++++++++ 11 files changed, 257 insertions(+), 25 deletions(-) create mode 100644 core/migrations/0032_alter_tradingtime_end_day_and_more.py create mode 100644 core/migrations/0033_alter_tradingtime_end_day_and_more.py create mode 100644 core/migrations/0034_alter_strategy_trading_times.py create mode 100644 core/migrations/0035_alter_tradingtime_end_day_and_more.py delete mode 100644 core/tests.py create mode 100644 core/tests/__init__.py create mode 100644 core/tests/lib/test_market.py diff --git a/Makefile b/Makefile index 07327c1..d26b4fe 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ stop: log: docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env logs -f +test: + docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES)" + migrate: docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate" diff --git a/core/lib/market.py b/core/lib/market.py index 658e17d..4540403 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -1,3 +1,4 @@ +from datetime import datetime from decimal import Decimal as D from core.exchanges import GenericAPIError @@ -7,15 +8,6 @@ from core.util import logs log = logs.get_logger(__name__) -# def to_usd(account, amount, from_currency): -# if account.exchange == "alpaca": -# separator = "/" -# elif account.exchange == "oanda": -# separator = "_" -# symbol = f"{from_currency.upper()}{separator}{to_currency.upper()}" -# prices = account.client.get_currencies([symbol]) - - def get_pair(account, base, quote, invert=False): """ Get the pair for the given account and currencies. @@ -273,6 +265,17 @@ def execute_strategy(callback, strategy): :param strategy: Strategy object """ + # 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") + return + # Get the account's balance in the native account currency cash_balance = strategy.account.client.get_balance() log.debug(f"Cash balance: {cash_balance}") diff --git a/core/migrations/0032_alter_tradingtime_end_day_and_more.py b/core/migrations/0032_alter_tradingtime_end_day_and_more.py new file mode 100644 index 0000000..1bee980 --- /dev/null +++ b/core/migrations/0032_alter_tradingtime_end_day_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0031_strategy_trading_times'), + ] + + operations = [ + migrations.AlterField( + model_name='tradingtime', + name='end_day', + field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255), + ), + migrations.AlterField( + model_name='tradingtime', + name='start_day', + field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255), + ), + ] diff --git a/core/migrations/0033_alter_tradingtime_end_day_and_more.py b/core/migrations/0033_alter_tradingtime_end_day_and_more.py new file mode 100644 index 0000000..860423e --- /dev/null +++ b/core/migrations/0033_alter_tradingtime_end_day_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0032_alter_tradingtime_end_day_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tradingtime', + name='end_day', + field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]), + ), + migrations.AlterField( + model_name='tradingtime', + name='start_day', + field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]), + ), + ] diff --git a/core/migrations/0034_alter_strategy_trading_times.py b/core/migrations/0034_alter_strategy_trading_times.py new file mode 100644 index 0000000..c8f6d16 --- /dev/null +++ b/core/migrations/0034_alter_strategy_trading_times.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-11-25 18:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0033_alter_tradingtime_end_day_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='strategy', + name='trading_times', + field=models.ManyToManyField(to='core.tradingtime'), + ), + ] diff --git a/core/migrations/0035_alter_tradingtime_end_day_and_more.py b/core/migrations/0035_alter_tradingtime_end_day_and_more.py new file mode 100644 index 0000000..deae4bf --- /dev/null +++ b/core/migrations/0035_alter_tradingtime_end_day_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.3 on 2022-11-25 19:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0034_alter_strategy_trading_times'), + ] + + operations = [ + migrations.AlterField( + model_name='tradingtime', + name='end_day', + field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]), + ), + migrations.AlterField( + model_name='tradingtime', + name='start_day', + field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]), + ), + ] diff --git a/core/models.py b/core/models.py index 43b12eb..16717bc 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import stripe from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -25,13 +27,13 @@ TIF_CHOICES = ( ("ioc", "IOC (Immediate Or Cancel)"), ) DAY_CHOICES = ( - ("monday", "Monday"), - ("tuesday", "Tuesday"), - ("wednesday", "Wednesday"), - ("thursday", "Thursday"), - ("friday", "Friday"), - ("saturday", "Saturday"), - ("sunday", "Sunday"), + (1, "Monday"), + (2, "Tuesday"), + (3, "Wednesday"), + (4, "Thursday"), + (5, "Friday"), + (6, "Saturday"), + (7, "Sunday"), ) @@ -225,13 +227,63 @@ class TradingTime(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) - start_day = models.CharField(choices=DAY_CHOICES, max_length=255) - end_day = models.CharField(choices=DAY_CHOICES, max_length=255) + start_day = models.IntegerField(choices=DAY_CHOICES) + end_day = models.IntegerField(choices=DAY_CHOICES) start_time = models.TimeField() end_time = models.TimeField() + def within_range(self, ts): + """ + Check if the specified time is within the configured trading times. + :param ts: Timestamp + :type ts: datetime + :return: whether or not the time is within the trading range + :rtype: bool + """ + start_day = self.start_day + end_day = self.end_day + # Check the day is between the start and end day + if not start_day <= ts.weekday() + 1 <= end_day: + return False + + start_time = self.start_time + end_time = self.end_time + + # Get what the start time would be this week + ts_monday = ts - timedelta(days=ts.weekday()) + + # Now we need to add our day of week to monday + # Let's set the offset now since it's off by one + offset_start = start_day - 1 + # Datetime: monday=0, tuesday=1, us: monday=1, tuesday=2, so we need to subtract + # one from ours to not be off by one + offset_end = end_day - 1 + + # Now we can add the offset to the monday + start = ts_monday + timedelta(days=offset_start) + start = start.replace( + hour=start_time.hour, + minute=start_time.minute, + second=start_time.second, + microsecond=start_time.microsecond, + ) + end = ts_monday + timedelta(days=offset_end) + end = end.replace( + hour=end_time.hour, + minute=end_time.minute, + second=end_time.second, + microsecond=end_time.microsecond, + ) + # Check if the ts is between the start and end times + # ts must be more than start and less than end + return ts >= start and ts <= end + return True + def __str__(self): - return f"{self.name} ({self.start_day} at {self.start_time} - {self.end_day} at {self.end_time})" + return ( + f"{self.name} ({self.get_start_day_display()} at {self.start_time} -" + f"{self.get_end_day_display()} at {self.end_time})" + ) class Strategy(models.Model): @@ -239,7 +291,7 @@ class Strategy(models.Model): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) account = models.ForeignKey(Account, on_delete=models.CASCADE) - trading_times = models.ManyToManyField(TradingTime, blank=True) + trading_times = models.ManyToManyField(TradingTime) order_type = models.CharField( choices=TYPE_CHOICES, max_length=255, default="market" ) diff --git a/core/templates/partials/trading-time-list.html b/core/templates/partials/trading-time-list.html index 761f208..3d64ebe 100644 --- a/core/templates/partials/trading-time-list.html +++ b/core/templates/partials/trading-time-list.html @@ -22,8 +22,8 @@ {{ item.user }} {{ item.name }} {{ item.description }} - {{ item.start_day }} at {{ item.start_time }} - {{ item.end_day }} at {{ item.end_time }} + {{ item.get_start_day_display }} at {{ item.start_time }} + {{ item.get_end_day_display }} at {{ item.end_time }}