使用 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 error
、if bs.qty<=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里的移动平均价,怎么解决
请教各位,FLuent UDF 让出口温度值等于入口温度平均值,下面这个UDF有啥错误呢?