交易数据从长格式变为宽格式,合并买入和卖出数据框。

4
我有一些长格式的买入和卖出交易数据,我想将其转换为宽格式。看下面的例子:
对于某个股票的每次买入交易,必须存在相同股票的卖出交易来关闭该头寸。如果不存在卖出交易或股票数量变为零,则在卖出价格处放置NA。
说明:
我们以34.56的价格购买了100股AIG股票。接下来,我们必须为同一股票AIG找到退出(SELL)交易。下面存在此交易,共有600股。因此,我们用100股写入此交易,从600股的SELL交易中减少股份至500股,并以买入价格和卖出价格将此交易记录在宽格式中。
下一个交易是GOOG。对于这支股票,我们找到了两个卖出交易,并将它们都记录在宽格式中,但还有100股未售,因此我们将此交易放置为“未完成”,在卖出价格处放置NA。
如有必要,我可以稍后在伪代码中提供算法。但我希望我的解释很清楚。
我的问题是:使用R编写干净且矢量化的代码很容易吗?这个算法在命令式编程语言(如C ++)中很容易实现。但是在R中,我遇到了麻烦。
EDIT 1:添加了R的输入和输出数据框。
inputDF1 <- data.frame(Ticker = c("AIG", "GOOG", rep("AIG", 3), rep("GOOG", 2), rep("NEM", 3)), Side = c(rep("BUY", 4), rep("SELL", 3), "BUY", rep("SELL", 2)), Shares = c(100, 400, 200, 400, 600, 200, 100, 100, 50, 50), Price = c(34.56, 457, 28.56, 24.65, 30.02, 460, 461, 45, 56, 78))
inputDF2 <- data.frame(Ticker = c(rep("AIG", 3), rep("GOOG", 3)), Side = c(rep("BUY", 2), "SELL", "BUY", rep("SELL", 2)), Shares = c(100, 100, 200, 300, 200, 100), Price = c(34, 35, 36, 457, 458, 459))
inputDF3 <- data.frame(Ticker = c(rep("AIG", 3), rep("GOOG", 3)), Side = c(rep("BUY", 2), "SELL", "BUY", rep("SELL", 2)), Shares = c(100, 100, 100, 300, 100, 100), Price = c(34, 35, 36, 457, 458, 459))

outputDF1 <- data.frame(Ticker = c("AIG", rep("GOOG", 3), rep("AIG", 3), rep("NEM", 2)), Side = rep("BUY", 9), Shares = c(100, 200, 100, 100, 200, 300, 100, 50, 50), BuyPrice = c(34.56, 457, 457, 457, 28.56, 24.65, 24.65, 45, 45), SellPrice = c(30.02, 460, 461, NA, 30.02, 30.02, NA, 56, 78))
outputDF2 <- data.frame(Ticker = c(rep("AIG", 2), rep("GOOG", 2)), Side = rep("BUY", 4), Shares = c(100, 100, 200, 100), BuyPrice = c(34, 35, 457, 457), SellPrice = c(36, 36, 458, 459))
outputDF3 <- data.frame(Ticker = c(rep("AIG", 2), rep("GOOG", 3)), Side = rep("BUY", 5), Shares = rep(100, 5), BuyPrice = c(34, 35, rep(457, 3)), SellPrice = c(36, NA, 458, 459, NA))

EDIT 2: 更新了 R 的示例、输入/输出数据


这看起来很熟悉。你不是刚刚问过这个吗?是的,你问过了。如果这与链接的重复内容不同,请编辑以显示差异。 - Matthew Lundberg
@MatthewLundberg 是的,你说得对。这是现在更复杂的版本。 - Eldar Agalarov
1
哦,没错。你需要匹配买卖双方的交易量。我会撤回重复的投票,但请编辑问题以包含R可以读取的数据。 - Matthew Lundberg
1
我猜您希望这种情况下也能灵活处理,即卖出的股票数量少于首次购买的数量?(例如,首先以34.56的价格购买100股AIG,然后以37的价格卖出50股,再以39的价格卖出另外50股) - Mike.Gahan
1
@Mike.Gahan 是的,那么必须有两行宽度:[AIG,BUY,50,34.56,37]和[AIG,BUY,50,34.56,39]。 - Eldar Agalarov
2个回答

3

原始回答(当问题还在开发中,我没有足够注意时)

使用reshape2中的dcast


使用reshape2中的dcast函数:
> t <- c("AIG", "GOOG", "AIG", "AIG", "AIG", "GOOG", "GOOG")
> sd <- c(rep("BUY", 4), rep("SELL", 3))
> sh <- c(100, 400, 200, 400, 600, 200, 100)
> pr <- c(34.56, 457, 28.56, 24.65, 30.02, 460, 461)
> df <- data.frame(Ticker = t, Side = sd, Shares = sh, Price = pr)
> 
> library(reshape2)
> df
  Ticker Side Shares  Price
1    AIG  BUY    100  34.56
2   GOOG  BUY    400 457.00
3    AIG  BUY    200  28.56
4    AIG  BUY    400  24.65
5    AIG SELL    600  30.02
6   GOOG SELL    200 460.00
7   GOOG SELL    100 461.00
> dcast(df, Ticker*Shares ~ Side, value.var="Price")
  Ticker Shares    BUY   SELL
1    AIG    100  34.56     NA
2    AIG    200  28.56     NA
3    AIG    400  24.65     NA
4    AIG    600     NA  30.02
5   GOOG    100     NA 461.00
6   GOOG    200     NA 460.00
7   GOOG    400 457.00     NA

新答案

这里的关键问题是,在R中,“基于向量”的概念通常与“函数式”(例如apply()系列)联系在一起,但纯粹的函数式方法在这里并不完全适用,因为您必须针对每个(每个部分的)购买交易更新销售清单。我真的觉得您可以通过一个精心设计的函数和aggregateby做出一些神奇的东西,但对我来说最好的可读解决方案涉及一个简单的for循环。

带有for的版本

inputDF <- data.frame(Ticker = c("AIG", "GOOG", "AIG", "AIG", "AIG", "GOOG", "GOOG"), 
                      Side = c(rep("BUY", 4), rep("SELL", 3)), 
                      Shares = c(100, 400, 200, 400, 600, 200, 100), 
                      Price = c(34.56, 457, 28.56, 24.65, 30.02, 460, 461))
buys <- subset(inputDF,Side=="BUY")
sells <- subset(inputDF,Side=="SELL")
transactions <- NULL

# go through every buy operation
for(i in 1:nrow(buys)){
    ticker <- buys[i,"Ticker"]
    bp <- buys[i,"Price"]
    shares <- buys[i,"Shares"]

    # keep going as long as we can find sellers
    while(shares > 0 & sum(sells[sells$Ticker == ticker,"Shares"]) > 0){
        sp <- sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Price"]
        if(sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Shares"] > shares){
            shares.sold <- shares
        }else{
            shares.sold <- sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Shares"]
        }
        shares <- shares - shares.sold
        sells[sells$Shares >= shares & sells$Ticker == ticker,][1,"Shares"] <- sells[sells$Shares >= shares & sells$Ticker == ticker,][1,"Shares"] - shares.sold
        transactions <- rbind(transactions,data.frame("Ticker"=ticker
                                                      ,"Side"="BUY"
                                                      ,"Shares"=shares.sold
                                                      ,"BuyPrice"=bp
                                                   ,"SellPrice"=sp))
    }
    # not enough sellers
    if(shares > 0){ 
        transactions <- rbind(transactions,data.frame("Ticker"=ticker
                                                  ,"Side"="BUY"
                                                  ,"Shares"=shares
                                                  ,"BuyPrice"=bp
                                                  ,"SellPrice"="NA"))

    }

}

print(transactions)

输出:

  Ticker Side Shares BuyPrice SellPrice
1    AIG  BUY    100    34.56     30.02
2   GOOG  BUY    200   457.00       460
3   GOOG  BUY    100   457.00       461
4   GOOG  BUY    100   457.00        NA
5    AIG  BUY    200    28.56     30.02
6    AIG  BUY    300    24.65     30.02
7    AIG  BUY    100    24.65        NA

如果我们尝试使用foreach包来自动并行化循环,更新将变得明显。很快就会发现我们在sell数据框上存在竞争条件。

使用apply的版本

上面的代码中有一些低效之处可以改进。通过rbind()进行的附加操作并不是非常高效,可能可以进行优化,减少对rbind()的调用次数或者完全消除它。您还可以将所有内容打包到一个函数中,并将其转换为对apply()的调用,即使是串行apply()也会更快,因为循环是在更优化的级别上完成的。(对于CPython也是如此--列表推导和str.join()比for循环快得多,因为它们"更了解"操作的总大小,并且因为它们是用优化的C语言编写的。)这里是第一次尝试--请注意,我们使用do.call(rbind, list(...))来简化从原始调用apply返回的小数据框列表。这并不是非常高效(data.table中的rbindlist要快得多,参见这里),但它没有任何外部依赖。您从apply()得到的列表实际上非常有趣--每个元素都是您需要完成一个完整的购买操作所需进行的交易列表。如果您向buys数据框添加行名,那么您可以按名称调用每组交易。

inputDF <- data.frame(Ticker = c("AIG", "GOOG", "AIG", "AIG", "AIG", "GOOG", "GOOG"), 
                      Side = c(rep("BUY", 4), rep("SELL", 3)), 
                      Shares = c(100, 400, 200, 400, 600, 200, 100), 
                      Price = c(34.56, 457, 28.56, 24.65, 30.02, 460, 461))
buys <- subset(inputDF,Side=="BUY")
sells <- subset(inputDF,Side=="SELL")
transactions <- NULL

# go through every buy operation
buy.operation <- function(x){
    ticker <- x["Ticker"]
    # apply() converts to matix implicity, and all the elements of a matrix have
    # have the same data type, so everything gets converted to characters
    # thus, we need to convert back
    bp <- as.numeric(x["Price"])
    shares <- as.numeric(x["Shares"])

    # keep going as long as we can find sellers
    while(shares > 0 & sum(sells[sells$Ticker == ticker,"Shares"]) > 0){
        sp <- sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Price"]
        if(sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Shares"] > shares){
            shares.sold <- shares
        }else{
            shares.sold <- sells[sells$Ticker == ticker & sells$Shares > 0,][1,"Shares"]
        }
        shares <- shares - shares.sold
        sells[sells$Shares >= shares & sells$Ticker == ticker,][1,"Shares"] <- sells[sells$Shares >= shares & sells$Ticker == ticker,][1,"Shares"] - shares.sold
        transactions <- rbind(transactions,data.frame("Ticker"=ticker
                                                      ,"Side"="BUY"
                                                      ,"Shares"=shares.sold
                                                      ,"BuyPrice"=bp
                                                      ,"SellPrice"=sp))
    }
    # not enough sellers
    if(shares > 0){ 
        transactions <- rbind(transactions,data.frame("Ticker"=ticker
                                                      ,"Side"="BUY"
                                                      ,"Shares"=shares
                                                      ,"BuyPrice"=bp
                                                      ,"SellPrice"="NA"))

    }

    transactions
}

transactions <- do.call(rbind, apply(buys,1,buy.operation) )
# get rid of weird row names
row.names(transactions) <- NULL
print(transactions)

输出:

  Ticker Side Shares BuyPrice SellPrice
1    AIG  BUY    100    34.56     30.02
2   GOOG  BUY    200   457.00       460
3   GOOG  BUY    100   457.00       461
4   GOOG  BUY    100   457.00        NA
5    AIG  BUY    200    28.56     30.02
6    AIG  BUY    400    24.65     30.02

很遗憾,最终未完成的AIG交易丢失了。我还没有想出如何解决这个问题。


差不多了。如何将“买入”与“卖出”匹配? - Matthew Lundberg
将其拆分为两个 dcast 操作,并通过 plyr 进行左合并。更新即将到来... - Livius
看起来不错,但使用for循环的方法只能通过第1和第2个测试,未能通过第3个测试。我会尝试查明原因。 - Eldar Agalarov
这个问题有点难以捉摸。此外,重点是解决更深层次的问题,即为什么将其作为向量/并行操作编写非平凡,并不是为别人编写代码。看起来其中一个循环条件不太对,但你还需要优化买入和卖出价格 - 你还需要尝试找到适当的极值来最大化你的利润。我发布的代码是按先来先服务的原则工作的。 - Livius
@EldarAgalarov,我喜欢修改后的评论中所体现出来的自我主动精神。 :-) - Livius

2
嗯,我在这个问题上花了太多时间!以下是我的尝试(使用 data.table )。
由于您没有提及真实数据的维数,因此我无法进一步优化它。如果您可以在真实数据集上运行它并写回您的发现(关于速度/扩展性),那就太好了。
首先,我们必须按Side拆分数据集并执行join。这是最直接的方法。我还看到@Mike.Gahan也沿着这条路尝试过。
require(data.table)
dt1 <- as.data.table(inputDF1)
d1 <- dt1[Side == "BUY"][, N := .N > 1L, by=Ticker]
d2 <- dt1[Side == "SELL"]
setkey(d2, Ticker)
ans = d2[d1, allow.cartesian=TRUE][, Side := NULL]

请注意,allow.cartesian并不执行笛卡尔积。这里使用的术语非常宽泛。请阅读?data.table获取更多信息或查看此帖子了解其用途。基本上,连接将会非常快,并且将会很好地扩展。这不是一个限制步骤。
现在我们相应地设置列顺序和名称:
setcolorder(ans, c("Ticker", "Side.1", "Shares.1", "Shares", "Price.1", "Price", "N"))
setnames(ans, c("Ticker", "Side", "Shares", "tmp", "BuyPrice", "SellPrice", "N"))

我们交换Sharestmp,以便Shares反映我们预期的实际输出,基于N的值如下:
ans[, c("Shares", "tmp") := if (!N[1L]) 
             { val = Shares[1L]; list(tmp, val) }, by = Ticker]

我们需要一些参数来聚合并获取最终结果:
ans[, `:=`(N2= rep(c(FALSE, TRUE), c(.N-1L, 1L)), 
           csum = sum(Shares)), by = Ticker][, N2 := !(N2 * (csum != tmp))]

最后,
ans1 = ans[(N2)][, c("N", "N2", "tmp", "csum") := NULL]
ans2 = ans[!(N2)][, N := N * 1L]
if (nrow(ans2) > 0) {
    ans2 = ans2[,  list("BUY", if (N[1L]) c(Shares+tmp-csum, csum-tmp) 
             else c(Shares, tmp-csum), BuyPrice, c(SellPrice, NA)), by=Ticker]
}
ans  = rbindlist(list(ans1, ans2))

#    Ticker Side Shares BuyPrice SellPrice
# 1:    AIG  BUY    100    34.56     30.02
# 2:   GOOG  BUY    200   457.00    460.00
# 3:    AIG  BUY    200    28.56     30.02
# 4:    NEM  BUY     50    45.00     56.00
# 5:    NEM  BUY     50    45.00     78.00
# 6:   GOOG  BUY    100   457.00    461.00
# 7:   GOOG  BUY    100   457.00        NA
# 8:    AIG  BUY    300    24.65     30.02
# 9:    AIG  BUY    100    24.65        NA

我猜这应该很快。但是,可能还可以进一步优化。如果你选择在此基础上继续构建,我会留给你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接