test(swap): 新增互换分红重复计算录制测试+排查SQL(TDD红灯)
分红被重复计入 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)。
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# 数据库结构快照(体积大且随库结构变化失效,仅本地参考,不入版本库)
|
||||||
|
项目文档/数据库/排查SQL/*_yltrs_ylcms.sql
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace YLErp.Modules.SwapModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 互换分红损益 - 黄金文件录制测试(A1:仅录制,不回放)
|
||||||
|
/// ============================================================================
|
||||||
|
/// 目的:
|
||||||
|
/// 从测试库录制一笔"带分红的互换交易"完整生命周期数据(5表快照),
|
||||||
|
/// 序列化为 JSON golden 文件。既作为:
|
||||||
|
/// (1) 人工/脚本验证"分红重复计算 2 次"的数据证据;
|
||||||
|
/// (2) 后续 EOD 重构(抽虚方法)后,回放回归测试的 golden source 种子。
|
||||||
|
///
|
||||||
|
/// 为什么是录制而不是回放:
|
||||||
|
/// 回放需要 SwapEodPositionService 把 DB 调用抽成虚方法(参考
|
||||||
|
/// refactor-swap-event-testable 分支的 TestableSwapEodPositionService)。
|
||||||
|
/// 当前 1.4.2 分支尚未做该重构,故先录制 golden 数据。
|
||||||
|
/// TDD 红灯:录制数据会暴露 RealizedPnl 中分红被计 2 次的事实,
|
||||||
|
/// 待"方向A:让 MarkClosePnl 不含分红"修复后,同一批 golden 用于回归守底。
|
||||||
|
///
|
||||||
|
/// 运行方式:
|
||||||
|
/// 全部标 [Ignore]+[TestCategory("DBRecording")],不会自动跑(不依赖测试库环境)。
|
||||||
|
/// 手动执行:在测试资源管理器取消忽略,或用 vstest:
|
||||||
|
/// vstest.console.exe UnitTestProject.dll /TestCaseFilter:"TestCategory=DBRecording"
|
||||||
|
/// 录制产物落 bin/$(Configuration)/net6.0/Resources/GoldenFiles/SwapDividend/*.json
|
||||||
|
/// ============================================================================
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class SwapDividendGoldenRecordTest
|
||||||
|
{
|
||||||
|
private static readonly string GoldenDir = Path.Combine(
|
||||||
|
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapDividend");
|
||||||
|
|
||||||
|
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
NullValueHandling = NullValueHandling.Include,
|
||||||
|
DateFormatString = "yyyy-MM-ddTHH:mm:ss",
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||||
|
};
|
||||||
|
|
||||||
|
// 已确认的样本交易(来自测试库 swap_flow_event EventType in(3,4) DividendIn<>0 筛选):
|
||||||
|
// 1875 = 纯分红型(MarkClosePnl==DividendIn,最干净,重复计算最直观)
|
||||||
|
// 1891 = 混合型 (MarkClosePnl 含价差成分,复杂场景)
|
||||||
|
private static readonly int[] SampleTradeIds = { 1875, 1891 };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Step0:列出库中所有"带分红的互换交易",确认样本有效性。
|
||||||
|
/// 打印 SwapTradeId / 分红合计 / 盯市合计 / eod快照数,供挑选样本。
|
||||||
|
///
|
||||||
|
/// 可直接运行:连不上测试库时返回 Inconclusive(不计入失败),不挡 CI;
|
||||||
|
/// 连得上时输出诊断表。这是日常排查"库里有啥分红交易"的入口。
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
[TestCategory("DBRecording")]
|
||||||
|
public void Step0_ListDividendSwapTrades()
|
||||||
|
{
|
||||||
|
YLContext db;
|
||||||
|
try { db = DbContextFactory.GetYLDbContext(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var trades = db.swap_flow_event
|
||||||
|
.Where(x => (x.EventType == (int)SwapFlowEventTypeEnum.互换
|
||||||
|
|| x.EventType == (int)SwapFlowEventTypeEnum.自动互换)
|
||||||
|
&& x.DividendIn != 0m
|
||||||
|
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
|
||||||
|
.AsEnumerable()
|
||||||
|
.GroupBy(x => x.SwapTradeId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
SwapTradeId = g.Key,
|
||||||
|
SwapTradeNo = g.Select(x => x.SwapTradeNo).FirstOrDefault(s => !string.IsNullOrEmpty(s)),
|
||||||
|
DividendSum = g.Sum(x => x.DividendIn),
|
||||||
|
MarkCloseSum = g.Sum(x => x.MarkClosePnl),
|
||||||
|
LastEventDate = g.Max(x => x.EventDate),
|
||||||
|
EodPositionCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key),
|
||||||
|
EodSwapCount = db.eod_swap.Count(e => e.SwapTradeId == g.Key)
|
||||||
|
})
|
||||||
|
.OrderByDescending(t => Math.Abs(t.DividendSum))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"=== 带分红的互换交易数: {trades.Count} ===");
|
||||||
|
Console.WriteLine($"{"TradeId",8} {"SwapTradeNo",-24} {"分红合计",14} {"盯市合计",14} {"eod持仓",8} {"eod汇总",8}");
|
||||||
|
foreach (var t in trades)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{t.SwapTradeId,8} {(t.SwapTradeNo ?? ""),-24} {t.DividendSum,14:F4} {t.MarkCloseSum,14:F4} {t.EodPositionCount,8} {t.EodSwapCount,8}");
|
||||||
|
// 直观诊断:分红型交易若 MarkCloseSum≈DividendSum,说明 MarkClosePnl 全是分红(重复计算铁证)
|
||||||
|
if (Math.Abs(t.MarkCloseSum - t.DividendSum) < 0.01m && t.DividendSum != 0m)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" ↳ ⚠ MarkClosePnl合计≈DividendIn合计 → 盯市列里全是分红,RealizedPnl 会计 2 次");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.IsTrue(trades.Count > 0, "库中应存在带分红的互换交易");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
db?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Step1:逐笔录制样本交易的完整快照(5表),并输出分红重复计算诊断。
|
||||||
|
///
|
||||||
|
/// 为何保留 [Ignore]:本方法有写文件副作用(落 golden JSON),
|
||||||
|
/// 不应随每次构建/CI 自动执行;只在需要"刷新 golden 种子"时手动触发。
|
||||||
|
/// 运行方式(三选一):
|
||||||
|
/// - VS 测试资源管理器:选中本方法 → 右键 → 运行(VS 默认会跑被 Ignore 的,除非全局过滤)
|
||||||
|
/// - 命令行:dotnet test --filter "FullyQualifiedName~Step1_RecordSampleTrades"
|
||||||
|
/// - 临时:删掉本方法上的 [Ignore] 再跑,跑完恢复
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
[TestCategory("DBRecording")]
|
||||||
|
// [Ignore] 临时取消以运行录制
|
||||||
|
public void Step1_RecordSampleTrades()
|
||||||
|
{
|
||||||
|
using var db = DbContextFactory.GetYLDbContext();
|
||||||
|
Directory.CreateDirectory(GoldenDir);
|
||||||
|
int recorded = 0;
|
||||||
|
|
||||||
|
foreach (var tradeId in SampleTradeIds)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n========== 录制 SwapTradeId={tradeId} ==========");
|
||||||
|
|
||||||
|
// 1. 交易主信息
|
||||||
|
var trade = db.trade.FirstOrDefault(t => t.id == tradeId);
|
||||||
|
if (trade == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"⚠ trade 表无 SwapTradeId={tradeId},跳过");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 五表快照
|
||||||
|
var positions = db.swap_position
|
||||||
|
.Where(p => p.SwapTradeId == tradeId)
|
||||||
|
.OrderByDescending(p => p.IsInitial).ThenBy(p => p.PositionId)
|
||||||
|
.ToList();
|
||||||
|
var flowEvents = db.swap_flow_event
|
||||||
|
.Where(e => e.SwapTradeId == tradeId)
|
||||||
|
.OrderBy(e => e.EventDate).ThenBy(e => e.EventType).ThenBy(e => e.id)
|
||||||
|
.ToList();
|
||||||
|
var eodPositions = db.eod_swap_position
|
||||||
|
.Where(e => e.SwapTradeId == tradeId)
|
||||||
|
.OrderBy(e => e.PositionId).ThenBy(e => e.ValueDate)
|
||||||
|
.ToList();
|
||||||
|
var eodSwaps = db.eod_swap
|
||||||
|
.Where(e => e.SwapTradeId == tradeId)
|
||||||
|
.OrderBy(e => e.ValueDate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 3. 关联的债券付息(理论应付分红来源)
|
||||||
|
var bondCode = positions.Select(p => p.UnderlyingCode).FirstOrDefault(c => !string.IsNullOrEmpty(c))
|
||||||
|
?? trade.UnderlyingCode;
|
||||||
|
List<BondPayment> bondPayments = new List<BondPayment>();
|
||||||
|
if (!string.IsNullOrEmpty(bondCode))
|
||||||
|
{
|
||||||
|
bondPayments = db.bondPayment
|
||||||
|
.Where(b => b.underlyingCode == bondCode)
|
||||||
|
.OrderBy(b => b.payment_date_pl)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 诊断:坐实分红重复计算(这是录制测试的核心价值)
|
||||||
|
var diagnosis = DiagnoseDividendDoubleCount(tradeId, flowEvents, eodPositions);
|
||||||
|
Console.WriteLine(diagnosis.Summary);
|
||||||
|
|
||||||
|
// 5. 序列化为 golden 文件
|
||||||
|
var golden = new SwapDividendGoldenModel
|
||||||
|
{
|
||||||
|
SwapTradeId = tradeId,
|
||||||
|
SwapTradeNo = trade.TradeNumber,
|
||||||
|
UnderlyingCode = bondCode,
|
||||||
|
RecordedAt = DateTime.Now,
|
||||||
|
SourceDb = "test",
|
||||||
|
Purpose = "分红重复计算验证 + EOD重构回归基线",
|
||||||
|
InputTrade = JObject.FromObject(trade, JsonSerializer.Create(JsonSettings)),
|
||||||
|
InputPositions = JArray.FromObject(positions, JsonSerializer.Create(JsonSettings)),
|
||||||
|
InputFlowEvents = JArray.FromObject(flowEvents, JsonSerializer.Create(JsonSettings)),
|
||||||
|
InputEodPositions = JArray.FromObject(eodPositions, JsonSerializer.Create(JsonSettings)),
|
||||||
|
InputEodSwaps = JArray.FromObject(eodSwaps, JsonSerializer.Create(JsonSettings)),
|
||||||
|
InputBondPayments = JArray.FromObject(bondPayments, JsonSerializer.Create(JsonSettings)),
|
||||||
|
Diagnosis = JObject.FromObject(diagnosis, JsonSerializer.Create(JsonSettings))
|
||||||
|
};
|
||||||
|
|
||||||
|
string fileName = $"dividend_trade_{tradeId}.json";
|
||||||
|
string filePath = Path.Combine(GoldenDir, fileName);
|
||||||
|
File.WriteAllText(filePath, JsonConvert.SerializeObject(golden, JsonSettings));
|
||||||
|
Console.WriteLine($"✅ 已保存: {filePath}");
|
||||||
|
recorded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(recorded > 0, "至少应录制 1 笔样本");
|
||||||
|
Console.WriteLine($"\n录制完成,共 {recorded} 笔,输出目录: {GoldenDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分红重复计算诊断:对照代码行号,把链路数据逐一算出来。
|
||||||
|
/// 重复计算根因链路(SwapEodPositionService.cs):
|
||||||
|
/// SetPriceInfoByFlowEvent:1610 TdCloseMtmPnl = Σ MarkClosePnl(互换事件里已含分红)
|
||||||
|
/// UpdateEodPosition:1486 RealizedMtmPnL += TdCloseMtmPnl ← 分红第1次(盯市列)
|
||||||
|
/// UpdateEodPosition:1488 TdCloseDividend = Σ DividendIn
|
||||||
|
/// UpdateEodPosition:1494 RealizedDividend += TdCloseDividend ← 分红第2次(分红列)
|
||||||
|
/// SaveEodSwap:1869 eod_swap.RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + ...)
|
||||||
|
/// → 分红在盯市列和分红列各计一次 = 2 次
|
||||||
|
/// </summary>
|
||||||
|
private DiagnoseResult DiagnoseDividendDoubleCount(
|
||||||
|
int tradeId,
|
||||||
|
List<swap_flow_event> flowEvents,
|
||||||
|
List<eod_swap_position> eodPositions)
|
||||||
|
{
|
||||||
|
var r = new DiagnoseResult { SwapTradeId = tradeId };
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
$"--- 分红重复计算诊断 SwapTradeId={tradeId} ---",
|
||||||
|
"",
|
||||||
|
"[流水层] 每条平仓/互换事件拆解 (MarkClosePnl = 价差 + 费CloseFee + 分红DividendIn):",
|
||||||
|
string.Format(" {0,-8}{1,-12}{2,-8}{3,16}{4,12}{5,10}{6,16}",
|
||||||
|
"id", "EventDate", "EvType", "MarkClosePnl", "DividendIn", "CloseFee", "价差(残差)")
|
||||||
|
};
|
||||||
|
|
||||||
|
// 仅取完成状态的平仓/互换事件(开仓事件 MarkClosePnl=0 不参与)
|
||||||
|
var closeSwapEvents = flowEvents
|
||||||
|
.Where(e => (e.EventType == (int)SwapFlowEventTypeEnum.平仓
|
||||||
|
|| e.EventType == (int)SwapFlowEventTypeEnum.互换
|
||||||
|
|| e.EventType == (int)SwapFlowEventTypeEnum.自动互换)
|
||||||
|
&& e.DataState == (int)SwapFlowDateStateEnum.完成)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
decimal totalPriceComponent = 0m; // 全交易价差成分合计(用于类型判定)
|
||||||
|
foreach (var e in closeSwapEvents)
|
||||||
|
{
|
||||||
|
string et = e.EventType switch
|
||||||
|
{
|
||||||
|
(int)SwapFlowEventTypeEnum.平仓 => "平仓",
|
||||||
|
(int)SwapFlowEventTypeEnum.互换 => "互换",
|
||||||
|
(int)SwapFlowEventTypeEnum.自动互换 => "自动互换",
|
||||||
|
_ => e.EventType.ToString()
|
||||||
|
};
|
||||||
|
// 残差 = MarkClosePnl - 分红 - 费 = 纯价差成分
|
||||||
|
decimal priceComp = e.MarkClosePnl - e.DividendIn - e.CloseFee;
|
||||||
|
totalPriceComponent += priceComp;
|
||||||
|
lines.Add(string.Format(" {0,-8}{1,-12}{2,-8}{3,16:F4}{4,12:F4}{5,10:F4}{6,16:F4}",
|
||||||
|
e.id, e.EventDate.ToString("yyyy-MM-dd"), et, e.MarkClosePnl, e.DividendIn, e.CloseFee, priceComp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 按 PositionId 拆解(避免双向腿抵消)=====
|
||||||
|
lines.Add("");
|
||||||
|
lines.Add("[EOD层] 按 PositionId 拆解盯市列成分:");
|
||||||
|
lines.Add(string.Format(" {0,-12}{1,16}{2,16}{3,12}{4,16}{5,16}{6,16}",
|
||||||
|
"PositionId", "盯市列合计", "价差成分", "费成分", "分红成分(重复)", "分红列累计", "重复计入"));
|
||||||
|
|
||||||
|
decimal totalRepeat = 0m;
|
||||||
|
var evByPos = closeSwapEvents.GroupBy(e => e.PositionId).ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
var eodByPos = eodPositions.GroupBy(e => e.PositionId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.ValueDate).First());
|
||||||
|
|
||||||
|
foreach (var pid in eodByPos.Keys.OrderBy(k => k))
|
||||||
|
{
|
||||||
|
var evs = evByPos.ContainsKey(pid) ? evByPos[pid] : new List<swap_flow_event>();
|
||||||
|
decimal mtmTotal = evs.Sum(e => e.MarkClosePnl);
|
||||||
|
decimal feeComp = evs.Sum(e => e.CloseFee);
|
||||||
|
decimal divComp = evs.Sum(e => e.DividendIn); // 盯市列里的分红成分(被重复计入)
|
||||||
|
decimal priceComp = mtmTotal - divComp - feeComp;
|
||||||
|
decimal realizedDiv = eodByPos[pid].RealizedDividend;
|
||||||
|
|
||||||
|
totalRepeat += divComp;
|
||||||
|
r.逐持仓拆解.Add(new PositionDiagnose
|
||||||
|
{
|
||||||
|
PositionId = pid,
|
||||||
|
盯市列合计 = mtmTotal,
|
||||||
|
盯市价差成分 = priceComp,
|
||||||
|
盯市费成分 = feeComp,
|
||||||
|
盯市分红成分 = divComp,
|
||||||
|
分红列累计 = realizedDiv,
|
||||||
|
重复计入分红 = divComp
|
||||||
|
});
|
||||||
|
lines.Add(string.Format(" {0,-12}{1,16:F4}{2,16:F4}{3,12:F4}{4,16:F4}{5,16:F4}{6,16:F4}",
|
||||||
|
pid, mtmTotal, priceComp, feeComp, divComp, realizedDiv, divComp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 全交易累计与类型判定 =====
|
||||||
|
r.最终累计盯市已实现 = eodByPos.Values.Sum(e => e.RealizedMtmPnL);
|
||||||
|
r.最终累计分红已实现 = eodByPos.Values.Sum(e => e.RealizedDividend);
|
||||||
|
r.最终持仓层累计已实现 = eodByPos.Values.Sum(e => e.RealizedPnl);
|
||||||
|
r.重复计入分红金额 = totalRepeat;
|
||||||
|
r.重复计算成立 = Math.Abs(totalRepeat) > 0.01m;
|
||||||
|
r.交易类型 = Math.Abs(totalPriceComponent) < 0.01m ? "纯分红型" : "混合型";
|
||||||
|
|
||||||
|
// 结论
|
||||||
|
if (r.重复计算成立)
|
||||||
|
{
|
||||||
|
r.结论 = string.Format(
|
||||||
|
"⚠ 坐实重复计算:盯市列含分红成分 {0:F4}(既在 RealizedMtmPnL 又在 RealizedDividend)," +
|
||||||
|
"修复后 RealizedPnl 应减少 {0:F4}。类型={1}。",
|
||||||
|
totalRepeat, r.交易类型);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r.结论 = "未检测到重复计算(盯市列分红成分≈0,可能已修复或无分红平仓/互换事件)。";
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add("");
|
||||||
|
lines.Add("[汇总]");
|
||||||
|
lines.Add($" 交易类型: {r.交易类型}(价差成分合计={totalPriceComponent:F4})");
|
||||||
|
lines.Add($" 盯市列里被重复计入的分红成分: {r.重复计入分红金额:F4}");
|
||||||
|
lines.Add($" 最终 RealizedMtmPnL(盯市列): {r.最终累计盯市已实现:F4}");
|
||||||
|
lines.Add($" 最终 RealizedDividend(分红列): {r.最终累计分红已实现:F4}");
|
||||||
|
lines.Add($" 最终 RealizedPnl(持仓层): {r.最终持仓层累计已实现:F4}");
|
||||||
|
lines.Add($" 重复计算成立: {r.重复计算成立}");
|
||||||
|
lines.Add($" [结论] {r.结论}");
|
||||||
|
|
||||||
|
r.Summary = string.Join("\n", lines);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Step2:校验已录制 golden 文件的完整性(离线,不连库)。
|
||||||
|
/// 确认每个 json 含 5 表数据、能正确反序列化。
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
[TestCategory("DBRecording")]
|
||||||
|
public void Step2_VerifyRecordedGoldenFiles()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(GoldenDir))
|
||||||
|
{
|
||||||
|
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Step1_RecordSampleTrades)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var files = Directory.GetFiles(GoldenDir, "dividend_trade_*.json");
|
||||||
|
Assert.IsTrue(files.Length > 0, $"应至少有 1 个 golden 文件 in {GoldenDir}");
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(file);
|
||||||
|
var golden = JsonConvert.DeserializeObject<SwapDividendGoldenModel>(json);
|
||||||
|
|
||||||
|
Assert.IsTrue(golden.SwapTradeId > 0, $"{file}: SwapTradeId 无效");
|
||||||
|
Assert.IsNotNull(golden.InputFlowEvents, $"{file}: InputFlowEvents 缺失");
|
||||||
|
Assert.IsTrue(golden.InputFlowEvents.Count > 0, $"{file}: InputFlowEvents 为空");
|
||||||
|
Assert.IsNotNull(golden.InputEodPositions, $"{file}: InputEodPositions 缺失");
|
||||||
|
Assert.IsNotNull(golden.Diagnosis, $"{file}: Diagnosis 缺失");
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ {Path.GetFileName(file)}: trade={golden.SwapTradeId}, " +
|
||||||
|
$"flow={golden.InputFlowEvents.Count}条, eod={golden.InputEodPositions.Count}条, " +
|
||||||
|
$"结论={golden.Diagnosis?["结论"]?.Value<string>()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分红黄金文件数据模型。5 表快照 + 诊断结论。
|
||||||
|
/// 字段名沿用数据库实体类名,反序列化时类型一致。
|
||||||
|
/// </summary>
|
||||||
|
public class SwapDividendGoldenModel
|
||||||
|
{
|
||||||
|
public int SwapTradeId { get; set; }
|
||||||
|
public string SwapTradeNo { get; set; }
|
||||||
|
public string UnderlyingCode { get; set; }
|
||||||
|
public DateTime RecordedAt { get; set; }
|
||||||
|
public string SourceDb { get; set; }
|
||||||
|
public string Purpose { get; set; }
|
||||||
|
|
||||||
|
public JObject InputTrade { get; set; }
|
||||||
|
public JArray InputPositions { get; set; } // swap_position
|
||||||
|
public JArray InputFlowEvents { get; set; } // swap_flow_event(分红核心)
|
||||||
|
public JArray InputEodPositions { get; set; } // eod_swap_position(重复计算发生处)
|
||||||
|
public JArray InputEodSwaps { get; set; } // eod_swap(汇总层)
|
||||||
|
public JArray InputBondPayments { get; set; } // bond_payment_info(理论应付)
|
||||||
|
|
||||||
|
public JObject Diagnosis { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重复计算诊断结果,随 golden 一起持久化,便于修复后对比。
|
||||||
|
/// 拆解原理:每条平仓/互换事件的 MarkClosePnl = 价差成分 + 费成分(CloseFee) + 分红成分(DividendIn)。
|
||||||
|
/// 盯市列(TdCloseMtmPnl=ΣMarkClosePnl) 含了分红成分一份,分红列(TdCloseDividend=ΣDividendIn) 又含一份,
|
||||||
|
/// 故"重复金额" = 进入盯市列的分红成分 = Σ(事件 DividendIn)。按 PositionId 分别拆解避免双向腿抵消。
|
||||||
|
/// </summary>
|
||||||
|
public class DiagnoseResult
|
||||||
|
{
|
||||||
|
public int SwapTradeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>"纯分红型"(价差成分≈0) 或 "混合型"(价差成分≠0)。基于 MarkClosePnl 是否含价差判定。</summary>
|
||||||
|
public string 交易类型 { get; set; }
|
||||||
|
|
||||||
|
// ===== 逐 PositionId 拆解 =====
|
||||||
|
public List<PositionDiagnose> 逐持仓拆解 { get; set; } = new List<PositionDiagnose>();
|
||||||
|
|
||||||
|
/// <summary>盯市列里被重复计入的分红成分合计(=修复后 RealizedPnl 应减少的金额)。</summary>
|
||||||
|
public decimal 重复计入分红金额 { get; set; }
|
||||||
|
public decimal 最终累计盯市已实现 { get; set; }
|
||||||
|
public decimal 最终累计分红已实现 { get; set; }
|
||||||
|
public decimal 最终持仓层累计已实现 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>若重复计入分红金额≠0,则重复计算成立。</summary>
|
||||||
|
public bool 重复计算成立 { get; set; }
|
||||||
|
public string 结论 { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个持仓腿(PositionId)的拆解结果。
|
||||||
|
/// </summary>
|
||||||
|
public class PositionDiagnose
|
||||||
|
{
|
||||||
|
public long PositionId { get; set; }
|
||||||
|
/// <summary>该腿盯市列合计 = Σ 事件 MarkClosePnl。</summary>
|
||||||
|
public decimal 盯市列合计 { get; set; }
|
||||||
|
/// <summary>盯市列里的价差成分 = Σ(MarkClosePnl - DividendIn - CloseFee)。</summary>
|
||||||
|
public decimal 盯市价差成分 { get; set; }
|
||||||
|
/// <summary>盯市列里的费成分 = Σ CloseFee。</summary>
|
||||||
|
public decimal 盯市费成分 { get; set; }
|
||||||
|
/// <summary>盯市列里的分红成分 = Σ DividendIn(这是被重复计入的部分)。</summary>
|
||||||
|
public decimal 盯市分红成分 { get; set; }
|
||||||
|
/// <summary>该腿分红列最终累计 RealizedDividend。</summary>
|
||||||
|
public decimal 分红列累计 { get; set; }
|
||||||
|
/// <summary>该腿被重复计入的分红 = 盯市分红成分。</summary>
|
||||||
|
public decimal 重复计入分红 { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,9 @@
|
|||||||
<None Update="Data\Calendars\chn.txt">
|
<None Update="Data\Calendars\chn.txt">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Resources\GoldenFiles\**\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="NLog.config">
|
<None Update="NLog.config">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
"ylcms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_ylcms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
||||||
"yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
"yladmin": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_admin;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
||||||
"ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
"ylclient": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=glms_yltrs_client;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;",
|
||||||
"bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;"
|
"bondoms": "server=192.168.2.96;uid=DBAdmin;pooling=true;port=3306;pwd=YieldChain$$2025;database=zszq_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;"
|
||||||
},
|
},
|
||||||
"LibreOffice": {
|
"LibreOffice": {
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 互换分红损益:数据导出与重复计算验证 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 主记录,作为"一笔带分红互换交易"的完整快照纳入版本库。
|
||||||
Reference in New Issue
Block a user