使用Python实现FIFO先进先出名义库存账龄计算解决方案
FIFO证券名义库存账龄计算方案实现指南
核心逻辑适配思路
你的需求属于典型的带状态FIFO冲销场景,每一步计算依赖之前的持仓结果,不适合用纯pandas向量化操作实现,用队列结构做逐行迭代是最直接、可维护性最高的方案,不需要复杂的中断循环逻辑。
具体处理步骤:
- 维护两个队列分别存储未冲销的正持仓、负持仓,每个队列元素记录持仓金额、产生日期,用来计算账龄
- 额外存储上一交易日的名义本金符号,用于判断是否触发持仓重置
- 逐行处理每日交易数据:
- 先校验当日
Notional和上一日Notional的符号是否相反,若相反直接清空两个持仓队列,将当日Notional全额存入对应符号的队列,结束当日处理(触发持仓重置规则) - 未触发重置的话计算当日
day trade净额:- 净额为正:优先从负持仓队列按先进先出规则逐笔冲销,冲销后仍有剩余的,将剩余金额作为新的正持仓存入正持仓队列
- 净额为负:优先从正持仓队列按先进先出规则逐笔冲销,冲销后仍有剩余的,将剩余金额作为新的负持仓存入负持仓队列
- 冲销过程中可同步记录每笔被冲销持仓的产生日期、持有天数,输出账龄统计结果
- 先校验当日
适用工具
- 标准库
collections.deque:专门用来实现队列结构,左端弹出(取出最早持仓)的时间复杂度为O(1),比Python原生list的pop(0)效率高很多,适合万级以上的交易数据处理 - pandas:用于数据预处理、结果输出,遍历数据优先用
itertuples(),效率比iterrows()高3~10倍
示例实现代码
import pandas as pd from collections import deque # 测试数据和你提供的一致 df = pd.DataFrame() df['Cusip'] = [123,123,123,123,123,123,123,123,123,123] df['Day'] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] df['Notional'] = [50, 200, 230, 130, -30, -40, -20, 100, -10, 30] df['In'] = [50, 150, 30, 0, 0, 0, 20, 120, 0, 40] df['Out'] = [0, 0, 0, -100, -160, -10, 0, 0, -110, 0] df['day_trade'] = df['In'] + df['Out'] # 初始化状态 pos_queue = deque() # 正持仓队列,元素格式(金额, 产生日期) neg_queue = deque() # 负持仓队列 prev_notional_sign = 0 result = [] for row in df.itertuples(): day = row.Day notional = row.Notional day_trade = row.day_trade # 步骤1:判断是否触发名义本金符号变更的持仓重置 current_notional_sign = 1 if notional > 0 else -1 if notional < 0 else prev_notional_sign if prev_notional_sign != 0 and current_notional_sign != prev_notional_sign: # 持仓重置 pos_queue.clear() neg_queue.clear() if notional > 0: pos_queue.append((notional, day)) elif notional < 0: neg_queue.append((abs(notional), day)) prev_notional_sign = current_notional_sign # 记录当日结果,可根据需要扩展账龄字段 result.append({ 'Day': day, 'Notional': notional, 'day_trade': day_trade, 'remaining_pos': sum(x[0] for x in pos_queue), 'remaining_neg': -sum(x[0] for x in neg_queue) }) continue # 步骤2:未触发重置,执行day trade的FIFO冲销 remaining_trade = day_trade while remaining_trade != 0: if remaining_trade > 0: # 正交易,优先冲负持仓 if not neg_queue: # 没有负持仓可以冲,剩余的加入正队列 pos_queue.append((remaining_trade, day)) remaining_trade = 0 else: neg_amt, neg_day = neg_queue[0] if remaining_trade >= neg_amt: # 冲销整笔负持仓 remaining_trade -= neg_amt neg_queue.popleft() # 这里可以加逻辑记录这笔负持仓的账龄:day - neg_day else: # 冲销部分负持仓 neg_queue[0] = (neg_amt - remaining_trade, neg_day) remaining_trade = 0 else: # 负交易,优先冲正持仓,交易金额取绝对值 abs_trade = abs(remaining_trade) if not pos_queue: # 没有正持仓可以冲,剩余的加入负队列 neg_queue.append((abs_trade, day)) remaining_trade = 0 else: pos_amt, pos_day = pos_queue[0] if abs_trade >= pos_amt: # 冲销整笔正持仓 abs_trade -= pos_amt pos_queue.popleft() # 这里可以加逻辑记录这笔正持仓的账龄:day - pos_day else: # 冲销部分正持仓 pos_queue[0] = (pos_amt - abs_trade, pos_day) abs_trade = 0 remaining_trade = -abs_trade # 更新上一日名义本金符号 prev_notional_sign = current_notional_sign # 记录当日结果 result.append({ 'Day': day, 'Notional': notional, 'day_trade': day_trade, 'remaining_pos': sum(x[0] for x in pos_queue), 'remaining_neg': -sum(x[0] for x in neg_queue) }) # 转成DataFrame查看结果 result_df = pd.DataFrame(result) print(result_df)
优化点说明
- 如果需要按不同证券(Cusip)分开计算,只需要按Cusip分组,每组单独初始化队列处理即可
- 持仓量大的时候可以把队列的总额单独存一个变量,不用每次sum计算,提升效率
- 冲销过程中可以扩展字段记录每笔持仓的账龄、冲销金额,满足后续统计需求
内容的提问来源于stack exchange,提问作者Leo.W
相关产品推荐
相关产品推荐

