Implement trading time limits
This commit is contained in:
parent
69a2b269ad
commit
4973582bdf
3
Makefile
3
Makefile
|
@ -10,6 +10,9 @@ stop:
|
||||||
log:
|
log:
|
||||||
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env logs -f
|
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:
|
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"
|
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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
|
||||||
|
@ -7,15 +8,6 @@ from core.util import logs
|
||||||
log = logs.get_logger(__name__)
|
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):
|
def get_pair(account, base, quote, invert=False):
|
||||||
"""
|
"""
|
||||||
Get the pair for the given account and currencies.
|
Get the pair for the given account and currencies.
|
||||||
|
@ -273,6 +265,17 @@ def execute_strategy(callback, strategy):
|
||||||
:param strategy: Strategy object
|
: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
|
# Get the account's balance in the native account currency
|
||||||
cash_balance = strategy.account.client.get_balance()
|
cash_balance = strategy.account.client.get_balance()
|
||||||
log.debug(f"Cash balance: {cash_balance}")
|
log.debug(f"Cash balance: {cash_balance}")
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,5 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import stripe
|
import stripe
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
@ -25,13 +27,13 @@ TIF_CHOICES = (
|
||||||
("ioc", "IOC (Immediate Or Cancel)"),
|
("ioc", "IOC (Immediate Or Cancel)"),
|
||||||
)
|
)
|
||||||
DAY_CHOICES = (
|
DAY_CHOICES = (
|
||||||
("monday", "Monday"),
|
(1, "Monday"),
|
||||||
("tuesday", "Tuesday"),
|
(2, "Tuesday"),
|
||||||
("wednesday", "Wednesday"),
|
(3, "Wednesday"),
|
||||||
("thursday", "Thursday"),
|
(4, "Thursday"),
|
||||||
("friday", "Friday"),
|
(5, "Friday"),
|
||||||
("saturday", "Saturday"),
|
(6, "Saturday"),
|
||||||
("sunday", "Sunday"),
|
(7, "Sunday"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,13 +227,63 @@ class TradingTime(models.Model):
|
||||||
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)
|
||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
start_day = models.CharField(choices=DAY_CHOICES, max_length=255)
|
start_day = models.IntegerField(choices=DAY_CHOICES)
|
||||||
end_day = models.CharField(choices=DAY_CHOICES, max_length=255)
|
end_day = models.IntegerField(choices=DAY_CHOICES)
|
||||||
start_time = models.TimeField()
|
start_time = models.TimeField()
|
||||||
end_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):
|
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):
|
class Strategy(models.Model):
|
||||||
|
@ -239,7 +291,7 @@ class Strategy(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||||
trading_times = models.ManyToManyField(TradingTime, blank=True)
|
trading_times = models.ManyToManyField(TradingTime)
|
||||||
order_type = models.CharField(
|
order_type = models.CharField(
|
||||||
choices=TYPE_CHOICES, max_length=255, default="market"
|
choices=TYPE_CHOICES, max_length=255, default="market"
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
<td>{{ item.user }}</td>
|
<td>{{ item.user }}</td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<td>{{ item.description }}</td>
|
<td>{{ item.description }}</td>
|
||||||
<td>{{ item.start_day }} at {{ item.start_time }}</td>
|
<td>{{ item.get_start_day_display }} at {{ item.start_time }}</td>
|
||||||
<td>{{ item.end_day }} at {{ item.end_time }}</td>
|
<td>{{ item.get_end_day_display }} at {{ item.end_time }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from core.models import TradingTime, User
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
class MarketTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create a test user
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="testuser",
|
||||||
|
email="test@example.com",
|
||||||
|
)
|
||||||
|
self.time_9 = time(9, 0, 0)
|
||||||
|
self.time_11 = time(11, 0, 0)
|
||||||
|
self.time_14 = time(14, 0, 0)
|
||||||
|
self.time_16 = time(16, 0, 0)
|
||||||
|
|
||||||
|
def test_time_within_range(self):
|
||||||
|
"""
|
||||||
|
Check the method of TradingTime model works as expected.
|
||||||
|
We check two dates, one that is within the range and one that is not.
|
||||||
|
"""
|
||||||
|
test_time = datetime(2022, 11, 28, 10, 0, 0)
|
||||||
|
|
||||||
|
trading_time = TradingTime.objects.create(
|
||||||
|
user = self.user,
|
||||||
|
start_day = 1,
|
||||||
|
end_day = 1,
|
||||||
|
start_time = self.time_9,
|
||||||
|
end_time = self.time_11,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(trading_time.within_range(test_time))
|
||||||
|
|
||||||
|
def test_time_within_range_fail(self):
|
||||||
|
"""
|
||||||
|
Check the method of TradingTime model works as expected.
|
||||||
|
We check two dates, one that is within the range and one that is not.
|
||||||
|
"""
|
||||||
|
test_time = datetime(2022, 11, 28, 10, 0, 0)
|
||||||
|
trading_time = TradingTime.objects.create(
|
||||||
|
user = self.user,
|
||||||
|
start_day = 1,
|
||||||
|
end_day = 1,
|
||||||
|
start_time = self.time_14,
|
||||||
|
end_time = self.time_16,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(trading_time.within_range(test_time))
|
||||||
|
|
||||||
|
def test_time_within_range_across_days(self):
|
||||||
|
test_time = datetime(2022, 11, 28, 20, 0, 0)
|
||||||
|
|
||||||
|
trading_time = TradingTime.objects.create(
|
||||||
|
user = self.user,
|
||||||
|
start_day = 1, # Monday
|
||||||
|
end_day = 6, # Saturday
|
||||||
|
start_time = self.time_9,
|
||||||
|
end_time = self.time_11,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(trading_time.within_range(test_time))
|
||||||
|
|
||||||
|
def test_time_within_range_across_days_fail(self):
|
||||||
|
# Set test time Monday, 28 November, 2022, 13:00:00
|
||||||
|
test_time = datetime(2022, 11, 28, 13, 0, 0)
|
||||||
|
trading_time = TradingTime.objects.create(
|
||||||
|
user = self.user,
|
||||||
|
start_day = 1,
|
||||||
|
end_day = 6,
|
||||||
|
start_time = self.time_14,
|
||||||
|
end_time = self.time_16,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(trading_time.within_range(test_time))
|
||||||
|
|
||||||
|
def test_time_within_range_day_mismatch(self):
|
||||||
|
"""
|
||||||
|
Check that the day of the week is taken into account.
|
||||||
|
"""
|
||||||
|
test_time = datetime(2022, 11, 29, 8, 0, 0)
|
||||||
|
|
||||||
|
trading_time = TradingTime.objects.create(
|
||||||
|
user = self.user,
|
||||||
|
start_day = 2, # Tuesday
|
||||||
|
end_day = 6,
|
||||||
|
start_time = self.time_9,
|
||||||
|
end_time = self.time_11,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(trading_time.within_range(test_time))
|
Loading…
Reference in New Issue