【发布时间】:2018-01-27 15:28:47
【问题描述】:
我有一个简单的应用程序可以跟踪用户的股票投资组合。基本上用户可以购买 JNJ、AAPL 或 MCD 等股票。他也可以出售部分/全部。股息可能会在支付后立即进行再投资(就像用户购买相同股票的股息价值一样)。我需要按月计算这个投资组合价值。
简单示例:
交易:
+----------+--------+------------+-------+
| buy/sell | amount | date | price |
+----------+--------+------------+-------+
| buy | 5 | 2015-01-01 | $60 |
| sell | 1 | 2015-03-01 | $70 |
+----------+--------+------------+-------+
从这笔交易中,我想得到这本股票字典:
{
u'JNJ': {
datetime.date(2015, 6, 1): Decimal('5.00000'),
datetime.date(2015, 7, 1): Decimal('5.00000'),
datetime.date(2015, 8, 1): Decimal('4.00000'),
datetime.date(2015, 9, 1): Decimal('4.00000'),
datetime.date(2015, 10, 1): Decimal('4.00000')}
}
这些是我按月计算的份额。假设在 2015 年 8 月 21 日有 0.75 美元的股息,在同一天,我在该日期购买了 JNJ 的部分股票:
股息示例:
交易:
+----------+--------+------------+-------+
| buy/sell | amount | date | price |
+----------+--------+------------+-------+
| buy | 5 | 2015-01-01 | $60 |
| sell | 1 | 2015-03-01 | $70 |
+----------+--------+------------+-------+
股息:
+------------+--------+-------+
| date | amount | price |
+------------+--------+-------+
| 2015-08-21 | 0.75 | 64 |
+------------+--------+-------+
派发股息时,我持有 4 股。对于 4 股,我收到 4*0.75 美元,我买了 0.031393889 股 JNJ。 结果:
{u'JNJ':
{
datetime.date(2015, 6, 1): Decimal('5.00000'),
datetime.date(2015, 7, 1): Decimal('5.00000'),
datetime.date(2015, 8, 1): Decimal('4.031393889'),
datetime.date(2015, 9, 1): Decimal('4.031393889'),
datetime.date(2015, 10, 1): Decimal('4.031393889')}
}
所以这是我必须计算的。可能有任意数量的交易和股息。必须至少有一次 Buy 交易,但可能不存在股息。
这些是我在 models.py 中的类:
代表 Stock 的 Stock 模型,例如 JNJ。
class Stock(models.Model):
name = models.CharField("Stock's name", max_length=200, default="")
symbol = models.CharField("Stock's symbol", max_length=20, default="", db_index=True)
price = models.DecimalField(max_digits=30, decimal_places=5, null=True, blank=True)
比我有 StockTransaction,它代表一个股票的一个投资组合的对象。交易与 StockTransaction 相关联,因为点滴适用于所有交易。
class StockTransaction(models.Model):
stock = models.ForeignKey('stocks.Stock')
portfolio = models.ForeignKey(Portfolio, related_name="stock_transactions")
drip = models.BooleanField(default=False)
事务类:
BUYCHOICE = [(True,'Buy'),(False,'Sell')]
class Transaction(models.Model):
amount = models.DecimalField(max_digits=20, decimal_places=5, validators=[MinValueValidator(Decimal('0.0001'))])
buy = models.BooleanField(choices=BUYCHOICE, default=True)
date = models.DateField('buy date')
price = models.DecimalField('price per share', max_digits=20, decimal_places=5, validators=[MinValueValidator(Decimal('0.0001'))])
stock_transaction = models.ForeignKey(StockTransaction, related_name="transactions", null=False)
最后是股息类:
class Dividend(models.Model):
date = models.DateField('pay date', db_index=True)
amount = models.DecimalField(max_digits=20, decimal_places=10)
price = models.DecimalField('price per share', max_digits=20, decimal_places=10)
stock_transaction = models.ManyToManyField('portfolio.StockTransaction', related_name="dividends", blank=True)
stock = models.ForeignKey(Stock, related_name="dividends")
我已经编写了我的方法,但我确实认为有更好的方法。我的方法太长,并且需要很长时间来处理 106 只股票(每 5 笔交易)的投资组合。这是我的方法:
def get_portfolio_month_shares(portfolio_id):
"""
Return number of dividends and shares per month respectfully in dict
{symbol: {year: decimal, year: decimal} }
:param portfolio: portfolio object for which to calculate shares and dividends
:return: total dividends and amount of shares, respectfully
"""
total_shares, total_dividends = {}, {}
for stock_transaction in StockTransaction.objects.filter(portfolio_id=portfolio_id)\
.select_related('stock').prefetch_related('dividends', 'transactions', 'stock__dividends'):
shares = 0 #number of shares
monthly_shares, monthly_dividends = {}, {}
transactions = list(stock_transaction.transactions.all())
first_transaction = transactions[0]
for dividend in stock_transaction.stock.dividends.all():
if dividend.date < first_transaction.date:
continue
try:
#transactions that are older than last dividend
while transactions[0].date < dividend.date:
if transactions[0].buy:
shares = shares + transactions[0].amount
else: #transaction is a sell
shares = shares - transactions[0].amount
monthly_shares[date(transactions[0].date.year, transactions[0].date.month, 1)] = shares
transactions.remove(transactions[0])
except IndexError: #no more transactions
pass
if dividend in stock_transaction.dividends.all(): # if drip is active for dividend
if dividend.price!=0:
shares += (dividend.amount * shares / dividend.price)
monthly_shares[date(dividend.date.year, dividend.date.month, 1)] = shares
try:
monthly_dividends[date(dividend.date.year, dividend.date.month, 1)] += shares * dividend.amount
except KeyError:
monthly_dividends[date(dividend.date.year, dividend.date.month, 1)] = shares * dividend.amount
#fill blank months with 0
if monthly_shares!={}:
for dt in rrule.rrule(rrule.MONTHLY,
dtstart=first_transaction.date,
until=datetime.now() + relativedelta.relativedelta(months=1)):
try:
monthly_shares[date(dt.year, dt.month, 1)]
except KeyError: #keyerror on dt year
dt_previous = dt - relativedelta.relativedelta(months=1)
monthly_shares[date(dt.year, dt.month, 1)] = monthly_shares[date(dt_previous.year, dt_previous.month, 1)]
try:
monthly_dividends[date(dt.year, dt.month, 1)]
except KeyError:
monthly_dividends[date(dt.year, dt.month, 1)] = 0
# for each transaction not covered by dividend for cycle
if transactions:
for transaction in transactions:
for dt in rrule.rrule(rrule.MONTHLY,
dtstart=transaction.date,
until=datetime.now() + relativedelta.relativedelta(months=1)):
if transaction.buy:
try:
monthly_shares[date(dt.year, dt.month, 1)] += transaction.amount
except KeyError:
monthly_shares[date(dt.year, dt.month, 1)] = transaction.amount
else: #sell
monthly_shares[date(dt.year, dt.month, 1)] -= transaction.amount
total_dividends[stock_transaction.stock.symbol] = monthly_dividends
total_shares[stock_transaction.stock.symbol] = monthly_shares
return total_dividends, total_shares
说明:
周期第一 - 投资组合中的每只股票。
周期秒 - 每只股票的股息
这条线if dividend in stock_transaction.dividends.all() 检查股息是否被再投资。 stock_transaction 和股息对象之间存在 m2m 关系。
带有 rrule 的循环将空白月份填充到上个月的值。
EDIT1:
我已经使用 django-debug-toolbar 优化了 sql 查询的数量(需要 4 个 sql 查询)。我的代码很慢可能是因为很多对象和大字典。
【问题讨论】: