"""
Models keeps track of all the persistent data around stocks
"""
import datetime
from datetime import date as os_date
import math
from django.db.models import Q
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.db import transaction
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator, MinValueValidator
from authentication.models import Profile
from .stock_helper import validate_ticker
[docs]class Stock(models.Model):
"""
Stock represents a single stock. For example GOOGL
"""
name = models.CharField(
max_length=255,
validators=[
MinLengthValidator(1, message="The name should not be empty.")
], )
ticker = models.CharField(
max_length=10,
unique=True,
validators=[
MinLengthValidator(1, message="The ticker should not be empty."),
validate_ticker
], )
[docs] def latest_quote(self, date=None):
"""
Returns the latest quote for the stock
"""
quote_query = self.daily_quote
if date is not None:
if isinstance(date, str):
date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
if isinstance(date, datetime.datetime):
date = date.date()
if date > datetime.datetime.now().date():
raise Exception("Date is later than now!")
quote_query = quote_query.filter(date__lte=date)
quote_query = quote_query.order_by('-date')
if quote_query:
return quote_query[0]
raise Exception("No quote found")
[docs] @staticmethod
def find_stock(text, first=None):
"""
Finds the stocks that contain >text<
"""
query = Stock.objects.filter(name__icontains=text)
if first:
query = query[:first]
return query
[docs] @staticmethod
def create_new_stock(ticker, name):
"""
Creates a new stock
"""
if not validate_ticker(ticker):
raise Exception("Invalid Ticker")
stock = Stock(name=name, ticker=ticker)
stock.save()
return stock
[docs] def quote_in_range(self, start=None, end=None):
"""
Returns a list of daily stock quotes in the given timerange
"""
query = self.daily_quote
if start:
query = query.filter(date__gte=start)
if end:
query = query.filter(date__lte=end)
query = query.order_by('date')
return query
[docs] def trades_for_profile(self, profile):
"""
Returns all trades the user made with this stock
"""
return self.trades.filter(account__profile=profile)
[docs]class DailyStockQuote(models.Model):
"""
DailyStockQuote is one day in the performance of a stock,
for example 2nd July GOOGL value is 281.31$
"""
value = models.FloatField(validators=[
MinValueValidator(
0.0, message="Daily stock quote can not be negative")
])
date = models.DateField()
stock = models.ForeignKey(Stock, related_name='daily_quote')
class Meta(object):
"""
We use this to define our uniqueness constraint
"""
unique_together = (
'stock',
'date', )
[docs]class InvestmentBucket(models.Model):
"""
An investment bucket represents a collection of stocks to invest in
"""
name = models.CharField(
max_length=255,
validators=[
MinLengthValidator(1, message="The name should not be empty.")
], )
owner = models.ForeignKey(Profile, related_name='owned_bucket')
public = models.BooleanField()
available = models.FloatField(validators=[
MinValueValidator(
0.0, message="The available money can not be negative.")
])
class Meta(object):
unique_together = ('name', 'owner')
[docs] @staticmethod
def accessible_buckets(profile):
"""
Finds all buckets that the user could view
"""
return InvestmentBucket.objects.filter(
Q(owner=profile) | Q(public=True))
[docs] @staticmethod
def create_new_bucket(name, public, owner, available=1000.0):
"""
Creates a new InvestmentBucket
"""
bucket = InvestmentBucket(
name=name, public=public, owner=owner, available=available)
bucket.save()
return bucket
[docs] def add_attribute(self, text, is_good=True):
"""
Adds an attribute to an investment bucket
"""
attribute = self.description.create(
text=text,
is_good=is_good, )
return attribute
[docs] def get_stock_configs(self, date=None):
"""
Get all associated configs
"""
if not date:
return self.stocks.filter(end=None)
return self.stocks.filter(
start__lte=date).filter(Q(end__gte=date) | Q(end=None))
def _sell_all(self):
"""
Sells all stocks held in the investment bucket
"""
with transaction.atomic():
current_configs = self.get_stock_configs()
balance_change = 0.0
for conf in current_configs:
balance_change += conf.value_on()
self.available += balance_change
current_configs.update(
end=datetime.datetime.now() - datetime.timedelta(days=31))
self.save()
[docs] def change_config(self, new_config):
"""
Changes the configuration of the investment bucket to new_config
"""
with transaction.atomic():
self._sell_all()
for conf in new_config:
stock = Stock.objects.get(id=conf.id)
quote = stock.latest_quote()
self.available -= quote.value * conf.quantity
self.stocks.create(
stock=stock,
quantity=conf.quantity,
start=datetime.datetime.now() - datetime.timedelta(
days=31), )
if self.available < 0:
raise Exception("Not enough money available")
self.save()
[docs] def value_on(self, date=None):
"""
The value of the bucket on a specific day
"""
values = []
for config in self.get_stock_configs(date):
try:
values.append(config.value_on(date))
except Exception: # pylint: disable=broad-except
pass
return sum(values) + self.available
[docs] def historical(self, count=None, skip=None):
"""
Fetches the historical value of the bucket.
"""
res = []
if count is None:
count = 30
if skip is None:
skip = 0
for i in range(skip, count + skip):
date = datetime.datetime.now().date() - datetime.timedelta(days=i)
res.append((date, self.value_on(date)))
return res
[docs]class InvestmentBucketDescription(models.Model):
"""
An investment bucket represents a collection of stocks to invest in
"""
text = models.CharField(
max_length=255,
validators=[
MinLengthValidator(
3,
message="The description should at least be 3 characters long."
)
])
bucket = models.ForeignKey(InvestmentBucket, related_name='description')
is_good = models.BooleanField()
class Meta(object):
unique_together = ('text', 'bucket')
[docs] def change_description(self, text):
"""
Changes the description to the given text
"""
self.text = text
self.save()
[docs]class InvestmentStockConfiguration(models.Model):
"""
Represents the configuration of how much of a stock to invest for a bucket
"""
quantity = models.FloatField(validators=[
MinValueValidator(0.0, message="The quantity can not be negative.")
])
stock = models.ForeignKey(Stock, related_name='bucket')
bucket = models.ForeignKey(InvestmentBucket, related_name='stocks')
start = models.DateField(default=os_date.today, blank=True)
end = models.DateField(null=True, blank=True)
[docs] def value_on(self, date=None):
"""
Returns the current value of the stock configuration
"""
value = self.stock.latest_quote(date).value * self.quantity
if math.isnan(value):
raise Exception("Not able to calculate value")
return value
[docs]@receiver(pre_save)
def pre_save_any(sender, instance, *_args, **_kwargs):
"""
Ensures that all constrains are met
"""
if sender.__name__ == 'Session':
return
try:
instance.full_clean()
except ValidationError as ex:
raise Exception(ex.messages[0])