使用 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
下面是解释
第一个订单:数量 = 225 |价格 = 卢比10 000.00
二阶:数量 = 75 |价格 = 卢比10 200.00
要计算平均价格,首先计算值(数量 x 价格)。因此:
第一笔交易:卢比。 2 250 000.00
第二笔交易:卢比。 765 000.00
总数量 = 300
前两个订单的总价值:Rs。 3 015 000.00
重点来了。 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 在您可能拥有多家公司的股票并希望过滤公司的情况下更加通用。
此 post 是我
盈亏分为两部分。一个是 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 |
下面是解释
第一个订单:数量 = 225 |价格 = 卢比10 000.00
二阶:数量 = 75 |价格 = 卢比10 200.00
要计算平均价格,首先计算值(数量 x 价格)。因此:
第一笔交易:卢比。 2 250 000.00
第二笔交易:卢比。 765 000.00
总数量 = 300
前两个订单的总价值:Rs。 3 015 000.00
重点来了。 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 在您可能拥有多家公司的股票并希望过滤公司的情况下更加通用。