使用 FIFO 方法通过 UDF 计算平均价格、已实现收益和未实现收益

Posted

技术标签:

【中文标题】使用 FIFO 方法通过 UDF 计算平均价格、已实现收益和未实现收益【英文标题】:Calculate Avg Price, Realized gain & Unrealized gain via UDF using FIFO method 【发布时间】:2021-07-12 17:50:05 【问题描述】:

这篇文章是我earlier post 的延续,我已经从Tom Sharpe 那里获得了有关如何根据下面给出的交易表和 UDF 使用 FIFO 方法计算股票平均价格的帮助。为了给它添加更多功能,我试图通过调整 UDF 来计算我的损益,但我没有成功,因此我为此创建了一个新线程。

盈亏分为两部分。一个是我卖出少量股票所获得的利润/损失,称为已实现收益,第二个是在证券交易所可用于我未售出股票的收益,称为未实现收益。如果出现亏损而不是盈利,两者都可能变为负数。

计算未实现的收益相当简单,因为已经提供了解决方案,答案是剩余数量 x 平均价格。参考表格,150 x 10 100 = 1 515 000我认为应该这样计算 - 如果我错了,请纠正我)。但是计算Realized Gain是我面临的挑战。根据表格,已实现收益为 -7 500,这是一个损失,计算为 (Sold Price - First Price) x Sold Quantity(希望这个逻辑背后的数学也是正确)。另外,当交易数量增加时,我面临着更大的困难。

简而言之,我期待拥有 3 样东西。投资平均价格(UDF 已经给出),未实现利润(可以根据 UDF 计算)。需要知道如何计算 Realized Profit 以及是否可以通过在公式中添加参数使用相同的 UDF 返回所有三项。

这是桌子

Date Side Qty Price Value Holding Avg Price
1-Jul Buy 225 10000 2250000 225 10000
2-Jul Buy 75 10200 765000 300 10050
3-Jul Sell -150 9950 -1492500 150 10100

下面是解释

第一个订单:数量 = 225 |价格 = 卢比。 10 000.00

二阶:数量 = 75 |价格 = 卢比。 10 200.00

要计算平均价格,首先要计算价值(数量 x 价格)。因此:

第一笔交易:卢比。 2 250 000.00

第二笔交易:卢比。 765 000.00

总数量 = 300

前两个订单的总价值:卢比。 3 015 000.00

现在问题来了。 7 月 3 日,我们下了一个卖单 150(共 300 个)@价格:卢比。 9 950.00

现在这里将应用 FIFO(先进先出)方法。该方法将检查第一笔交易(在买方)。在这种情况下,它是 225。150 卖出的股票将从 225 中扣除(第一次持有)。第一次持有的余额是 225,现在是 225 - 150 = 75

先进先出后,表格扣除卖出数量后变成这样转换。看到第一个数量从 225 变为 75,因为卖出了 150 只股票,因此平均价格为 10100(我可以从下面的 UDF 中得到它。

Date Side Qty Price Value Holding Avg Price
1-Jul Buy 75 10000 750000 75 10000
2-Jul Buy 75 10200 765000 150 10100

如果卖出数量超过225,则转移到下一笔交易以扣除剩余数量

感谢Tom Sharpe 提供这个称为=avgRate(qtyRange,rateRange) 的UDF

程序使用了 BuySell 类,因此您需要创建一个类模块,将其重命名为 BuySell 并包含这些行

Public rate As Double
Public qty As Double

这里是 UDF

Function avgRate(qtyRange As Range, rateRange As Range)


    ' Create the queue

    Dim queue As Object
    Set queue = CreateObject("System.Collections.Queue") 'Create the Queue

    ' Declare some variables
    Dim bs As Object
    Dim qty As Double
    Dim rate As Double
    Dim qtySold As Double
    Dim qtyBought As Double
    Dim qtyRemaining As Double
    Dim rateBought As Double
    Dim i As Long
    Dim sumRate As Double, totQty As Double

    For i = 1 To qtyRange.Cells().Count

        qty = qtyRange.Cells(i).Value()
        rate = rateRange.Cells(i).Value()

        If qty > 0 Then

            'Buy
            Set bs = New BuySell

            bs.rate = rate
            bs.qty = qty

            queue.Enqueue bs

        Else

            'Sell
            qtyRemaining = -qty

            'Work through the 'buy' transactions in the queue starting at the oldest.

            While qtyRemaining > 0

                If qtyRemaining < queue.peek().qty Then

                'More than enough stocks in this 'buy' to cover the sale so just work out what's left

                    queue.peek().qty = queue.peek().qty - qtyRemaining
                    qtyRemaining = 0

                ElseIf qtyRemaining = queue.peek().qty Then

                'Exactly enough stocks in this 'buy' to cover the sale so remove from queue

                    Set bs = queue.dequeue()
                    qtyRemaining = 0

                Else

                'Not enough stocks in this 'buy' to cover the sale so remove from queue and reduce amount of sale remaining

                    Set bs = queue.dequeue()
                    qtyRemaining = qtyRemaining - bs.qty

                End If
            Wend
        End If
    Next i

    'Calculate average rate over remaining stocks

    sumRate = 0
    totQty = 0

    For Each bs In queue
        sumRate = sumRate + bs.qty * bs.rate
        totQty = totQty + bs.qty
    Next

    avgRate = sumRate / totQty

End Function

算法:

If 'buy' transaction, just add to the queue.

If 'sell' transaction (negative quantity)

  Repeat 

    Take as much as possible from earliest transaction

    If more is required, look at next transaction

  until sell amount reduced to zero.

编辑: 添加我尝试使用提供的解决方案的更大样本的图像

【问题讨论】:

未实现收益看起来很简单,但实现收益对我来说看起来很棘手。如果您像示例中那样卖出 150 只股票,这很简单,因为它们都来自您第一次购买 225 @ 10000。如果您卖出 250 股,我想 225 来自您的第一批 10000,其余 25 来自您的第二手在10200。然后如果你多买多卖,那就更复杂了。换句话说,在我看来,您似乎必须为每笔销售进行平均价格计算。您可能必须在队列中存储销售以及购买。 也许还不错,我认为您只需要在每次销售时进行平均价格计算,因此希望不会有太多额外代码。 感谢您的回复。你想象的没错。您是否尝试使用基于 udf 的公式而不是额外的编码来建议计算?假设我每次都进行平均价格计算,那么 UDF 会给我前 3 笔交易的 10000、10050 和 10100 平均价格。现在我第三次卖出时,显示的平均价格是 10100,在这种情况下,使用什么公式可以得到我的已实现收益(在这个例子中是损失)?抱歉,我没有得到你想要的建议...... 不,对于每次销售,我们必须查看与该销售相关的股票的购买价格,并将它们的总成本从销售价格 * 数量中扣除,但我认为这只是意味着添加一些原始 UDF 的代码行。 同时我正在尝试添加一个额外的列来计算已实现的利润。使用相同的示例,我卖掉了所有 300 只股票而不是 150 只,这会导致错误。我应该添加哪一行以及在哪里使其结果为 0?即,当队列中的购买数量变为 0 或变为负数时。我试过on errorif bs.qty&lt;=0,但后来所有单元格都出错了。 【参考方案1】:

需要通过使用现有代码从队列中删除最早购买的股票来获得每次卖出交易的收益(或损失),但要添加额外的行来解决:

gain = sale price * sale quantity - ∑ buy price * buy quantity

按时间顺序对满足销售数量的不同“购买”交易进行总和。

我现在添加了 OP 建议的额外计算并添加了一些基本的错误处理(例如,用户不会尝试出售比可用库存更多的股票,从而使队列变为空)。

UDF 接受作为范围或数组的单列参数。

UDF

像以前一样需要一个 BuySell 类:

Public rate As Double
Public qty As Double

Option Explicit

Function avgRate(qtyRange As Variant, rateRange As Variant, Optional calcNumber As Integer = 1)
 
    ' Create the queue
    
    Dim queue As Object
    Set queue = CreateObject("System.Collections.Queue")
    
    ' Declare some variables
    
    Dim bs As Object
    Dim qty As Double
    Dim rate As Double
    Dim qtySold As Double
    Dim qtyBought As Double
    Dim qtyRemaining As Double
    Dim rateBought As Double
    Dim i As Long
    Dim sumRate As Double, totalQty As Double
    Dim avRate As Double
    Dim saleValue As Double
    Dim purchaseValue As Double
    Dim gainForThisSale As Double
    Dim totalGain As Double
    Dim totalCost As Double
    Dim totalProfit As Double
    Dim overallCost As Double
    Dim tempQty() As Variant, workQty() As Variant, tempRate() As Variant, workRate() As Variant
    Dim nRows As Long
    Dim argType As Integer
    
    
    
    'Copy from range or array - assuming single column or single element in both cases.
    

    If TypeOf qtyRange Is Range Then
        If IsArray(qtyRange) Then
        ' column range
            argType = 1
        Else
        ' Single element range
            argType = 2
        End If
    Else
        If UBound(qtyRange, 1) > 1 Then
        ' Column array
            argType = 3
        Else
        ' Single element array
            argType = 4
        End If
    End If
    
    Debug.Print ("Argtype=" & argType)
        
     Select Case argType
        Case 1
            tempQty = qtyRange.Value
            tempRate = rateRange.Value
        Case 2
            nRows = 1
            ReDim workQty(1 To nRows)
            ReDim workRate(1 To nRows)
            workQty(1) = qtyRange.Value
            workRate(1) = rateRange.Value
        Case 3
             tempQty = qtyRange
             tempRate = rateRange
        Case 4
            nRows = 1
            ReDim workQty(1 To nRows)
            ReDim workRate(1 To nRows)
            workQty(1) = qtyRange(1)
            workRate(1) = rateRange(1)
    End Select
        
    If argType = 1 Or argType = 3 Then
            nRows = UBound(tempQty, 1)
    
            ReDim workQty(1 To nRows)
            ReDim workRate(1 To nRows)
            For i = 1 To nRows
               workQty(i) = tempQty(i, 1)
               workRate(i) = tempRate(i, 1)
            Next i
    End If
            

      ' Loop over rows
    
    totalProfit = 0
    overallCost = 0
    
    For i = 1 To nRows
   
        qty = workQty(i)
                
        ' Do nothing if qty is zero
        
        If qty = 0 Then GoTo Continue:
        
        rate = workRate(i)
        
        overallCost = overallCost + rate * qty
        
        If qty > 0 Then
        
            'Buy
            
            Set bs = New BuySell
            
            bs.rate = rate
            bs.qty = qty
            
            queue.Enqueue bs
        
            
        Else
        
            'Sell
        
            qtyRemaining = -qty
            
            'Code for realized Gain
            
            purchaseValue = 0
            saleValue = rate * qtyRemaining
            
            totalProfit = totalProfit + saleValue
            
            'Work through the 'buy' transactions in the queue starting at the oldest.
            
            While qtyRemaining > 0
            
                If queue.Count = 0 Then
                    avgRate = CVErr(xlErrNum)
                    Exit Function
                End If
            
                If qtyRemaining < queue.peek().qty Then
                
                'More than enough stocks in this 'buy' to cover the sale so just work out what's left
                
                    queue.peek().qty = queue.peek().qty - qtyRemaining
                    
                    'Code for realized gain
                
                    purchaseValue = purchaseValue + qtyRemaining * queue.peek().rate

                    
                    qtyRemaining = 0
                    
                    
                ElseIf qtyRemaining = queue.peek().qty Then
                
                'Exactly enough stocks in this 'buy' to cover the sale so remove from queue
                
                    Set bs = queue.dequeue()
                    qtyRemaining = 0
                    
                    'Code for realized gain
                
                    purchaseValue = purchaseValue + bs.qty * bs.rate

                    
                Else
                
                'Not enough stocks in this 'buy' to cover the sale so remove from queue and reduce amount of sale remaining
                
                    Set bs = queue.dequeue()
                    qtyRemaining = qtyRemaining - bs.qty
                    
                    'Code for realized gain
                
                    purchaseValue = purchaseValue + bs.qty * bs.rate
           
                    
                End If
                
            Wend
            
            'Code for realized gain
            
            gainForThisSale = saleValue - purchaseValue

            
            totalGain = totalGain + gainForThisSale
            
        End If
        
Continue:
        
    Next i
    
    'Calculate average rate
    
    If queue.Count = 0 Then
    
        avRate = 0
        
    Else

        totalCost = 0
        totalQty = 0
        
        For Each bs In queue
            totalCost = totalCost + bs.qty * bs.rate
            totalQty = totalQty + bs.qty
        Next
        
        avRate = totalCost / totalQty
        
    End If
    

    
    Select Case calcNumber
        Case 1
        'Average rate
            avgRate = avRate
        Case 2
        'Realized gain
            avgRate = totalGain
        Case 3
        'Invested
            avgRate = totalCost
        Case 4
        'Bal qty
            avgRate = totalQty
        Case 5
        'Net worth (total quantity times most recent rate)
            avgRate = totalQty * rate
        Case 6
        'Total profit (total sale amounts)
            avgRate = totalProfit
        Case 7
        'Unrealized gain
            avgRate = totalProfit - totalGain
        Case 8
        'Overall cost
            avgRate = overallCost
        Case Else
            avgRate = CVErr(xlErrNum)
    End Select
    
     
End Function


我添加了一个新版本,它测试第一个参数是一个数组还是一个范围(并假设第二个参数是相同的类型)。 OP 要求我检查它是单元素数组还是单单元格范围的情况。允许数组等的要点是您可以进行如下函数调用:

=avgRate(FILTER($C2:$C10,C2:C10=10),FILTER($A2:$A10,C2:C10=10),8)

=avgrate($C$2,$A$2,8)

只选择(在这种情况下)第一行。这使得 UDF 在您可能拥有来自多个公司的股票并希望过滤该公司的情况下更加通用。

【讨论】:

感谢测试稿。我尝试了一个更大的样本(添加了一张图片作为对我原始帖子的编辑),它给出了正确的结果。但是,根据我的说法,您对未实现收益的理解是投资于交易所的金额。看来我这边有误会。也许,我可能没有正确解释它。对于那个很抱歉。每当有卖出交易时,当日数量 x 当前汇率成为入账利润。该预定利润分为未实现收益(留在交易所)和已实现收益(已提取)。 未实现收益是预定利润和已实现收益之间的差额。所以根据张贴的图片,950 成为投资价格(而不是未实现收益)。未实现收益为 1175(以粉红色标记),这是 2050 年入账利润和已实现收益 875 的差值。在这里,我们甚至可以绘制总账面利润(2050 年)和净资产(1250 - 这是 bal qty x 当前汇率)。非常感谢您的友好帮助。我非常感谢您的时间和努力。从 sub 到 udf 的转换(根据您的方便时间)会非常好。再次感谢... 我认为已实现的收益是可以的,因此根据您的定义更改未实现的收益不会很困难。我认为这是您表中的底线,您不需要每笔交易的结果。明天去看看。 我在您更新 udf 之前发表了评论。我现在正在看 udf,还没有尝试过。我将带有少量大数据的事务性新样本更新为彩色图片,因为它很容易解释。使用 vba 解决方案,只有底线就足够了。每个事务的结果总是很好,这是 udf 请求的另一个原因。谢谢你的udf。我会在一段时间内尝试这个解决方案。 感谢您首先提出问题以及您的所有反馈和鼓励。我很喜欢这样做,并在此过程中学到了很多东西。就 Google 脚本而言,我也不确定可以使用哪些集合,尽管它看起来好像在 javascript 中可以使用双端队列。这是另一个有趣的项目!一切顺利。

以上是关于使用 FIFO 方法通过 UDF 计算平均价格、已实现收益和未实现收益的主要内容,如果未能解决你的问题,请参考以下文章

sap系统每月有几个物料的实际与标准价格差异很大,看了下实际价格取的MM03里的移动平均价,怎么解决

没有 UDF 的 Spark 数据集的加权平均值

请教各位,FLuent UDF 让出口温度值等于入口温度平均值,下面这个UDF有啥错误呢?

如何使用 groupby 计算 vwap(交易量加权平均价格)并申请?

lifo和fifo的优缺点

python 计算时间加权平均价格:权衡近期价格