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

Calculate Avg Price, Realized gain & Unrealized gain via UDF using FIFO method

此 post 是我 where I have already got assistance from Tom Sharpe 关于如何根据交易 table 和 UDF 使用 FIFO 方法计算股票平均价格的延续。为了给它添加更多功能,我努力通过调整 UDF 来计算我的 profit/loss,但我没有成功,因此我为此开了一个新线程。

盈亏分为两部分。一个是 profit/loss 我通过出售几只股票赚取的收益,称为已实现收益,第二个是我未售出股票在证券交易所可用的收益,称为未实现收益。如果出现亏损而不是盈利,两者都可能变为负值。

计算未实现收益 相当简单,因为已经提供了解决方案,答案是剩余数量 x 平均价格。参考 table, 150 x 10 100 = 1 515 000我认为这是应该计算的方式 - 如果我错了请纠正我).但是计算 Realized Gain 是我面临的挑战。根据 table,已实现收益计算为 -7 500,这是计算为(卖出价 - 第一价)x 卖出数量(希望这个逻辑背后的数学也是正确的)。再加上交易量增加,我的难度就更大了。

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

这是table

日期 侧面 数量 价格 价值 持有 平均价格
7 月 1 日 购买 225 10000 2250000 225 10000
7 月 2 日 购买 75 10200 765000 300 10050
7 月 3 日 卖出 -150 9950 -1492500 150 10100

下面是解释

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

重点来了。 7 月 3 日,我们下了 150 个卖单(共 300 个)@ 价格:Rs。 9 950.00

这里将采用FIFO(先进先出)的方法。该方法将检查第一笔交易(在买方)。在本例中为225。150卖出的股票将从225(首次持有)中扣除。最初持有的余额为 225,现在为 225 - 150 = 75

经过FIFO后,table在扣除卖出数量后就变成这样了。看到第一个数量从 225 变为 75,因为售出了 ​​150 只股票,因此平均价格为 10100(我可以从下面的 UDF 中得到它。

日期 侧面 数量 价格 价值 持有 平均价格
7 月 1 日 购买 75 10000 750000 75 10000
7 月 2 日 购买 75 10200 765000 150 10100

如果卖出数量大于225,则转入下一单扣除剩余数量

感谢 Tom Sharpe 这个 UDF,它被称为 =avgRate(qtyRange,rateRange)

该程序使用 class BuySell,因此您需要创建一个 class 模块,将其重命名为 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.

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

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

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

其中满足销售数量的不同 'buy' 笔交易的总和,按时间顺序排列。

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

UDF 接受范围或数组形式的单列参数。

UDF

像以前一样需要 BuySell class:

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,$A,8)

到 select(在本例中)只是第一行。这使得 UDF 在您可能拥有多家公司的股票并希望过滤公司的情况下更加通用。