分红被重复计入 RealizedPnl 两次的根因:互换/平仓事件的 MarkClosePnl
已含分红成分(DividendIn),导致分红既进 RealizedMtmPnL(盯市列) 又进
RealizedDividend(分红列)。本次提供数据证据与回归基线:
- SwapDividendGoldenRecordTest: 连库录制1875(纯分红型)/1891(混合型)
5表快照→JSON golden,录制时逐事件拆解 MarkClosePnl=价差+费+分红,
按 PositionId 汇总出重复计入的分红金额。Step2 离线校验已通过。
诊断结论:1875 重复302400,1891 重复-18.66,均与 eod 分红列吻合。
- golden JSON 纳入源码树(全量复制规则),修复后可作回归基线。
- 排查SQL工具箱: 基于真实DDL核对全部列名(swap_flow_event.EventDate/
bond_payment_info.paying_interest 等),含定位样本/导出/验证/守恒4步。
- appsettings.json 测试库 zszq→glms 前缀修正。
详见 项目文档/互换分红损益字段语义与重复计算分析.md(9e6af84e)。
304 lines
16 KiB
SQL
304 lines
16 KiB
SQL
-- ============================================================================
|
||
-- 互换分红损益:数据导出与重复计算验证 SQL 工具箱
|
||
-- ----------------------------------------------------------------------------
|
||
-- ⚠ 本脚本所有列名均取自真实库 DDL(glms_yltrs_ylcms.sql),非实体类属性名。
|
||
-- 实体类属性名与库列名存在差异(如 swap_flow_event.EventDate、
|
||
-- bond_payment_info.paying_interest、swap_position.IsInitial 等)。
|
||
--
|
||
-- 目的:
|
||
-- 1) 定位并导出一笔"带分红的互换交易"完整数据,作 golden source 种子;
|
||
-- 2) 用真实库数据验证"分红被重复计算 2 次";
|
||
-- 3) 生命周期守恒校验。
|
||
--
|
||
-- 涉及表与关键列(取自 DDL,列名以反引号为准):
|
||
-- swap_event 事件主表(窄列):id,SwapTradeId,ValueDate,EventType,
|
||
-- EventReason,EventData(json),Invalid,BackId,OptId,OptTime,ClientCashId
|
||
-- ※ 无 SwapTradeNo/UnderlyingCode/金额,这些在 EventData 或流水表
|
||
-- swap_position 持仓:PositionId,SwapTradeId,UnderlyingCode,PosiQuantity,
|
||
-- PosiNotionalValue,PosiNetPrice,PosiGrossPrice,IsInitial(0实时/1期初),
|
||
-- PosiDividendIncome,PosiTradingFeePending,Invalid
|
||
-- swap_flow_event 流水(分红核心):EventDate(非ValueDate!),SwapTradeId,SwapTradeNo,
|
||
-- EventType,PositionId,Quantity,MarkClosePnl,DividendIn,DividendPending,
|
||
-- CloseFee,DataState(0废弃/1等待/100完成)
|
||
-- eod_swap_position 日终归档:ValueDate,SwapTradeId,PositionId,TdCloseMtmPnl,TdCloseDividend,
|
||
-- RealizedMtmPnL,RealizedDividend,RealizedFee,RealizedInterest,
|
||
-- RealizedInterestFee,RealizedPnl,DV01(大写),PosiDividendSum,Invalid
|
||
-- eod_swap 日终汇总:ValueDate,SwapTradeId,SwapTradeNo,TdRealizedPnL,RealizedPnL,
|
||
-- PostionValue
|
||
-- bond_payment_info 债券付息:underlying_code,pay_date_PL,paying_interest(非payment_interest!),
|
||
-- paying_principal,paying_price
|
||
--
|
||
-- EventType 枚举(注意 swap_event 与 swap_flow_event 取值不同!):
|
||
-- swap_event.EventType (SwapEventTypeEnum): 展期1/平仓2/互换3/自动互换4/回退5/合成持仓6...
|
||
-- swap_flow_event.EventType (SwapFlowEventTypeEnum): 开仓1/平仓2/互换3/自动互换4
|
||
-- → 分红型互换在两表均为 EventType IN (3,4)
|
||
-- ============================================================================
|
||
|
||
|
||
-- ============================================================================
|
||
-- 第 0 步:定位一笔"带分红的互换交易"作为样本
|
||
-- ============================================================================
|
||
-- 思路:互换/自动互换事件(EventType in 3,4) 的 DividendIn != 0,即发生过分红型互换。
|
||
-- swap_flow_event 自带 SwapTradeNo(人类可读),无需关联 swap_event。
|
||
-- DataState=100 仅取已完成流水。
|
||
|
||
SELECT SwapTradeId,
|
||
MAX(SwapTradeNo) AS SwapTradeNo,
|
||
COUNT(*) AS 互换事件数,
|
||
SUM(DividendIn) AS 互换事件分红合计,
|
||
SUM(MarkClosePnl) AS 互换事件盯市合计,
|
||
MAX(EventDate) AS 最近事件日
|
||
FROM swap_flow_event
|
||
WHERE EventType IN (3, 4) -- 互换 / 自动互换
|
||
AND DividendIn <> 0 -- 真正发生过分红
|
||
AND DataState = 100 -- 仅完成的
|
||
GROUP BY SwapTradeId
|
||
ORDER BY SUM(DividendIn) DESC, MAX(EventDate) DESC
|
||
LIMIT 20;
|
||
-- 选定其中一行 SwapTradeId,填入下面 @TargetTradeId。
|
||
|
||
|
||
-- ============================================================================
|
||
-- 第 1 步:单笔交易完整数据导出(golden source 种子)
|
||
-- ============================================================================
|
||
-- 用法:把 @TargetTradeId 改为第 0 步选出的值,逐段执行。
|
||
|
||
SET @TargetTradeId := 1874; -- ← 替换为实际样本 SwapTradeId
|
||
|
||
-- 1.1 互换事件主表(窄列;EventData 是 json,含详细快照)
|
||
SELECT id, SwapTradeId, ValueDate, EventType, EventReason,
|
||
Invalid, BackId, OptId, OptName, OptTime, ClientCashId
|
||
FROM swap_event
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
ORDER BY ValueDate, id;
|
||
|
||
-- 1.2 互换持仓(区分期初/实时:IsInitial 1=期初, 0=实时)
|
||
SELECT PositionId, SwapTradeId, UnderlyingCode, PosiDirection, PositionType,
|
||
PosiQuantity, PosiNotionalValue, PosiNetPrice, PosiGrossPrice,
|
||
ContractSize, CountRatio, PosiTradingFee, PosiTradingFeePending,
|
||
PosiDividendIncome, IsInitial, Invalid
|
||
FROM swap_position
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
ORDER BY IsInitial DESC, PositionId;
|
||
-- 说明:IsInitial=1 是期初开仓腿;=0 是实时持仓(会随平仓/互换变动)。
|
||
|
||
-- 1.3 流水事件(分红核心表,导出全字段便于复盘)
|
||
SELECT id, EventDate, SwapTradeId, SwapTradeNo, EventType, EventReason,
|
||
PositionId, PayDirection, PositionType, UnderlyingCode, Quantity,
|
||
TradingAmount, TradingAmountAvg, TradingAmountFeeAvg,
|
||
TradingFee, TradingFeePending, DividendPending,
|
||
MarkClosePnl, DividendIn, CloseFee, DataState, ClientCashId
|
||
FROM swap_flow_event
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
ORDER BY EventDate, EventType, id;
|
||
|
||
-- 1.4 日终持仓归档(按日快照,含所有 TdClose* / Realized* 字段)
|
||
SELECT id, ValueDate, SwapTradeId, PositionId, PosiDirection, PositionType,
|
||
UnderlyingCode, PosiQuantity, PosiNotionalValue,
|
||
PosiNetPrice, PosiGrossPrice, UnderlyingPrice, UnderlyingMarketValue,
|
||
TdPosiDividend, PosiMtmPnL, PosiDividendSum, PosiFeePending, PosiProfitSum,
|
||
TdCloseQty, TdCloseMtmPnl, TdCloseDividend, TdCloseFee,
|
||
RealizedMtmPnL, RealizedDividend, RealizedFee, RealizedInterest, RealizedInterestFee,
|
||
RealizedPnl, DV01, PosiStatus, Invalid
|
||
FROM eod_swap_position
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
ORDER BY PositionId, ValueDate;
|
||
|
||
-- 1.5 日终互换层汇总
|
||
SELECT id, ValueDate, SwapTradeId, SwapTradeNo, TdCloseQty,
|
||
TdRealizedPnL, RealizedPnL, PostionValue
|
||
FROM eod_swap
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
ORDER BY ValueDate;
|
||
|
||
-- 1.6 债券付息明细(理论应付分红来源)
|
||
-- 先从 swap_position 取该交易挂钩的标的代码:
|
||
SELECT DISTINCT UnderlyingCode
|
||
FROM swap_position
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
AND UnderlyingCode IS NOT NULL;
|
||
|
||
-- 再用取到的 UnderlyingCode 查付息明细(替换 @BondCode):
|
||
SET @BondCode := 'PUT_UNDERLYING_CODE_HERE';
|
||
SELECT id, underlying_code, inner_code,
|
||
pay_date_PL, pay_date_act, paying_interest, paying_principal, paying_price,
|
||
interest_tax_rate, event_type, info_source, insert_time
|
||
FROM bond_payment_info
|
||
WHERE underlying_code = @BondCode
|
||
ORDER BY pay_date_PL;
|
||
|
||
|
||
-- ============================================================================
|
||
-- 第 2 步:分红重复计算验证(证明"分红被算 2 次")
|
||
-- ============================================================================
|
||
-- 根因链路(SwapEodPositionService.cs):
|
||
-- SetPriceInfoByFlowEvent:1610 TdCloseMtmPnl = Σ unwindEvents.MarkClosePnl
|
||
-- (互换/平仓事件的 MarkClosePnl 已含分红)
|
||
-- UpdateEodPosition:1486 RealizedMtmPnL += TdCloseMtmPnl ← 分红第1次进"盯市"列
|
||
-- UpdateEodPosition:1488/1607 TdCloseDividend = Σ DividendIn
|
||
-- UpdateEodPosition:1494 RealizedDividend += TdCloseDividend ← 分红第2次进"分红"列
|
||
-- SaveEodSwap:1869 eod_swap.RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + ...)
|
||
-- → 分红在盯市列和分红列各计一次 = 2 次
|
||
--
|
||
-- 验证思路:若 MarkClosePnl 含分红,则同一事件日同一持仓满足:
|
||
-- 该日"盯市列中扣除纯平仓价差后的余额" ≈ "分红列",且两者都进了 RealizedPnL → 重复。
|
||
|
||
SET @TargetTradeId := 1874; -- ← 替换为实际样本
|
||
|
||
-- 2.1 逐日核对:盯市列 vs 分红列
|
||
-- 关键比对:盯市列里扣除"纯平仓事件(EventType=2)的价差"后,剩余是否≈分红列。
|
||
-- 若是,说明互换/自动互换事件(EventType in 3,4)的 MarkClosePnl 含分红。
|
||
SELECT esp.ValueDate,
|
||
esp.PositionId,
|
||
esp.TdCloseMtmPnl AS 当日盯市列,
|
||
esp.TdCloseDividend AS 当日分红列,
|
||
-- 当日纯平仓事件(EventType=2)的盯市价差合计(理论上=纯价差,不含分红)
|
||
(SELECT COALESCE(SUM(sfe2.MarkClosePnl), 0)
|
||
FROM swap_flow_event sfe2
|
||
WHERE sfe2.SwapTradeId = esp.SwapTradeId
|
||
AND sfe2.PositionId = esp.PositionId
|
||
AND sfe2.EventType = 2
|
||
AND sfe2.DataState = 100
|
||
AND sfe2.EventDate = esp.ValueDate) AS 纯平仓盯市价差,
|
||
-- 盯市列 - 纯平仓价差 = 互换/自动互换事件贡献的盯市成分(若≈分红列→含分红)
|
||
(esp.TdCloseMtmPnl - (
|
||
SELECT COALESCE(SUM(sfe2.MarkClosePnl), 0)
|
||
FROM swap_flow_event sfe2
|
||
WHERE sfe2.SwapTradeId = esp.SwapTradeId
|
||
AND sfe2.PositionId = esp.PositionId
|
||
AND sfe2.EventType = 2
|
||
AND sfe2.DataState = 100
|
||
AND sfe2.EventDate = esp.ValueDate
|
||
)) AS 盯市列扣除纯平仓后余额,
|
||
esp.TdCloseDividend AS 分红列,
|
||
esp.RealizedMtmPnL AS 累计盯市已实现,
|
||
esp.RealizedDividend AS 累计分红已实现,
|
||
esp.RealizedPnl AS 持仓层累计已实现
|
||
FROM eod_swap_position esp
|
||
WHERE esp.SwapTradeId = @TargetTradeId
|
||
AND (esp.TdCloseDividend <> 0 OR esp.TdCloseMtmPnl <> 0)
|
||
ORDER BY esp.PositionId, esp.ValueDate;
|
||
|
||
-- 2.2 全生命周期汇总:盯市列累计 + 分红列累计 vs RealizedPnL
|
||
-- 若 MarkClosePnl 含分红:盯市累计里多算了一份分红,导致
|
||
-- RealizedMtmPnL + RealizedDividend > 真实盯市价差 + 分红 (多出 ≈ 分红金额)
|
||
SELECT esp.PositionId,
|
||
MAX(esp.RealizedMtmPnL) AS 最终累计盯市已实现,
|
||
MAX(esp.RealizedDividend) AS 最终累计分红已实现,
|
||
MAX(esp.RealizedPnl) AS 最终持仓层累计已实现,
|
||
-- 互换层汇总公式(SaveEodSwap:1869)的口径:
|
||
(MAX(esp.RealizedMtmPnL) + MAX(esp.RealizedDividend)
|
||
+ COALESCE(MAX(esp.RealizedFee),0)
|
||
+ COALESCE(MAX(esp.RealizedInterest),0)
|
||
+ COALESCE(MAX(esp.RealizedInterestFee),0))
|
||
AS 按互换层公式重算,
|
||
-- 理论上不含费的纯盯市价差(用纯平仓 EventType=2 的 MarkClosePnl 估算):
|
||
(SELECT COALESCE(SUM(sfe.MarkClosePnl), 0)
|
||
FROM swap_flow_event sfe
|
||
WHERE sfe.SwapTradeId = esp.SwapTradeId
|
||
AND sfe.PositionId = esp.PositionId
|
||
AND sfe.EventType = 2
|
||
AND sfe.DataState = 100) AS 纯平仓盯市价差合计
|
||
FROM eod_swap_position esp
|
||
WHERE esp.SwapTradeId = @TargetTradeId
|
||
GROUP BY esp.PositionId;
|
||
|
||
-- 2.3 一句话诊断:互换/自动互换分红事件的 MarkClosePnl 是否含分红
|
||
-- 对每个 EventType in (3,4) 且 DividendIn<>0 的事件日,
|
||
-- 检查当日 eod 盯市列是否也包含了等额成分。
|
||
SELECT sfe.EventDate,
|
||
sfe.PositionId,
|
||
sfe.EventType,
|
||
sfe.DividendIn AS 事件分红流入,
|
||
sfe.MarkClosePnl AS 事件盯市含费,
|
||
esp.TdCloseMtmPnl AS 当日盯市列,
|
||
esp.TdCloseDividend AS 当日分红列,
|
||
CASE
|
||
-- 同一互换事件自身:若 MarkClosePnl≈DividendIn,则该事件盯市就含分红
|
||
WHEN ABS(sfe.MarkClosePnl - sfe.DividendIn) < 0.01 AND sfe.DividendIn <> 0
|
||
THEN '⚠该事件MarkClosePnl≈DividendIn→盯市含分红(根因)'
|
||
-- 当日整列:盯市列≈分红列
|
||
WHEN esp.TdCloseMtmPnl <> 0
|
||
AND ABS(esp.TdCloseMtmPnl - esp.TdCloseDividend) < 0.01
|
||
THEN '⚠当日盯市列≈分红列→重复'
|
||
ELSE '需人工核对'
|
||
END AS 诊断
|
||
FROM swap_flow_event sfe
|
||
JOIN eod_swap_position esp
|
||
ON esp.SwapTradeId = sfe.SwapTradeId
|
||
AND esp.PositionId = sfe.PositionId
|
||
AND esp.ValueDate = sfe.EventDate
|
||
WHERE sfe.SwapTradeId = @TargetTradeId
|
||
AND sfe.EventType IN (3, 4)
|
||
AND sfe.DividendIn <> 0
|
||
AND sfe.DataState = 100
|
||
ORDER BY sfe.EventDate;
|
||
|
||
|
||
-- ============================================================================
|
||
-- 第 3 步:生命周期守恒校验(累计已实现分红 vs 理论应付分红)
|
||
-- ============================================================================
|
||
-- 含义:一笔互换交易从开仓到全部平仓,"已实现分红收益总额"应等于持仓期间
|
||
-- 该债券应付分红(税后)的累加。任何偏差说明核算有误。
|
||
-- 若存在第 2 步的重复计算,累计已实现分红会被放大,本步量化偏差作修复后回归基线。
|
||
|
||
SET @TargetTradeId := 1874;
|
||
|
||
-- 3.1 实际已实现分红(从事件流水 DividendIn 累加)
|
||
SELECT
|
||
SUM(CASE WHEN EventType IN (2,3,4) THEN DividendIn ELSE 0 END) AS 事件流水已实现分红合计,
|
||
SUM(CASE WHEN EventType IN (3,4) THEN DividendIn ELSE 0 END) AS 其中互换事件分红,
|
||
SUM(CASE WHEN EventType = 2 THEN DividendIn ELSE 0 END) AS 其中平仓事件分红
|
||
FROM swap_flow_event
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
AND DataState = 100;
|
||
|
||
-- 3.2 日终表口径的最终累计已实现分红(应与 3.1 一致)
|
||
SELECT PositionId,
|
||
MAX(RealizedDividend) AS 日终表累计已实现分红,
|
||
MAX(RealizedPnl) AS 日终表累计已实现盈亏
|
||
FROM eod_swap_position
|
||
WHERE SwapTradeId = @TargetTradeId
|
||
GROUP BY PositionId;
|
||
|
||
-- 3.3 理论应付分红(税后)—— 需人工带入持仓区间与标的
|
||
-- 业务口径(BondPaymentService.CalcPayment):
|
||
-- totalPayment = CalcPayment(UnderlyingCode, StartDate, EndDate, Qty, shortRatio, dirRatio)
|
||
-- 理论税后分红 = totalPayment / (1 + tax) * (1 - tax)
|
||
-- 这里给出从 bond_payment_info 直接估算的简化版(仅供量级对照):
|
||
SET @BondCode := 'PUT_UNDERLYING_CODE_HERE';
|
||
SET @StartDate := '2024-01-01';
|
||
SET @EndDate := '2024-12-31';
|
||
SELECT underlying_code,
|
||
SUM(COALESCE(paying_interest, 0)) * 0.01 AS 区间每张利息合计_相对值,
|
||
COUNT(*) AS 付息次数
|
||
FROM bond_payment_info
|
||
WHERE underlying_code = @BondCode
|
||
AND pay_date_PL BETWEEN @StartDate AND @EndDate
|
||
GROUP BY underlying_code;
|
||
-- 说明:paying_interest 为"每张兑付利息额",×0.01 转相对价后还需 ×持仓数量 ×方向,
|
||
-- 再做税后调整,才能与 3.1/3.2 对齐。精确口径见 BondPaymentService.CalcPayment。
|
||
|
||
|
||
-- ============================================================================
|
||
-- 附录:导出为 json / csv 的方式
|
||
-- ============================================================================
|
||
-- 【方式A:MySQL 客户端导出(推荐,最简单)】
|
||
-- 在 Navicat / DBeaver / MySQL Workbench 中执行上述任一 SELECT,结果区右键
|
||
-- "导出" → 选 JSON / CSV / Excel。推荐把 1.1~1.6 各导一份,按表名命名:
|
||
-- swap_event.json / swap_position.json / swap_flow_event.json /
|
||
-- eod_swap_position.json / eod_swap.json / bond_payment_info.json
|
||
--
|
||
-- 【方式B:命令行 mysqldump(整表+DDL,含 CREATE)】
|
||
-- mysqldump -h<host> -u<user> -p<db> swap_flow_event \
|
||
-- --where="SwapTradeId=1874 AND DataState=100" \
|
||
-- --skip-add-drop-table --no-create-info > sfe_1874.sql
|
||
--
|
||
-- 【方式C:SELECT ... INTO OUTFILE(服务端导 csv,需 FILE 权限)】
|
||
-- SELECT ... FROM swap_flow_event WHERE SwapTradeId=1874
|
||
-- INTO OUTFILE '/tmp/sfe_1874.csv'
|
||
-- FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\r\n';
|
||
--
|
||
-- golden source 种子建议:用方式A导出 1.1~1.6 共 6 个 json,
|
||
-- 连同 trade 主记录,作为"一笔带分红互换交易"的完整快照纳入版本库。
|