【问题标题】:how to calculate monthly portfolio shares and dividends如何计算每月的投资组合份额和股息
【发布时间】: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 查询)。我的代码很慢可能是因为很多对象和大字典。

【问题讨论】:

    标签: python django


    【解决方案1】:

    这里只是在黑暗中开枪(我不熟悉股票股息,所以我无法评论数学)。

    看起来这可能是你的瓶颈:

        for dividend in stock_transaction.stock.dividends.all():
    

    您在stock 上选择_rel​​ated 并在dividends 上预取_related,但您没有抓住stock__dividends。您可以使用Django Debug Toolbar 检查这是否是瓶颈。

    如果这个重复查询是根本问题,那么您可以尝试将其添加到:

        ...select_related('stock', 'stock__dividends')...
    

    【讨论】:

    • 我已经使用调试工具栏优化了 sql 查询。我在 prefetch_related() 中有 stock__dividends 并且它不会在您提到的线上进行任何额外的 sql 查询。不过感谢您的回答。我将更新我已经优化 sql 查询的问题,并且整个方法 get_portfolio_month_shares(portfolio_id) 只进行 4 个查询(1 个用于 StockTransaction 的 sql 查询和 3 个用于 prefetch_related)。
    • 嗯...当数据库查询不是瓶颈时,这绝不是一件好事。你可能必须提供赏金才能获得 Python 专家的建议。或者只是增加服务器中的 RAM。
    • 好的,谢谢你的建议。可能我先尝试某种 django 分析器,如果它没有帮助,我会尝试赏金提供。再次感谢您的时间和帮助。
    猜你喜欢
    • 2021-10-06
    • 1970-01-01
    • 1970-01-01
    • 2020-09-27
    • 2018-03-25
    • 1970-01-01
    • 2021-04-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多