I am currently building a Django application to track and analyse my stock porfolio. I currently have two [model] classes: Stock and Transaction. A stock holds information about a company and the transaction records all bought/sold shares from a stock.
In my Stock class I have a 'stock' attribute. This is simply a holding attribute. All methods for updating this are done via the Transaction model (e.g. when I sell or buy shares). My reasoning is that everytime I view the object page, or call it, I don't have to perform a calculation to determin the amount of shares a stock own; this happens when we are selling or buying.
The question I have is: is this a good way of writing software?
Personally, I think this is a pros and cons case. But it would be nice to hear others thoughts and experiance. Perhaps I should just move the methods to the Stock model? Many things to consider here...
Stock:
class Stock(models.Model):
"""
Stock model refrences an organisation on the stock market.
"""
# DESC: Name of the company.
name = models.CharField(
unique=True, max_length=50, validators=[utils.alphabetical_chars_only]
)
# DESC: Uniquely identifies a stock globally.
isin = models.CharField(
unique=True,
verbose_name="ISIN",
max_length=ISIN_LEN,
validators=[MinLengthValidator(ISIN_LEN)],
)
# DESC: It's name in the market.
symbol = models.CharField(
unique=True, max_length=12, validators=[utils.alphabetical_chars_only]
)
# DESC: The number of shares we own.
# NOTE:
# - We don't directly access with this field. All operations are done through
# the transaction models.
shares = models.PositiveIntegerField(default=0)
# DESC: Store our profit or loss.
profits = models.FloatField(default=DEFAULT_FLOAT_VAL)
# DESC: Which exchange our stock belongs on.
exchange = models.ForeignKey("Exchange", on_delete=models.DO_NOTHING)
slug = models.SlugField(unique=True, max_length=12)
def __str__(self):
return "%s: %s" % (self.symbol, self.name)
def get_absolute_url(self):
return reverse("stocks:stock_view", kwargs={"slug": self.slug})
def total_amount_recieved_from_dividends(self):
dividends = Dividend.objects.filter(stock__id=self.id).values_list('amount', flat=True)
# If we have recieved none then return 0
if not dividends:
return 0
return sum(dividends)
Transaction:
class Transaction(models.Model):
"""
Transaction model refrences all recorded share transactions.
"""
BUY = "Buy"
SELL = "Sell"
TRANSACTION_TYPE_CHOICES = [(BUY, BUY), (SELL, SELL)]
objects = TransactionQuerySet.as_manager()
# DESC: What shares we buying from a stock we own.
stock = models.ForeignKey("Stock", on_delete=models.CASCADE)
# DESC: What type of transaction is this? (buy or sell)
tt = models.CharField(
max_length=4,
choices=TRANSACTION_TYPE_CHOICES,
default=BUY,
verbose_name="Transaction type",
)
# DESC: How many shares we have bought.
quantity = models.PositiveIntegerField(default=0)
# DESC: Price per share.
pps = models.FloatField(
default=0.0,
validators=[MinValueValidator(DEFAULT_FLOAT_VAL)],
verbose_name="Price per share",
)
# DESC: Cost of commission.
commission = models.FloatField(
default=DEFAULT_FLOAT_VAL, validators=[MinValueValidator(DEFAULT_FLOAT_VAL)]
)
# DESC: Stamp duty
sd = models.FloatField(
default=DEFAULT_FLOAT_VAL,
validators=[MinValueValidator(DEFAULT_FLOAT_VAL)],
verbose_name="Stamp duty",
)
# DESC: Datetime.
dt = CustomDateTimeField(verbose_name="Datetime")
# DESC: Unique ID of the order.
order_id = models.CharField(
unique=True,
max_length=ORDER_ID_LEN,
validators=[MinLengthValidator(ORDER_ID_LEN)],
)
def __str__(self):
return "%s: %i - %s" % (self.stock, self.quantity, self.dt)
def get_absolute_url(self):
return reverse("stocks:stock_view", kwargs={"slug": self.stock.slug})
def check_if_share_quantity_is_zero(self):
# DESC: Prevent the user from selling nothing.
if self.quantity is 0:
raise ValidationError("This value cannot be 0.")
def add_or_sell_shares(self):
if self.tt == Transaction.BUY:
self.stock.shares += self.quantity
else:
if self.stock.shares - self.quantity < 0:
raise ValidationError(
"You are trying to sell more shares than you own!"
)
self.stock.shares -= self.quantity
self.stock.save()
def clean(self):
self.check_if_share_quantity_is_zero()
self.add_or_sell_shares()
def delete(self, *args, **kwargs):
if self.tt == Transaction.BUY:
self.stock.shares -= self.quantity
else:
if self.stock.shares - self.quantity < 0:
raise ValidationError(
"You are trying to sell more shares than you own!"
)
self.stock.shares += self.quantity
self.stock.save()
super(Transaction, self).delete(*args, **kwargs)