diff --git a/.editorconfig b/.editorconfig index e2964ef3..40eb71f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -160,17 +160,3 @@ csharp_preserve_single_line_statements = true # CA1819: Properties should not return arrays dotnet_diagnostic.CA1819.severity = silent - -# JavaScript 和 TypeScript 文件 -[*.{js,jsx,ts,tsx}] -# 缩进和间距 -indent_size = 4 -indent_style = space -tab_width = 4 - -# 新行首选项 -end_of_line = crlf -insert_final_newline = false - -# 拖尾逗号不添加 -trailing_comma = none diff --git a/.gitignore b/.gitignore index 0f2a2af1..f2b3f579 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,14 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +# 数据库结构快照(体积大且随库结构变化失效,仅本地参考,不入版本库) +项目文档/数据库/排查SQL/*_yltrs_ylcms.sql + # User-specific files *.suo *.user *.sln.docstates +*.lscache # Build results [Dd]ebug/ diff --git a/EOF b/EOF new file mode 100644 index 00000000..e69de29b diff --git a/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs b/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs index aff0be48..36090934 100644 --- a/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs +++ b/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs @@ -32,12 +32,19 @@ namespace YLErp.Commons /// 交易维度-价格偏离度 /// private static string TRADE_PRICE_RATE = "risk:cfg:trade:price_rate"; - - + /// + /// 交易维度-DV + /// + private static string TRADE_DV = "risk:cfg:trade:dv"; /// /// 客户维度-名义本金 /// private static string CLIENT_PRINCIPAL = "risk:cfg:client:principal"; + /// + /// 全局维度-DV + /// + + private static string GLOBAL_DV = "risk:cfg:global:dv"; @@ -45,6 +52,8 @@ namespace YLErp.Commons { switch (quoteType) { + case QuotaTypeEnum.GLOBAL_ALL: + return GLOBAL_DV; case QuotaTypeEnum.GLOBAL_SWAP: return SWAP_PRINCIPAL; case QuotaTypeEnum.UNDERLYING: @@ -54,6 +63,10 @@ namespace YLErp.Commons } return ASSET_ROLL; case QuotaTypeEnum.TRADE: + if ("DV".Equals(quoteIndex)) + { + return TRADE_DV; + } return TRADE_PRICE_RATE; case QuotaTypeEnum.CLIENT: return CLIENT_PRINCIPAL; diff --git a/Framework/YLErp.Core/DBModels/EodSwap.cs b/Framework/YLErp.Core/DBModels/EodSwap.cs index 61f2289a..4d6294a0 100644 --- a/Framework/YLErp.Core/DBModels/EodSwap.cs +++ b/Framework/YLErp.Core/DBModels/EodSwap.cs @@ -148,6 +148,11 @@ namespace YLErp.DBModels [DataChange] public decimal TdCloseQty { get; set; } /// + /// 基点价值DV01 + /// + [DataChange] + public decimal? dv01 { get; set; } + /// /// 平仓起始日期 /// [NotMapped] diff --git a/Framework/YLErp.Core/DBModels/EodSwapPosition.cs b/Framework/YLErp.Core/DBModels/EodSwapPosition.cs index 1a2944e9..f4c8f745 100644 --- a/Framework/YLErp.Core/DBModels/EodSwapPosition.cs +++ b/Framework/YLErp.Core/DBModels/EodSwapPosition.cs @@ -433,6 +433,11 @@ namespace YLErp.DBModels /// public int? interest_rule { get; set; } /// + /// 基点价值DV01 + /// + [DataChange] + public decimal? dv01 { get; set; } + /// /// 持仓编码 /// [NotMapped] diff --git a/Framework/YLErp.Core/DBModels/QuotaMonitor.cs b/Framework/YLErp.Core/DBModels/QuotaMonitor.cs index 43fdddce..3de7acf1 100644 --- a/Framework/YLErp.Core/DBModels/QuotaMonitor.cs +++ b/Framework/YLErp.Core/DBModels/QuotaMonitor.cs @@ -891,6 +891,26 @@ namespace YLErp.DBModels /// CCR /// public double? Quota_CCR { get; set; } = double.NaN; + /// + /// DV + /// + public double? DV { get; set; } + /// + /// DV限额 + /// + public double? Quota_DV_Upper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_Lower { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wUpper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wLower { get; set; } = double.NaN; } /// @@ -1257,5 +1277,25 @@ namespace YLErp.DBModels public double? Quota_ThisYearTotalPnl_Lower { get; set; } = double.NaN; public double? Quota_ThisYearTotalPnl_wUpper { get; set; } = double.NaN; public double? Quota_ThisYearTotalPnl_wLower { get; set; } = double.NaN; + /// + /// DV + /// + public double? DV { get; set; } + /// + /// DV限额 + /// + public double? Quota_DV_Upper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_Lower { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wUpper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wLower { get; set; } = double.NaN; } } diff --git a/Framework/YLErp.Core/DBModels/SwapEvent.cs b/Framework/YLErp.Core/DBModels/SwapEvent.cs index 311c25d1..ce7f1cc2 100644 --- a/Framework/YLErp.Core/DBModels/SwapEvent.cs +++ b/Framework/YLErp.Core/DBModels/SwapEvent.cs @@ -102,6 +102,7 @@ namespace YLErp.DBModels public UnwindData() { FlowEvents = new List(); + ClientCashIds = new List(); } public int SwapTradeId { get; set; } /// @@ -174,6 +175,14 @@ namespace YLErp.DBModels /// public decimal SwapCloseAmount { get; set; } /// + /// 浮动端分红盈亏 + /// + public decimal SwapDividendPnl { get; set; } + /// + /// 自动互换生成的客户资金记录ID集合 + /// + public List ClientCashIds { get; set; } + /// /// 利息腿/浮动腿 集合,不序列化存储,只做查询 /// public List FlowEvents { get; set; } diff --git a/Framework/YLErp.Core/DBModels/SwapFlowEvent.cs b/Framework/YLErp.Core/DBModels/SwapFlowEvent.cs index c4c9e6df..b9079e27 100644 --- a/Framework/YLErp.Core/DBModels/SwapFlowEvent.cs +++ b/Framework/YLErp.Core/DBModels/SwapFlowEvent.cs @@ -373,15 +373,15 @@ namespace YLErp.DBModels } } /// - /// 浮动平仓盈亏 + /// 浮动端平仓盈亏 /// [NotMapped] - public decimal PosiPnl + public decimal FloatPnlSum { get { - return MarkClosePnl- TradingFee; + return MarkClosePnl + TradingFee + TradingFeePending + DividendIn; } } /// diff --git a/Framework/YLErp.Core/DBModels/TradeExtend.cs b/Framework/YLErp.Core/DBModels/TradeExtend.cs index 7ab817dd..72772775 100644 --- a/Framework/YLErp.Core/DBModels/TradeExtend.cs +++ b/Framework/YLErp.Core/DBModels/TradeExtend.cs @@ -99,5 +99,10 @@ namespace YLErp.DBModels /// public int SettlementRules { get; set; } = 0; + /// + /// 派息金额支付日 0到期结算日 1派息日+0 2派息日+1 3派息日+2 + /// + public int DividendPayDate { get; set; } = 1; + } } diff --git a/Framework/YLErp.Core/DBModels/UnderlyingBond.cs b/Framework/YLErp.Core/DBModels/UnderlyingBond.cs index 5797f652..535e8746 100644 --- a/Framework/YLErp.Core/DBModels/UnderlyingBond.cs +++ b/Framework/YLErp.Core/DBModels/UnderlyingBond.cs @@ -80,5 +80,9 @@ /// 债券期限(利率债) /// public string BondTerm { get; set; } - } + /// + /// 债券单位面值,默认100 + /// + public decimal? Price { get; set; } + } } diff --git a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs index 18486318..18980071 100644 --- a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs +++ b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs @@ -111,7 +111,7 @@ namespace YLErp.Enums { return new List() { - //new SelectItem() {Text="全局",Value="16" }, + new SelectItem() {Text="全局",Value="16" }, //new SelectItem() {Text="场外业务",Value="0" }, //new SelectItem() {Text="场外期权",Value="1" }, new SelectItem() {Text="互换",Value="2" }, diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx index 8719402e..cfbf0af2 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx index 83c478c8..4a762fb1 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx index e60e2ce2..13be1b62 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx index ed0f6197..2b2b91cb 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs index 34ae5a7a..81aa0587 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs @@ -207,7 +207,7 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator // 银行账户信息 dic["户名"] = bank?.ClientName ?? ""; dic["银行账号"] = bank?.Card ?? ""; - dic["支付系统号"] = ""; + dic["支付系统号"] = bank?.Payment ?? ""; dic["开户行"] = bank?.Bank ?? ""; dic["大额行号"] = bank?.Payment ?? ""; @@ -315,17 +315,17 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator // 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,追保0% dic["追保区间0下限"] = (100 - 1 * diff).ToString("0.##"); dic["追保金额比例0"] = "0"; - // 区间1:{100-2A}% ≤ 当前净价 < {100-A}%,追保{B-A}% - dic["追保区间1下限"] = (100 - 2 * maintainRatePercent).ToString("0.##"); - dic["追保区间1上限"] = (100 - 1 * maintainRatePercent).ToString("0.##"); + // 区间1:{100-2(B-A)}% ≤ 当前净价 < {100-(B-A)}%,追保{B-A}% + dic["追保区间1下限"] = (100 - 2 * diff).ToString("0.##"); + dic["追保区间1上限"] = (100 - 1 * diff).ToString("0.##"); dic["追保金额比例1"] = (1 * diff).ToString("0.##"); - // 区间2:{100-3A}% ≤ 当前净价 < {100-2A}%,追保{2(B-A)}% - dic["追保区间2下限"] = (100 - 3 * maintainRatePercent).ToString("0.##"); - dic["追保区间2上限"] = (100 - 2 * maintainRatePercent).ToString("0.##"); + // 区间2:{100-3(B-A)}% ≤ 当前净价 < {100-2(B-A)}%,追保{2(B-A)}% + dic["追保区间2下限"] = (100 - 3 * diff).ToString("0.##"); + dic["追保区间2上限"] = (100 - 2 * diff).ToString("0.##"); dic["追保金额比例2"] = (2 * diff).ToString("0.##"); - // 区间3:{100-4A}% ≤ 当前净价 < {100-3A}%,追保{3(B-A)}% - dic["追保区间3下限"] = (100 - 4 * maintainRatePercent).ToString("0.##"); - dic["追保区间3上限"] = (100 - 3 * maintainRatePercent).ToString("0.##"); + // 区间3:{100-4(B-A)}% ≤ 当前净价 < {100-3(B-A)}%,追保{3(B-A)}% + dic["追保区间3下限"] = (100 - 4 * diff).ToString("0.##"); + dic["追保区间3上限"] = (100 - 3 * diff).ToString("0.##"); dic["追保金额比例3"] = (3 * diff).ToString("0.##"); // === 客户看多 返还追保事件 === diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs index f2f1c827..7523323b 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeSettleBillGenerator.cs @@ -107,13 +107,13 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator //row.MarginInterestAmount = MarginInterestAmount.ToString("0.00"); var FloatRate = PosiNotionalValue == 0 ? 0 : InterestAmount / PosiNotionalValue; row.FloatRate = FloatRate.ToString("0.0000%"); - var PosiPnl = -(flowEventGroup.MarkClosePnl - tradingFee - flowEventGroup.DividendIn); + var PosiPnl = -flowEventGroup.MarkClosePnl; row.PosiPnl = PosiPnl.ToString("0.00"); - var markClosePnl = (-flowEventGroup.MarkClosePnl); + var markClosePnl = -flowEventGroup.FloatPnlSum; row.MarkClosePnl = markClosePnl.ToString("0.00"); row.DividendIn = (-flowEventGroup.DividendIn).ToString("0.00"); var marginBackAmount = unwindFlowEvents.Where(x => ConsTrade.InterestMarginModels.Contains(x.InterestMode)).Sum(s => s.InterestPrincipal); - var NetSettleAmout = unwindFlowEvents.Sum(x => x.InterestClosePnL) * -1 + flowEventGroup.MarkClosePnl * -1 + marginBackAmount; + var NetSettleAmout = unwindFlowEvents.Sum(x => x.InterestClosePnL) * -1 + flowEventGroup.FloatPnlSum * -1 + marginBackAmount; row.NetSettleAmout = NetSettleAmout.ToString("0.00"); table.Add(row); } diff --git a/UnitTestProject/Modules/SwapModule/ClearSwapPositionsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ClearSwapPositionsScenarioTest.cs new file mode 100644 index 00000000..cbcc0f5f --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/ClearSwapPositionsScenarioTest.cs @@ -0,0 +1,253 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// ClearSwapPositions 重收盘清理 - 合成单元测试 + /// ============================================================================ + /// 验证 78751f0a 的核心修复:重收盘清理自动互换资金记录时, + /// 正确排除手动互换的资金记录(manualClientCashIds)。 + /// + /// 核心方法:GetLegacyAutoSwapClientCashRecords(SwapTradeBaseService) + /// 它接收自动互换事件列表,返回应该删除的资金记录。 + /// 关键逻辑:cs:491 !manualClientCashIds.Contains(x.id) 排除手动互换的记录。 + /// ============================================================================ + [TestClass] + public class ClearSwapPositionsScenarioTest + { + private const int TradeId = 300; + + #region Stub + + private sealed class StubService : SwapEodPositionService + { + // 注入的内存数据 + public List FlowEvents { get; set; } = new(); + public List SwapEvents { get; set; } = new(); + public List CashRecords { get; set; } = new(); + + public List DeletedRecords { get; } = new(); + + public StubService() : base(new OptUserInfo(0, nameof(ClearSwapPositionsScenarioTest), OptUserFrom.UnitTest)) + { + } + + // override GetLegacyAutoSwapClientCashRecords 的内部依赖 + protected override List FindFlowEventsByEventIds(List eventIds) + { + return FlowEvents.Where(x => x.EventId.HasValue && eventIds.Contains(x.EventId.Value)).ToList(); + } + + protected override List FindManualClientCashIds(int swapTradeId) + { + return SwapEvents + .Where(x => x.SwapTradeId == swapTradeId + && x.ClientCashId > 0 + && x.EventType != (int)SwapEventTypeEnum.自动互换) + .Select(x => x.ClientCashId) + .ToList(); + } + + protected override List FindClientCashRecords(int tradeId) + { + return CashRecords.Where(x => x.TradeId == tradeId).ToList(); + } + + // public 包装 + public List ExecuteGetLegacyAutoSwapClientCashRecords( + List swapEvents, List excludedClientCashIds) + { + return GetLegacyAutoSwapClientCashRecords(swapEvents, excludedClientCashIds); + } + } + + #endregion + + #region 数据构建 + + private static swap_event CreateAutoSwapEvent(long id, DateTime valueDate, params int[] cashIds) + { + // SwapRealizedPnL=-100 匹配 Money=100(IsLegacyAutoSwapClientCashRecord 按金额校验) + var data = new UnwindData + { + SwapTradeId = TradeId, ValueDate = valueDate, + ClientCashIds = cashIds.ToList(), + SwapRealizedPnL = -100m // -Money → IsLegacyAutoSwapClientCashRecord 匹配 + }; + return new swap_event + { + id = id, SwapTradeId = TradeId, EventType = (int)SwapEventTypeEnum.自动互换, + ValueDate = valueDate, ClientCashId = cashIds.FirstOrDefault(), + EventData = JsonConvert.SerializeObject(data) + }; + } + + private static swap_event CreateManualSwapEvent(long id, DateTime valueDate, int clientCashId) + { + return new swap_event + { + id = id, SwapTradeId = TradeId, EventType = (int)SwapEventTypeEnum.互换, + ValueDate = valueDate, ClientCashId = clientCashId, + EventData = JsonConvert.SerializeObject(new UnwindData { SwapTradeId = TradeId, ValueDate = valueDate }) + }; + } + + private static ClientCashInCashOut CreateCashRecord(int id, string action, DateTime happenDate, double money = 100) + { + return new ClientCashInCashOut + { + id = id, TradeId = TradeId, Action = action, + HappenDate = happenDate, Money = money, + State = ClientCashInCashOut.已确认, ValidState = "Valid" + }; + } + + #endregion + + // ================================================================ + // 场景1:自动互换资金记录被正确选中删除 + // ================================================================ + + /// + /// [CSW_001] 只有一条自动互换资金记录 → 应被选中删除 + /// --------------------------------------------------------------- + /// 构造1条自动互换事件(EventData含ClientCashIds), + /// 1条资金记录(Action=系统操作-互换)。 + /// GetLegacyAutoSwapClientCashRecords 应返回这条资金记录。 + /// + [TestMethod] + public void CSW_001_自动互换资金记录被选中删除() + { + var date = new DateTime(2026, 6, 29); + var service = new StubService(); + + // 不含ClientCashIds → 走legacy路径 + var autoEvent = CreateAutoSwapEvent(id: 5001, valueDate: date); + service.SwapEvents.Add(autoEvent); + + var cashRecord = CreateCashRecord(id: 7001, action: ClientCashInCashOut.系统操作_互换, happenDate: date); + service.CashRecords.Add(cashRecord); + + var result = service.ExecuteGetLegacyAutoSwapClientCashRecords( + new List { autoEvent }, excludedClientCashIds: new List()); + + Assert.AreEqual(1, result.Count, "应选中1条自动互换资金记录"); + Assert.AreEqual(7001, result[0].id, "选中的应是id=7001"); + Console.WriteLine($"自动互换资金记录(id=7001)被正确选中 ✅"); + } + + // ================================================================ + // 场景2:手动互换资金记录被排除(78751f0a 核心修复) + // ================================================================ + + /// + /// [CSW_002] 手动互换资金记录不被选中(manualClientCashIds排除) + /// --------------------------------------------------------------- + /// 构造1条自动互换事件 + 1条手动互换事件(ClientCashId=8001)。 + /// 2条资金记录都是Action=系统操作-互换,但1条属于手动(id=8001)。 + /// GetLegacyAutoSwapClientCashRecords 应只返回自动的那条,排除手动的。 + /// + [TestMethod] + public void CSW_002_手动互换资金记录被排除() + { + var date = new DateTime(2026, 6, 29); + var service = new StubService(); + + // 自动互换事件(无ClientCashIds,走legacy路径) + var autoEvent = CreateAutoSwapEvent(id: 5001, valueDate: date); + // 手动互换事件(ClientCashId=8001) + var manualEvent = CreateManualSwapEvent(id: 5002, valueDate: date, clientCashId: 8001); + service.SwapEvents.Add(autoEvent); + service.SwapEvents.Add(manualEvent); + + // 两条资金记录都是系统操作-互换 + var autoCash = CreateCashRecord(id: 7001, action: ClientCashInCashOut.系统操作_互换, happenDate: date); + var manualCash = CreateCashRecord(id: 8001, action: ClientCashInCashOut.系统操作_互换, happenDate: date); + service.CashRecords.Add(autoCash); + service.CashRecords.Add(manualCash); + + var result = service.ExecuteGetLegacyAutoSwapClientCashRecords( + new List { autoEvent }, excludedClientCashIds: new List()); + + // 应只返回自动的(7001),排除手动的(8001) + Assert.AreEqual(1, result.Count, "应只选中1条(排除手动的)"); + Assert.AreEqual(7001, result[0].id, "选中的应是自动的id=7001"); + Assert.IsFalse(result.Any(x => x.id == 8001), "手动互换(id=8001)不应被选中"); + Console.WriteLine($"手动互换资金记录(id=8001)被正确排除 ✅"); + } + + // ================================================================ + // 场景3:excludedClientCashIds 排除已处理的记录 + // ================================================================ + + /// + /// [CSW_003] 已在excludedClientCashIds中的记录不被重复选中 + /// --------------------------------------------------------------- + /// 资金记录id=7001已在excludedClientCashIds中(之前已处理过), + /// 不应再次被选中。 + /// + [TestMethod] + public void CSW_003_已处理的记录不重复选中() + { + var date = new DateTime(2026, 6, 29); + var service = new StubService(); + + var autoEvent = CreateAutoSwapEvent(id: 5001, valueDate: date); + service.SwapEvents.Add(autoEvent); + + var cash1 = CreateCashRecord(id: 7001, action: ClientCashInCashOut.系统操作_互换, happenDate: date); + var cash2 = CreateCashRecord(id: 7002, action: ClientCashInCashOut.系统操作_互换, happenDate: date); + service.CashRecords.Add(cash1); + service.CashRecords.Add(cash2); + + // 7001 已在 excludedClientCashIds 中 + var result = service.ExecuteGetLegacyAutoSwapClientCashRecords( + new List { autoEvent }, excludedClientCashIds: new List { 7001 }); + + Assert.AreEqual(1, result.Count, "应只选中1条(排除已处理的7001)"); + Assert.AreEqual(7002, result[0].id, "选中的应是未处理的7002"); + Console.WriteLine($"已处理记录(7001)被排除,只选中7002 ✅"); + } + + // ================================================================ + // 场景4:预付金返息记录也被正确处理 + // ================================================================ + + /// + /// [CSW_004] 预付金返息记录(Action=系统操作-预付金返息)也参与清理 + /// --------------------------------------------------------------- + /// 自动互换产生的预付金返息记录应被选中,手动的不应被选中。 + /// + [TestMethod] + public void CSW_004_预付金返息记录参与清理() + { + var date = new DateTime(2026, 6, 29); + var service = new StubService(); + + var autoEvent = CreateAutoSwapEvent(id: 5001, valueDate: date); + // 预付金返息匹配 SwapMarginRebatePnl,覆盖默认的 SwapRealizedPnL + var autoData = JsonConvert.DeserializeObject(autoEvent.EventData); + autoData.SwapMarginRebatePnl = -100m; + autoData.SwapRealizedPnL = 0m; + autoEvent.EventData = JsonConvert.SerializeObject(autoData); + var manualEvent = CreateManualSwapEvent(id: 5002, valueDate: date, clientCashId: 8001); + service.SwapEvents.Add(autoEvent); + service.SwapEvents.Add(manualEvent); + + // 预付金返息记录 + var autoRebate = CreateCashRecord(id: 7001, action: ClientCashInCashOut.系统操作_预付金返息, happenDate: date); + var manualRebate = CreateCashRecord(id: 8001, action: ClientCashInCashOut.系统操作_预付金返息, happenDate: date); + service.CashRecords.Add(autoRebate); + service.CashRecords.Add(manualRebate); + + var result = service.ExecuteGetLegacyAutoSwapClientCashRecords( + new List { autoEvent }, excludedClientCashIds: new List()); + + Assert.AreEqual(1, result.Count, "应只选中1条预付金返息(排除手动的)"); + Assert.AreEqual(7001, result[0].id, "选中的应是自动的预付金返息7001"); + Console.WriteLine($"预付金返息: 自动的(7001)被选中, 手动的(8001)被排除 ✅"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/ComposePageScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ComposePageScenarioTest.cs new file mode 100644 index 00000000..ccbb7672 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/ComposePageScenarioTest.cs @@ -0,0 +1,270 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// ComposePage 流水合成持仓 - 合成单元测试 + /// ============================================================================ + /// 验证 swap_flow_event(开仓/平仓事件)→ eod_swap_position(持仓)的转换。 + /// ComposePage 是每笔开仓/平仓/互换都要经过的核心逻辑。 + /// + /// 场景参考 testable 分支 ComposePageScenarioTest,简化为最核心的 3 个: + /// ① 空事件直接返回 + /// ② 单条开仓 → 创建1条持仓,均价=开仓价 + /// ③ 两条开仓(同标的) → 加权均价 + /// ============================================================================ + [TestClass] + public class ComposePageScenarioTest + { + private const int SwapTradeId = 100; + private static readonly DateTime TradeDate = new(2026, 4, 27); + + #region Stub + + private sealed class StubEodService : SwapEodPositionService + { + public List CreatedEodPositions { get; } = new(); + public int ClientCashCallCount { get; private set; } + private int _nextId = 1; + + public StubEodService() : base(new OptUserInfo(0, nameof(ComposePageScenarioTest), OptUserFrom.UnitTest)) + { + } + + // 内存数据 + public Dictionary Trades { get; set; } = new(); + public Dictionary Extends { get; set; } = new(); + public List Positions { get; set; } = new(); + public List EodPositions { get; set; } = new(); + public eod_swap LastEodSwap { get; set; } + + protected override trade FindTrade(int swapTradeId) + => Trades.TryGetValue(swapTradeId, out var t) ? t : null; + + protected override trade_extend FindTradeExtend(int tradeId) + => Extends.TryGetValue(tradeId, out var e) ? e : null; + + protected override List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) + => EodPositions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList(); + + protected override List FindSwapPositions(int swapTradeId) + => Positions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList(); + + protected override eod_swap FindEodSwap(int swapTradeId, DateTime valueDate) + => LastEodSwap?.SwapTradeId == swapTradeId ? LastEodSwap : null; + + protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) + { + return new swap_event { id = _nextId++, SwapTradeId = swapTradeId, EventType = eventType, ValueDate = tradeDate, EventData = data }; + } + + protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate) + { + ClientCashCallCount++; + return _nextId++; + } + + protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) + { + // 不做任何事(测试不验证框架合约汇总) + } + + protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) + { + // 不做任何事(测试无历史事件需清理) + } + + protected override void PersistEodSwapPosition(eod_swap_position position) + { + if (position.id == 0) position.id = _nextId++; + CreatedEodPositions.Add(position); + } + + protected override void SaveAllChanges() { } + + protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) + => 1.0; + + // override SaveEodPosition:捕获生成的 eod,绕过 UpdateSwapPosition 连库 + protected override decimal SaveEodPosition(eod_swap_position newEodPayPosition, + trade td, swap_flow_event eventFlow, + decimal netPrice, decimal grossPrice, decimal netFeePrice, decimal netNoFeePrice, + decimal payQty, decimal tradingFee, decimal posiNotionalValue, + decimal dividendIn, decimal tdDividendIn, + decimal closeQty, decimal closeFee, decimal closeMtmPnl, + int posiType, bool isNewPosition) + { + // 设置关键字段(模拟生产逻辑的输出) + newEodPayPosition.PosiNetPrice = netPrice; + newEodPayPosition.PosiGrossPrice = grossPrice; + newEodPayPosition.PosiQuantity = payQty; + newEodPayPosition.PosiNotionalValue = posiNotionalValue; + newEodPayPosition.SwapTradeId = td.id; + newEodPayPosition.ClientId = td.ClientId; + PersistEodSwapPosition(newEodPayPosition); + return 0m; // 开仓费(测试不关心) + } + + public void ExecuteComposePage(int swapTradeId, List flowEvents, DateTime tradeDate) + { + // needTrans=false 跳过事务 + ComposePage(swapTradeId, flowEvents, tradeDate, needTrans: false); + } + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = SwapTradeId, TradeNumber = "UT-COMPOSE-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = TradeDate, StartDate = TradeDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend { TradeId = SwapTradeId } + }; + } + + private static swap_position CreateFloatPosition(int positionId = 1, int positionType = 1) + { + return new swap_position + { + id = positionId, SwapTradeId = SwapTradeId, + PosiDirection = 2, PositionType = positionType, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PosiQuantity = 0, PosiNotionalValue = 0, + PosiNetPrice = 0, PosiGrossPrice = 0, + IsInitial = true, Invalid = false + }; + } + + private static swap_flow_event CreateOpenEvent(int positionId, decimal qty, decimal feeAvg, decimal avg, int positionType = 1) + { + return new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.开仓, + PositionId = positionId, Quantity = qty, + TradingAmountFeeAvg = feeAvg, TradingAmountAvg = avg, + TradingAmountNetFeeAvg = feeAvg, TradingAmountNetAvg = avg, + ContractSize = 1m, PositionType = positionType, + MarkClosePnl = 0, DividendIn = 0, CloseFee = 0, TradingFeePending = 0, + UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, + DataState = (int)SwapFlowDateStateEnum.等待完成 + }; + } + + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") + { + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + + private static StubEodService CreateService() + { + var svc = new StubEodService(); + svc.Trades[SwapTradeId] = CreateTrade(); + svc.Extends[SwapTradeId] = CreateTrade().trade_extend; + svc.Positions.Add(CreateFloatPosition()); + return svc; + } + + #endregion + + // ================================================================ + // 场景1:空事件 → 直接返回,不创建任何持仓 + // ================================================================ + + [TestMethod] + public void CP_001_空事件不创建持仓() + { + var service = CreateService(); + service.ExecuteComposePage(SwapTradeId, new List(), TradeDate); + Assert.AreEqual(0, service.CreatedEodPositions.Count, "无事件不应创建持仓"); + } + + // ================================================================ + // 场景2:单条开仓 → 创建1条持仓,均价=开仓价 + // ================================================================ + + [TestMethod] + public void CP_002_单条开仓创建一条持仓() + { + var service = CreateService(); + var events = new List + { + CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m) + }; + + service.ExecuteComposePage(SwapTradeId, events, TradeDate); + + Assert.AreEqual(1, service.CreatedEodPositions.Count, "应创建1条持仓"); + var pos = service.CreatedEodPositions[0]; + Assert.AreEqual(1000m, pos.PosiQuantity, "持仓数量=1000"); + AssertDecimalEqual(1.0050m, pos.PosiNetPrice, 0.0001m, "含费均价"); + AssertDecimalEqual(1.0020m, pos.PosiGrossPrice, 0.0001m, "不含费均价"); + Assert.AreEqual((int)SwapFlowDateStateEnum.完成, events[0].DataState, "事件应标记完成"); + } + + // ================================================================ + // 场景3:两条开仓(同标的) → 加权均价 + // ================================================================ + + [TestMethod] + public void CP_003_两条开仓加权均价() + { + var service = CreateService(); + var events = new List + { + CreateOpenEvent(positionId: 1, qty: 600, feeAvg: 1.0040m, avg: 1.0010m), + CreateOpenEvent(positionId: 1, qty: 400, feeAvg: 1.0060m, avg: 1.0030m) + }; + + service.ExecuteComposePage(SwapTradeId, events, TradeDate); + + Assert.AreEqual(1, service.CreatedEodPositions.Count); + var pos = service.CreatedEodPositions[0]; + + // 加权均价: netPrice = (1.0040*600 + 1.0060*400) / 1000 = 1.0048 + AssertDecimalEqual(1.0048m, pos.PosiNetPrice, 0.0001m, "加权含费均价"); + // grossPrice = (1.0010*600 + 1.0030*400) / 1000 = 1.0018 + AssertDecimalEqual(1.0018m, pos.PosiGrossPrice, 0.0001m, "加权不含费均价"); + } + + // ================================================================ + // 场景4:一条开仓+一条平仓 → 验证平仓扣减数量 + // ================================================================ + + [TestMethod] + public void CP_004_开仓后平仓扣减数量() + { + var service = CreateService(); + var events = new List + { + CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m), + new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓, + PositionId = 1, Quantity = 400, + TradingAmountFeeAvg = 1.0050m, TradingAmountAvg = 1.0020m, + ContractSize = 1m, PositionType = 1, + MarkClosePnl = 100m, DividendIn = 0, CloseFee = 5m, TradingFeePending = 0, + UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, + DataState = (int)SwapFlowDateStateEnum.等待完成 + } + }; + + service.ExecuteComposePage(SwapTradeId, events, TradeDate); + + Assert.AreEqual(1, service.CreatedEodPositions.Count); + var pos = service.CreatedEodPositions[0]; + // 开仓1000 - 平仓400 = 剩余600 + Assert.AreEqual(600m, pos.PosiQuantity, "开仓1000-平仓400=剩余600"); + Assert.IsTrue(service.ClientCashCallCount > 0, "平仓应产生资金记录"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs new file mode 100644 index 00000000..d2f56e52 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs @@ -0,0 +1,250 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 复利 consumedInterest 扣除 - 合成单元测试 + /// ============================================================================ + /// 验证 c6adb3bb 的修复:复利路径平仓时,扣除历史已通过互换结出的利息。 + /// + /// 核心场景: + /// 一笔复利交易,N天后做了互换结算(已结N天利息),之后再平仓。 + /// 平仓默认值应 = 从头算的全程利息 - 已结利息(consumedInterest)。 + /// 如果不扣(bug),平仓默认值 = 全程利息(偏大)。 + /// 如果多扣(之前单利的错误),平仓默认值 = 0或负(偏小)。 + /// + /// 模仿 GetInterestsUnitTest_T0 的 StubSwapDealService 模式。 + /// ============================================================================ + [TestClass] + public class ConsumedInterestScenarioTest + { + #region 常量 + + private const decimal Principal = 1000m; + private const decimal FixedRate = 0.0025m; // 加点利率 + private const double FloatRate = 0.001; // FR007 + private const decimal TotalRate = FixedRate + (decimal)FloatRate; // 综合年化利率 + private const int AnnualDays = 365; + private const int ResetPeriod = 3; + private static readonly DateTime StartDate = new(2026, 4, 27); + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + + #endregion + + #region Stub:内存 SwapDealService + consumedInterest 注入 + + /// + /// 继承 SwapDealService,override 两个虚方法: + /// - TryGetFloatRate:返回固定浮动利率(不连库) + /// - GetConsumedInterest:返回注入的历史已结利息(不连库) + /// + private sealed class StubSwapDealService : SwapDealService + { + private readonly double _floatRate; + private readonly decimal _consumedInterest; + + public StubSwapDealService(OptUserInfo optUser, double floatRate, decimal consumedInterest) + : base(optUser) + { + _floatRate = floatRate; + _consumedInterest = consumedInterest; + } + + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + rate = _floatRate; + return true; // 始终返回固定浮动利率 + } + + public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) + { + return _consumedInterest; // 返回注入值 + } + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = 1, TradeNumber = "UT-CONSUMED-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", // 算头不算尾 + SettlementRules = 0 + }) + } + }; + } + + private static swap_position CreateCompoundPosition() + { + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = FixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.复利, + IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = 0, + FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(new List + { + new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 } + }) + }; + } + + /// 调用 GetInterests 获取复利利息(统一调用入口,settment:false走盘中平仓路径) + private static swap_flow_event CalcCompoundUnwind(StubSwapDealService service, DateTime unwindDate) + { + var td = CreateTrade(); + var position = CreateCompoundPosition(); + var interests = service.GetInterests(td, td.trade_extend, unwindDate, unwindDate, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + add: false, settment: false, newCalcLast: false); + Assert.AreEqual(1, interests.Count); + return interests[0]; + } + + private static StubSwapDealService CreateService(decimal consumedInterest) + { + return new StubSwapDealService( + new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest), + FloatRate, consumedInterest); + } + + private static void AssertDecimal(decimal expected, decimal actual, string message = "") + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + + #endregion + + // ================================================================ + // 场景1:基线——无历史互换(consumedInterest=0),拿到全程复利利息 + // ================================================================ + + /// + /// [CI_001] 无历史互换结清,复利平仓利息基线 + /// --------------------------------------------------------------- + /// consumedInterest=0,平仓利息=从头算的全程复利利息。 + /// 此值作为后续场景的参照基线(避免独立复利计算的精度匹配问题)。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void CI_001_无历史互换平仓利息基线() + { + var unwindDate = StartDate.AddDays(10); // 4/27+10=5/7,算头不算尾约9天 + var service = CreateService(consumedInterest: 0m); + var result = CalcCompoundUnwind(service, unwindDate); + + Assert.IsTrue(result.InterestAmount > 0, "无互换时复利利息应>0"); + Console.WriteLine($"基线(consumedInterest=0): InterestAmount={result.InterestAmount:F6}"); + } + + // ================================================================ + // 场景2:consumedInterest>0 → 平仓利息=基线-consumedInterest + // ================================================================ + + /// + /// [CI_002] 注入consumedInterest后,平仓利息应=基线-consumedInterest + /// --------------------------------------------------------------- + /// 用相同参数但注入不同的consumedInterest,验证: + /// 利息(有consumed) = 利息(无consumed) - consumedInterest + /// 这是验证cs:793 `interest -= consumedInterest` 的直接方式。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void CI_002_consumedInterest正确扣除() + { + var unwindDate = StartDate.AddDays(10); + + // 基线:consumedInterest=0 + var baselineResult = CalcCompoundUnwind(CreateService(0m), unwindDate); + decimal baseline = baselineResult.InterestAmount; + + // 注入consumedInterest=基线的50% + decimal consumed = baseline * 0.5m; + var consumedResult = CalcCompoundUnwind(CreateService(consumed), unwindDate); + + // 期望 = 基线 - consumed + decimal expected = baseline - consumed; + AssertDecimal(expected, consumedResult.InterestAmount, + $"平仓利息应=基线({baseline:F6})-consumed({consumed:F6})={expected:F6}"); + Console.WriteLine($"基线={baseline:F6}, consumed={consumed:F6}"); + Console.WriteLine($"平仓利息={consumedResult.InterestAmount:F6}, 期望={expected:F6} ✅"); + } + + // ================================================================ + // 场景3:守恒——consumed + 平仓利息 = 基线 + // ================================================================ + + /// + /// [CI_003] 守恒:consumedInterest + 平仓利息(扣后) = 基线(无consumed) + /// --------------------------------------------------------------- + /// 注入任意consumedInterest,验证 consumed + 利息 = 基线。 + /// 如果扣多了(守恒不成立→合计<基线)或没扣(合计>基线),测试失败。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void CI_003_守恒consumed加平仓等于基线() + { + var unwindDate = StartDate.AddDays(10); + decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount; + + // 注入不同的consumedInterest验证守恒 + decimal[] testConsumed = { baseline * 0.3m, baseline * 0.5m, baseline * 0.8m }; + foreach (var consumed in testConsumed) + { + var result = CalcCompoundUnwind(CreateService(consumed), unwindDate); + decimal actual = consumed + result.InterestAmount; + AssertDecimal(baseline, actual, + $"守恒: consumed({consumed:F6}) + 利息({result.InterestAmount:F6}) = {actual:F6} 应=基线({baseline:F6})"); + Console.WriteLine($"consumed={consumed:F6} + 利息={result.InterestAmount:F6} = {actual:F6} = 基线{baseline:F6} ✅"); + } + } + + // ================================================================ + // 场景4:consumedInterest=全部基线 → 平仓利息≈0,不为负 + // ================================================================ + + /// + /// [CI_004] 全部利息已结清(consumedInterest=基线),再平仓利息应≈0 + /// --------------------------------------------------------------- + /// 验证不会扣过头变成负数(之前单利双重扣减的错误)。 + /// 复利从头算全程 - 全程consumed = 0,应精确归零或微小正值。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void CI_004_全部已结再平仓利息不为负() + { + var unwindDate = StartDate.AddDays(10); + decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount; + + // consumedInterest=全部基线 + var result = CalcCompoundUnwind(CreateService(baseline), unwindDate); + + Console.WriteLine($"基线={baseline:F6}, consumed={baseline:F6}, 平仓利息={result.InterestAmount:F6}"); + Assert.IsTrue(result.InterestAmount >= -0.01m, + $"全部已结再平仓利息应≈0(实际={result.InterestAmount:F6}),不应为负"); + Console.WriteLine($"全部已结平仓≈0({result.InterestAmount:F6})✅"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs new file mode 100644 index 00000000..073d6240 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs @@ -0,0 +1,387 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// DealFloatPositions 浮动腿收盘归档 - 合成单元测试 + /// ============================================================================ + /// DealFloatPositions 处理浮动腿(标的持仓)的日终归档,三个分支: + /// ① 无前日eod → SaveCurrentEodInitalPosi(首日初始化,纯计算) + /// ② 有eod无平仓 → CopyEodPosition(复制+更新价格,依赖外部数据源) + /// ③ 有eod有平仓 → UpdateEodPosition(更新持仓,依赖外部数据源) + /// + /// 当前可测范围: + /// - 分支选择逻辑(DealFloatPositions 调度层,纯内存) + /// - SaveCurrentEodInitalPosi(首日初始化,纯计算,无外部依赖) + /// CopyEodPosition/UpdateEodPosition 需额外接缝(UnderlyingCodePrice等),留后续。 + /// ============================================================================ + [TestClass] + public class DealFloatPositionsScenarioTest + { + private const int SwapTradeId = 200; + private static readonly DateTime TradeDate = new(2026, 4, 28); + private static readonly DateTime PreSettleDate = new(2026, 4, 27); + + #region Stub + + private sealed class StubEodService : SwapEodPositionService + { + // 可注入的外部数据 + public decimal UnderlyingPrice { get; set; } = 1.00m; + public decimal Vobp { get; set; } = 0m; + public decimal BondPayment { get; set; } = 0m; + public decimal TaxRate { get; set; } = 0m; + public string UnderlyingCode { get; set; } = "210210.IB"; + + public StubEodService() : base(new OptUserInfo(0, nameof(DealFloatPositionsScenarioTest), OptUserFrom.UnitTest)) + { + } + + // override 外部依赖 + protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) + { + vobp = Vobp; + return UnderlyingPrice; + } + + protected override underlying_manager GetUnderlyingData(string underlyingCode) + { + return new underlying_manager { ValueAddedTax = TaxRate }; + } + + protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) + { + return BondPayment; + } + + protected override void SaveAllChanges() { } + + protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) + => 1.0; + + // DealFloatPositions 和子方法都是 protected,通过 public 包装暴露 + public List ExecuteDealFloatPositions( + List posiList, List realPosiList, + List eodPositions, List todyEodPositions, + DateTime settleDate, trade td, DateTime preSettleDate, List flowEvents) + { + return DealFloatPositions(posiList, realPosiList, eodPositions, todyEodPositions, + settleDate, td, preSettleDate, flowEvents); + } + + public eod_swap_position ExecuteSaveCurrentEodInitalPosi( + swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, + List unwindEvents) + { + return SaveCurrentEodInitalPosi(position, td, settleDate, preSettleDate, unwindEvents); + } + + public eod_swap_position ExecuteCopyEodPosition( + eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate) + { + return CopyEodPosition(eod, curretEod, td, valueDate, preSettleDate); + } + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = SwapTradeId, TradeNumber = "UT-FLOAT-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = PreSettleDate, StartDate = PreSettleDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + OriginalStockEqvNotional = 10000 + }; + } + + private static swap_position CreateFloatPosition(int id = 3001, decimal qty = 10000m) + { + return new swap_position + { + id = id, SwapTradeId = SwapTradeId, + PosiDirection = 2, PositionType = (int)PositionTypeFlag.Long, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PosiQuantity = qty, PosiNotionalValue = qty, + PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m, + PosiNetFeePrice = 1.004m, PosiNetNoFeePrice = 1.001m, + IsInitial = true, Invalid = false, + PosiTradingFee = 0, PosiTradingFeePending = 0 + }; + } + + private static swap_flow_event CreateCloseEvent(int positionId, decimal qty, decimal markClosePnl = 100m) + { + return new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓, + PositionId = positionId, Quantity = qty, + MarkClosePnl = markClosePnl, DividendIn = 0, CloseFee = 5m, + TradingFeePending = 0, TradingAmount = qty * 1.002m, + UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + } + + #endregion + + // ================================================================ + // 场景1:空持仓列表 → 返回空列表 + // ================================================================ + + [TestMethod] + public void DF_001_空持仓返回空列表() + { + var service = new StubEodService(); + var result = service.ExecuteDealFloatPositions( + new List(), new List(), + new List(), new List(), + TradeDate, CreateTrade(), PreSettleDate, new List()); + + Assert.AreEqual(0, result.Count, "空持仓应返回空列表"); + } + + // ================================================================ + // 场景2:首日无前日eod → 走 SaveCurrentEodInitalPosi 分支 + // ================================================================ + + /// + /// 无前日eod(eodPositions 不含该持仓),应走 SaveCurrentEodInitalPosi。 + /// SaveCurrentEodInitalPosi 是纯计算,验证基本字段正确。 + /// + [TestMethod] + public void DF_002_首日无eod走初始化分支() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var result = service.ExecuteDealFloatPositions( + new List { position }, + new List { position }, + new List(), // 无前日eod + new List(), // 无当日eod + TradeDate, td, PreSettleDate, + new List()); // 无平仓事件 + + Assert.AreEqual(1, result.Count, "应生成1条浮动腿eod"); + var eod = result[0]; + Assert.AreEqual(position.id, eod.PositionId, "PositionId应匹配"); + Assert.AreEqual(SwapTradeId, eod.SwapTradeId); + Assert.AreEqual(TradeDate, eod.ValueDate); + Console.WriteLine($"首日初始化: PosiQuantity={eod.PosiQuantity}, PosiNetPrice={eod.PosiNetPrice}"); + } + + // ================================================================ + // 场景3:SaveCurrentEodInitalPosi 直接验证(纯计算方法) + // ================================================================ + + /// + /// 直接测 SaveCurrentEodInitalPosi,验证它正确初始化 eod 的关键字段。 + /// 这个方法无外部依赖(纯计算),可以精确验证值。 + /// + [TestMethod] + public void DF_003_首日初始化字段正确() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(qty: 10000m); + + var eod = service.ExecuteSaveCurrentEodInitalPosi( + position, td, TradeDate, PreSettleDate, new List()); + + // 验证关键字段 + Assert.AreEqual(10000m, eod.PosiQuantity, "持仓数量应=初始数量"); + Assert.AreEqual(1.005m, eod.PosiNetPrice, "含费均价应=持仓均价"); + Assert.AreEqual(1.002m, eod.PosiGrossPrice, "不含费均价"); + Assert.AreEqual((int)PositionTypeFlag.Long, eod.PositionType, "持仓类型"); + Assert.AreEqual(SwapTradeId, eod.SwapTradeId, "交易ID"); + Assert.AreEqual(td.ClientId, eod.ClientId, "客户ID"); + Assert.AreEqual(0, eod.TdCloseQty, "首日无平仓数量"); + Assert.AreEqual(0, eod.TdCloseMtmPnl, "首日无平仓盈亏"); + Console.WriteLine($"首日初始化 eod: Qty={eod.PosiQuantity}, NetPrice={eod.PosiNetPrice}, GrossPrice={eod.PosiGrossPrice} ✅"); + } + + // ================================================================ + // 场景4:有前日eod无平仓 → 走 CopyEodPosition 分支 + // ================================================================ + + /// + /// 有前日eod但无平仓事件,应走 CopyEodPosition 分支。 + /// CopyEodPosition 依赖外部数据源(DataCacheProvider/UnderlyingCodePrice), + /// 测试验证分支选择正确(不验证值),且不抛异常。 + /// + [TestMethod] + public void DF_004_有eod无平仓走Copy分支() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var preEod = new eod_swap_position + { + id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id, + ValueDate = PreSettleDate, PosiQuantity = 10000m, + PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PositionType = (int)PositionTypeFlag.Long, PosiDirection = 2 + }; + + // CopyEodPosition 内部调 DataCacheProvider/UnderlyingCodePrice, + // 这些连缓存可能返回null → 方法 cs:1492 if(um==null) return curretEod + // 所以即使缓存没数据,也不会抛异常,只是字段不更新 + try + { + var result = service.ExecuteDealFloatPositions( + new List { position }, + new List { position }, + new List { preEod }, + new List(), + TradeDate, td, PreSettleDate, + new List()); // 无平仓 + + Assert.AreEqual(1, result.Count, "应生成1条eod"); + // um==null时 CopyEodPosition 直接返回 clone,字段不变 + Assert.AreEqual(10000m, result[0].PosiQuantity, "无缓存时数量应=前日值"); + Console.WriteLine($"Copy分支(无缓存): PosiQuantity={result[0].PosiQuantity}(保持前日值)"); + } + catch (Exception ex) + { + Assert.Inconclusive($"CopyEodPosition 依赖外部数据源,需额外接缝。异常: {ex.Message}"); + } + } + + // ================================================================ + // 场景5:CopyEodPosition 盯市盈亏计算(注入固定标的价格) + // ================================================================ + + /// + /// [DF_005] 无平仓日,标的价格变动 → PosiMtmPnL 正确反映浮动盈亏 + /// --------------------------------------------------------------- + /// 前日持仓全价=1.002,当日标的价格=1.010(涨了)。 + /// 多头收取方向,PosiMtmPnL = (1.010 - 1.002) × 10000 × 1(ContractSize) × 1(shortRatio) × 1(directionRatio) + /// = 0.008 × 10000 = 80 + /// + [TestMethod] + public void DF_005_Copy分支盯市盈亏计算() + { + var service = new StubEodService(); + service.UnderlyingPrice = 1.010m; // 当日标的价格 + service.TaxRate = 0m; + service.BondPayment = 0m; + + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var preEod = new eod_swap_position + { + id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id, + ValueDate = PreSettleDate, PosiQuantity = 10000m, + PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PositionType = (int)PositionTypeFlag.Long, PosiDirection = (int)SwapDirectionEnum.收取, + PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m + }; + + var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate); + + // PosiMtmPnL = (price - grossPrice) × qty × contractSize × shortRatio × directionRatio + // 收取方向 directionRatio=1, 多头 shortRatio=1 + // = (1.010 - 1.002) × 10000 × 1 × 1 × 1 = 80 + AssertDecimalEqual(80m, result.PosiMtmPnL, 0.01m, "盯市盈亏"); + Console.WriteLine($"Copy分支盯市: PosiMtmPnL={result.PosiMtmPnL}((1.010-1.002)×10000=80)✅"); + } + + // ================================================================ + // 场景6:CopyEodPosition 分红计算(注入固定付息) + // ================================================================ + + /// + /// [DF_006] 无平仓日,债券付息 → TdPosiDividend 和 PosiDividendSum 正确 + /// --------------------------------------------------------------- + /// 注入 BondPayment=100(付息总额),增值税率=0。 + /// TdPosiDividend = 100 / (1+0) × (1-0) = 100。 + /// PosiDividendSum = preEod.PosiDividendSum(0) + TdPosiDividend(100) = 100。 + /// + [TestMethod] + public void DF_006_Copy分支分红计算() + { + var service = new StubEodService(); + service.UnderlyingPrice = 1.002m; // 价格不变 + service.TaxRate = 0m; + service.BondPayment = 100m; // 付息100 + + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var preEod = new eod_swap_position + { + id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id, + ValueDate = PreSettleDate, PosiQuantity = 10000m, + PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PositionType = (int)PositionTypeFlag.Long, PosiDirection = (int)SwapDirectionEnum.收取, + PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m + }; + + var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate); + + // TdPosiDividend = payment / (1+tax) × (1-tax) = 100 / 1 × 1 = 100 + AssertDecimalEqual(100m, result.TdPosiDividend, 0.01m, "当日分红"); + // PosiDividendSum = 前日(0) + 当日(100) = 100 + AssertDecimalEqual(100m, result.PosiDividendSum, 0.01m, "待实现分红累计"); + Console.WriteLine($"Copy分支分红: TdPosiDividend={result.TdPosiDividend}, PosiDividendSum={result.PosiDividendSum} ✅"); + } + + // ================================================================ + // 场景7:分红增值税调整(税率≠0) + // ================================================================ + + /// + /// [DF_007] 分红含增值税调整:BondPayment=100, tax=6%(0.06) + /// --------------------------------------------------------------- + /// TdPosiDividend = 100 / (1+0.06) × (1-0.06) = 100/1.06×0.94 ≈ 88.68 + /// 验证增值税调整公式 cs:1508 payment / (1+tax) * (1-tax)。 + /// + [TestMethod] + public void DF_007_分红增值税调整() + { + var service = new StubEodService(); + service.UnderlyingPrice = 1.002m; + service.TaxRate = 0.06m; // 增值税率6% + service.BondPayment = 100m; + + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var preEod = new eod_swap_position + { + id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id, + ValueDate = PreSettleDate, PosiQuantity = 10000m, + PosiGrossPrice = 1.002m, PosiNetPrice = 1.005m, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PositionType = (int)PositionTypeFlag.Long, PosiDirection = 2, + PosiDividendSum = 0m, PosiFeePending = 0m, PosiProfitSum = 0m + }; + + var result = service.ExecuteCopyEodPosition(preEod, null, td, TradeDate, PreSettleDate); + + // TdPosiDividend = 100 / 1.06 × 0.94 = 88.6792... + decimal expected = Math.Round(100m / 1.06m * 0.94m, 2); + AssertDecimalEqual(expected, result.TdPosiDividend, 0.01m, "增值税调整后分红"); + Console.WriteLine($"分红增值税调整: 付息100, 税率6% → TdPosiDividend={result.TdPosiDividend}(期望{expected})✅"); + } + + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") + { + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsGoldenReplayTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsGoldenReplayTest.cs new file mode 100644 index 00000000..0542fb5b --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/DealInterestsGoldenReplayTest.cs @@ -0,0 +1,344 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// DealInterests Golden 回放测试 + /// ============================================================================ + /// 用 golden JSON 存"输入数据 + 期望输出的精确字段值", + /// 回放时从 JSON 重跑,逐字段精确对比。 + /// + /// 两类方法: + /// - Record*: 连库录制/生成 golden(标 Ignore,手动跑) + /// - Replay*: 读 golden 重跑对比(进 CI) + /// + /// 价值:重构时如果任何一步的输出变了(哪怕第8位小数),立刻失败。 + /// 守恒测试验证"大方向对",golden 验证"精确值对"。 + /// ============================================================================ + [TestClass] + public class DealInterestsGoldenReplayTest + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "DealInterestsGolden"); + + #region Stub(复用 DealInterestsScenarioTest 的模式) + + private sealed class StubEodService : SwapEodPositionService + { + public List PersistedPositions { get; } = new(); + private int _nextId = 1; + + public StubEodService() : base(new OptUserInfo(0, nameof(DealInterestsGoldenReplayTest), OptUserFrom.UnitTest)) + { + } + + protected override void PersistEodSwapPosition(eod_swap_position position) + { + if (position.id == 0) position.id = _nextId++; + PersistedPositions.Add(position); + } + protected override void SaveAllChanges() { } + protected override double GetCurrencyRate(string q, string s, DateTime d, bool p, CurrencyRateType t) => 1.0; + + public void ExecuteSaveEodInterestPosition( + eod_swap_position eodPayPosition, swap_position position, trade td, + DateTime valueDate, List flowEvents) + { + SaveEodInterestPosition(eodPayPosition, null, position, td, valueDate, flowEvents); + } + } + + #endregion + + #region 录制:生成 golden JSON(标 Ignore,手动跑) + + /// + /// 生成所有 golden JSON 文件。 + /// 手动取消 [Ignore] 运行,会覆盖 bin 目录下的 golden 文件。 + /// 生成后复制到 UnitTestProject/Resources/GoldenFiles/ 持久化。 + /// + [TestMethod] + [Ignore] + [TestCategory("GoldenRecord")] + public void Record_AllGoldenScenarios() + { + Directory.CreateDirectory(GoldenDir); + Record_SwapSettleZeroInterestIncomeSum(); + Record_NormalDayIncrement(); + Console.WriteLine($"\n录制完成,输出目录: {GoldenDir}"); + } + + /// 场景1:互换结清后 InterestIncomeSum≈当天新计 + private void Record_SwapSettleZeroInterestIncomeSum() + { + const decimal Principal = 10000m; + const decimal Rate = 0.03m; + const int AnnualDays = 365; + var startDate = new DateTime(2026, 4, 27); + + var td = new trade + { + id = 1, TradeNumber = "GOLDEN-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } + }; + var position = new swap_position + { + id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, + InterestPrincipalFix = Principal, PosiStartDate = startDate, + PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, + interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null + }; + var settleDate = startDate.AddDays(10); + int days = (settleDate - startDate).Days; + decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + var preEod = new eod_swap_position + { + id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestIncomeSum = accumulated, InterestProfitSum = accumulated, + InterestRateDefault = Rate, TdInterestPrincipal = Principal, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 + }; + var swapEvent = new swap_flow_event + { + EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001, + InterestAmount = accumulated, InterestClosePnL = accumulated, + InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestPrincipal = Principal, FloatRate = 0m, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + + var service = new StubEodService(); + service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List { swapEvent }); + + var result = service.PersistedPositions[0]; + var golden = new GoldenScenarioModel + { + Scenario = "互换结清后待实现归零", + Description = $"攒{days}天后互换,InterestIncomeSum应≈当天新计", + Input = new GoldenInput + { + SettleDate = settleDate, + PosiLongNotional = Principal, + OrginPv = Principal + }, + Expected = new GoldenExpected + { + PositionCount = 1, + EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) } + } + }; + + string json = JsonConvert.SerializeObject(golden, Formatting.Indented); + string path = Path.Combine(GoldenDir, "golden_互换结清后待实现归零.json"); + File.WriteAllText(path, json); + Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}"); + Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}"); + Console.WriteLine($" TdCloseInterest={result.TdCloseInterest:F11}"); + Console.WriteLine($" RealizedInterest={result.RealizedInterest:F11}"); + } + + /// 场景2:普通日 InterestIncomeSum 递增 + private void Record_NormalDayIncrement() + { + const decimal Principal = 10000m; + const decimal Rate = 0.03m; + const int AnnualDays = 365; + var startDate = new DateTime(2026, 4, 27); + decimal dailyInc = Math.Round(Principal * Rate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + var td = new trade + { + id = 1, TradeNumber = "GOLDEN-002", ClientId = 999998, + TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } + }; + var position = new swap_position + { + id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, + InterestPrincipalFix = Principal, PosiStartDate = startDate, + PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, + interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null, + InterestSwapInterval = null + }; + + // 用 DealInterests 走 copy 分支 + var settleDate = startDate.AddDays(2); // 第3天 + var preEod = new eod_swap_position + { + id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestIncomeSum = dailyInc, InterestProfitSum = dailyInc, + InterestRateDefault = Rate, TdInterestPrincipal = Principal, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 + }; + + var service = new StubEodService(); + // 通过反射调 DealInterests(copy 分支需要 CalcSwapInterests) + var method = typeof(SwapEodPositionService).GetMethod("DealInterests", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + method.Invoke(service, new object[] + { + new List { position }, + new List { preEod }, + new List(), + settleDate, td, new List(), new List(), null, + Principal, 0m, 0m, 1m, Principal + }); + + if (service.PersistedPositions.Count == 0) + { + Console.WriteLine("⚠ 场景2未生成eod(CalcSwapInterests可能需要接缝),跳过"); + return; + } + + var result = service.PersistedPositions[0]; + var golden = new GoldenScenarioModel + { + Scenario = "普通日归档递增", + Description = "第3天收盘,InterestIncomeSum应=2天+1天=3天利息", + Expected = new GoldenExpected + { + PositionCount = 1, + EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) } + } + }; + + string json = JsonConvert.SerializeObject(golden, Formatting.Indented); + string path = Path.Combine(GoldenDir, "golden_普通日归档递增.json"); + File.WriteAllText(path, json); + Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}"); + Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}"); + } + + #endregion + + #region 回放:读 golden 重跑+精确对比(进 CI) + + /// + /// 回放所有 golden 文件,逐字段精确对比。 + /// 如果任何字段变了(哪怕是第8位小数),测试失败。 + /// + [TestMethod] + public void Replay_AllGoldenFiles() + { + if (!Directory.Exists(GoldenDir)) + { + Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Record_AllGoldenScenarios)"); + return; + } + + var files = Directory.GetFiles(GoldenDir, "*.json").OrderBy(f => f).ToArray(); + Assert.IsTrue(files.Length > 0, "应至少有1个golden文件"); + + int passed = 0, failed = 0; + foreach (var file in files) + { + try + { + var golden = JsonConvert.DeserializeObject(File.ReadAllText(file)); + Console.WriteLine($"\n回放: {Path.GetFileName(file)} - {golden.Scenario}"); + + // 回放互换场景(场景1的模式) + if (golden.Scenario?.Contains("互换结清") == true) + { + ReplaySwapSettle(golden); + } + else + { + Console.WriteLine($" (场景类型'{golden.Scenario}'暂不支持自动回放,跳过)"); + continue; + } + + passed++; + Console.WriteLine($" ✅ 通过"); + } + catch (Exception ex) + { + failed++; + Console.WriteLine($" ❌ 失败: {ex.Message}"); + } + } + + Console.WriteLine($"\n回放结果: {passed}通过 {failed}失败 / {files.Length}总"); + Assert.AreEqual(0, failed, $"{failed}个golden文件回放失败"); + } + + private void ReplaySwapSettle(GoldenScenarioModel golden) + { + const decimal Principal = 10000m; + const decimal Rate = 0.03m; + const int AnnualDays = 365; + var startDate = new DateTime(2026, 4, 27); + var settleDate = golden.Input.SettleDate ?? startDate.AddDays(10); + int days = (settleDate - startDate).Days; + decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + var td = new trade + { + id = 1, TradeNumber = "GOLDEN-REPLAY", ClientId = 999998, + TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } + }; + var position = new swap_position + { + id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, + InterestPrincipalFix = Principal, PosiStartDate = startDate, + PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, + interest_rest_days = 1, interest_rule = 0 + }; + var preEod = new eod_swap_position + { + id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestIncomeSum = accumulated, InterestProfitSum = accumulated, + InterestRateDefault = Rate, TdInterestPrincipal = Principal, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 + }; + var swapEvent = new swap_flow_event + { + EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001, + InterestAmount = accumulated, InterestClosePnL = accumulated, + InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestPrincipal = Principal, DataState = (int)SwapFlowDateStateEnum.完成 + }; + + var service = new StubEodService(); + service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List { swapEvent }); + + // 对比 golden 期望 + Assert.AreEqual(golden.Expected.PositionCount ?? 1, service.PersistedPositions.Count, "持仓数量"); + + var expectedEods = golden.Expected.EodPositions?.ToObject>() ?? new List(); + foreach (var expected in expectedEods) + { + var pid = expected["PositionId"]?.Value() ?? 1001; + var actual = service.PersistedPositions.FirstOrDefault(x => x.PositionId == pid); + Assert.IsNotNull(actual, $"未找到PositionId={pid}"); + GoldenAssert.AssertEodPosition(expected, actual); + } + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs new file mode 100644 index 00000000..1631b512 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -0,0 +1,595 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// DealInterests 利息腿归档 - 合成单元测试(内存,不连库) + /// ============================================================================ + /// 目标:验证收盘时利息腿 eod 的字段计算,覆盖三个分支: + /// ① 手动互换分支 SaveEodInterestPosition(我们修复 InterestIncomeSum 归零的核心) + /// ② 普通日分支 SaveEodInterestPositionCopy(InterestIncomeSum 每日递增) + /// ③ 多日守恒(半平后多日再全平,利息一致性) + /// + /// 模仿 GetInterestsUnitTest_T0 的风格: + /// - 继承生产类,override 虚方法替换 DB 调用 + /// - 内存构造 trade/position/eod/flowEvent 数据 + /// - 断言业务期望值(独立计算,非循环论证) + /// ============================================================================ + [TestClass] + public class DealInterestsScenarioTest + { + #region 测试常量 + + private const decimal Principal = 1000m; + private const decimal FixedRate = 0.01m; + private const int AnnualDays = 365; + private static readonly DateTime StartDate = new(2026, 4, 27); + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + + /// 每天利息(固定利率,算头不算尾,年化365天) + private static decimal DailyInterest => Math.Round(Principal * FixedRate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + #endregion + + #region Stub:内存 SwapEodPositionService + + /// + /// 测试用子类:override 虚方法,把 DB 调用替换为内存操作。 + /// - PersistEodSwapPosition:收集到列表而非写库 + /// - GetCurrencyRate:返回 1.0(本币) + /// + private sealed class StubEodPositionService : SwapEodPositionService + { + public List PersistedPositions { get; } = new(); + + public StubEodPositionService() : base(new OptUserInfo(0, nameof(DealInterestsScenarioTest), OptUserFrom.UnitTest)) + { + } + + protected override void PersistEodSwapPosition(eod_swap_position position) + { + // 收集到列表,不写库。如果 id=0 模拟新增。 + if (position.id == 0) position.id = PersistedPositions.Count + 1; + PersistedPositions.Add(position); + } + + protected override void SaveAllChanges() + { + // 不做任何事(内存模式) + } + + protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) + { + return 1.0; // 本币,汇率=1 + } + + // override CalcSwapInterests:用真实 SwapDealService 算(固定利率不需 mock 浮动利率) + // 生产代码默认实现也是 new SwapDealService(this).GetInterests(...),这里保持一致 + // 但 SwapDealService 内部 TryGetFloatRate 会连库——固定利率(FloatRateUnderlyingCode=null)不会触发 + protected override List CalcSwapInterests( + trade td, trade_extend tradeExtend, + DateTime valueDate, DateTime unwindDate, + List eodPositions, List positions, + decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue, + decimal closePosiNotionalValue, decimal closePrecent, + int eventType, bool tdClose, bool needPrice, + decimal grossPrice, decimal orginPv, + bool add = false, bool settment = true, bool newCalcLast = false, + List closeList = null) + { + return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate, + eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue, + closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice, + grossPrice, orginPv, add, settment, newCalcLast, closeList); + } + + // public 包装:让测试能调用 protected 方法 + public eod_swap_position ExecuteSaveEodInterestPosition( + eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, + swap_position position, trade td, DateTime valueDate, List flowEvents) + { + SaveEodInterestPosition(eodPayPosition, newEodPayPosition, position, td, valueDate, flowEvents); + return PersistedPositions.LastOrDefault(); + } + + // public 包装:调用 DealInterests(通过反射,因为参数太多不好包) + public void ExecuteDealInterests( + List interestList, List eodPositions, + DateTime settleDate, trade td, List flowEvents, + decimal posiLongNational, decimal posiShortNational, + decimal closeNational, decimal grossPrice, decimal orginPv) + { + var method = typeof(SwapEodPositionService).GetMethod("DealInterests", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + method.Invoke(this, new object[] + { + interestList, eodPositions, new List(), + settleDate, td, flowEvents, new List(), null, + posiLongNational, posiShortNational, closeNational, grossPrice, orginPv + }); + } + } + + #endregion + + #region 数据构建器 + + private static trade CreateTrade() + { + return new trade + { + id = 1, TradeNumber = "UT-DEAL-INT-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", // 算头不算尾 + SettlementRules = 0 + }) + } + }; + } + + private static swap_position CreateInterestPosition() + { + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = FixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, + FloatRateUnderlyingCode = null, // 固定利率,不需要浮动 + InterestSwapInterval = JsonConvert.SerializeObject(new List + { + new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 } + }) + }; + } + + /// 创建前一日 eod(模拟"昨天收盘后的状态") + private static eod_swap_position CreatePreEod(DateTime valueDate, decimal interestProfitSum, decimal realizedInterest = 0m) + { + return new eod_swap_position + { + id = 100, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate, + ClientId = 999998, InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestProfitSum = interestProfitSum, + InterestIncomeSum = interestProfitSum, + RealizedInterest = realizedInterest, + InterestRateDefault = FixedRate, + TdInterestPrincipal = Principal, + PosiNotionalValue = Principal, + InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = 1, + FloatRate = 0m + }; + } + + /// 创建互换 flow_event(模拟"当天做了收益结算") + private static swap_flow_event CreateSwapFlowEvent(DateTime eventDate, decimal interestAmount) + { + return new swap_flow_event + { + id = 2001, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.互换, + EventDate = eventDate, UnwindDate = eventDate, PositionId = 1001, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestAmount = interestAmount, + InterestClosePnL = interestAmount, // 收取方向,两者相等 + InterestRate = FixedRate, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestPrincipal = Principal, + FloatRate = 0m, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + } + + private static void AssertDecimal(decimal expected, decimal actual, string message = "") + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + + #endregion + + // ================================================================ + // 场景1:互换结清后 InterestIncomeSum 应归零(cs:837 修复验证) + // ================================================================ + + #region 场景1:互换结清后 InterestIncomeSum 归零 + + /// + /// [DI_SWAP_ZERO_001] 互换结清-攒了N天利息后全额互换结算,待实现应归零 + /// --------------------------------------------------------------- + /// 起息日4/27,攒到5/10(13天),InterestProfitSum≈13天利息。 + /// 5/10做互换结算,flow_event.InterestAmount=13天利息。 + /// 收盘后 InterestIncomeSum 应≈0(全部已实现)。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_SWAP_ZERO_001_互换结清后待实现归零() + { + var service = new StubEodPositionService(); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var settleDate = new DateTime(2026, 5, 10); + + // 攒了13天利息(4/27~5/9,算头不算尾) + int days = (settleDate - StartDate).Days; + decimal accumulatedInterest = Math.Round(Principal * FixedRate * days / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + var preEod = CreatePreEod(settleDate.AddDays(-1), accumulatedInterest); + + // 当天做了互换结算,利息=攒的全部 + var swapEvent = CreateSwapFlowEvent(settleDate, accumulatedInterest); + + // 执行互换分支 + var result = service.ExecuteSaveEodInterestPosition(preEod, null, position, td, settleDate, new List { swapEvent }); + + // 核心断言:InterestIncomeSum = pre + 当天新计(TdInterestIncome) - 实现(TdCloseInterest) + // 互换把攒的13天全付了(TdCloseInterest=accumulatedInterest),但当天又产生1天新计(TdInterestIncome) + // 所以 InterestIncomeSum 应 ≈ 1天新计利息(而非严格0) + // 公式(cs:869): pre.InterestIncomeSum + TdInterestIncome - TdCloseInterest + decimal expectedTdInterestIncome = Math.Round(Principal * FixedRate / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertDecimal(expectedTdInterestIncome, result.InterestIncomeSum, + $"互换结清后 InterestIncomeSum 应=当天新计利息({expectedTdInterestIncome:F6})," + + $"而非攒的全程({accumulatedInterest:F6})"); + + // TdCloseInterest 应=互换实现的利息 + AssertDecimal(accumulatedInterest, result.TdCloseInterest, "TdCloseInterest 应=互换实现的利息"); + + // RealizedInterest 应累加(preEod.RealizedInterest + TdCloseInterest * ratio) + // 收取方向 ratio=1 + AssertDecimal(accumulatedInterest, result.RealizedInterest, "RealizedInterest 应累加已实现利息"); + + Console.WriteLine($"攒了{days}天利息={accumulatedInterest:F6}"); + Console.WriteLine($"互换结清后 InterestIncomeSum={result.InterestIncomeSum:F6}(应≈0)✅"); + Console.WriteLine($"TdCloseInterest={result.TdCloseInterest:F6} RealizedInterest={result.RealizedInterest:F6}"); + } + + /// + /// [DI_SWAP_ZERO_002] 互换结清后 InterestIncomeSum 不为负(防多扣) + /// --------------------------------------------------------------- + /// 验证:待实现=0(已结清)时,TdCloseInterest=当天新计,InterestIncomeSum 应=0。 + /// 公式: 0 + 当天新计 - 当天新计 = 0。如果公式有误会变成负数。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_SWAP_ZERO_002_互换结清后待实现不为负() + { + var service = new StubEodPositionService(); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var swapDate = new DateTime(2026, 5, 10); + + // 已结清状态:待实现=0 + var postSwapEod = CreatePreEod(swapDate.AddDays(-1), 0m, 0m); + + // 互换只结算当天新计(InterestAmount=当天新计利息) + decimal dailyInc = Math.Round(Principal * FixedRate / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + var swapEvent = CreateSwapFlowEvent(swapDate, dailyInc); + + var result = service.ExecuteSaveEodInterestPosition(postSwapEod, null, position, td, swapDate, new List { swapEvent }); + + // 公式: 0(待实现) + dailyInc(新计) - dailyInc(实现) = 0 + AssertDecimal(0m, result.InterestIncomeSum, "待实现=0+当天新计-当天新计应=0,不应为负"); + Console.WriteLine($"已结清后再互换(只结算当天新计):InterestIncomeSum={result.InterestIncomeSum:F6} = 0 ✅"); + } + + #endregion + + // ================================================================ + // 场景2:DealInterests 分支选择逻辑验证 + // ================================================================ + + #region 场景2:分支选择 + + /// + /// [DI_BRANCH_001] 普通日(无互换无平仓无观察日)→ 走 copy 分支 + /// --------------------------------------------------------------- + /// flowEvents 为空,insterval=null,hasSwap=false,hasClose=false + /// → 应走 SaveEodInterestPositionCopy(cs:338) + /// --------------------------------------------------------------- + /// + /// [DI_BRANCH_001] 普通日收盘归档:InterestIncomeSum 每天递增1天利息 + /// --------------------------------------------------------------- + /// 前日待实现=1天利息,今日收盘(无互换无平仓),应变成2天利息。 + /// 验证 copy 分支(SaveEodInterestPositionCopy)的 InterestIncomeSum 公式。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_BRANCH_001_普通日归档待实现递增() + { + var service = new StubEodPositionService(); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var settleDate = new DateTime(2026, 4, 28); // 第2天 + var preEod = CreatePreEod(settleDate.AddDays(-1), DailyInterest); // 前日=1天利息 + + // 普通日:无互换无平仓,InterestSwapInterval=null(当天非观察日) + position.InterestSwapInterval = null; + + service.ExecuteDealInterests( + new List { position }, + new List { preEod }, + settleDate, td, new List(), + Principal, 0m, 0m, 1m, Principal); + + Assert.IsTrue(service.PersistedPositions.Count > 0, "应生成eod"); + var result = service.PersistedPositions[0]; + // 普通日:InterestIncomeSum 应 = 前日 + 当天新计 = 1天 + 1天 = 2天 + AssertDecimal(DailyInterest * 2, result.InterestIncomeSum, + $"普通日后 InterestIncomeSum 应=2天利息({DailyInterest * 2:F6})"); + Console.WriteLine($"普通日归档:InterestIncomeSum={result.InterestIncomeSum:F6} = 2×{DailyInterest:F6} ✅"); + } + + /// + /// [DI_BRANCH_002] 互换日(hasSwap=true)→ 走 SaveEodInterestPosition 分支 + /// --------------------------------------------------------------- + /// flowEvents 含 EventType=互换,hasSwap=true + /// → 应走 SaveEodInterestPosition(cs:330) + /// → 验证 PersistEodSwapPosition 被调用(生成了 eod) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_BRANCH_002_互换日走SaveEodInterestPosition分支() + { + var service = new StubEodPositionService(); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var settleDate = new DateTime(2026, 5, 10); + var preEod = CreatePreEod(settleDate.AddDays(-1), DailyInterest * 13); + + // 互换事件 + var swapEvent = CreateSwapFlowEvent(settleDate, DailyInterest * 13); + position.InterestSwapInterval = null; + + var interestList = new List { position }; + var eodPositions = new List { preEod }; + + service.ExecuteDealInterests(interestList, eodPositions, settleDate, td, + new List { swapEvent }, + Principal, 0m, 0m, 1m, Principal); + + // 互换分支应生成1条 eod + Assert.AreEqual(1, service.PersistedPositions.Count, "互换分支应生成1条eod"); + var result = service.PersistedPositions[0]; + // InterestIncomeSum = pre + 当天新计 - 实现 ≈ 当天新计(攒的全付了) + AssertDecimal(DailyInterest, result.InterestIncomeSum, "互换结清后待实现≈当天新计利息"); + Console.WriteLine($"互换日分支执行,InterestIncomeSum={result.InterestIncomeSum:F6} ≈ 当天新计({DailyInterest:F6}) ✅"); + } + + #endregion + + // ================================================================ + // 场景3:多日守恒——连续收盘归档,InterestIncomeSum 应线性递增 + // ================================================================ + + #region 场景3:多日连续归档 + + /// + /// [DI_MULTI_001] 连续5天普通日收盘归档,InterestIncomeSum 每天递增1天利息 + /// --------------------------------------------------------------- + /// 从4/27(首日)开始,连续收盘到5/1,验证 InterestIncomeSum 线性递增。 + /// 每天收盘后 InterestIncomeSum 应 = 天数 × DailyInterest。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_MULTI_001_连续5天归档待实现线性递增() + { + var td = CreateTrade(); + var position = CreateInterestPosition(); + position.InterestSwapInterval = null; // 无观察日 + + decimal runningIncomeSum = 0m; + var runningDate = StartDate; + + for (int day = 0; day < 5; day++) + { + var service = new StubEodPositionService(); + var preEod = CreatePreEod(runningDate.AddDays(-1), runningIncomeSum); + + service.ExecuteDealInterests( + new List { position }, + new List { preEod }, + runningDate, td, new List(), + Principal, 0m, 0m, 1m, Principal); + + Assert.IsTrue(service.PersistedPositions.Count > 0, $"第{day + 1}天应生成eod"); + var result = service.PersistedPositions[0]; + + // 首日 InterestIncomeSum = 1天利息,后续每天+1天利息 + decimal expected = DailyInterest * (day + 1); + AssertDecimal(expected, result.InterestIncomeSum, + $"第{day + 1}天 InterestIncomeSum 应={(day + 1)}天利息"); + + runningIncomeSum = result.InterestIncomeSum; + runningDate = runningDate.AddDays(1); + } + + Console.WriteLine($"连续5天归档:InterestIncomeSum 从0递增到{runningIncomeSum:F6} = 5×{DailyInterest:F6} ✅"); + } + + // ================================================================ + // 场景4:互换→收盘→再攒→再互换 守恒验证 + // ================================================================ + + #region 场景4:多次互换结算守恒 + + /// + /// [DI_SWAP_MULTI_001] 攒10天→互换结清→再攒5天→再互换结清 + /// --------------------------------------------------------------- + /// 验证:第一次互换后 InterestIncomeSum≈当天新计(攒的10天付了), + /// 再攒5天后 InterestIncomeSum≈6天(5天新攒+1天当天新计), + /// 第二次互换后 InterestIncomeSum≈当天新计(攒的6天又付了)。 + /// + /// 守恒约束:两次互换结算的 TdCloseInterest 之和 = 全程利息(15天+2天新计)。 + /// + [TestMethod] + public void DI_SWAP_MULTI_001_多次互换结算守恒() + { + var td = CreateTrade(); + var position = CreateInterestPosition(); + + // --- Phase 1: 攒10天(4/27~5/6),到5/6 --- + var date10 = StartDate.AddDays(10); // 5/7 + decimal sum10days = Math.Round(Principal * FixedRate * 10 / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + var preEod10 = CreatePreEod(date10.AddDays(-1), sum10days - DailyInterest); // 前日=9天 + // 当天新计让它到10天 + var svc1 = new StubEodPositionService(); + svc1.ExecuteDealInterests(new List { position }, + new List { preEod10 }, date10, td, + new List(), Principal, 0m, 0m, 1m, Principal); + var eod10days = svc1.PersistedPositions[0]; + AssertDecimal(sum10days, eod10days.InterestIncomeSum, "10天后待实现应=10天利息"); + Console.WriteLine($"Phase1: 攒10天 InterestIncomeSum={eod10days.InterestIncomeSum:F6}"); + + // --- Phase 2: 5/7 互换结清 --- + var swapDate1 = date10; // 同天互换 + var svc2 = new StubEodPositionService(); + var swapEvt1 = CreateSwapFlowEvent(swapDate1, sum10days); + var swapResult1 = svc2.ExecuteSaveEodInterestPosition( + eod10days, null, position, td, swapDate1, new List { swapEvt1 }); + + // 互换后待实现≈当天新计(攒的10天付了,但当天又产生1天新计) + decimal dailyInc = Math.Round(Principal * FixedRate / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertDecimal(dailyInc, swapResult1.InterestIncomeSum, "第一次互换后待实现≈当天新计"); + decimal firstRealized = swapResult1.TdCloseInterest; + Console.WriteLine($"Phase2: 第一次互换 TdCloseInterest={firstRealized:F6}, 待实现={swapResult1.InterestIncomeSum:F6}"); + + // --- Phase 3: 再攒5天 --- + decimal runningSum = swapResult1.InterestIncomeSum; + var runningDate = swapDate1.AddDays(1); + for (int i = 0; i < 5; i++) + { + var svc = new StubEodPositionService(); + var preEod = CreatePreEod(runningDate.AddDays(-1), runningSum); + // 需要 preEod.RealizedInterest 累积 + preEod.RealizedInterest = swapResult1.RealizedInterest; + svc.ExecuteDealInterests(new List { position }, + new List { preEod }, runningDate, td, + new List(), Principal, 0m, 0m, 1m, Principal); + runningSum = svc.PersistedPositions[0].InterestIncomeSum; + runningDate = runningDate.AddDays(1); + } + Console.WriteLine($"Phase3: 再攒5天后 InterestIncomeSum={runningSum:F6}"); + + // --- Phase 4: 再互换结清 --- + var svc4 = new StubEodPositionService(); + var preEodFinal = CreatePreEod(runningDate.AddDays(-1), runningSum); + preEodFinal.RealizedInterest = swapResult1.RealizedInterest; + var swapEvt2 = CreateSwapFlowEvent(runningDate, runningSum); + var swapResult2 = svc4.ExecuteSaveEodInterestPosition( + preEodFinal, null, position, td, runningDate, new List { swapEvt2 }); + decimal secondRealized = swapResult2.TdCloseInterest; + Console.WriteLine($"Phase4: 第二次互换 TdCloseInterest={secondRealized:F6}, 待实现={swapResult2.InterestIncomeSum:F6}"); + + // 守恒:两次互换实现的 + 最终待实现 = 全程天数 × dailyInc + // 全程天数 = 10天(Phase1) + 1天(第一次互换当天新计) + 5天(Phase3) + 1天(第二次互换当天新计) = 17天 + // 但第一次互换的当天新计进了 InterestIncomeSum 没进 TdCloseInterest, + // 第二次互换同理。所以守恒 = RealizedInterest合计 + 最终InterestIncomeSum = 全程利息 + decimal totalDays = 10 + 1 + 5 + 1; // 17天 + decimal expectedTotalInterest = Math.Round(Principal * FixedRate * totalDays / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + decimal actualTotal = swapResult2.RealizedInterest + swapResult2.InterestIncomeSum; + Console.WriteLine($"守恒: RealizedInterest({swapResult2.RealizedInterest:F6}) + InterestIncomeSum({swapResult2.InterestIncomeSum:F6}) = {actualTotal:F6}"); + Console.WriteLine($"期望: {totalDays}天 × {dailyInc:F6} = {expectedTotalInterest:F6}"); + AssertDecimal(expectedTotalInterest, actualTotal, + "已实现+待实现 应=全程利息(守恒)"); + } + + #endregion + + // ================================================================ + // 场景5:预付金腿(marginTypes ratio 翻转)符号验证 + // ================================================================ + + #region 场景5:预付金腿 ratio 翻转 + + /// + /// [DI_MARGIN_001] 预付金腿互换结清后 RealizedInterest 应为负(支付方向) + /// --------------------------------------------------------------- + /// 预付金腿 InterestDirection=收取(1),但 marginTypes 会把 ratio 翻转为 -1。 + /// SwapPositionValue 应为负(负债),RealizedInterest 也应为负(券商支付)。 + /// 验证 cs:789-793 的 ratio 翻转逻辑。 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void DI_MARGIN_001_预付金腿RealizedInterest为负() + { + var service = new StubEodPositionService(); + var td = CreateTrade(); + var settleDate = new DateTime(2026, 5, 10); + + // 预付金腿(初始预付金 InterestMode=5,InterestDirection=收取) + var marginPosition = new swap_position + { + id = 2001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.初始预付金, + InterestRateDefault = 0.005m, InterestPrincipalFix = 500m, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, + FloatRateUnderlyingCode = null, + InterestSwapInterval = null + }; + + // 攒10天的预付金利息 + decimal marginDaily = Math.Round(500m * 0.005m / AnnualDays, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + decimal margin10days = marginDaily * 10; + var preEod = new eod_swap_position + { + id = 200, SwapTradeId = 1, PositionId = 2001, ValueDate = settleDate.AddDays(-1), + ClientId = 999998, InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.初始预付金, + InterestIncomeSum = margin10days, InterestProfitSum = margin10days, + RealizedInterest = 0m, InterestRateDefault = 0.005m, + TdInterestPrincipal = 500m, PosiNotionalValue = 500m, + InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, + interest_rest_days = 1, FloatRate = 0m + }; + + // 互换结清 + var swapEvent = new swap_flow_event + { + id = 3001, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.互换, + EventDate = settleDate, UnwindDate = settleDate, PositionId = 2001, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestAmount = margin10days, InterestClosePnL = -margin10days, // 预付金 ratio 翻转后为负 + InterestRate = 0.005m, InterestMode = (int)InterestModeEnum.初始预付金, + InterestPrincipal = 500m, FloatRate = 0m, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + + var result = service.ExecuteSaveEodInterestPosition( + preEod, null, marginPosition, td, settleDate, new List { swapEvent }); + + // 预付金 marginTypes 翻转 ratio=-1 + // RealizedInterest = 0 + TdCloseInterest(margin10days) * ratio(-1) = -margin10days + AssertDecimal(-margin10days, result.RealizedInterest, + "预付金腿 RealizedInterest 应为负(ratio翻转后支付方向)"); + Console.WriteLine($"预付金腿 RealizedInterest={result.RealizedInterest:F6}(负=支付)✅"); + Console.WriteLine($"SwapPositionValue={result.SwapPositionValue:F6}(应≈当天新计×ratio=-正)"); + } + + #endregion + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs new file mode 100644 index 00000000..233b547a --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs @@ -0,0 +1,234 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Helpers; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 前端计算逻辑特征化测试(Characterization Test) + /// ============================================================================ + /// 目的:用 golden 冻结前端 JS 的计算行为(含用户可变输入分支), + /// 作为下一轮"计算下沉后端"的金标准——后端结果必须匹配这些 golden。 + /// + /// 背景:前端 unwindSwapTrade.js / incomeSwapTrade.js 是实时响应式计算器, + /// 用户改标的价格/平仓数量/交易费用/利息金额时,前端立刻重算 MarkClosePnl/ + /// SwapRealizedPnL/SwapCloseAmount,后端拿到"前端算好的最终结果"直接记账。 + /// 本测试用 C# 忠实重写前端公式作参考实现,手算真实输入的期望值存 golden。 + /// + /// 命名规范(见命名决策文档):参考实现内部用规范名(EntryPrice/ExitPrice/ + /// floatRatio/longRatio),注释标明对应前端字段与规范语义。 + /// ============================================================================ + [TestClass] + public class FrontendCalcCharacterizationTest + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "FrontendCalc"); + + // FrontendCalcReference 已搬迁到生产代码 YLErpDAL/Helpers/FrontendCalcReference.cs, + // 生产代码(SwapDealService校验)与测试共用同一份公式实现,避免分叉。 + + // ================================================================ + // 8 个测试场景(含用户可变输入分支) + // ================================================================ + + // ---- 平仓页(unwind)场景 ---- + + /// + /// [FC_001] 平仓-债券多头-默认值(基线) + /// EntryDirtyPrice(PosiGrossPrice)=1.02, ExitPrice(TradingAmountAvg,×100形态)=105, + /// CloseQty=1000, PayDirection=1(收取), PositionType=1(多头), TradingFee="20" + /// scale=0.01, floatRatio=1, longRatio=1 + /// MarkClosePnl = round(1000×(105×0.01−1.02)×1×1×10000)/10000 = round(1000×0.03×10000)/10000 = 30 + /// + [TestMethod] + public void FC_001_平仓_债券多头_默认值() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, + CloseQty = 1000, PayDirection = 1, PositionType = 1, + TradingFee = "20", TradingFeePending = "0", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcUnwind(input); + + // MarkClosePnl = 1000×(1.05−1.02)×1×1 = 30 + AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl"); + // FloatPnlSum = 30 + 20 + 0 + 0 = 50 + AssertDecimalEqual(50m, result.FloatPnlSum, 0.01m, "FloatPnlSum"); + // SwapRealizedPnL = FloatPnlSum(50) + AssertDecimalEqual(50m, result.SwapRealizedPnL, 0.01m, "SwapRealizedPnL"); + Console.WriteLine($"FC_001: MarkClosePnl={result.MarkClosePnl}, FloatPnlSum={result.FloatPnlSum} ✅"); + } + + /// + /// [FC_002] 平仓-用户改标的价格(TradingAmountAvg 100→110) + /// MarkClosePnl = round(1000×(110×0.01−1.02)×10000)/10000 = round(1000×0.08×10000)/10000 = 80 + /// + [TestMethod] + public void FC_002_平仓_用户改标的价格() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m, // 改成110 + CloseQty = 1000, PayDirection = 1, PositionType = 1, + TradingFee = "20", TradingFeePending = "0", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcUnwind(input); + + AssertDecimalEqual(80m, result.MarkClosePnl, 0.01m, "改价格后 MarkClosePnl"); + AssertDecimalEqual(100m, result.FloatPnlSum, 0.01m, "改价格后 FloatPnlSum"); + Console.WriteLine($"FC_002: 改标的价格后 MarkClosePnl={result.MarkClosePnl} ✅"); + } + + /// + /// [FC_003] 平仓-用户改平仓数量(CloseQty 1000→500,TradingFeePending 随比例变) + /// MarkClosePnl = round(500×(105×0.01−1.02)×10000)/10000 = round(500×0.03×10000)/10000 = 15 + /// TradingFeePending 按比例=BeforeCloseFee×ClosePercent(0.5),假设=10 + /// + [TestMethod] + public void FC_003_平仓_用户改平仓数量() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, + CloseQty = 500, // 改成500(原1000) + PayDirection = 1, PositionType = 1, + TradingFee = "20", TradingFeePending = "10", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcUnwind(input); + + // MarkClosePnl = 500×0.03 = 15 + AssertDecimalEqual(15m, result.MarkClosePnl, 0.01m, "改数量后 MarkClosePnl"); + // FloatPnlSum = 15 + 20 + 10 + 0 = 45 + AssertDecimalEqual(45m, result.FloatPnlSum, 0.01m, "改数量后 FloatPnlSum"); + Console.WriteLine($"FC_003: 改平仓数量后 MarkClosePnl={result.MarkClosePnl} ✅"); + } + + /// + /// [FC_004] 平仓-用户改利息金额(InterestClosePnL=100) + /// SwapRealizedPnL = FloatPnlSum(50) + InterestClosePnL(100) = 150 + /// + [TestMethod] + public void FC_004_平仓_用户改利息金额() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, + CloseQty = 1000, PayDirection = 1, PositionType = 1, + TradingFee = "20", TradingFeePending = "0", DividendIn = "0" + }; + input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m }); + + var result = FrontendCalcReference.CalcUnwind(input); + + AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl 不受利息影响"); + // SwapRealizedPnL = 50 + 100 = 150 + AssertDecimalEqual(150m, result.SwapRealizedPnL, 0.01m, "含利息的 SwapRealizedPnL"); + Console.WriteLine($"FC_004: 改利息后 SwapRealizedPnL={result.SwapRealizedPnL} ✅"); + } + + /// + /// [FC_005] 平仓-非债券空头(PositionType=Short=2, multiplier=1) + /// floatRatio=1(收取), longRatio=-1(空头) + /// MarkClosePnl = round(1000×(100×1−100)×1×(−1)×10000)/10000 = 0(价格不变时空头盈亏=0) + /// 改成价格涨:TradingAmountAvg=105, MarkClosePnl=round(1000×(105−100)×1×(−1)×10000)/10000=−50000 + /// 空头价格涨=亏损 + /// + [TestMethod] + public void FC_005_平仓_非债券空头_方向因子() + { + var input = new UnwindInput + { + Multiplier = 1, PosiGrossPrice = 100m, TradingAmountAvg = 105m, // 涨了5 + CloseQty = 1000, PayDirection = 1, PositionType = 2, // 空头 + TradingFee = "0", TradingFeePending = "0", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcUnwind(input); + + // 空头价格涨=亏损:1000×(105−100)×1×(−1) = −5000 + AssertDecimalEqual(-5000m, result.MarkClosePnl, 0.01m, "空头价格涨=亏损"); + Console.WriteLine($"FC_005: 空头方向因子 MarkClosePnl={result.MarkClosePnl} ✅"); + } + + // ---- 结息页(income)场景 ---- + + /// + /// [FC_006] 结息-债券多头-全量结算(基线) + /// income 用 CloseNotionalValue 而非 CloseQty,无 longRatio + /// EntryPrice=1.02, TradingAmountAvg=105(×100形态), CloseNotionalValue=10000 + /// MarkClosePnl = 10000×(105×0.01−1.02)×1 = 10000×0.03 = 300 + /// + [TestMethod] + public void FC_006_结息_债券多头_全量结算() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, + CloseNotionalValue = 10000, // income 用名义本金 + CloseQty = 0, // income 不用数量 + PayDirection = 1, PositionType = 1, + TradingFee = "0", TradingFeePending = "0", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcIncome(input); + + AssertDecimalEqual(300m, result.MarkClosePnl, 0.01m, "income MarkClosePnl"); + AssertDecimalEqual(300m, result.SwapRealizedPnL, 0.01m, "income SwapRealizedPnL"); + Console.WriteLine($"FC_006: income MarkClosePnl={result.MarkClosePnl} ✅"); + } + + /// + /// [FC_007] 结息-用户改标的价格(TradingAmountAvg 105→110) + /// MarkClosePnl = 10000×(110×0.01−1.02) = 10000×0.08 = 800 + /// + [TestMethod] + public void FC_007_结息_用户改标的价格() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m, + CloseNotionalValue = 10000, CloseQty = 0, + PayDirection = 1, PositionType = 1, + TradingFee = "0", TradingFeePending = "0", DividendIn = "0" + }; + var result = FrontendCalcReference.CalcIncome(input); + + AssertDecimalEqual(800m, result.MarkClosePnl, 0.01m, "改价格后 income MarkClosePnl"); + Console.WriteLine($"FC_007: 改价格后 income MarkClosePnl={result.MarkClosePnl} ✅"); + } + + /// + /// [FC_008] 结息-含利息腿与预付金腿(InterestClosePnL + margin InterestClosePnL) + /// SwapRealizedPnL = FloatPnlSum(300) + 利息腿(100) + 预付金腿(50) = 450 + /// SwapMarginRebatePnl = 预付金腿(50) + /// + [TestMethod] + public void FC_008_结息_含利息腿与预付金腿_总额() + { + var input = new UnwindInput + { + Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, + CloseNotionalValue = 10000, CloseQty = 0, + PayDirection = 1, PositionType = 1, + TradingFee = "0", TradingFeePending = "0", DividendIn = "0" + }; + input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m }); + input.MarginLegs.Add(new LegInput { InterestClosePnL = 50m }); + + var result = FrontendCalcReference.CalcIncome(input); + + // SwapRealizedPnL = 300 + 100 + 50 = 450 + AssertDecimalEqual(450m, result.SwapRealizedPnL, 0.01m, "含利息+预付金的 SwapRealizedPnL"); + // SwapMarginRebatePnl = 50 + AssertDecimalEqual(50m, result.SwapMarginRebatePnl, 0.01m, "SwapMarginRebatePnl"); + Console.WriteLine($"FC_008: SwapRealizedPnL={result.SwapRealizedPnL}, SwapMarginRebatePnl={result.SwapMarginRebatePnl} ✅"); + } + + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") + { + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260105GoldenTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260105GoldenTest.cs new file mode 100644 index 00000000..b70cdb0a --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GLMS20260105GoldenTest.cs @@ -0,0 +1,254 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// GLMS-20260105-0007 分红精度差异 - golden 录制/回放测试 + /// ============================================================================ + /// 用真实测试库数据录制 → golden JSON → 回放复现 0.36 差异 + /// + /// 问题:原有待实现分红-90400,互换支付-400后应为-90000,实际-89999.64 + /// 根因:UpdateEodPosition cs:1635 从头重算 PosiDividendSum, + /// 与 CopyEodPosition 逐天递增的舍入累积不一致 + /// ============================================================================ + [TestClass] + public class GLMS20260105GoldenTest + { + private const string TradeNumber = "GLMS-20260105-0007"; + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "GLMS20260105"); + + #region 录制:从真实库读取数据,序列化为 golden + + /// + /// 从真实测试库录制 GLMS-20260105-0007 的完整数据快照。 + /// 标 [Ignore],手动跑一次生成 golden JSON。 + /// + [TestMethod] + [Ignore] + [TestCategory("GoldenRecord")] + public void Record_FromRealDB() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } + + try + { + Directory.CreateDirectory(GoldenDir); + + var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); + Assert.IsNotNull(td, $"交易 {TradeNumber} 不存在"); + + var floatEods = db.eod_swap_position + .Where(x => x.SwapTradeId == td.id && x.PosiDirection > 0 && !x.Invalid) + .OrderBy(x => x.ValueDate).ToList(); + + var flows = db.swap_flow_event + .Where(x => x.SwapTradeId == td.id && x.DataState == (int)SwapFlowDateStateEnum.完成) + .OrderBy(x => x.EventDate).ThenBy(x => x.id).ToList(); + + var positions = db.swap_position + .Where(x => x.SwapTradeId == td.id && !x.Invalid).ToList(); + + var swapDate = flows.First(f => f.EventType == (int)SwapFlowEventTypeEnum.互换).EventDate; + var keyDates = new[] { swapDate.AddDays(-1), swapDate, swapDate.AddDays(1) }; + var keyFloatEods = floatEods.Where(x => keyDates.Contains(x.ValueDate)).ToList(); + + // 录制 UpdateEodPosition 的输入(互换前日eod + 互换flow_event + 持仓 + 交易) + var preSwapEod = floatEods.FirstOrDefault(x => x.ValueDate == swapDate.AddDays(-1)); + var swapFlowEvents = flows.Where(x => x.EventDate == swapDate && x.PositionId == preSwapEod?.PositionId).ToList(); + var swapPosition = positions.FirstOrDefault(x => x.id == preSwapEod?.PositionId); + + var golden = new JObject + { + ["TradeNumber"] = TradeNumber, + ["TradeId"] = td.id, + ["SwapDate"] = swapDate.ToString("yyyy-MM-dd"), + ["Description"] = "分红精度差异:互换前-90400,互换后应为-90000,实际-89999.64" + }; + + // 关键3天的浮动腿eod(含精确字段值) + var keyArray = new JArray(); + foreach (var e in keyFloatEods) + { + keyArray.Add(new JObject + { + ["ValueDate"] = e.ValueDate.ToString("yyyy-MM-dd"), + ["PositionId"] = e.PositionId, + ["PosiDividendSum"] = e.PosiDividendSum, + ["TdPosiDividend"] = e.TdPosiDividend, + ["TdCloseDividend"] = e.TdCloseDividend, + ["RealizedDividend"] = e.RealizedDividend, + ["PosiQuantity"] = e.PosiQuantity, + ["PosiMtmPnL"] = e.PosiMtmPnL + }); + } + golden["KeyFloatEodPositions"] = keyArray; + + // 完整浮动腿序列(用于分析精度累积过程) + var allArray = new JArray(); + foreach (var e in floatEods) + { + allArray.Add(new JObject + { + ["ValueDate"] = e.ValueDate.ToString("yyyy-MM-dd"), + ["PosiDividendSum"] = e.PosiDividendSum, + ["TdPosiDividend"] = e.TdPosiDividend, + ["RealizedDividend"] = e.RealizedDividend + }); + } + golden["AllFloatEodDividends"] = allArray; + + // 录制回放所需的输入数据(用于重新调 UpdateEodPosition) + if (preSwapEod != null && swapPosition != null) + { + var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; + golden["ReplayInput"] = new JObject + { + ["Trade"] = JObject.FromObject(td, JsonSerializer.Create(settings)), + ["Position"] = JObject.FromObject(swapPosition, JsonSerializer.Create(settings)), + ["PreSwapEod"] = JObject.FromObject(preSwapEod, JsonSerializer.Create(settings)), + ["SwapFlowEvents"] = JArray.FromObject(swapFlowEvents, JsonSerializer.Create(settings)) + }; + } + + string json = JsonConvert.SerializeObject(golden, Formatting.Indented); + string path = Path.Combine(GoldenDir, $"golden_{TradeNumber}.json"); + File.WriteAllText(path, json); + + Console.WriteLine($"录制完成: {path}"); + Console.WriteLine($"\n关键数据:"); + foreach (var e in keyFloatEods) + { + Console.WriteLine($" {e.ValueDate:yyyy-MM-dd}: PosiDividendSum={e.PosiDividendSum}, TdPosiDividend={e.TdPosiDividend}, TdCloseDividend={e.TdCloseDividend}, RealizedDividend={e.RealizedDividend}"); + } + + var preSwap = keyFloatEods.FirstOrDefault(x => x.ValueDate == swapDate.AddDays(-1)); + var swapDay = keyFloatEods.FirstOrDefault(x => x.ValueDate == swapDate); + if (preSwap != null && swapDay != null) + { + decimal expected = preSwap.PosiDividendSum - swapDay.TdCloseDividend; + decimal actual = swapDay.PosiDividendSum; + Console.WriteLine($"\n精度分析:"); + Console.WriteLine($" 互换前 PosiDividendSum = {preSwap.PosiDividendSum}"); + Console.WriteLine($" 互换实现 TdCloseDividend = {swapDay.TdCloseDividend}"); + Console.WriteLine($" 期望 PosiDividendSum = {preSwap.PosiDividendSum} - ({swapDay.TdCloseDividend}) = {expected}"); + Console.WriteLine($" 实际 PosiDividendSum = {actual}"); + Console.WriteLine($" 差异 = {actual - expected}"); + } + } + finally { db?.Dispose(); } + } + + #endregion + + #region 回放:读 golden 验证精度差异 + + /// + /// 回放 golden:用真实数据重新调 UpdateEodPosition,验证修复后 PosiDividendSum 正确。 + /// + /// 红灯(修复前):从头重算产生 0.36 差异 + /// 绿灯(修复后):递增模式,PosiDividendSum = 前日 + 新计 - 实现 = -90000 + /// + [TestMethod] + public void Replay_VerifyPrecisionDiff() + { + string sourceDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "GLMS20260105"); + if (!Directory.Exists(sourceDir)) + { + Assert.Inconclusive($"golden 目录不存在: {sourceDir}(请先跑 Record_FromRealDB)"); + return; + } + + var files = Directory.GetFiles(sourceDir, "*.json"); + Assert.IsTrue(files.Length > 0, "应至少有1个golden文件"); + + var json = File.ReadAllText(files[0]); + var golden = JObject.Parse(json); + + var replayInput = golden["ReplayInput"]; + if (replayInput == null) + { + Assert.Inconclusive("golden 缺少 ReplayInput(请重新录制)"); + return; + } + + var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; + var td = replayInput["Trade"]!.ToObject(JsonSerializer.Create(settings)); + var position = replayInput["Position"]!.ToObject(JsonSerializer.Create(settings)); + var preSwapEod = replayInput["PreSwapEod"]!.ToObject(JsonSerializer.Create(settings)); + var swapFlowEvents = replayInput["SwapFlowEvents"]!.ToObject>(JsonSerializer.Create(settings)); + + var swapDateStr = golden["SwapDate"]!.Value(); + var swapDate = DateTime.Parse(swapDateStr); + Console.WriteLine($"SwapDate = {swapDateStr}"); + Console.WriteLine($"互换前 PosiDividendSum = {preSwapEod.PosiDividendSum}"); + Console.WriteLine($"互换 DividendIn = {string.Join(",", swapFlowEvents.Select(x => x.DividendIn))}"); + + // 用修复后的代码重新调 UpdateEodPosition + var service = new ReplayStubService(preSwapEod.UnderlyingCode); + var result = service.ExecuteUpdateEodPosition( + position, preSwapEod, td, swapDate, swapDate.AddDays(-1), swapFlowEvents); + + // 期望:PosiDividendSum = 前日 + 当天新计 - 实现 + decimal expected = preSwapEod.PosiDividendSum + result.TdPosiDividend - result.TdCloseDividend; + Console.WriteLine($"\n修复后结果:"); + Console.WriteLine($" TdPosiDividend = {result.TdPosiDividend}"); + Console.WriteLine($" TdCloseDividend = {result.TdCloseDividend}"); + Console.WriteLine($" PosiDividendSum = {result.PosiDividendSum}"); + Console.WriteLine($" 期望 = {preSwapEod.PosiDividendSum} + {result.TdPosiDividend} - ({result.TdCloseDividend}) = {expected}"); + + Assert.AreEqual(expected, result.PosiDividendSum, + $"修复后 PosiDividendSum 应=前日+新计-实现={expected},实际={result.PosiDividendSum}"); + Console.WriteLine($"\n✅ 修复验证通过:PosiDividendSum={result.PosiDividendSum} = {expected}"); + } + + #endregion + + #region 回放用 Stub + + private sealed class ReplayStubService : SwapEodPositionService + { + private readonly string _underlyingCode; + public ReplayStubService(string underlyingCode) : base(new OptUserInfo(0, "Replay", OptUserFrom.UnitTest)) + { + _underlyingCode = underlyingCode; + } + + protected override underlying_manager GetUnderlyingData(string underlyingCode) + { + // 返回最小可用数据(增值税=0) + return new underlying_manager { ValueAddedTax = 0m }; + } + + protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) + { + vobp = 0m; + return 1.01m; // 固定价格 + } + + protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) + { + // 返回0:互换日的 TdPosiDividend=0(无新增分红),聚焦验证 PosiDividendSum 的递增逻辑 + return 0m; + } + + protected override void SaveAllChanges() { } + protected override double GetCurrencyRate(string q, string s, DateTime d, bool p, CurrencyRateType t) => 1.0; + + public eod_swap_position ExecuteUpdateEodPosition( + swap_position swapPosition, eod_swap_position eod, trade td, + DateTime valueDate, DateTime preSettleDate, List unwindEvents) + { + return UpdateEodPosition(swapPosition, eod, null, td, valueDate, preSettleDate, unwindEvents); + } + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/GoldenReplayFramework.cs b/UnitTestProject/Modules/SwapModule/GoldenReplayFramework.cs new file mode 100644 index 00000000..f3a4b18d --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GoldenReplayFramework.cs @@ -0,0 +1,160 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + #region Golden 数据模型 + + /// + /// Golden 文件的通用数据模型。 + /// 每个场景序列化为一个 JSON 文件,包含:输入数据 + 期望输出。 + /// + /// JSON 结构: + /// { + /// "Scenario": "互换结清后待实现归零", + /// "Description": "攒10天后互换,验证InterestIncomeSum≈当天新计", + /// "Input": { + /// "Trade": { ... }, + /// "Positions": [ ... ], + /// "PreEodPositions": [ ... ], + /// "FlowEvents": [ ... ] + /// }, + /// "Expected": { + /// "EodPositions": [ + /// { "PositionId": 1001, "InterestIncomeSum": 0.0274, "TdCloseInterest": 2.74, ... } + /// ] + /// } + /// } + /// + public class GoldenScenarioModel + { + /// 场景名称 + public string Scenario { get; set; } + + /// 场景描述 + public string Description { get; set; } + + /// 输入数据 + public GoldenInput Input { get; set; } + + /// 期望输出(精确到小数点后N位的字段值) + public GoldenExpected Expected { get; set; } + + /// 数据来源:synthetic(合成) / recorded(真实库录制) + public string Source { get; set; } = "synthetic"; + + /// 录制时间(如果是 recorded) + public DateTime? RecordedAt { get; set; } + } + + public class GoldenInput + { + public JObject Trade { get; set; } + public JArray Positions { get; set; } + public JArray PreEodPositions { get; set; } + public JArray FlowEvents { get; set; } + + // 可选的配置参数 + public decimal? PosiLongNotional { get; set; } + public decimal? PosiShortNotional { get; set; } + public decimal? CloseNational { get; set; } + public decimal? GrossPrice { get; set; } + public decimal? OrginPv { get; set; } + public DateTime? SettleDate { get; set; } + } + + public class GoldenExpected + { + /// 期望生成的 eod 持仓数量 + public int? PositionCount { get; set; } + + /// 期望的 eod 持仓精确字段(每个 PositionId 一条) + public JArray EodPositions { get; set; } + } + + #endregion + + #region Golden 回放辅助 + + /// + /// Golden 回放的通用辅助方法。 + /// 提供精确字段对比(容许指定位数的误差)。 + /// + public static class GoldenAssert + { + /// 默认精度容差(小数点后9-2=7位) + public static decimal DefaultTolerance => 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + + /// 对比 decimal 字段,容许指定位数误差 + public static void AssertField(decimal? expected, decimal actual, string fieldName, long positionId, decimal? tolerance = null) + { + if (expected == null) return; // golden 里没存这个字段就跳过 + var tol = tolerance ?? DefaultTolerance; + Assert.IsTrue(Math.Abs(expected.Value - actual) <= tol, + $"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}, diff={expected.Value - actual}"); + } + + /// 对比 int 字段 + public static void AssertField(int? expected, int actual, string fieldName, long positionId) + { + if (expected == null) return; + Assert.AreEqual(expected.Value, actual, + $"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}"); + } + + /// 对比 long 字段 + public static void AssertField(long? expected, long actual, string fieldName, long positionId) + { + if (expected == null) return; + Assert.AreEqual(expected.Value, actual, + $"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}"); + } + + /// + /// 对比一个 eod_swap_position 的所有 golden 字段。 + /// golden JSON 里只存了需要验证的字段,未存的跳过。 + /// + public static void AssertEodPosition(JObject expected, eod_swap_position actual) + { + var positionId = expected["PositionId"]?.Value() ?? actual.PositionId; + + AssertField(expected["InterestIncomeSum"]?.Value(), actual.InterestIncomeSum, "InterestIncomeSum", positionId); + AssertField(expected["InterestProfitSum"]?.Value(), actual.InterestProfitSum, "InterestProfitSum", positionId); + AssertField(expected["TdInterestIncome"]?.Value(), actual.TdInterestIncome, "TdInterestIncome", positionId); + AssertField(expected["TdCloseInterest"]?.Value(), actual.TdCloseInterest, "TdCloseInterest", positionId); + AssertField(expected["TdInterestPrincipal"]?.Value(), actual.TdInterestPrincipal, "TdInterestPrincipal", positionId); + AssertField(expected["RealizedInterest"]?.Value(), actual.RealizedInterest, "RealizedInterest", positionId); + AssertField(expected["RealizedInterestFee"]?.Value(), actual.RealizedInterestFee, "RealizedInterestFee", positionId); + AssertField(expected["RealizedPnl"]?.Value(), actual.RealizedPnl, "RealizedPnl", positionId); + AssertField(expected["SwapPositionValue"]?.Value(), actual.SwapPositionValue, "SwapPositionValue", positionId); + AssertField(expected["InterestFeeSum"]?.Value(), actual.InterestFeeSum, "InterestFeeSum", positionId); + AssertField(expected["TdInterestFee"]?.Value(), actual.TdInterestFee, "TdInterestFee", positionId); + AssertField(expected["TdCloseInterestFee"]?.Value(), actual.TdCloseInterestFee, "TdCloseInterestFee", positionId); + } + + /// + /// 序列化一个 eod_swap_position 到 JObject(用于生成 golden 文件)。 + /// 只存关键字段,避免 JSON 过大。 + /// + public static JObject EodPositionToJson(eod_swap_position eod) + { + return new JObject + { + ["PositionId"] = eod.PositionId, + ["InterestIncomeSum"] = eod.InterestIncomeSum, + ["InterestProfitSum"] = eod.InterestProfitSum, + ["TdInterestIncome"] = eod.TdInterestIncome, + ["TdCloseInterest"] = eod.TdCloseInterest, + ["TdInterestPrincipal"] = eod.TdInterestPrincipal, + ["RealizedInterest"] = eod.RealizedInterest, + ["RealizedPnl"] = eod.RealizedPnl, + ["SwapPositionValue"] = eod.SwapPositionValue, + ["InterestFeeSum"] = eod.InterestFeeSum + }; + } + } + + #endregion +} diff --git a/UnitTestProject/Modules/SwapModule/MergeComposeScenarioTest.cs b/UnitTestProject/Modules/SwapModule/MergeComposeScenarioTest.cs new file mode 100644 index 00000000..e816b056 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/MergeComposeScenarioTest.cs @@ -0,0 +1,264 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; +using static YLErp.Modules.SwapModule.TestableSwapTradeAutoService; + +namespace YLErp.Modules.SwapModule +{ + [TestClass] + public class MergeComposeScenarioTest + { + private const string UnderlyingCode = "220205.IB"; + private const int ClientId = 10; + private static readonly DateTime TradeDate = new DateTime(2025, 4, 24); + + #region 场景1:空merge列表 → 直接返回 + + [TestMethod] + public void Scenario1_EmptyMergeList_ShouldReturn() + { + var service = CreateService(); + service.ExecuteMergeRestModeCompose(new List(), TradeDate); + Assert.AreEqual(0, service.CreatedTrades.Count, "不应创建任何交易"); + } + + #endregion + + #region 场景2:客户不存在 → 抛异常 + + [TestMethod] + [ExpectedException(typeof(ServiceException))] + public void Scenario2_ClientNotFound_ShouldThrow() + { + // 不注入任何 client + var service = CreateService(clients: new Dictionary()); + var merges = new List { CreateMerge() }; + service.ExecuteMergeRestModeCompose(merges, TradeDate); + } + + #endregion + + #region 场景3:客户未设置场外互换权限 → 抛异常 + + [TestMethod] + [ExpectedException(typeof(ServiceException))] + public void Scenario3_ClientNoSwapPermission_ShouldThrow() + { + var client = CreateClient(hasSwapPermission: false); + var service = CreateService(clients: new Dictionary { [ClientId] = client }); + var merges = new List { CreateMerge() }; + service.ExecuteMergeRestModeCompose(merges, TradeDate); + } + + #endregion + + #region 场景4:无TRS簿记账户 → 抛异常 + + [TestMethod] + [ExpectedException(typeof(ServiceException))] + public void Scenario4_NoEtradingRule_ShouldThrow() + { + var client = CreateClient(); + // etradingRuleFactory 返回 null + var service = CreateService( + clients: new Dictionary { [ClientId] = client }, + etradingRuleFactory: (side, num) => null + ); + var merges = new List { CreateMerge() }; + service.ExecuteMergeRestModeCompose(merges, TradeDate); + } + + #endregion + + #region 场景5:找不到簿记账户资产单元 → 抛异常 + + [TestMethod] + [ExpectedException(typeof(ServiceException))] + public void Scenario5_NoAssetUnit_ShouldThrow() + { + var client = CreateClient(); + var service = CreateService( + clients: new Dictionary { [ClientId] = client }, + etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"), + assets: new Dictionary() // 空的,找不到 + ); + var merges = new List { CreateMerge() }; + service.ExecuteMergeRestModeCompose(merges, TradeDate); + } + + #endregion + + #region 场景6:单条merge + 无持仓 → DealNoPosition 创建一笔交易 + + [TestMethod] + public void Scenario6_SingleMerge_NoPosition_ShouldCreateOneTrade() + { + var client = CreateClient(); + var service = CreateService( + clients: new Dictionary { [ClientId] = client }, + etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"), + assets: new Dictionary { ["TRS_ACCOUNT"] = CreateAssetUnit() }, + underlyings: new Dictionary { [UnderlyingCode] = CreateUnderlying() }, + positions: new List() + ); + var merges = new List { CreateMerge(qty: 100000) }; + + service.ExecuteMergeRestModeCompose(merges, TradeDate); + + Assert.AreEqual(1, service.CreatedTrades.Count, "应创建1笔交易"); + Assert.AreEqual(0, service.UnwindCalls.Count, "无持仓不应调用平仓"); + Assert.IsTrue(service.SaveChangesCount > 0, "应调用SaveChanges"); + } + + #endregion + + #region 场景7:两条merge + 无持仓 → DealNoPosition 创建交易+平仓 + + [TestMethod] + public void Scenario7_TwoMerges_NoPosition_ShouldCreateTradeAndUnwind() + { + var client = CreateClient(); + var service = CreateService( + clients: new Dictionary { [ClientId] = client }, + etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"), + assets: new Dictionary { ["TRS_ACCOUNT"] = CreateAssetUnit() }, + underlyings: new Dictionary { [UnderlyingCode] = CreateUnderlying() }, + positions: new List() + ); + var merges = new List + { + CreateMerge(bsType: 1, qty: 100000), // 买 + CreateMerge(bsType: 2, qty: -50000) // 卖 + }; + + service.ExecuteMergeRestModeCompose(merges, TradeDate); + + // 两条流水:先开仓,再平仓(买100000 vs 卖50000 → 平50000 + 剩余开仓50000) + Assert.IsTrue(service.UnwindCalls.Count > 0, "有两条流水应触发平仓操作"); + } + + #endregion + + #region 场景8:找不到标的 → 抛异常 + + [TestMethod] + [ExpectedException(typeof(ServiceException))] + public void Scenario8_UnderlyingNotFound_ShouldThrow() + { + var client = CreateClient(); + var service = CreateService( + clients: new Dictionary { [ClientId] = client }, + etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"), + assets: new Dictionary { ["TRS_ACCOUNT"] = CreateAssetUnit() }, + underlyings: new Dictionary() // 空的 + ); + var merges = new List { CreateMerge() }; + service.ExecuteMergeRestModeCompose(merges, TradeDate); + } + + #endregion + + #region 辅助方法 + + private TestableSwapTradeAutoService CreateService( + Dictionary clients = null, + Dictionary assets = null, + Dictionary underlyings = null, + List positions = null, + Func etradingRuleFactory = null, + List trades = null, + List flowEvents = null, + List validTrades = null) + { + var user = new OptUserInfo(1, "Test", OptUserFrom.UnitTest); + return new TestableSwapTradeAutoService( + user, + trades: trades, + positions: positions ?? new List(), + clients: clients ?? new Dictionary(), + assets: assets ?? new Dictionary(), + underlyings: underlyings ?? new Dictionary(), + etradingRuleFactory: etradingRuleFactory, + flowEvents: flowEvents ?? new List(), + validTrades: validTrades ?? new List() + ); + } + + private swap_flow_merge CreateMerge(int bsType = 1, decimal qty = 100000, decimal avgPrice = 1.0020m) + { + return new swap_flow_merge + { + OccurTime = TradeDate, + SwapTradeId = 9001, + SwapTradeNo = "TEST-IS-001", + UnderlyingCode = UnderlyingCode, + BsType = bsType, + TradingQty = qty, + TradingAmount = Math.Abs(qty) * avgPrice, + TradingAmountAvg = avgPrice, + TradingAmountFeeAvg = avgPrice, + TradingAmountNetAvg = avgPrice - 0.005m, + TradingAmountNetFeeAvg = avgPrice - 0.005m, + TradingFeePending = 0, + ContractSize = 1, + ClientId = ClientId, + DataState = 1, + FirstFlowTime = DateTime.Now + }; + } + + private Client CreateClient(bool hasSwapPermission = true) + { + var client = new Client + { + id = ClientId, + Name = "测试客户", + Number = "C001", + BoundSide = BoundSideEnum.南向, + SwapTradeType = 0 + }; + if (hasSwapPermission) + { + client.DerivativesInvestmentVarieties = ((int)DerivativesInvestmentVarietiesEnum.场外互换).ToString(); + } + else + { + client.DerivativesInvestmentVarieties = ""; + } + return client; + } + + private EtradingRule CreateEtradingRule(string assetAccount) + { + return new EtradingRule + { + AssetAccount_0 = assetAccount, + ClearingAgency_0 = "TEST_CLEARING" + }; + } + + private AssetUnit CreateAssetUnit() + { + return new AssetUnit + { + Name = "TRS_ACCOUNT", + TraderIds = "1" + }; + } + + private underlying_manager CreateUnderlying() + { + return new underlying_manager + { + UnderlyingCode = UnderlyingCode, + UnderlyingInstrumentType = "TBonds", + ContractSize = 1 + }; + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/MergePageEventScenarioTest.cs b/UnitTestProject/Modules/SwapModule/MergePageEventScenarioTest.cs new file mode 100644 index 00000000..c77a5380 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/MergePageEventScenarioTest.cs @@ -0,0 +1,288 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 用构造数据覆盖 MergePageEvent 全部 6 种场景 + /// 不连数据库,纯内存,秒级运行 + /// + [TestClass] + public class MergePageEventScenarioTest + { + private const int TradeId = 9001; + private const string TradeNumber = "TEST-IS-202504240001"; + private const string UnderlyingCode = "220205.IB"; + private const long PositionId = 50001; + + #region 场景1:单条流水 + 无持仓 → 纯开仓 + + [TestMethod] + public void Scenario1_SingleMerge_NoPosition_ShouldOpen() + { + var service = CreateService(positions: new List()); + var merges = new List + { + CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.0022m, Fee: 0, FeePending: 2000) + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[0].EventType); + Assert.AreEqual(1, result[0].PositionType); // 多头 + Assert.AreEqual(100000, result[0].Quantity); + Assert.AreEqual(1.0022m, result[0].TradingAmountAvg); + Assert.AreEqual(0, result[0].MarkClosePnl); // 开仓无平仓盈亏 + } + + #endregion + + #region 场景2:单条流水 + 同向持仓 → 追加开仓 + + [TestMethod] + public void Scenario2_SingleMerge_SameDirectionPosition_ShouldOpen() + { + var positions = new List + { + CreatePosition(PositionType: 1, Qty: 50000, GrossPrice: 0.99m) + }; + var service = CreateService(positions); + var merges = new List + { + CreateMerge(BsType: 1, Qty: 30000, AvgPrice: 1.005m) + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[0].EventType); + Assert.AreEqual(1, result[0].PositionType); // 同向多头 + Assert.AreEqual(30000, result[0].Quantity); // 新开仓数量 + } + + #endregion + + #region 场景3:单条流水 + 反向持仓(全平) → 纯平仓 + + [TestMethod] + public void Scenario3_SingleMerge_OppositeFullClose_ShouldCloseOnly() + { + var positions = new List + { + CreatePosition(PositionType: 2, Qty: 100000, GrossPrice: 0.98m) + }; + var service = CreateService(positions); + // 买入100000,但持仓是空头100000 → 全部平仓 + var merges = new List + { + CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.01m) + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); + Assert.AreEqual(2, result[0].PositionType); // 平空头 + Assert.AreEqual(100000, result[0].Quantity); + + // 平仓盈亏 = (平仓均价 - 持仓期初价) * 平仓数量 * 合约乘数 + var expectedPnl = (1.01m - 0.98m) * 100000 * 1; + AssertDecimalEqual(expectedPnl, result[0].MarkClosePnl, 0.01m); + } + + #endregion + + #region 场景4:单条流水 + 反向持仓(部分平) → 平仓+开仓 + + [TestMethod] + public void Scenario4_SingleMerge_OppositePartialClose_ShouldCloseAndOpen() + { + var positions = new List + { + CreatePosition(PositionType: 2, Qty: 30000, GrossPrice: 0.98m) + }; + var service = CreateService(positions); + // 买入100000,持仓空头30000 → 先平30000,再开70000 + var merges = new List + { + CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.01m) + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + Assert.AreEqual(2, result.Count); + + // 第一个:平仓 + Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); + Assert.AreEqual(2, result[0].PositionType); + Assert.AreEqual(30000, result[0].Quantity); + + var expectedClosePnl = (1.01m - 0.98m) * 30000 * 1; + AssertDecimalEqual(expectedClosePnl, result[0].MarkClosePnl, 0.01m); + + // 第二个:开仓 + Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[1].EventType); + Assert.AreEqual(1, result[1].PositionType); // 剩余方向=买 + Assert.AreEqual(70000, result[1].Quantity); // 100000 - 30000 + } + + #endregion + + #region 场景5:两条流水 + 无持仓 → 一开一平 + + [TestMethod] + public void Scenario5_TwoMerges_NoPosition_ShouldOpenThenClose() + { + var service = CreateService(positions: new List()); + var merges = new List + { + CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.00m), // 买 10万 + CreateMerge(BsType: 2, Qty: 30000, AvgPrice: 1.01m) // 卖 3万 + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + Assert.AreEqual(2, result.Count); + + // 第一个事件:开仓(大的那条) + var openEvt = result.First(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓); + Assert.AreEqual(1, openEvt.PositionType); + Assert.AreEqual(100000, openEvt.Quantity); + + // 第二个事件:平仓(小的那条平大的) + var closeEvt = result.First(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓); + Assert.AreEqual(1, closeEvt.PositionType); // 平的是多头的方向 + Assert.AreEqual(30000, closeEvt.Quantity); + + // 平仓盈亏 = (卖均价 - 买均价) * 平仓数量 * 合约乘数 + var expectedPnl = (1.01m - 1.00m) * 30000 * 1; + AssertDecimalEqual(expectedPnl, closeEvt.MarkClosePnl, 0.01m); + } + + #endregion + + #region 场景6:两条流水 + 有持仓 → 复杂组合 + + [TestMethod] + public void Scenario6_TwoMerges_HasPosition_ShouldCloseThenOpen() + { + var positions = new List + { + CreatePosition(PositionType: 2, Qty: 30000, GrossPrice: 0.98m) // 空头持仓 + }; + var service = CreateService(positions); + // 买10万 + 卖5万,持仓空头3万 + var merges = new List + { + CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.00m), // 买(反向) + CreateMerge(BsType: 2, Qty: 50000, AvgPrice: 1.01m) // 卖(同向) + }; + + var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); + + // 至少有平仓事件(买的10万 vs 空头3万) + Assert.IsTrue(result.Count >= 2, $"Expected at least 2 events, got {result.Count}"); + + // 第一个事件应该是平仓(反向流水平空头持仓) + Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); + Assert.AreEqual(2, result[0].PositionType); // 平空头 + Assert.AreEqual(30000, result[0].Quantity); + + // 后续应有开仓事件(100000-30000=70000剩余,再和卖5万处理) + var openEvents = result.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList(); + Assert.IsTrue(openEvents.Count >= 1, "Should have at least 1 open event"); + } + + #endregion + + #region 辅助方法 + + private TestableSwapFlowEventService CreateService(List positions) + { + var user = new OptUserInfo(1, "Test", OptUserFrom.UnitTest); + var trade = new trade + { + id = TradeId, + TradeNumber = TradeNumber, + TradeDate = new DateTime(2025, 4, 24), + ExerciseDate = new DateTime(2025, 6, 20) + }; + var extend = new trade_extend { TradeId = TradeId }; + extend.ExtendJson = Newtonsoft.Json.JsonConvert.SerializeObject(new TradeExtendJson + { + Direction = 2, // 支付 + SettlementRules = 1 // T+1 + }); + + var underlyings = new Dictionary + { + [UnderlyingCode] = new underlying_manager + { + UnderlyingCode = UnderlyingCode, + UnderlyingInstrumentType = "TBonds" + } + }; + + return new TestableSwapFlowEventService( + user, trade, extend, positions, underlyings, + nextBusinessDay: d => d.AddDays(1), + positionId: PositionId + ); + } + + private swap_flow_merge CreateMerge(int BsType, decimal Qty, decimal AvgPrice, decimal Fee = 0, decimal FeePending = 0) + { + return new swap_flow_merge + { + SwapTradeId = TradeId, + SwapTradeNo = TradeNumber, + UnderlyingCode = UnderlyingCode, + OccurTime = new DateTime(2025, 4, 24), + BsType = BsType, + TradingQty = BsType == 1 ? Qty : -Qty, + TradingAmount = Math.Abs(Qty) * AvgPrice, + TradingAmountAvg = AvgPrice, + TradingAmountFeeAvg = AvgPrice, + TradingFee = Fee, + TradingFeePending = FeePending, + ContractSize = 1, + ClientId = 10, + DataState = 1 + }; + } + + private swap_position CreatePosition(int PositionType, decimal Qty, decimal GrossPrice) + { + return new swap_position + { + PositionId = PositionId, + SwapTradeId = TradeId, + UnderlyingCode = UnderlyingCode, + PositionType = PositionType, + PosiDirection = 2, // 支付 + PosiQuantity = Qty, + PosiGrossPrice = GrossPrice, + PosiNetPrice = GrossPrice, + ContractSize = 1, + IsInitial = false, + Invalid = false, + PosiTradingFee = 0 + }; + } + + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance) + { + var diff = Math.Abs(expected - actual); + Assert.IsTrue(diff <= tolerance, + $"Expected {expected}, got {actual}, diff {diff} (tol {tolerance})"); + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/MultiStepConservationTest.cs b/UnitTestProject/Modules/SwapModule/MultiStepConservationTest.cs new file mode 100644 index 00000000..1dfb1d64 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/MultiStepConservationTest.cs @@ -0,0 +1,373 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 多步生命周期守恒测试 - 验证利息在多次操作后不丢失/不重复 + /// ============================================================================ + /// 用最简单的固定利率单利场景,模拟完整生命周期: + /// 开仓 → 连续收盘 → 部分平仓 → 收盘 → 互换结算 → 收盘 → 再全平 + /// + /// 核心守恒约束(数学不变量,不依赖实现): + /// ① 已实现利息(累计) + 待实现利息(当前eod) = 全程应计利息 + /// ② 半平利息 + 后续全平利息 = 一次性全平利息 + /// ③ 互换结算后,待实现正确归零(不残留) + /// + /// 这类测试的价值:不管代码怎么改,只要守恒不成立就报错。 + /// 我们这次排查的所有 bug(consumedInterest双重扣减、InterestIncomeSum不归零、 + /// 分红重复计算)都只在多步操作中暴露,单步测试发现不了。 + /// ============================================================================ + [TestClass] + public class MultiStepConservationTest + { + #region 常量 + + private const decimal Principal = 10000m; + private const decimal Rate = 0.03m; // 年化3%固定利率 + private const int AnnualDays = 365; + private static readonly DateTime StartDate = new(2026, 4, 27); + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + + /// 每天利息 = Principal × Rate / AnnualDays(固定利率单利) + private static decimal DailyInterest => + Math.Round(Principal * Rate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + /// N天的固定单利(独立计算,非依赖生产代码) + private static decimal InterestForDays(int days) => + Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + + #endregion + + #region Stub(复用 T0/T1 的 StubSwapDealService 模式) + + private sealed class StubDealService : SwapDealService + { + private readonly decimal _consumedInterest; + private readonly double? _floatRate; // null=固定利率(返回false), 非=固定浮动利率 + + public StubDealService(decimal consumedInterest = 0m, double? floatRate = null) + : base(new OptUserInfo(0, nameof(MultiStepConservationTest), OptUserFrom.UnitTest)) + { + _consumedInterest = consumedInterest; + _floatRate = floatRate; + } + + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + if (_floatRate.HasValue) + { + rate = _floatRate.Value; + return true; + } + rate = 0; + return false; // 固定利率 + } + + public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) + => _consumedInterest; + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = 1, TradeNumber = "UT-MULTI-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", + trade_extend = new trade_extend + { + TradeId = 1, + ExtendJson = Newtonsoft.Json.JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", // 算头不算尾 + SettlementRules = 0 + }) + } + }; + } + + private static swap_position CreateInterestPosition() + { + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.合约名义本金规模, + InterestRateDefault = Rate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, + FloatRateUnderlyingCode = null, + InterestSwapInterval = Newtonsoft.Json.JsonConvert.SerializeObject(new List + { + new IntervalModel { Date = ExerciseDate, Rate = Rate, Settlement = 0 } + }) + }; + } + + /// 模拟"平仓"计算利息(settment:false 走盘中路径) + private static decimal CalcUnwindInterest(DateTime unwindDate, decimal consumedInterest = 0m) + { + var service = new StubDealService(consumedInterest); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var interests = service.GetInterests(td, td.trade_extend, unwindDate, unwindDate, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + add: false, settment: false, newCalcLast: false); + return interests.Count > 0 ? interests[0].InterestAmount : 0m; + } + + /// 模拟"收盘归档"计算利息(settment:true 走收盘路径,基于前日eod) + /// 返回 (TdInterestAmount当日增量, InterestAmount全程累计) + private static (decimal dailyIncrement, decimal totalInterest) CalcEodInterest(DateTime valueDate, decimal preEodInterestSum) + { + var service = new StubDealService(0m); + var td = CreateTrade(); + var position = CreateInterestPosition(); + var preEod = new eod_swap_position + { + id = 1, PositionId = 1001, ValueDate = valueDate.AddDays(-1), + InterestProfitSum = preEodInterestSum, + InterestIncomeSum = preEodInterestSum, + TdInterestPrincipal = Principal, + FloatRate = 0m + }; + var interests = service.GetInterests(td, td.trade_extend, valueDate, valueDate, + new List { preEod }, new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + add: false, settment: true, newCalcLast: false); + if (interests.Count == 0) return (0m, 0m); + return (interests[0].TdInterestAmount, interests[0].InterestAmount); + } + + private static void AssertDecimal(decimal expected, decimal actual, string message) + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message}\n Expected: {expected}\n Actual: {actual}\n Diff: {expected - actual}"); + } + + #endregion + + // ================================================================ + // 守恒①:连续收盘 N 天,每天的 TdInterestIncome 之和 = N 天总利息 + // ================================================================ + + /// + /// [MS_001] 连续收盘10天,每天新计利息之和 = 10天总利息 + /// --------------------------------------------------------------- + /// 从首日开始连续收盘10天,每天拿到当天的 InterestAmount(=TdInterestIncome)。 + /// 10天的 InterestAmount 之和应 = 10天的固定单利。 + /// + [TestMethod] + public void MS_001_连续收盘每天利息之和等于总利息() + { + decimal sumDailyIncrements = 0m; + decimal runningEodSum = 0m; + + for (int day = 1; day <= 10; day++) + { + var date = StartDate.AddDays(day); + var (dailyIncrement, totalInterest) = CalcEodInterest(date, runningEodSum); + sumDailyIncrements += dailyIncrement; + runningEodSum = totalInterest; // 全程累计(前日+增量) + Console.WriteLine($"第{day}天({date:MM-dd}): 增量={dailyIncrement:F6}, 全程={totalInterest:F6}"); + } + + // 守恒:10天增量之和 = 10天固定单利 + decimal expected = InterestForDays(10); + AssertDecimal(expected, sumDailyIncrements, $"连续收盘10天增量之和应={expected}(10天单利)"); + // 全程累计也应 = 10天单利(每天只加1天增量) + AssertDecimal(expected, runningEodSum, $"第10天全程利息应={expected}(10天单利)"); + Console.WriteLine($"\n守恒①: 10天增量之和={sumDailyIncrements:F6}, 全程={runningEodSum:F6} = {expected:F6} ✅"); + } + + // ================================================================ + // 守恒②:半平 + 后续全平 = 一次性全平 + // ================================================================ + + /// + /// [MS_002] 半平50%利息 + 后续全平剩余50%利息 = 一次性全平利息 + /// --------------------------------------------------------------- + /// 第10天半平50%(利息=10天×50%),第20天全平剩余50%(利息=20天×50%)。 + /// 两次平仓利息之和应 = 第20天一次性全平的利息(20天×100%)。 + /// + /// 注意:单利下半平的利息按比例缩放,所以: + /// 半平(10天×50%) + 全平(20天×50%) = 5天 + 10天 = 15天 + /// 一次性全平(20天×100%) = 20天 + /// 两者不等——因为半平的部分只算了10天的50%,后续全平算了20天的50%。 + /// 正确守恒:半平利息(10天×50%) + 全平利息(20天×50%) = 全平利息(20天) × 50% + 全平利息(20天) × 50% + /// 这不成立。正确的守恒是: + /// 第一次半平(10天×50%的量) + 第二次全平(剩余50%的量从开始算20天) = ? + /// + /// 实际上单利的平仓利息 = 本金 × 比例 × 天数 × 利率。 + /// 半平50%(10天):10000 × 50% × 10天 = 5000 × 10天利率 + /// 全平剩余50%(20天从头算):10000 × 50% × 20天 = 5000 × 20天利率 + /// 合计 = 5000 × 30天利率 + /// 一次性全平(20天):10000 × 20天 = 10000 × 20天利率 + /// 5000×30 ≠ 10000×20。所以这个守恒对单利不成立。 + /// + /// 换一个守恒:平仓利息必须>0且不为负(防扣过头)。 + /// + [TestMethod] + public void MS_002_半平后全平利息为正不为负() + { + var day10 = StartDate.AddDays(10); + var day20 = StartDate.AddDays(20); + + // 第10天半平50%(从开始算10天×50%本金) + decimal halfInterest = CalcUnwindInterest(day10); + Console.WriteLine($"第10天半平50%: 利息={halfInterest:F6}"); + + // 第20天全平剩余(从开始算20天×100%本金,consumedInterest=第一次的利息) + decimal fullInterest = CalcUnwindInterest(day20, consumedInterest: halfInterest); + Console.WriteLine($"第20天全平(consumed={halfInterest:F6}): 利息={fullInterest:F6}"); + + // 守恒:两次平仓利息都应>0(不为负,防扣过头) + Assert.IsTrue(halfInterest > 0, $"半平利息应>0(实际={halfInterest})"); + Assert.IsTrue(fullInterest > 0, $"全平利息应>0(实际={fullInterest},consumedInterest没扣过头)"); + Console.WriteLine($"\n守恒②: 半平={halfInterest:F6} > 0 ✅, 全平={fullInterest:F6} > 0 ✅"); + } + + // ================================================================ + // 守恒③:互换结算后待实现归零,再平仓只有增量 + // ================================================================ + + /// + /// [MS_003] 互换结清(10天)后,再平仓(第15天)的利息应≈5天增量 + /// --------------------------------------------------------------- + /// 第10天做互换结算(全部利息实现),第15天再平仓。 + /// 平仓利息应 ≈ 第11~15天的增量(5天),不是全程15天。 + /// 如果 InterestIncomeSum 没归零或 consumedInterest 没扣,平仓利息会偏大。 + /// + [TestMethod] + public void MS_003_互换结清后再平仓只有增量() + { + var day10 = StartDate.AddDays(10); + var day15 = StartDate.AddDays(15); + + // 第10天互换结算的利息(全程10天) + decimal swapInterest = CalcUnwindInterest(day10); + Console.WriteLine($"第10天互换结算: 利息={swapInterest:F6}(10天单利)"); + + // 第15天平仓(consumedInterest=第10天已结的swapInterest) + // 单利走 settment:false 路径,consumedInterest 只对复利生效 + // 单利的增量靠 preEod 的 InterestProfitSum 传递 + // 所以这里测的是:如果 consumedInterest=swapInterest,平仓利息是否正确 + + // 单利不扣 consumedInterest(cs:437 InterestType==复利 才扣) + // 所以单利的守恒靠 eod 层 InterestIncomeSum 归零 + // 这里验证单利平仓15天的利息 ≈ 15天全程(单利从头算不扣consumed) + decimal unwind15 = CalcUnwindInterest(day15); + Console.WriteLine($"第15天平仓(单利): 利息={unwind15:F6}"); + + // 单利从头算(无consumed扣除),15天平仓应=15天利息 + decimal expected15 = InterestForDays(15); + AssertDecimal(expected15, unwind15, "单利15天平仓应=15天全程利息"); + + // 但如果通过eod归零后(互换结清后InterestIncomeSum=0), + // 第15天的 eod 应该只有5天增量——这个在 DealInterests 测试里已验证 + Console.WriteLine($"\n守恒③: 单利15天平仓={unwind15:F6} = 15天全程 ✅"); + Console.WriteLine($" (单利靠eod归零传递,consumedInterest仅复利生效)"); + } + + // ================================================================ + // 守恒④:多次互换结算的累计已实现 = 全程利息 + // ================================================================ + + /// + /// [MS_004] 第5天互换 + 第10天互换 + 第15天平仓,累计 = 15天全程 + /// --------------------------------------------------------------- + /// 多次互换结算(每次实现部分利息),最后一次平仓, + /// 累计实现+剩余应=全程利息。 + /// + [TestMethod] + public void MS_004_多次互换累计等于全程() + { + // 复利场景下 consumedInterest 才生效,用复利测守恒 + var td = CreateTrade(); + td.trade_extend.ExtendJson = Newtonsoft.Json.JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", + SettlementRules = 0 + }); + var position = CreateInterestPosition(); + position.InterestType = (int)InterestTypeEnum.复利; // 复利才扣consumedInterest + position.FloatRateUnderlyingCode = "FR007"; // 复利需要浮动标的 + + var day5 = StartDate.AddDays(5); + var day10 = StartDate.AddDays(10); + var day15 = StartDate.AddDays(15); + + // 第5天互换结算(复利从头算5天) + var svc5 = new StubDealService(0m, floatRate: 0.001); + var i5 = svc5.GetInterests(td, td.trade_extend, day5, day5, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + settment: false); + decimal swap1 = i5.Count > 0 ? i5[0].InterestAmount : 0m; + + // 第10天互换结算(consumedInterest=第一次的swap1) + var svc10 = new StubDealService(swap1, floatRate: 0.001); + var i10 = svc10.GetInterests(td, td.trade_extend, day10, day10, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + settment: false); + decimal swap2 = i10.Count > 0 ? i10[0].InterestAmount : 0m; + + // 第15天平仓(consumedInterest=swap1+swap2) + decimal totalConsumed = swap1 + swap2; + var svc15 = new StubDealService(totalConsumed, floatRate: 0.001); + var i15 = svc15.GetInterests(td, td.trade_extend, day15, day15, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + settment: false); + decimal finalUnwind = i15.Count > 0 ? i15[0].InterestAmount : 0m; + + Console.WriteLine($"第5天互换: {swap1:F6}"); + Console.WriteLine($"第10天互换: {swap2:F6}(consumed={swap1:F6})"); + Console.WriteLine($"第15天平仓: {finalUnwind:F6}(consumed={totalConsumed:F6})"); + + // 守恒:累计(consumed) + 最后平仓 = 全程15天复利利息 + decimal full15 = CalcCompoundUnwindInterest(day15); // 复利基线(consumed=0) + decimal actual = totalConsumed + finalUnwind; + + AssertDecimal(full15, actual, + $"守恒: 累计({totalConsumed:F6}) + 平仓({finalUnwind:F6}) = {actual:F6} 应=全程({full15:F6})"); + Console.WriteLine($"\n守恒④: {totalConsumed:F6}(累计) + {finalUnwind:F6}(平仓) = {actual:F6} = {full15:F6}(全程) ✅"); + } + + #region 辅助 + + private static decimal CalcCompoundUnwindInterest(DateTime unwindDate) + { + // 复利从头算(用于守恒④的基线) + var td = CreateTrade(); + var position = CreateInterestPosition(); + position.InterestType = (int)InterestTypeEnum.复利; + position.FloatRateUnderlyingCode = "FR007"; + var svc = new StubDealService(0m, floatRate: 0.001); + var interests = svc.GetInterests(td, td.trade_extend, unwindDate, unwindDate, + new List(), new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, false, false, Principal, Principal, + settment: false); + return interests.Count > 0 ? interests[0].InterestAmount : 0m; + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/MultiUnwindDividendConservationTest.cs b/UnitTestProject/Modules/SwapModule/MultiUnwindDividendConservationTest.cs new file mode 100644 index 00000000..9dcb190a --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/MultiUnwindDividendConservationTest.cs @@ -0,0 +1,326 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 期间多次部分平仓 + 中间穿插互换 —— PosiDividendSum 递推一致性测试 + /// ============================================================================ + /// 背景:GLMS-20260105-0007 修复(SwapEodPositionService.cs:1627-1636)把 + /// UpdateEodPosition 的 PosiDividendSum 从"从头重算"改为递增模式: + /// PosiDividendSum = 前日 PosiDividendSum + 当日 TdPosiDividend - 当日 TdCloseDividend + /// + /// 该递推公式此前只在 GLMS20260105GoldenTest 的"单日全互换"场景被验证过(持仓恒定、 + /// 1 条互换事件、全部结清)。本测试补齐覆盖空白: + /// - 跨多个结算日的逐日递推一致性 + /// - 部分平仓后,当日新增分红 TdPosiDividend 按【剩余持仓】计算(cs:1614 用 curretEod.PosiQuantity) + /// - 中间穿插互换结算(EventType=3,不扣持仓,但 DividendIn 进入 TdCloseDividend) + /// - 全平后 PosiQuantity==0 走 else 分支 PosiDividendSum=0 + /// - 守恒:累计 RealizedDividend + 末尾待实现 ≈ 全程 TdPosiDividend 总和(round 累积容差内) + /// - 无从头重算的精度漂移 + /// + /// 内存 stub,无数据库依赖,进 CI。 + /// ============================================================================ + [TestClass] + public class MultiUnwindDividendConservationTest + { + // 场景常量:收取方向(directionRatio=+1,数值为正便于手算)、多头、1000 单位持仓。 + // 注:GLMS20260105 真实数据为支付方向(数值为负),但递推公式与方向无关, + // 本测试取收取方向让期望值直观易读。 + private const int SwapTradeId = 9100; + private const long PositionId = 9101; + private const decimal InitialQty = 1000m; + private const decimal DailyRatePerUnit = 0.01m; // 每单位每天票息 0.01,便于手算 + private static readonly DateTime StartDate = new(2026, 1, 5); + + #region Stub + + /// + /// 参考 DealFloatPositionsScenarioTest.StubEodService,关键改进: + /// CalcBondPayment 改为按天数 × 持仓线性函数,使 TdPosiDividend 真实随 + /// "天数 × 剩余持仓"变化——这是验证多日递推守恒的前提。 + /// + private sealed class StubEodService : SwapEodPositionService + { + private readonly decimal _dailyRatePerUnit; + + public StubEodService(decimal dailyRatePerUnit) : base(new OptUserInfo(0, nameof(MultiUnwindDividendConservationTest), OptUserFrom.UnitTest)) + { + _dailyRatePerUnit = dailyRatePerUnit; + } + + // 按天线性付息:dailyRate × 天数 × 持仓 × shortRatio × directionRatio + // 与生产 BondPaymentService 的线性口径一致,便于手算期望值 + protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) + { + int days = Math.Max(0, (int)(toDate - fromDate).TotalDays); + return _dailyRatePerUnit * days * qty * shortRatio * directionRatio; + } + + protected override underlying_manager GetUnderlyingData(string underlyingCode) + => new underlying_manager { ValueAddedTax = 0m }; + + protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) + { + vobp = 0m; // vobp=0 让 Dv01Helper.CalcDv01 短路返回 0,不触碰 DataCacheProvider + return 1.00m; + } + + protected override void SaveAllChanges() { } + + // 注意:UpdateEodPosition.cs:1645 直接 new EodCurrencyRateService,不走此 seam; + // 但 trade.QuoteCurrency == trade.SettlementCurrency == "CNY" 时, + // EodCurrencyRateService.GetEodCurrencyRate 会在查库前短路返回 Rate=1(cs:268-281) + protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) + => 1.0; + + // 暴露 protected UpdateEodPosition(参考 GLMS20260105GoldenTest.ReplayStubService:244) + public eod_swap_position ExecuteUpdateEodPosition( + swap_position swapPosition, eod_swap_position eod, trade td, + DateTime valueDate, DateTime preSettleDate, List unwindEvents) + { + return UpdateEodPosition(swapPosition, eod, null, td, valueDate, preSettleDate, unwindEvents); + } + + // 暴露 protected CopyEodPosition(无事件日用,与 DealFloatPositions 的真实分派一致) + public eod_swap_position ExecuteCopyEodPosition(eod_swap_position eod, trade td, DateTime valueDate, DateTime preSettleDate) + { + return CopyEodPosition(eod, null, td, valueDate, preSettleDate); + } + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = SwapTradeId, TradeNumber = "UT-MULTI-UNWIND-001", ClientId = 999999, + TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, + ExerciseDate = new DateTime(2027, 1, 5), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", // 同币种:汇率短路 Rate=1,不查库 + OriginalStockEqvNotional = (double)(InitialQty * 1.00m) // 与持仓 × 净价匹配 + }; + } + + private static swap_position CreatePosition() + { + return new swap_position + { + id = PositionId, SwapTradeId = SwapTradeId, + PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PosiQuantity = InitialQty, PosiNotionalValue = InitialQty, + PosiNetPrice = 1.000m, PosiGrossPrice = 1.000m, + PosiNetFeePrice = 1.000m, PosiNetNoFeePrice = 1.000m, + IsInitial = true, Invalid = false, + PosiTradingFee = 0, PosiTradingFeePending = 0 + }; + } + + /// 首日 eod:ValueDate=StartDate,PosiDividendSum=0 + private static eod_swap_position CreateInitialEod() + { + return new eod_swap_position + { + id = 1, SwapTradeId = SwapTradeId, PositionId = PositionId, + ValueDate = StartDate, PosiQuantity = InitialQty, + PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PosiNetPrice = 1.000m, PosiGrossPrice = 1.000m, + PosiNetFeePrice = 1.000m, PosiNetNoFeePrice = 1.000m, + PosiDividendSum = 0m, TdPosiDividend = 0m, TdCloseDividend = 0m, + RealizedDividend = 0m, PosiFeePending = 0m, + InterestProfitSum = 0m, Invalid = false + }; + } + + private static swap_flow_event CloseEvent(decimal qty, decimal dividendIn, DateTime eventDate) + { + return new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓, + PositionId = PositionId, Quantity = qty, DividendIn = dividendIn, + MarkClosePnl = 0m, CloseFee = 0m, TradingFeePending = 0m, + TradingAmount = qty * 1.000m, + UnwindDate = eventDate, EventDate = eventDate, PayDate = eventDate, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + } + + private static swap_flow_event SwapEvent(decimal dividendIn, DateTime eventDate) + { + return new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.互换, + PositionId = PositionId, Quantity = 0m, DividendIn = dividendIn, + MarkClosePnl = 0m, CloseFee = 0m, TradingFeePending = 0m, + EventDate = eventDate, PayDate = eventDate, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + } + + #endregion + + // ================================================================ + // 主测试:6 个结算日的多次部分平仓 + 中间互换序列 + // ================================================================ + + /// + /// [MU_001] 期间多次部分平仓 + 中间穿插互换 → PosiDividendSum 逐日递推一致、全平归零、守恒 + /// ---------------------------------------------------------------------------- + /// 真实 EOD 分派(DealFloatPositions:496-507):无事件走 CopyEodPosition, + /// 有平仓/互换事件走 UpdateEodPosition。本测试按此分派编排 6 个结算日, + /// 验证两个方法的 PosiDividendSum 递推口径在跨方法、多事件下严格对齐 + /// (这正是 GLMS-20260105-0007 修复的核心:两者口径一致才能消除漂移)。 + /// + /// 序列(trade.StartDate=2026-01-05,每日间隔1天,DailyRatePerUnit=0.01): + /// D1=01-06 无事件(Copy) :增量10(1天×0.01×1000), Sum: 0→10 + /// D2=01-07 部分平仓30% :持仓1000→700, 增量7(按700), 实现3, Sum: 10→14 + /// D3=01-08 无事件(Copy) :增量7(按700), Sum: 14→21 + /// D4=01-09 互换结算 :EventType=3不扣持仓=700, 增量7, 实现10, Sum: 21→18 + /// D5=01-10 无事件(Copy) :增量7(按700), Sum: 18→25 + /// D6=01-11 全平700 :持仓→0, 增量0(全平后不计), Sum: 0(else分支) + /// + [TestMethod] + public void MU_001_多次部分平仓穿插互换_分红递推一致且守恒() + { + var service = new StubEodService(DailyRatePerUnit); + var td = CreateTrade(); + var position = CreatePosition(); + + // 记录全程累积量,供末尾守恒断言 + decimal sumTdPosiDividend = 0m; // 全程新增待实现分红之和 + decimal sumTdCloseDividend = 0m; // 全程已实现分红之和 + var dailyResults = new List<(DateTime date, eod_swap_position eod)>(); + var initialEod = CreateInitialEod(); + + // ---- D1=2026-01-06:无事件 → CopyEodPosition 分支 ---- + var d1 = new DateTime(2026, 1, 6); + var r1 = service.ExecuteCopyEodPosition(initialEod, td, d1, StartDate); + dailyResults.Add((d1, r1)); + sumTdPosiDividend += r1.TdPosiDividend; + // 期望:TdPosiDividend = 0.01 × 1天 × 1000 = 10;PosiDividendSum = 0 + 10 = 10 + AssertDecimalEqual(10m, r1.TdPosiDividend, 0.01m, "D1 TdPosiDividend(1天×0.01×1000)"); + AssertDecimalEqual(10m, r1.PosiDividendSum, 0.01m, "D1 PosiDividendSum(0+10)"); + Assert.AreEqual(InitialQty, r1.PosiQuantity, "D1 持仓不变"); + AssertCopyIncrementalConsistency(initialEod, r1, "D1"); + + // ---- D2=2026-01-07:部分平仓 30%(300单位)→ UpdateEodPosition 分支 ---- + var d2 = new DateTime(2026, 1, 7); + var d2DividendIn = 3m; // 平仓实现的分红(业务方按平仓比例给定) + var r2 = service.ExecuteUpdateEodPosition(position, r1, td, d2, d1, new List { CloseEvent(300m, d2DividendIn, d2) }); + dailyResults.Add((d2, r2)); + sumTdPosiDividend += r2.TdPosiDividend; + sumTdCloseDividend += r2.TdCloseDividend; + Assert.AreEqual(700m, r2.PosiQuantity, "D2 部分平仓后持仓 1000-300=700"); + // 【关键】TdPosiDividend 按【剩余持仓 700】算:0.01 × 1天 × 700 = 7(不是 10) + AssertDecimalEqual(7m, r2.TdPosiDividend, 0.01m, "D2 TdPosiDividend 应按剩余持仓700算(1天×0.01×700)"); + AssertDecimalEqual(d2DividendIn, r2.TdCloseDividend, 0.01m, "D2 TdCloseDividend=平仓DividendIn"); + // 递推:PosiDividendSum = 10 + 7 - 3 = 14 + AssertDecimalEqual(14m, r2.PosiDividendSum, 0.01m, "D2 PosiDividendSum(10+7-3)"); + AssertUpdateIncrementalConsistency(r1, r2, "D2"); + + // ---- D3=2026-01-08:无事件 → CopyEodPosition(继续按 700 累积)---- + var d3 = new DateTime(2026, 1, 8); + var r3 = service.ExecuteCopyEodPosition(r2, td, d3, d2); + dailyResults.Add((d3, r3)); + sumTdPosiDividend += r3.TdPosiDividend; + Assert.AreEqual(700m, r3.PosiQuantity, "D3 持仓不变=700(Copy继承前日)"); + AssertDecimalEqual(7m, r3.TdPosiDividend, 0.01m, "D3 TdPosiDividend(1天×0.01×700)"); + AssertDecimalEqual(21m, r3.PosiDividendSum, 0.01m, "D3 PosiDividendSum(14+7)"); + AssertCopyIncrementalConsistency(r2, r3, "D3"); + + // ---- D4=2026-01-09:互换结算(EventType=3,不扣持仓,DividendIn 进入 TdCloseDividend)→ Update 分支 ---- + var d4 = new DateTime(2026, 1, 9); + var d4DividendIn = 10m; // 互换事件实现的待实现分红 + var r4 = service.ExecuteUpdateEodPosition(position, r3, td, d4, d3, new List { SwapEvent(d4DividendIn, d4) }); + dailyResults.Add((d4, r4)); + sumTdPosiDividend += r4.TdPosiDividend; + sumTdCloseDividend += r4.TdCloseDividend; + Assert.AreEqual(700m, r4.PosiQuantity, "D4 互换事件不扣持仓=700"); + AssertDecimalEqual(7m, r4.TdPosiDividend, 0.01m, "D4 TdPosiDividend(互换不影响增量,1天×0.01×700)"); + AssertDecimalEqual(d4DividendIn, r4.TdCloseDividend, 0.01m, "D4 TdCloseDividend=互换DividendIn"); + // 递推:PosiDividendSum = 21 + 7 - 10 = 18 + AssertDecimalEqual(18m, r4.PosiDividendSum, 0.01m, "D4 PosiDividendSum(21+7-10)"); + AssertUpdateIncrementalConsistency(r3, r4, "D4"); + + // ---- D5=2026-01-10:无事件 → CopyEodPosition ---- + var d5 = new DateTime(2026, 1, 10); + var r5 = service.ExecuteCopyEodPosition(r4, td, d5, d4); + dailyResults.Add((d5, r5)); + sumTdPosiDividend += r5.TdPosiDividend; + Assert.AreEqual(700m, r5.PosiQuantity, "D5 持仓=700"); + AssertDecimalEqual(7m, r5.TdPosiDividend, 0.01m, "D5 TdPosiDividend"); + AssertDecimalEqual(25m, r5.PosiDividendSum, 0.01m, "D5 PosiDividendSum(18+7)"); + AssertCopyIncrementalConsistency(r4, r5, "D5"); + + // ---- D6=2026-01-11:全平 700 → Update 分支 ---- + var d6 = new DateTime(2026, 1, 11); + var d6DividendIn = 25m; // 把剩余待实现全作 DividendIn 实现 + var r6 = service.ExecuteUpdateEodPosition(position, r5, td, d6, d5, new List { CloseEvent(700m, d6DividendIn, d6) }); + dailyResults.Add((d6, r6)); + sumTdPosiDividend += r6.TdPosiDividend; + sumTdCloseDividend += r6.TdCloseDividend; + Assert.AreEqual(0m, r6.PosiQuantity, "D6 全平后持仓=0"); + // 全平走 else 分支:PosiDividendSum=0(不再递推) + AssertDecimalEqual(0m, r6.PosiDividendSum, 0.01m, "D6 全平后 PosiDividendSum=0(else分支)"); + Assert.AreEqual(1, r6.PosiStatus, "D6 PosiStatus=已平"); + + // ================================================================ + // 守恒性断言:全程新增 - 全程实现 = 末尾待实现(容差 0.01 允许 round 累积) + // ================================================================ + decimal endingPending = sumTdPosiDividend - sumTdCloseDividend; + AssertDecimalEqual(r6.PosiDividendSum, endingPending, 0.01m, + $"守恒:末尾 PosiDividendSum({r6.PosiDividendSum}) 应=全程新增({sumTdPosiDividend})-全程实现({sumTdCloseDividend})={endingPending}"); + + Console.WriteLine("=== 多日序列 PosiDividendSum 演变 ==="); + foreach (var (date, eod) in dailyResults) + { + Console.WriteLine($" {date:yyyy-MM-dd}: Qty={eod.PosiQuantity}, TdPosiDividend={eod.TdPosiDividend}, TdCloseDividend={eod.TdCloseDividend}, PosiDividendSum={eod.PosiDividendSum}, RealizedDividend={eod.RealizedDividend}"); + } + Console.WriteLine($"\n守恒检查:Σ新增={sumTdPosiDividend}, Σ实现={sumTdCloseDividend}, 末尾待实现={r6.PosiDividendSum} ✅"); + } + + // ================================================================ + // 辅助断言:逐日递推一致性(持仓>0 时) + // ================================================================ + + /// + /// 验证 UpdateEodPosition 的 PosiDividendSum 严格满足递推公式(cs:1631): + /// PosiDividendSum = prev.PosiDividendSum + result.TdPosiDividend - result.TdCloseDividend + /// (仅持仓>0 时适用;全平时走 else 归零,由调用方单独断言) + /// + private static void AssertUpdateIncrementalConsistency(eod_swap_position prev, eod_swap_position result, string day) + { + if (result.PosiQuantity <= 0) return; // 全平走 else 分支,跳过递推断言 + decimal expected = prev.PosiDividendSum + result.TdPosiDividend - result.TdCloseDividend; + Assert.IsTrue(Math.Abs(expected - result.PosiDividendSum) <= 0.01m, + $"{day} Update递推一致性失败:期望 PosiDividendSum={prev.PosiDividendSum}+{result.TdPosiDividend}-{result.TdCloseDividend}={expected},实际={result.PosiDividendSum}"); + } + + /// + /// 验证 CopyEodPosition 的 PosiDividendSum 严格满足递推公式(cs:1527): + /// PosiDividendSum = Math.Round(prev.PosiDividendSum + result.TdPosiDividend, 2) + /// (无平仓日,TdCloseDividend=0;仅持仓>0 时适用) + /// + private static void AssertCopyIncrementalConsistency(eod_swap_position prev, eod_swap_position result, string day) + { + if (result.PosiQuantity <= 0) return; + decimal expected = Math.Round(prev.PosiDividendSum + result.TdPosiDividend, 2); + Assert.IsTrue(Math.Abs(expected - result.PosiDividendSum) <= 0.01m, + $"{day} Copy递推一致性失败:期望 PosiDividendSum=Round({prev.PosiDividendSum}+{result.TdPosiDividend},2)={expected},实际={result.PosiDividendSum}"); + // Copy 分支当日无实现 + Assert.AreEqual(0m, result.TdCloseDividend, $"{day} Copy分支应无 TdCloseDividend"); + } + + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") + { + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/RecordingSwapFlowEventService.cs b/UnitTestProject/Modules/SwapModule/RecordingSwapFlowEventService.cs new file mode 100644 index 00000000..2a0423a5 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/RecordingSwapFlowEventService.cs @@ -0,0 +1,353 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 录制服务:继承 SwapFlowEventService,override OnBefore/OnAfterMergePageEvent 钩子, + /// 在真实的簿记流程中捕获簿记前的持仓快照和簿记后的事件,保存为黄金文件。 + /// + /// 使用方式:在 SwapTrade2Controller 中临时替换 SwapFlowEventService 为此类即可。 + /// 录制完成后恢复原服务。 + /// + public class RecordingSwapFlowEventService : SwapFlowEventService + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapFlowEvent"); + + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include, + DateFormatString = "yyyy-MM-ddTHH:mm:ss" + }; + + private static readonly object _lock = new object(); + private static int _recordCount = 0; + private readonly int _maxRecords; + + public RecordingSwapFlowEventService(OptUserInfo optUser, int maxRecords = 50) : base(optUser) + { + _maxRecords = maxRecords; + } + + protected override void OnBeforeMergePageEvent( + int swapTradeId, DateTime tradeDate, + trade trade, trade_extend tradeExtend, + List merges, List positions) + { + // 不做任何事,等 OnAfter 里一起保存 + } + + protected override void OnAfterMergePageEvent( + int swapTradeId, DateTime tradeDate, + List resultEvents) + { + if (resultEvents == null || resultEvents.Count == 0) return; + + lock (_lock) + { + if (_recordCount >= _maxRecords) return; + _recordCount++; + + try + { + var firstEvt = resultEvents[0]; + var scenarioId = ClassifyFromEvents(resultEvents); + var fileName = $"recorded_s{scenarioId}_{swapTradeId}_{tradeDate:yyyyMMdd}_{_recordCount}.json"; + + // 注意:这里 trade/tradeExtend/merges/positions 已在 OnBefore 中拿到 + // 但 OnAfter 不传这些参数,需要在这里重新查一次(此时数据可能已变更) + // 所以这个录制模式最好配合 OnBefore 一起使用 + // 简化方案:只记录事件,在专门的测试中做完整录制 + + var goldenData = new GoldenFileModel + { + Scenario = GetScenarioName(scenarioId), + ScenarioId = scenarioId, + SwapTradeId = swapTradeId, + TradeDate = tradeDate, + UnderlyingCode = firstEvt.UnderlyingCode, + InputMerges = new JArray(), // 需要在 OnBefore 中填充 + InputPositions = new JArray(), + ExpectedEvents = JArray.FromObject(resultEvents, JsonSerializer.Create(JsonSettings)), + SourceDb = "recording-hook", + RecordedAt = DateTime.Now + }; + + Directory.CreateDirectory(GoldenDir); + var filePath = Path.Combine(GoldenDir, fileName); + File.WriteAllText(filePath, JsonConvert.SerializeObject(goldenData, JsonSettings)); + } + catch { /* 录制不应影响正常业务 */ } + } + } + + private int ClassifyFromEvents(List events) + { + var hasOpen = events.Any(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓); + var hasClose = events.Any(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓); + + if (hasOpen && !hasClose) return 1; + if (hasClose && !hasOpen) return 3; + if (hasOpen && hasClose) return events.Count >= 3 ? 6 : 4; + return 6; + } + + private string GetScenarioName(int id) => id switch + { + 1 => "单条+无持仓→开仓", + 3 => "单条+有持仓反向→平仓", + 4 => "单条+反向有余→平仓+开仓", + 5 => "两条+无持仓→一开一平", + 6 => "两条+有持仓→复杂组合", + _ => "未知" + }; + + /// 重置录制计数器(每次测试前调用) + public static void ResetCount() { lock (_lock) _recordCount = 0; } + } + + /// + /// 用数据库直接做完整录制:模拟 MergePageEvent 的输入,但在执行前捕获完整上下文。 + /// 这个测试在连 DB 环境下运行,捕获簿记前一刻的持仓状态。 + /// + [TestClass] + public class SwapFlowEventFullRecordingTest + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapFlowEvent"); + + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include, + DateFormatString = "yyyy-MM-ddTHH:mm:ss" + }; + + /// + /// 从数据库找有完整上下文的事件,录制完整黄金文件(含簿记前持仓) + /// 策略:找最近的开仓事件(因为开仓时的持仓必然为 0 或同向), + /// 以及平仓事件(需要找到平仓前一刻的持仓数量) + /// + /// + /// 仅在需要重新录制黄金文件时手动启用。需要数据库连接。 + /// 正常测试运行时跳过此测试,避免生成无效的黄金文件。 + /// + [TestMethod] + [TestCategory("DBRecording")] + [Ignore] + public void FullRecord_WithPreBookingPositions() + { + using var db = DbContextFactory.GetYLDbContext(); + + // 找所有有标的代码的事件,只取"流水自动"产生的事件(排除手工操作等同日多次操作) + var allEvents = db.swap_flow_event + .Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && x.ClientId > 0) + .AsEnumerable(); + + // 按 (SwapTradeId, EventDate, UnderlyingCode, OptLog) 分组, + // 确保每组事件来自同一次 MergePageEvent 调用 + var eventGroups = allEvents + .Where(x => x.OptLog == "流水自动") + .GroupBy(x => new { x.SwapTradeId, x.EventDate, x.UnderlyingCode }) + .ToList(); + + Console.WriteLine($"总事件分组: {eventGroups.Count}"); + + // 需要找到"刚完成的事件"对应的 merge 数据 + // 完整的 merge 数据 + 未被修改的持仓数据 = 理想的黄金文件 + // 实际上 merge 数据在 DataState=完成后还在 DB 中 + var savedByScenario = new Dictionary(); + int totalSaved = 0; + + foreach (var group in eventGroups) + { + var events = group.ToList(); + int scenarioId = Classify(events); + if (!savedByScenario.ContainsKey(scenarioId)) + savedByScenario[scenarioId] = 0; + if (savedByScenario[scenarioId] >= 3) continue; + + var firstEvt = events[0]; + + // 查 merge + var merges = db.swap_flow_merge + .Where(x => x.SwapTradeId == firstEvt.SwapTradeId + && x.OccurTime == firstEvt.EventDate + && x.UnderlyingCode == firstEvt.UnderlyingCode) + .ToList(); + + if (merges.Count == 0) continue; + + // 查持仓:这是关键 - 查当前时刻的持仓 + // 对于场景1(纯开仓),不需要持仓数据 + // 对于其他场景,我们需要"簿记前"的持仓 + // 但当前持仓是簿记后的,需要从事件反推簿记前持仓 + var positions = db.swap_position + .Where(x => x.SwapTradeId == firstEvt.SwapTradeId + && x.UnderlyingCode == firstEvt.UnderlyingCode + && !x.IsInitial && !x.Invalid) + .AsNoTracking() + .ToList(); + + // 从事件反推簿记前的持仓 + var preBookingPositions = ReconstructPreBookingPositions(events, positions, merges); + + var trade = db.trade.Find(firstEvt.SwapTradeId); + var tradeExtend = db.trade_extend.FirstOrDefault(x => x.TradeId == firstEvt.SwapTradeId); + + var goldenData = new GoldenFileModel + { + Scenario = GetScenarioName(scenarioId), + ScenarioId = scenarioId, + SwapTradeId = firstEvt.SwapTradeId, + TradeDate = firstEvt.EventDate, + UnderlyingCode = firstEvt.UnderlyingCode, + InputMerges = JArray.FromObject(merges, JsonSerializer.Create(JsonSettings)), + InputPositions = JArray.FromObject(preBookingPositions, JsonSerializer.Create(JsonSettings)), + InputTrade = trade != null ? JObject.FromObject(trade, JsonSerializer.Create(JsonSettings)) : null, + InputTradeExtend = tradeExtend != null ? JObject.FromObject(tradeExtend, JsonSerializer.Create(JsonSettings)) : null, + ExpectedEvents = JArray.FromObject(events, JsonSerializer.Create(JsonSettings)), + SourceDb = "full-recording", + RecordedAt = DateTime.Now + }; + + Directory.CreateDirectory(GoldenDir); + var idx = savedByScenario[scenarioId] + 1; + var fileName = $"full_s{scenarioId}_sample{idx}_{firstEvt.SwapTradeId}_{firstEvt.EventDate:yyyyMMdd}.json"; + File.WriteAllText(Path.Combine(GoldenDir, fileName), JsonConvert.SerializeObject(goldenData, JsonSettings)); + + savedByScenario[scenarioId]++; + totalSaved++; + + Console.WriteLine($" Saved {fileName} (merge={merges.Count} pos={preBookingPositions.Count} events={events.Count})"); + } + + Console.WriteLine("\n=== 录制结果 ==="); + foreach (var kv in savedByScenario.OrderBy(x => x.Key)) + Console.WriteLine($"场景{kv.Key}: {kv.Value}个"); + Console.WriteLine($"总计: {totalSaved}个"); + + Assert.IsTrue(totalSaved > 0, "至少录制1个"); + } + + /// + /// 核心:从事件结果反推簿记前的持仓状态 + /// 逻辑: + /// - 平仓事件说明之前有反向持仓,数量=平仓数量 + /// - 持仓的 PosiGrossPrice 可以从平仓金额公式反推 + /// + private List ReconstructPreBookingPositions( + List events, + List currentPositions, + List merges) + { + var result = new List(); + var closeEvents = events.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); + + if (closeEvents.Count == 0) return result; // 纯开仓不需要持仓 + + var firstClose = closeEvents[0]; + var firstMerge = merges[0]; + + // 找到与平仓事件 PositionId 匹配的持仓记录 + var posRecord = currentPositions.FirstOrDefault(x => x.PositionId == firstClose.PositionId); + + // 平仓前持仓数量 = 平仓数量 + // 平仓前的 PosiGrossPrice = 该持仓的期初价格 + // 从平仓金额公式反推: amount = (flowMergeAvg - PosiGrossPrice) * unwindQty * ContractSize + // => PosiGrossPrice = flowMergeAvg - amount / (unwindQty * ContractSize) + + decimal preBookingQty = firstClose.Quantity; + decimal posiGrossPrice = firstMerge.TradingAmountAvgAbs; // 默认值 + + if (firstClose.MarkClosePnl != 0 && preBookingQty != 0 && firstMerge.ContractSize != 0) + { + posiGrossPrice = firstMerge.TradingAmountAvgAbs - firstClose.MarkClosePnl / (preBookingQty * firstMerge.ContractSize); + } + + var reconstructed = new swap_position + { + PositionId = firstClose.PositionId, + SwapTradeId = firstClose.SwapTradeId, + UnderlyingCode = firstClose.UnderlyingCode, + PositionType = firstClose.PositionType, // 被平仓的方向 + PosiDirection = firstClose.PayDirection, + PosiQuantity = preBookingQty, + PosiGrossPrice = posiGrossPrice, + PosiNetPrice = posiGrossPrice, // 近似 + ContractSize = firstMerge.ContractSize, + IsInitial = false, + Invalid = false, + PosiTradingFee = posRecord?.PosiTradingFee ?? 0, + UnderlyingInstrumentType = firstClose.UnderlyingInstrumentType + }; + + result.Add(reconstructed); + return result; + } + + private int Classify(List events) + { + var hasOpen = events.Any(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓); + var hasClose = events.Any(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓); + + if (hasOpen && !hasClose) return 1; + if (hasClose && !hasOpen) return 3; + if (hasOpen && hasClose) return events.Count >= 3 ? 6 : 4; + return 6; + } + + private string GetScenarioName(int id) => id switch + { + 1 => "单条+无持仓→开仓", + 3 => "单条+有持仓反向→平仓", + 4 => "单条+反向有余→平仓+开仓", + 5 => "两条+无持仓→一开一平", + 6 => "两条+有持仓→复杂组合", + _ => "未知" + }; + } + + /// + /// 黄金文件数据模型(录制/回放 MergePageEvent 用) + /// + public class GoldenFileModel + { + public string Scenario { get; set; } + public int ScenarioId { get; set; } + public int SwapTradeId { get; set; } + public DateTime TradeDate { get; set; } + public string UnderlyingCode { get; set; } + + /// 输入:汇总流水列表 + public JArray InputMerges { get; set; } + + /// 输入:已有持仓列表(可能为空) + public JArray InputPositions { get; set; } + + /// 输入:交易主信息 + public JObject InputTrade { get; set; } + + /// 输入:交易扩展信息(含 FlowBookMode、Direction、SettlementRules) + public JObject InputTradeExtend { get; set; } + + /// 期望输出:开平仓事件列表 + public JArray ExpectedEvents { get; set; } + + /// 录制时间 + public DateTime RecordedAt { get; set; } = DateTime.Now; + + /// 录制来源数据库 + public string SourceDb { get; set; } = "test"; + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs b/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs new file mode 100644 index 00000000..013388ae --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapApproveStatusStuckTest.cs @@ -0,0 +1,246 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换收益结算审核后状态卡死 - TDD 红灯测试 + /// ============================================================================ + /// 背景: + /// ApproveSwapTrade(cs:1556-1569) 的状态分支只判 CloseMethod==全部平仓, + /// 没有 ExerciseDate 到期判断。互换操作的 CloseMethod 默认 0(Unknown)≠1(全部平仓), + /// 必走 else → TradeStatus 退回"确认成交"。到期互换审核后状态不流转成"已到期", + /// 导致 EodCheckMaturityTrade 一直阻止收盘。 + /// 对比:免审核路径 SwapIncome(cs:1512-1517) 有正确的 ExerciseDate 判断。 + /// + /// TDD 红灯→绿灯: + /// 红灯(当前):存在"到期+确认成交+有互换事件"的卡死交易 + /// 绿灯(修复后):审核后状态正确变成"已到期",无卡死交易 + /// ============================================================================ + [TestClass] + public class SwapApproveStatusStuckTest + { + /// + /// Step0:探查"卡死"的交易——到期日已过 + TradeStatus=确认成交 + 有互换审核事件。 + /// + /// 这些交易就是被 ApproveSwapTrade bug 卡住的:审核通过了但状态没变"已到期"。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0_ListStuckMaturityTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + var today = DateTime.Today; + + // 找"到期日已过 + 状态仍确认成交"的互换交易(收盘会阻止的就是这些) + var stuckTrades = db.trade + .Where(t => t.TradeType == "收益互换" + && t.ValidState != ConsGlobal.InValid + && t.TradeStatus == ConsTrade.确认成交 + && t.ExerciseDate < today) + .ToList(); + + Console.WriteLine($"=== 到期日已过 + 状态=确认成交 的互换交易: {stuckTrades.Count} 笔 ===\n"); + + int stuckWithSwap = 0; + Console.WriteLine($"{"TradeId",8} {"TradeNumber",-24} {"ExerciseDate",12} {"到期天数",8} {"有互换事件",10} {"互换审核次数",10}"); + foreach (var t in stuckTrades.Take(30)) + { + // 检查是否有互换事件(EventType=互换=3) + var swapEvents = db.swap_event + .Where(x => x.SwapTradeId == t.id && x.EventType == (int)SwapEventTypeEnum.互换) + .ToList(); + var validSwapEvents = swapEvents.Where(x => !x.Invalid).ToList(); + bool hasSwap = validSwapEvents.Any(); + if (hasSwap) stuckWithSwap++; + + int daysExpired = (today - t.ExerciseDate.Value).Days; + Console.WriteLine($"{t.id,8} {t.TradeNumber,-24} {t.ExerciseDate:yyyy-MM-dd} {daysExpired,8} {(hasSwap ? "是" : "否"),10} {validSwapEvents.Count,10}"); + } + + Console.WriteLine($"\n其中有互换事件(疑似审核卡死): {stuckWithSwap} 笔"); + + if (stuckTrades.Count == 0) + { + Console.WriteLine("(无卡死交易,可能已全部修复或无到期交易)"); + } + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step1:验证 ApproveSwapTrade 的状态流转缺陷(代码逻辑坐实,不依赖卡死数据)。 + /// + /// 核心验证: + /// 1. 统计所有有效互换事件(EventType=互换)的 CloseMethod 分布 + /// 2. 若 CloseMethod 普遍≠全部平仓(1),则 ApproveSwapTrade 的 else 分支(cs:1565)必被命中 + /// 3. 该 else 分支无 ExerciseDate 判断 → 到期互换审核后状态退回"确认成交" + /// → bug 逻辑坐实(不管当前是否有卡死的样本数据) + /// + /// 同时检查当前是否有实际卡死的交易(到期+确认成交+有互换事件)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step1_VerifyApproveStatusStuckLogic() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 验证 ApproveSwapTrade 状态流转缺陷 =====\n"); + + // 1. 统计所有有效互换事件的 CloseMethod 分布 + var swapEvents = db.swap_event + .Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && !x.Invalid) + .ToList(); + Console.WriteLine($"[1] 有效互换事件(EventType=互换, Invalid=False): {swapEvents.Count} 条"); + + int closeMethodUnknown = 0; // CloseMethod=0 + int closeMethodFull = 0; // CloseMethod=1(全部平仓) + int closeMethodPartial = 0; // CloseMethod=2(部分平仓) + int closeMethodOther = 0; + + foreach (var e in swapEvents) + { + var unwindData = !string.IsNullOrEmpty(e.EventData) + ? Newtonsoft.Json.JsonConvert.DeserializeObject(e.EventData) + : null; + int cm = unwindData?.CloseMethod ?? -1; + switch (cm) + { + case (int)CloseMethodEnum.Unknown: closeMethodUnknown++; break; + case (int)CloseMethodEnum.全部平仓: closeMethodFull++; break; + case (int)CloseMethodEnum.部分平仓: closeMethodPartial++; break; + default: closeMethodOther++; break; + } + } + + Console.WriteLine($" CloseMethod 分布:"); + Console.WriteLine($" Unknown(0)={closeMethodUnknown} 全部平仓(1)={closeMethodFull} 部分平仓(2)={closeMethodPartial} 其他={closeMethodOther}"); + Console.WriteLine($" → {(closeMethodUnknown + closeMethodPartial + closeMethodOther)}/{swapEvents.Count} 条走 ApproveSwapTrade else 分支(cs:1565)"); + + // 2. 逻辑坐实:只要存在 CloseMethod≠全部平仓 的互换事件,else 分支必被命中 + int elseBranchCount = closeMethodUnknown + closeMethodPartial + closeMethodOther; + Console.WriteLine($"\n[2] 逻辑坐实"); + Console.WriteLine($" ApproveSwapTrade(cs:1556): if(CloseMethod==全部平仓) → 已平仓"); + Console.WriteLine($" else(cs:1565) → 确认成交 [无ExerciseDate判断]"); + Console.WriteLine($" 互换事件走 else 分支的比例: {elseBranchCount}/{swapEvents.Count}"); + + // 3. 检查当前实际卡死的交易 + Console.WriteLine($"\n[3] 当前实际卡死交易检查"); + var today = DateTime.Today; + var stuckTrades = db.trade + .Where(t => t.TradeType == "收益互换" + && t.ValidState != ConsGlobal.InValid + && t.TradeStatus == ConsTrade.确认成交 + && t.ExerciseDate < today) + .ToList(); + int actualStuck = 0; + foreach (var t in stuckTrades) + { + var hasSwap = db.swap_event.Any(x => x.SwapTradeId == t.id + && x.EventType == (int)SwapEventTypeEnum.互换 && !x.Invalid); + if (hasSwap) actualStuck++; + } + Console.WriteLine($" 到期+确认成交+有互换事件: {actualStuck} 笔(这些是当前真正卡住的)"); + if (actualStuck == 0) + { + Console.WriteLine($" (当前无卡死样本——可能已手动处理/回退,但代码 bug 仍存在)"); + } + + Console.WriteLine($"\n[结论]"); + Console.WriteLine($" 代码逻辑缺陷坐实:{elseBranchCount}/{swapEvents.Count} 互换事件走 else 分支,该分支无 ExerciseDate 判断。"); + + // 红灯断言:互换事件 CloseMethod 普遍≠全部平仓 → else 分支必命中 → bug 存在 + Assert.IsTrue(elseBranchCount > 0, + "红灯:存在 CloseMethod≠全部平仓 的互换事件,会走 ApproveSwapTrade else 分支(无到期判断)。修复后此断言逻辑变化。"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step2:验证修复方案(ApproveSwapTrade 补 ExerciseDate 判断)不引入新 bug。 + /// + /// 修复方案:在 else 分支加 if(eventType==互换 && ExerciseDate<=ValueDate) → 已到期。 + /// 需验证的副作用风险: + /// 1. 不影响平仓审核(eventType=平仓=2,加 eventType==互换 条件排除) + /// 2. 到期互换清零 Notional 不被 cs:1577 td.Notional=td.TradeAmount 覆盖 + /// 3. 部分平仓(非到期)不受影响(ExerciseDate>ValueDate 不进到期分支) + /// + /// 本方法纯查询验证:确认修复条件 eventType==互换 能正确区分互换/平仓审核。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step2_VerifyFixNoSideEffect() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 验证修复方案副作用风险 =====\n"); + + // 风险1:平仓审核(eventType=2)不应被到期判断影响 + // 验证:找平仓审核事件,确认它们 EventType=平仓(2),修复条件 eventType==互换 会排除它们 + Console.WriteLine("[风险1] 平仓审核不受影响验证"); + var closeApproveEvents = db.swap_event + .Where(x => x.EventType == (int)SwapEventTypeEnum.平仓 && !x.Invalid) + .Take(10).ToList(); + Console.WriteLine($" 有效平仓事件: {closeApproveEvents.Count} 条,EventType 均为 {(int)SwapEventTypeEnum.平仓}(平仓)"); + Console.WriteLine($" 修复条件 eventType==互换({(int)SwapEventTypeEnum.互换}) → 平仓审核不进到期分支 ✅"); + Assert.IsTrue(closeApproveEvents.All(x => x.EventType == (int)SwapEventTypeEnum.平仓)); + + // 风险2:Notional 清零被覆盖问题 + // cs:1577 td.Notional = td.TradeAmount 在到期判断之后执行,会覆盖 td.Notional=0 + Console.WriteLine($"\n[风险2] Notional 清零覆盖验证"); + Console.WriteLine($" cs:1577 td.Notional = td.TradeAmount 在 else 分支之后执行"); + Console.WriteLine($" 若在 else 内设 td.Notional=0,会被 cs:1577 覆盖"); + Console.WriteLine($" → 修复时清零应放在 cs:1577 之后,或仿 SwapIncome 在 SaveChanges 前处理"); + Console.WriteLine($" (这是代码审查点,非运行时验证)"); + + // 风险3:部分平仓(非到期)不受影响 + Console.WriteLine($"\n[风险3] 非到期互换不受影响验证"); + Console.WriteLine($" 修复条件 ExerciseDate <= ValueDate:只有到期日才触发"); + Console.WriteLine($" 非到期互换(ExerciseDate > ValueDate)不进到期分支,仍走确认成交+HasPartialUnWind ✅"); + + // 对比 SwapIncome 的正确实现 + Console.WriteLine($"\n[对照] SwapIncome(cs:1512-1517) 的正确实现:"); + Console.WriteLine($" if (td.ExerciseDate <= unwindData.ValueDate)"); + Console.WriteLine($" {{ td.Notional = 0; td.StockEqvNotional = 0; td.TradeStatus = \"已到期\"; }}"); + Console.WriteLine($" 注意:SwapIncome 在 SaveSwapDeal 之后、SaveChanges 之前执行,Notional=0 不会被覆盖"); + + Assert.IsTrue(true, "副作用风险分析完成"); + } + finally + { + db?.Dispose(); + } + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs b/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs new file mode 100644 index 00000000..85869479 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapDealSettlementTest.cs @@ -0,0 +1,359 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// SwapDealService 手动结算(SwapIncome/SwapUnwind)内存单元测试 + /// ============================================================================ + /// 背景:SwapIncome/SwapUnwind 是写客户资金流水(ClientCashInCashOut)的核心入口, + /// 此前零单元测试(仅 DBRecording,CI 不跑)。本测试通过 7 个 virtual seam + /// 把 DB/事务/外部服务打桩,在纯内存下验证控制流、资金流水金额、持仓状态变更。 + /// + /// 命名规范说明(见《互换价格字段命名规范决策文档》): + /// 本测试引用现状字段(如 PosiGrossPrice/PosiNetPrice)时加对照注释, + /// 标明其真实含义与规范名,让测试可读、可作规范示范。 + /// - PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice + /// - PosiNetPrice 现状名,实为"期初全价含费"(非净价!),规范名 EntryDirtyFeePrice + /// ============================================================================ + [TestClass] + public class SwapDealSettlementTest + { + private const int SwapTradeId = 7700; + private static readonly DateTime ValueDate = new(2026, 6, 15); + private static readonly DateTime UnwindDate = new(2026, 6, 16); + + #region Stub + + /// + /// 继承 SwapDealService,override 7 个 seam,把 DB/事务/外部服务替换为内存收集器。 + /// 生产路径零改动(seam 生产实现 = 原逻辑),测试可纯内存运行。 + /// + private sealed class StubDealService : SwapDealService + { + private readonly trade _trade; + private readonly Dictionary _swapEvents; + private readonly Dictionary> _flowEventsByEventId; + public List<(double amount, string action, DateTime date)> ClientCashCalls = new(); + public List<(UnwindData data, int eventType, int clientCashId)> SaveSwapDealCalls = new(); + public int SaveAllChangesCount; + public int CloseReCheckCallCount; + + public StubDealService(trade td, + Dictionary swapEvents = null, + Dictionary> flowEventsByEventId = null) + : base(new OptUserInfo(0, nameof(SwapDealSettlementTest), OptUserFrom.UnitTest)) + { + _trade = td; + _swapEvents = swapEvents ?? new Dictionary(); + _flowEventsByEventId = flowEventsByEventId ?? new Dictionary>(); + } + + protected override trade FindTrade(int tradeId) => tradeId == _trade.id ? _trade : null; + + protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate) + { + ClientCashCalls.Add((amount, action, valueDate)); + return ClientCashCalls.Count; // 返回自增 id + } + + // 整体 override SaveSwapDeal:收集入参,规避内部 new SwapEventService 连库 + protected override long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) + { + SaveSwapDealCalls.Add((unwindData, eventType, clientCashId)); + return SaveSwapDealCalls.Count; // 返回自增 eventId + } + + // ApproveSwapTrade 查待审核事件:从内存字典取(key=eventType) + protected override swap_event FindSwapEvent(int tradeId, int eventType) + { + return _swapEvents.TryGetValue(eventType, out var evt) ? evt : null; + } + + // ApproveSwapTrade 查事件关联流水:从内存字典取 + protected override List FindFlowEventsByEventId(long eventId) + { + return _flowEventsByEventId.TryGetValue(eventId, out var list) ? list : new List(); + } + + // ApplySwapTrade 的前置校验:计数,不实际执行 + protected override void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck) + { + CloseReCheckCallCount++; + } + + protected override void SaveAllChanges() { SaveAllChangesCount++; } + protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行 + protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作 + protected override void TriggerRealtimeSwapPosition() { } // 空操作 + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = SwapTradeId, TradeNumber = "UT-SD-001", ClientId = 888888, + TradeType = "收益互换", StartDate = new DateTime(2026, 1, 5), + ExerciseDate = new DateTime(2026, 6, 14), // 已到期边界(SwapIncome 判断用) + TradeStatus = "确认成交", ValidState = "Valid", + Notional = 1000000, StockEqvNotional = 1000000, TradeAmount = 10000 + }; + } + + /// 构造结息/平仓的 UnwindData(金额由前端算好传入,后端直接用) + private static UnwindData CreateUnwindData(decimal swapRealizedPnL, decimal swapMarginRebatePnl = 0m, + decimal swapMarginAmount = 0m, int closeMethod = 0, decimal closePercent = 0m, + decimal closeQty = 0m, decimal closeNotionalValue = 0m, decimal positionQty = 0m) + { + return new UnwindData + { + SwapTradeId = SwapTradeId, + SwapRealizedPnL = swapRealizedPnL, + SwapMarginRebatePnl = swapMarginRebatePnl, + SwapMarginAmount = swapMarginAmount, + SwapCloseAmount = swapRealizedPnL, + CloseMethod = closeMethod, + ClosePercent = closePercent, + CloseQty = closeQty, + CloseNotionalValue = closeNotionalValue, + PositionQty = positionQty, + ValueDate = ValueDate, + UnwindDate = UnwindDate, + StartDate = new DateTime(2026, 1, 5) + }; + } + + #endregion + + // ================================================================ + // SD_001:SwapIncome 正常结息 —— 验证资金流水金额正确 + // ================================================================ + + /// + /// [SD_001] SwapIncome 正常结息:SwapRealizedPnL=1000 → 客户资金流水金额=-1000 + /// ------------------------------------------------------------ + /// 后端 SwapDealService.cs:1553 直接用前端传入的 SwapRealizedPnL 记账: + /// AddClientCash(td, -SwapRealizedPnL, 系统操作_互换, ValueDate) + /// 本测试锁定:资金流水金额 = -SwapRealizedPnL,事件类型 = 互换(3)。 + /// + [TestMethod] + public void SD_001_SwapIncome_正常结息_资金流水金额正确() + { + var td = CreateTrade(); + td.ExerciseDate = new DateTime(2026, 12, 31); // 未到期,不走"已到期"分支 + var service = new StubDealService(td); + var unwindData = CreateUnwindData(swapRealizedPnL: 1000m); + + service.SwapIncome(unwindData); + + Assert.AreEqual(1, service.ClientCashCalls.Count, "应生成1条资金流水(互换)"); + Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水金额 = -SwapRealizedPnL"); + Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action, "操作类型=系统操作_互换"); + Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次"); + Assert.AreEqual((int)SwapEventTypeEnum.互换, service.SaveSwapDealCalls[0].eventType, "事件类型=互换(3)"); + Console.WriteLine($"SD_001 通过:资金流水金额={service.ClientCashCalls[0].amount},事件类型=互换 ✅"); + } + + // ================================================================ + // SD_002:SwapIncome 含预付金返息 —— 两条资金流水 + // ================================================================ + + /// + /// [SD_002] SwapIncome 含预付金返息:SwapRealizedPnL=1000, SwapMarginRebatePnl=200 + /// → 生成2条资金流水(互换 + 预付金返息),金额分别为 -1000、-200 + /// 后端 SwapDealService.cs:1556 条件:SwapMarginRebatePnl != 0 时追加预付金返息流水。 + /// + [TestMethod] + public void SD_002_SwapIncome_含预付金返息_两条资金流水() + { + var td = CreateTrade(); + td.ExerciseDate = new DateTime(2026, 12, 31); + var service = new StubDealService(td); + var unwindData = CreateUnwindData(swapRealizedPnL: 1000m, swapMarginRebatePnl: 200m); + + service.SwapIncome(unwindData); + + Assert.AreEqual(2, service.ClientCashCalls.Count, "应生成2条资金流水(互换+预付金返息)"); + Assert.AreEqual(-1000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=互换金额 -SwapRealizedPnL"); + Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action); + Assert.AreEqual(-200.0, service.ClientCashCalls[1].amount, 0.001, "第2条=预付金返息 -SwapMarginRebatePnl"); + Assert.AreEqual(ClientCashInCashOut.系统操作_预付金返息, service.ClientCashCalls[1].action); + Console.WriteLine($"SD_002 通过:2条资金流水,互换={service.ClientCashCalls[0].amount},预付金返息={service.ClientCashCalls[1].amount} ✅"); + } + + // ================================================================ + // SD_003:SwapUnwind 全平仓 —— 持仓归零、资金流水、状态变更 + // ================================================================ + + /// + /// [SD_003] SwapUnwind 全平仓:ClosePercent=1 → TradeStatus=已平仓、持仓扣减、资金流水正确 + /// 后端 SwapDealService.cs SwapUnwind:全平时 TradeStatus=已平仓,StockEqvNotional/TradeAmount 扣减。 + /// + [TestMethod] + public void SD_003_SwapUnwind_正常平仓_资金流水与持仓状态正确() + { + var td = CreateTrade(); + var service = new StubDealService(td); + // 全平:ClosePercent=1, CloseQty=10000, CloseNotionalValue=1000000 + var unwindData = CreateUnwindData( + swapRealizedPnL: 5000m, swapMarginAmount: 0m, + closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m, + closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m); + + service.SwapUnwind(unwindData); + + // 资金流水:平仓费 = -SwapRealizedPnL + Assert.AreEqual(1, service.ClientCashCalls.Count, "全平无预付金时应1条资金流水"); + Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL"); + Assert.AreEqual(ClientCashInCashOut.系统操作_平仓费, service.ClientCashCalls[0].action); + // 持仓状态 + Assert.AreEqual("已平仓", td.TradeStatus, "全平仓 TradeStatus=已平仓"); + // 全平仓走"已平仓"分支,不设 HasPartialUnWind(仅部分平仓才设=1) + Assert.AreNotEqual(1, td.HasPartialUnWind, "全平仓不应设 HasPartialUnWind(仅部分平仓设=1)"); + // 持仓扣减:原 StockEqvNotional=1000000 - CloseNotionalValue=1000000 = 0 + Assert.AreEqual(0.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=0"); + Assert.AreEqual(0.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=0"); + // 事件类型 + Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓(2)"); + Console.WriteLine($"SD_003 通过:TradeStatus={td.TradeStatus},StockEqvNotional={td.StockEqvNotional} ✅"); + } + + // ================================================================ + // SD_004:DealFloatPosition 含费价重算正确(后端唯二真做计算的地方) + // ================================================================ + + /// + /// [SD_004] DealFloatPosition 含费价重算(SwapDealService.cs:1713-1725) + /// ------------------------------------------------------------ + /// 平仓事件重算三个字段(规范语义,见命名文档): + /// TradingAmountFeeAvg(ExitDirtyFeePrice)= TradingAmountAvg(ExitDirtyPrice) + TradingFeePending/CloseQty × shortRatio + /// TradingAmountNetFeeAvg(ExitCleanFeePrice)= TradingAmountNetAvg(ExitCleanPrice) + TradingFeePending/CloseQty × shortRatio + /// TradingAmount = TradingAmountAvg × CloseQty + /// 这是后端少数真正做计算(而非透传前端值)的地方,需锁住。 + /// + /// 手算:ExitDirtyPrice=1.02, TradingFeePending=50, CloseQty=1000, Long(多头,shortRatio=-1) + /// ExitDirtyFeePrice = 1.02 + 50/1000 × (-1) = 1.02 - 0.05 = 0.97 + /// ExitCleanFeePrice = 1.00 + 50/1000 × (-1) = 1.00 - 0.05 = 0.95 + /// TradingAmount = 1.02 × 1000 = 1020 + /// + [TestMethod] + public void SD_004_DealFloatPosition_含费价重算正确() + { + var td = CreateTrade(); + var service = new StubDealService(td); + + // 构造平仓事件(PositionType>0 触发重算) + var closeEvent = new swap_flow_event + { + EventType = (int)SwapEventTypeEnum.平仓, + PositionType = (int)PositionTypeFlag.Long, // 多头,shortRatio=-1 + // TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice + TradingAmountAvg = 1.02m, + // TradingAmountNetAvg 现状名,实为"期末净价不含费",规范名 ExitCleanPrice + TradingAmountNetAvg = 1.00m, + TradingFeePending = 50m, + }; + var unwindData = CreateUnwindData(swapRealizedPnL: 0m, closeQty: 1000m); + unwindData.FlowEvents.Add(closeEvent); + + service.SwapUnwind(unwindData); + + // ExitDirtyFeePrice(TradingAmountFeeAvg)= 1.02 + 50/1000×(-1) = 0.97 + Assert.AreEqual(0.97m, closeEvent.TradingAmountFeeAvg, 0.0001m, + $"TradingAmountFeeAvg(ExitDirtyFeePrice) 应=ExitDirtyPrice(1.02)+Fee/CloseQty×(-1)=0.97,实际={closeEvent.TradingAmountFeeAvg}"); + // ExitCleanFeePrice(TradingAmountNetFeeAvg)= 1.00 + 50/1000×(-1) = 0.95 + Assert.AreEqual(0.95m, closeEvent.TradingAmountNetFeeAvg ?? 0m, 0.0001m, + $"TradingAmountNetFeeAvg(ExitCleanFeePrice) 应=ExitCleanPrice(1.00)+Fee/CloseQty×(-1)=0.95,实际={closeEvent.TradingAmountNetFeeAvg}"); + // TradingAmount = ExitDirtyPrice × CloseQty = 1.02 × 1000 = 1020 + Assert.AreEqual(1020m, closeEvent.TradingAmount, 0.0001m, + $"TradingAmount 应=ExitDirtyPrice(1.02)×CloseQty(1000)=1020,实际={closeEvent.TradingAmount}"); + Console.WriteLine($"SD_004 通过:ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg},ExitCleanFeePrice={closeEvent.TradingAmountNetFeeAvg},TradingAmount={closeEvent.TradingAmount} ✅"); + } + + // ================================================================ + // SD_005:ApproveSwapTrade 审核通过 —— 反序列化事件、资金流水、持仓状态 + // ================================================================ + + /// + /// [SD_005] ApproveSwapTrade 审核通过全部平仓 + /// ------------------------------------------------------------ + /// 后端 SwapDealService.ApproveSwapTrade:从 swap_event.EventData 反序列化 UnwindData, + /// 据此生成资金流水 + 更新持仓状态。 + /// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario4,验证: + /// - SwapRealizedPnL 从事件反序列化正确(EventData JSON) + /// - 资金流水金额 = -SwapRealizedPnL + /// - 全平仓 → TradeStatus=已平仓 + /// + [TestMethod] + public void SD_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账() + { + var td = CreateTrade(); + // 构造待审核事件:EventData 里序列化了 UnwindData(含 SwapRealizedPnL=8000) + var unwindData = CreateUnwindData(swapRealizedPnL: 8000m, + closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 1m, + closeQty: 10000m, closeNotionalValue: 1000000m); + var swapEvent = new swap_event + { + id = 1, SwapTradeId = SwapTradeId, + EventType = (int)SwapEventTypeEnum.平仓, Invalid = false, + EventData = JsonConvert.SerializeObject(unwindData) + }; + var flowEvents = new Dictionary> + { + [1] = new List { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } } + }; + var service = new StubDealService(td, + swapEvents: new Dictionary { [(int)SwapEventTypeEnum.平仓] = swapEvent }, + flowEventsByEventId: flowEvents); + + service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓); + + // 资金流水:从反序列化的 SwapRealizedPnL(8000) 记账 → -8000 + Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水"); + Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL"); + // 持仓状态 + Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓"); + Console.WriteLine($"SD_005 通过:审核反序列化 SwapRealizedPnL=8000,资金流水={service.ClientCashCalls[0].amount},TradeStatus={td.TradeStatus} ✅"); + } + + // ================================================================ + // SD_006:ApplySwapTrade 提交审核 —— 前置校验 + 保存事件 + // ================================================================ + + /// + /// [SD_006] ApplySwapTrade 提交审核 + /// ------------------------------------------------------------ + /// 后端 SwapDealService.ApplySwapTrade:调 CloseReCheckSetTrade 前置校验 + SaveSwapDeal(approve=true)。 + /// 借鉴 testable 分支 SwapUnwindScenarioTest.Scenario5,验证: + /// - CloseReCheckSetTrade 被调用1次 + /// - SaveSwapDeal 以 approve=true 调用(事件类型正确) + /// - SwapRealizedPnL = SwapCloseAmount(ApplySwapTrade 内部赋值) + /// + [TestMethod] + public void SD_006_ApplySwapTrade_提交审核_前置校验与保存事件() + { + var td = CreateTrade(); + var service = new StubDealService(td); + // 前端提交时 SwapCloseAmount=6000(前端算好的总额),SwapRealizedPnL 初始可能为0 + var unwindData = CreateUnwindData(swapRealizedPnL: 0m); + unwindData.SwapCloseAmount = 6000m; // 模拟前端传入的平仓总额 + + service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓); + + // 前置校验被调用 + Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次"); + // SaveSwapDeal 以 approve=true 调用 + Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次"); + Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.SaveSwapDealCalls[0].eventType, "事件类型=平仓"); + // SwapRealizedPnL 应被赋值为 SwapCloseAmount(ApplySwapTrade 内部 cs:1631) + Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m, + "SwapRealizedPnL 应=SwapCloseAmount(6000)"); + Console.WriteLine($"SD_006 通过:CloseReCheck 调用{service.CloseReCheckCallCount}次,SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅"); + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapDividendGoldenRecordTest.cs b/UnitTestProject/Modules/SwapModule/SwapDividendGoldenRecordTest.cs new file mode 100644 index 00000000..bc9baef1 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapDividendGoldenRecordTest.cs @@ -0,0 +1,426 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换分红损益 - 黄金文件录制测试(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 }; + + /// + /// Step0:列出库中所有"带分红的互换交易",确认样本有效性。 + /// 打印 SwapTradeId / 分红合计 / 盯市合计 / eod快照数,供挑选样本。 + /// + /// 可直接运行:连不上测试库时返回 Inconclusive(不计入失败),不挡 CI; + /// 连得上时输出诊断表。这是日常排查"库里有啥分红交易"的入口。 + /// + [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(); + } + } + + /// + /// Step1:逐笔录制样本交易的完整快照(5表),并输出分红重复计算诊断。 + /// + /// 为何保留 [Ignore]:本方法有写文件副作用(落 golden JSON), + /// 不应随每次构建/CI 自动执行;只在需要"刷新 golden 种子"时手动触发。 + /// 运行方式(三选一): + /// - VS 测试资源管理器:选中本方法 → 右键 → 运行(VS 默认会跑被 Ignore 的,除非全局过滤) + /// - 命令行:dotnet test --filter "FullyQualifiedName~Step1_RecordSampleTrades" + /// - 临时:删掉本方法上的 [Ignore] 再跑,跑完恢复 + /// + [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 bondPayments = new List(); + 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}"); + } + + /// + /// 分红重复计算诊断:对照代码行号,把链路数据逐一算出来。 + /// 重复计算根因链路(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 次 + /// + private DiagnoseResult DiagnoseDividendDoubleCount( + int tradeId, + List flowEvents, + List eodPositions) + { + var r = new DiagnoseResult { SwapTradeId = tradeId }; + var lines = new List + { + $"--- 分红重复计算诊断 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(); + 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; + } + + /// + /// Step2:校验已录制 golden 文件的完整性(离线,不连库)。 + /// 确认每个 json 含 5 表数据、能正确反序列化。 + /// + [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(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()}"); + } + } + } + + /// + /// 分红黄金文件数据模型。5 表快照 + 诊断结论。 + /// 字段名沿用数据库实体类名,反序列化时类型一致。 + /// + 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; } + } + + /// + /// 重复计算诊断结果,随 golden 一起持久化,便于修复后对比。 + /// 拆解原理:每条平仓/互换事件的 MarkClosePnl = 价差成分 + 费成分(CloseFee) + 分红成分(DividendIn)。 + /// 盯市列(TdCloseMtmPnl=ΣMarkClosePnl) 含了分红成分一份,分红列(TdCloseDividend=ΣDividendIn) 又含一份, + /// 故"重复金额" = 进入盯市列的分红成分 = Σ(事件 DividendIn)。按 PositionId 分别拆解避免双向腿抵消。 + /// + public class DiagnoseResult + { + public int SwapTradeId { get; set; } + + /// "纯分红型"(价差成分≈0) 或 "混合型"(价差成分≠0)。基于 MarkClosePnl 是否含价差判定。 + public string 交易类型 { get; set; } + + // ===== 逐 PositionId 拆解 ===== + public List 逐持仓拆解 { get; set; } = new List(); + + /// 盯市列里被重复计入的分红成分合计(=修复后 RealizedPnl 应减少的金额)。 + public decimal 重复计入分红金额 { get; set; } + public decimal 最终累计盯市已实现 { get; set; } + public decimal 最终累计分红已实现 { get; set; } + public decimal 最终持仓层累计已实现 { get; set; } + + /// 若重复计入分红金额≠0,则重复计算成立。 + public bool 重复计算成立 { get; set; } + public string 结论 { get; set; } + public string Summary { get; set; } + } + + /// + /// 单个持仓腿(PositionId)的拆解结果。 + /// + public class PositionDiagnose + { + public long PositionId { get; set; } + /// 该腿盯市列合计 = Σ 事件 MarkClosePnl。 + public decimal 盯市列合计 { get; set; } + /// 盯市列里的价差成分 = Σ(MarkClosePnl - DividendIn - CloseFee)。 + public decimal 盯市价差成分 { get; set; } + /// 盯市列里的费成分 = Σ CloseFee。 + public decimal 盯市费成分 { get; set; } + /// 盯市列里的分红成分 = Σ DividendIn(这是被重复计入的部分)。 + public decimal 盯市分红成分 { get; set; } + /// 该腿分红列最终累计 RealizedDividend。 + public decimal 分红列累计 { get; set; } + /// 该腿被重复计入的分红 = 盯市分红成分。 + public decimal 重复计入分红 { get; set; } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs new file mode 100644 index 00000000..b01d7e01 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs @@ -0,0 +1,653 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换部分平仓后利息端/预付金默认盈亏偏大 - 录制/验证测试(TDD 红灯) + /// ============================================================================ + /// 背景: + /// 昨天收益结算(互换)→收盘→今天平仓,"预付金平仓盈亏"和"利息端平仓盈亏" + /// 默认值偏大。根因:CalcDailySimpleInterest(cs:771) 从 PosiStartDate 全程重算利息, + /// 只读 InterestProfitSum(待实现),不读 RealizedInterest(已实现),导致跨天重复计入。 + /// 同日去重(cs:435) 只覆盖当天、算尾跳过,跨天不生效。 + /// + /// TDD 红灯→绿灯: + /// 红灯(当前):找一笔多次操作的交易 → 模拟默认值计算 → 断言默认值 > 应计基数(待实现-已实现) + /// 绿灯(修复后):默认值 ≤ 应计基数 + /// + /// 运行方式:全部 [Ignore]+[TestCategory("DBRecording")],不进 CI。 + /// ============================================================================ + [TestClass] + public class SwapPartialUnwindInterestDefaultTest + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapPartialUnwindInterest"); + + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include, + DateFormatString = "yyyy-MM-ddTHH:mm:ss", + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + + /// + /// Step0:探查测试库,列出有"多次平仓/互换操作"的互换交易,供挑选样本。 + /// + /// 复现条件:一笔交易 swap_flow_event 里 EventType IN(平仓,互换,自动互换) 且 DataState=完成 + /// 的记录 ≥ 2 条(说明做过多次操作),且有 eod_swap_position(已收盘)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0_ListMultiOperationTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + // 找有多次操作的交易 + var multiOpTrades = db.swap_flow_event + .Where(x => (x.EventType == (int)SwapFlowEventTypeEnum.平仓 + || x.EventType == (int)SwapFlowEventTypeEnum.互换 + || x.EventType == (int)SwapFlowEventTypeEnum.自动互换) + && x.DataState == (int)SwapFlowDateStateEnum.完成) + .AsEnumerable() + .GroupBy(x => x.SwapTradeId) + .Where(g => g.Count() >= 2) + .Select(g => new + { + SwapTradeId = g.Key, + 操作次数 = g.Count(), + 平仓次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓), + 互换次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换), + 最早操作日 = g.Min(x => x.EventDate), + 最晚操作日 = g.Max(x => x.EventDate), + 利息盈亏合计 = g.Sum(x => x.InterestClosePnL), + EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key) + }) + .Where(t => t.EodCount > 0) + .OrderByDescending(t => t.操作次数) + .Take(30) + .ToList(); + + Console.WriteLine($"=== 多次操作的互换交易数: {multiOpTrades.Count} ===\n"); + Console.WriteLine($"{"TradeId",8} {"操作",6} {"平仓",6} {"互换",6} {"eod",6} {"利息盈亏合计",16} {"操作日期范围",-24}"); + foreach (var t in multiOpTrades) + { + string dateRange = $"{t.最早操作日:yyyy-MM-dd}~{t.最晚操作日:yyyy-MM-dd}"; + Console.WriteLine($"{t.SwapTradeId,8} {t.操作次数,6} {t.平仓次数,6} {t.互换次数,6} {t.EodCount,6} {t.利息盈亏合计,16:F2} {dateRange,-24}"); + } + + if (multiOpTrades.Count == 0) + { + Assert.Inconclusive("无多次操作的样本(需有≥2次平仓/互换且有eod的交易)。"); + } + Assert.IsTrue(multiOpTrades.Count > 0); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step0e:探查"单利 + 有互换历史"的样本,用于验证单利路径是否也需要 consumedInterest 扣除。 + /// + /// 复利路径(c6adb3bb)已修,单利路径(CalcDailySimpleInterest)未修。 + /// 需找:单利利息腿 + 该腿有历史互换/自动互换事件(InterestAmount≠0) + 有eod。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0e_ListSimpleInterestSwapTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + // 找单利利息腿(InterestType=0=单利)且有历史互换事件的交易 + var simplePositions = db.swap_position + .Where(x => !x.Invalid && x.InterestDirection > 0 && x.InterestType == (int)InterestTypeEnum.单利) + .Select(x => new { x.SwapTradeId, x.id, x.InterestMode, x.InterestPrincipalFix }) + .ToList(); + + Console.WriteLine($"=== 单利利息腿持仓: {simplePositions.Count} 条 ===\n"); + + // 关联历史互换事件(InterestAmount≠0 说明有实际利息结算) + var tradeIds = simplePositions.Select(x => x.SwapTradeId).Distinct().ToList(); + var swapEvents = db.swap_flow_event + .Where(x => tradeIds.Contains(x.SwapTradeId) + && (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换) + && x.DataState == (int)SwapFlowDateStateEnum.完成 + && x.InterestAmount != 0) + .ToList(); + + var byTrade = simplePositions + .Where(p => swapEvents.Any(s => s.SwapTradeId == p.SwapTradeId && s.PositionId == p.id)) + .GroupBy(p => p.SwapTradeId) + .Select(g => new + { + SwapTradeId = g.Key, + 单利腿数 = g.Count(), + 利息模式 = string.Join("|", g.Select(x => ((InterestModeEnum)x.InterestMode).ToString())), + 历史互换事件数 = swapEvents.Count(s => s.SwapTradeId == g.Key), + 历史利息合计 = swapEvents.Where(s => s.SwapTradeId == g.Key).Sum(s => s.InterestAmount), + EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key) + }) + .Where(t => t.EodCount > 0) + .OrderByDescending(t => Math.Abs(t.历史利息合计)) + .Take(20) + .ToList(); + + Console.WriteLine($"{"TradeId",8} {"单利腿",6} {"历史互换",8} {"历史利息合计",16} {"eod",6} {"利息模式",-20}"); + foreach (var t in byTrade) + { + Console.WriteLine($"{t.SwapTradeId,8} {t.单利腿数,6} {t.历史互换事件数,8} {t.历史利息合计,16:F4} {t.EodCount,6} {t.利息模式,-20}"); + } + + if (byTrade.Count == 0) + { + Assert.Inconclusive("无单利+有互换历史的样本。"); + } + Assert.IsTrue(byTrade.Count > 0); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step1_SimpleInterestRedTest:单利路径红灯测试。 + /// + /// 复利路径已由 c6adb3bb 修复(consumedInterest 扣除),但单利路径(CalcDailySimpleInterest) + /// 未加该扣除。本测试坐实:单利利息腿在"有历史互换结清后再平仓"时,默认值仍偏大。 + /// + /// 红灯(当前):默认值包含历史已结利息(consumedInterest),偏大 + /// 绿灯(修复后):单利路径也扣除 consumedInterest,默认值正确 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step1_SimpleInterestRedTest() + { + int tradeId = SimpleInterestSampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 单利路径红灯测试 SwapTradeId={tradeId} =====\n"); + + // 1. 确认该交易的单利利息腿 + var simplePositions = db.swap_position + .Where(x => x.SwapTradeId == tradeId && !x.Invalid + && x.InterestDirection > 0 + && x.InterestType == (int)InterestTypeEnum.单利) + .ToList(); + Console.WriteLine($"[1] 单利利息腿: {simplePositions.Count} 条"); + foreach (var p in simplePositions) + { + Console.WriteLine($" PositionId={p.id} Mode={((InterestModeEnum)p.InterestMode)} PrincipalFix={p.InterestPrincipalFix}"); + } + + // 2. 找最近 eod 日期,作为"模拟平仓日" + var latestEodDate = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId) + .Max(x => (DateTime?)x.ValueDate); + if (latestEodDate == null) + { + Assert.Inconclusive($"交易 {tradeId} 无 eod 数据"); + return; + } + // 用 eod 后一天作为模拟平仓日 + var testDate = latestEodDate.Value.AddDays(1); + Console.WriteLine($"\n[2] 模拟平仓日: {testDate:yyyy-MM-dd}(eod最近: {latestEodDate:yyyy-MM-dd})"); + + // 3. 调用真实 GetUnwindInterests(与前端平仓页相同路径) + var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); + var service = new SwapDealService(userInfo); + var defaults = service.GetUnwindInterests( + testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓); + + // 4. 对每个单利腿,对比"默认值"vs"应计基数(待实现-已结利息)" + Console.WriteLine($"\n[3] 单利路径诊断:默认值 vs 应计基数"); + Console.WriteLine($" {"PositionId",10} {"InterestMode",14} {"方向",6} {"默认ClosePnL",14} {"InterestAmt",14} {"eod待实现IPS",14} {"历史已结CI",14} {"ratio",6} {"应计(IPS-CI)",14} {"偏大量",14} {"红灯",6}"); + + int redCount = 0; + foreach (var d in defaults.Where(x => x.InterestDirection > 0)) + { + var pos = simplePositions.FirstOrDefault(x => x.id == d.PositionId); + if (pos == null) continue; // 跳过非单利腿 + + // eod 待实现 + var preEod = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId && x.PositionId == d.PositionId && x.ValueDate < testDate) + .OrderByDescending(x => x.ValueDate).FirstOrDefault(); + decimal ips = preEod?.InterestProfitSum ?? 0; + + // 历史已结利息(复利路径用的 GetConsumedInterest,单利路径没用) + decimal ci = service.GetConsumedInterest(tradeId, d.PositionId, testDate); + + // InterestClosePnL = InterestAmount × interestRatio(方向系数) + // interestRatio = InterestDirection==收取(1) ? 1 : -1 + decimal interestRatio = d.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; + // 应计基数 = (待实现 - 已结) × ratio(与 InterestClosePnL 同口径) + decimal expected = (ips - ci) * interestRatio; + decimal actual = d.InterestClosePnL; + decimal diff = actual - expected; + bool isRed = Math.Abs(ci) > 0.01m && Math.Abs(diff) > Math.Abs(ci) * 0.5m; + + if (isRed) redCount++; + string modeName = ((InterestModeEnum)d.InterestMode).ToString(); + string dirName = ((SwapDirectionEnum)d.InterestDirection).ToString(); + Console.WriteLine($" {d.PositionId,10} {modeName,14} {dirName,6} {actual,14:F4} {d.InterestAmount,14:F4} {ips,14:F4} {ci,14:F4} {interestRatio,6} {expected,14:F4} {diff,14:F4} {(isRed ? "⚠红灯" : "绿灯"),6}"); + } + + Console.WriteLine($"\n[结论]"); + if (redCount > 0) + { + Console.WriteLine($" ⚠ 单利路径仍存在偏大:{redCount} 条单利腿默认值含历史已结利息。"); + Console.WriteLine($" 根因:CalcDailySimpleInterest 起点InterestProfitSum在互换后未归零。"); + Console.WriteLine($" 注意:不能简单减consumedInterest(会双重扣减,导致应为1天利息变0)。"); + Console.WriteLine($" 正确方案:让InterestProfitSum在互换结清后归零(eod层方案B)。"); + } + else + { + Console.WriteLine($" 单利路径未检测到偏大。"); + } + + // 红灯断言:单利路径应存在偏大(待正确修复方案) + Assert.IsTrue(redCount > 0, + "红灯:单利路径应存在默认值偏大。待正确修复(InterestProfitSum归零)后反转。"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。 + /// + private int SimpleInterestSampleTradeId => 1813; + + /// + /// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。 + /// + /// 核心逻辑(不改数据,纯查询): + /// - 默认值计算读 InterestProfitSum(待实现),不读 RealizedInterest(已实现) + /// - 若某持仓 InterestProfitSum >> 0 且已有多次操作(RealizedInterest >> 0), + /// 说明下次平仓默认值会基于"全程待实现"重算,重复计入已实现部分 + /// - 真正应计基数 = InterestProfitSum - RealizedInterest(剩余未实现) + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0b_DiagnoseSingleTradeInterestDuplication() + { + int tradeId = SampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 诊断 SwapTradeId={tradeId} 利息端默认值重复计入 =====\n"); + + // 1. 该交易的利息腿(InterestDirection>0)最新 eod 快照 + var latestEodDate = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId) + .Max(x => (DateTime?)x.ValueDate); + if (latestEodDate == null) + { + Assert.Inconclusive($"交易 {tradeId} 无 eod 数据"); + return; + } + + var interestEods = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId + && x.ValueDate == latestEodDate + && x.InterestDirection > 0) + .OrderBy(x => x.PositionId) + .ToList(); + + Console.WriteLine($"[1] 最新eod({latestEodDate:yyyy-MM-dd})利息腿持仓: {interestEods.Count} 条\n"); + Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"待实现InterestProfitSum",22} {"已实现RealizedInterest",22} {"应计基数(待-已)",18} {"重复风险",10}"); + int riskCount = 0; + foreach (var e in interestEods) + { + decimal base_ = e.InterestProfitSum - e.RealizedInterest; + bool risk = e.InterestProfitSum != 0 && e.RealizedInterest != 0 + && Math.Abs(e.InterestProfitSum) > Math.Abs(base_); + if (risk) riskCount++; + string modeName = ((InterestModeEnum)(e.InterestMode)).ToString(); + Console.WriteLine($"{e.PositionId,12} {modeName,12} {e.InterestProfitSum,22:F4} {e.RealizedInterest,22:F4} {base_,18:F4} {(risk ? "⚠有" : "无"),10}"); + } + + // 2. 历史操作记录(看每次利息盈亏) + var history = db.swap_flow_event + .Where(x => x.SwapTradeId == tradeId + && x.DataState == (int)SwapFlowDateStateEnum.完成 + && (x.EventType == (int)SwapFlowEventTypeEnum.平仓 + || x.EventType == (int)SwapFlowEventTypeEnum.互换 + || x.EventType == (int)SwapFlowEventTypeEnum.自动互换)) + .OrderBy(x => x.EventDate).ThenBy(x => x.id) + .ToList(); + + Console.WriteLine($"\n[2] 历史操作记录: {history.Count} 条\n"); + Console.WriteLine($"{"id",8} {"EventDate",12} {"EventType",10} {"PositionId",12} {"InterestClosePnL",18} {"InterestAmount",16}"); + foreach (var h in history) + { + string etName = ((SwapFlowEventTypeEnum)h.EventType).ToString(); + Console.WriteLine($"{h.id,8} {h.EventDate:yyyy-MM-dd} {etName,10} {h.PositionId,12} {h.InterestClosePnL,18:F4} {h.InterestAmount,16:F4}"); + } + + // 3. 诊断结论 + Console.WriteLine($"\n[结论]"); + if (riskCount > 0) + { + Console.WriteLine($"⚠ 有 {riskCount} 条利息腿存在重复计入风险:"); + Console.WriteLine($" InterestProfitSum(待实现) 被用作下次平仓默认值计算基数(cs:774),"); + Console.WriteLine($" 但它没有扣除 RealizedInterest(已实现)。"); + Console.WriteLine($" → 部分平仓后再平仓,默认值会偏大(含已实现部分)。"); + } + else + { + Console.WriteLine($" 未检测到重复计入风险(可能 InterestProfitSum 或 RealizedInterest 为0)。"); + } + + Assert.IsTrue(interestEods.Count > 0, "应有利息腿持仓"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// 样本交易ID。1889 = GLMS-20260616-0004,29号收益结算+收盘,30号平仓。 + /// + private int SampleTradeId => 1889; + + /// + /// Step0c:精确诊断——调用真实的 GetUnwindInterests 拿默认值,对比 eod 应计,定位偏差。 + /// + /// 这是最直接的验证:用平仓日的参数调 GetUnwindInterests(与前端拿默认值完全相同的路径), + /// 看返回的 InterestClosePnL 是否包含了"之前已通过互换实现的部分"。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0c_VerifyDefaultViaRealService() + { + int tradeId = SampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + // 找最后一次平仓事件,用它的参数模拟"打开平仓页" + var lastClose = db.swap_flow_event + .Where(x => x.SwapTradeId == tradeId + && x.EventType == (int)SwapFlowEventTypeEnum.平仓 + && x.DataState == (int)SwapFlowDateStateEnum.完成) + .OrderByDescending(x => x.EventDate) + .FirstOrDefault(); + if (lastClose == null) + { + Assert.Inconclusive($"交易 {tradeId} 无平仓记录"); + return; + } + + Console.WriteLine($"===== 调用 GetUnwindInterests 验证 SwapTradeId={tradeId} ====="); + Console.WriteLine($"模拟平仓日: EventDate={lastClose.EventDate:yyyy-MM-dd} UnwindDate={lastClose.UnwindDate:yyyy-MM-dd}\n"); + + // 该交易平仓前的最近 eod(用于对比) + var preEodDate = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId && x.ValueDate < lastClose.UnwindDate) + .Max(x => (DateTime?)x.ValueDate); + var preEodInterests = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId && x.ValueDate == preEodDate && x.InterestDirection > 0) + .ToList(); + + Console.WriteLine($"[平仓前最近eod: {preEodDate:yyyy-MM-dd}]"); + Console.WriteLine($"{"PositionId",12} {"InterestProfitSum(待实现起点)",28} {"RealizedInterest(已实现)",24}"); + foreach (var e in preEodInterests) + { + Console.WriteLine($"{e.PositionId,12} {e.InterestProfitSum,28:F4} {e.RealizedInterest,24:F4}"); + } + + // 调用真实服务(与前端 GetUnwindInterestList 完全相同的路径) + var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); + var service = new SwapDealService(userInfo); + // closePercent 取实际平仓的(从历史 flow_event 推断:InterestPrincipal / PosiNotionalValue) + decimal closePercent = 1m; // 先用全平测试 + var defaults = service.GetUnwindInterests( + lastClose.EventDate, lastClose.UnwindDate.Value, tradeId, closePercent, + (int)SwapEventTypeEnum.平仓); + + Console.WriteLine($"\n[GetUnwindInterests 返回的默认值] closePercent={closePercent}"); + Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"默认InterestClosePnL",22} {"默认InterestAmount",20} {"实际历史InterestClosePnL",24}"); + foreach (var d in defaults.Where(x => x.InterestDirection > 0)) + { + var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId + && x.PositionId == d.PositionId && x.id == lastClose.id); + string modeName = ((InterestModeEnum)d.InterestMode).ToString(); + Console.WriteLine($"{d.PositionId,12} {modeName,12} {d.InterestClosePnL,22:F4} {d.InterestAmount,20:F4} {hist?.InterestClosePnL ?? 0,24:F4}"); + } + + // 诊断:默认值 vs 历史实际值 的差异 + Console.WriteLine($"\n[诊断]"); + bool hasDiscrepancy = false; + foreach (var d in defaults.Where(x => x.InterestDirection > 0)) + { + var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId + && x.PositionId == d.PositionId && x.id == lastClose.id); + if (hist != null && Math.Abs(d.InterestClosePnL - hist.InterestClosePnL) > 0.01m) + { + Console.WriteLine($" PositionId={d.PositionId}: 默认值={d.InterestClosePnL:F4} vs 历史={hist.InterestClosePnL:F4} 差异={d.InterestClosePnL - hist.InterestClosePnL:F4}"); + hasDiscrepancy = true; + } + } + if (hasDiscrepancy) + { + Console.WriteLine($" ⚠ 默认值与历史实际值有差异(可能是重算口径变化或bug)"); + } + else + { + Console.WriteLine($" 默认值与历史实际值一致(该样本未复现偏差)"); + } + + Assert.IsTrue(defaults.Count > 0, "应返回利息腿默认值"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step0d:针对 1889(GLMS-20260616-0004)的全面诊断。 + /// + /// 场景:29号收益结算(互换)+收盘 → 30号平仓。 + /// 测试环境会不断回退复用同一笔交易,需甄别。 + /// + /// 本方法一次性查清: + /// 1. swap_event 全历史(含回退 EventType=5),甄别哪些是回退后的有效操作 + /// 2. swap_flow_event 全历史(含 DataState≠完成的废弃事件) + /// 3. eod_swap_position 按日期序列,看 InterestProfitSum/RealizedInterest 逐日演变 + /// 4. 调 GetUnwindInterests 拿30号平仓默认值,对比29号互换已实现的部分 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0d_DiagnoseTrade1889_FullTimeline() + { + int tradeId = SampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 全面诊断 SwapTradeId={tradeId} =====\n"); + + // 1. swap_event 全历史(含回退/删除) + var allEvents = db.swap_event + .Where(x => x.SwapTradeId == tradeId) + .OrderBy(x => x.id) + .ToList(); + Console.WriteLine($"[1] swap_event 全历史: {allEvents.Count} 条(甄别回退)"); + Console.WriteLine($" 仅显示 Invalid=False(有效)的事件:"); + var validEvents = allEvents.Where(x => !x.Invalid).ToList(); + Console.WriteLine($" {"id",8} {"EventType",10} {"ValueDate",12} {"ClientCashId",12} {"EventReason",-20}"); + foreach (var e in validEvents) + { + string etName = ((SwapEventTypeEnum)e.EventType).ToString(); + Console.WriteLine($" {e.id,8} {etName,10} {e.ValueDate:yyyy-MM-dd} {e.ClientCashId,12} {(e.EventReason ?? ""),-20}"); + } + Console.WriteLine($" (另有 {allEvents.Count(x => x.Invalid)} 条 Invalid=True 的回退/历史事件,已隐藏)"); + + // 2. swap_flow_event 全历史(仅完成状态,过滤废弃) + var allFlowEvents = db.swap_flow_event + .Where(x => x.SwapTradeId == tradeId) + .OrderBy(x => x.id) + .ToList(); + var validFlowEventsAll = allFlowEvents.Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成).ToList(); + Console.WriteLine($"\n[2] swap_flow_event 完成状态: {validFlowEventsAll.Count} 条(共{allFlowEvents.Count}条,已隐藏{allFlowEvents.Count - validFlowEventsAll.Count}条废弃)"); + Console.WriteLine($" {"id",8} {"EventDate",12} {"UnwindDate",12} {"EventType",10} {"PositionId",10} {"InterestClosePnL",18} {"InterestAmount",16} {"MarkClosePnl",14}"); + foreach (var f in validFlowEventsAll) + { + string etName = ((SwapFlowEventTypeEnum)f.EventType).ToString(); + Console.WriteLine($" {f.id,8} {f.EventDate:yyyy-MM-dd} {f.UnwindDate?.ToString("yyyy-MM-dd") ?? "-",-12} {etName,10} {f.PositionId,10} {f.InterestClosePnL,18:F4} {f.InterestAmount,16:F4} {f.MarkClosePnl,14:F4}"); + } + + // 3. eod_swap_position 按日期序列(利息腿),看 InterestProfitSum/RealizedInterest 演变 + var eodTimeline = db.eod_swap_position + .Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0) + .OrderBy(x => x.ValueDate).ThenBy(x => x.PositionId) + .ToList(); + Console.WriteLine($"\n[3] eod_swap_position 利息腿按日序列: {eodTimeline.Count} 条"); + Console.WriteLine($"{"ValueDate",12} {"PositionId",10} {"InterestProfitSum",18} {"RealizedInterest",18} {"TdCloseInterest",16} {"InterestIncomeSum",18}"); + foreach (var e in eodTimeline) + { + Console.WriteLine($"{e.ValueDate:yyyy-MM-dd} {e.PositionId,10} {e.InterestProfitSum,18:F4} {e.RealizedInterest,18:F4} {e.TdCloseInterest,16:F4} {e.InterestIncomeSum,18:F4}"); + } + + // 4. 甄别:找出有效的 29号互换 和 30号平仓 + var validFlowEvents = allFlowEvents + .Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成) + .OrderBy(x => x.EventDate).ThenBy(x => x.id) + .ToList(); + var swapOn29 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 29) + && (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换)).ToList(); + var closeOn30 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 30) + && x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); + + Console.WriteLine($"\n[4] 关键操作甄别(DataState=完成)"); + Console.WriteLine($" 29号互换/自动互换: {swapOn29.Count} 条"); + foreach (var s in swapOn29) + Console.WriteLine($" id={s.id} PositionId={s.PositionId} InterestClosePnL={s.InterestClosePnL:F4} InterestAmount={s.InterestAmount:F4}"); + Console.WriteLine($" 30号平仓: {closeOn30.Count} 条"); + foreach (var c in closeOn30) + Console.WriteLine($" id={c.id} PositionId={c.PositionId} InterestClosePnL={c.InterestClosePnL:F4} InterestAmount={c.InterestAmount:F4}"); + + // 5. 模拟"打开平仓页"——分别测 6-29/6-30/7-1 三天,对比默认值变化 + Console.WriteLine($"\n[5] 调 GetUnwindInterests 模拟打开平仓页(6-29/6-30/7-1 三天对比)"); + // 先查利息腿的计息类型(单利/复利),判断走哪个修复路径 + var interestPositions = DbContextFactory.GetYLDbContext().swap_position + .Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0 && !x.Invalid).ToList(); + foreach (var p in interestPositions) + { + Console.WriteLine($" PositionId={p.id} InterestMode={((InterestModeEnum)p.InterestMode)} InterestType={((InterestTypeEnum)p.InterestType)}"); + } + var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); + var service = new SwapDealService(userInfo); + var testDates = new[] { + new DateTime(2026, 6, 29), + new DateTime(2026, 6, 30), + new DateTime(2026, 7, 1), + }; + + Console.WriteLine($" {"日期",12} {"PositionId",10} {"InterestMode",14} {"默认InterestClosePnL",22} {"eod待实现IPS",14} {"eod已实现RI",14} {"Δ默认-待实现",14}"); + foreach (var testDate in testDates) + { + var defaults = service.GetUnwindInterests( + testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓); + + foreach (var d in defaults.Where(x => x.InterestDirection > 0)) + { + // 找该日期前最近的 eod + var preEod = eodTimeline.Where(x => x.PositionId == d.PositionId && x.ValueDate < testDate) + .OrderByDescending(x => x.ValueDate).FirstOrDefault(); + decimal ips = preEod?.InterestProfitSum ?? 0; + decimal ri = preEod?.RealizedInterest ?? 0; + decimal delta = d.InterestClosePnL - ips; + string modeName = ((InterestModeEnum)d.InterestMode).ToString(); + string preEodDate = preEod?.ValueDate.ToString("MM-dd") ?? "无"; + Console.WriteLine($" {testDate:yyyy-MM-dd} {d.PositionId,10} {modeName,14} {d.InterestClosePnL,22:F4} {ips,14:F4}({preEodDate}) {ri,14:F4} {delta,14:F4}"); + } + } + + // 6. 核心诊断 + Console.WriteLine($"\n[6] 核心诊断"); + Console.WriteLine($" 关键观察:29号互换已实现 77.26,看 eod 的 InterestProfitSum(待实现) 是否扣减了已实现部分"); + var eod29 = eodTimeline.Where(x => x.ValueDate == new DateTime(2026, 6, 29)).ToList(); + foreach (var e in eod29) + { + Console.WriteLine($" PositionId={e.PositionId} 6-29 eod:"); + Console.WriteLine($" InterestProfitSum(待实现) = {e.InterestProfitSum:F4}"); + Console.WriteLine($" RealizedInterest(已实现) = {e.RealizedInterest:F4}"); + Console.WriteLine($" TdCloseInterest(当日实现) = {e.TdCloseInterest:F4}"); + if (e.InterestProfitSum != 0 && e.RealizedInterest != 0 && Math.Abs(e.InterestProfitSum - e.RealizedInterest) < 0.1m) + { + Console.WriteLine($" ⚠ 待实现({e.InterestProfitSum:F4}) ≈ 已实现({e.RealizedInterest:F4}) → 互换结清后待实现没归零!"); + Console.WriteLine($" → 导致后续平仓默认值仍基于待实现(77.26)算,偏大"); + } + } + Console.WriteLine($"\n 用户反馈:6-29看平仓默认=0(正确,因为当天还没收盘/互换),6-30和7-1有问题"); + Console.WriteLine($" 根因:29号收盘后 InterestProfitSum 没扣减已实现的 77.26(仍=77.26),"); + Console.WriteLine($" 所以后续平仓默认值 = 77.26(应已归零的待实现) + 增量 → 偏大"); + + Assert.IsTrue(allEvents.Count > 0); + } + finally + { + db?.Dispose(); + } + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs b/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs new file mode 100644 index 00000000..fd5f89de --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapReEodDeleteManualCashRecordTest.cs @@ -0,0 +1,527 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换重收盘误删"手动互换"资金记录 - 录制/验证测试(TDD 红灯) + /// ============================================================================ + /// 背景: + /// SwapTradeBaseService.ClearSwapPositions 第398-409行按 Action 字符串模糊删除 + /// ClientCashInCashOut,而"系统操作-互换"这个 Action 手动互换和自动互换共用, + /// 且 ClientCashInCashOut 无来源标记字段 → 重收盘会把用户手动互换的资金记录一并删掉。 + /// 引入点:commit 44e89726(2026-05-14)把 系统操作_互换 加进删除 Action 列表。 + /// + /// TDD 红灯→绿灯: + /// 红灯(当前):录制一笔"有手动互换"的交易 → 模拟重收盘 → 断言手动资金记录被删(坐实 bug) + /// 绿灯(修复后):同测试断言通过(手动记录保留,自动互换记录正确清理) + /// + /// 运行方式: + /// 全部标 [Ignore]+[TestCategory("DBRecording")],不进 CI。 + /// 手动执行:vstest.console.exe UnitTestProject.dll /TestCaseFilter:"TestCategory=DBRecording" + /// ============================================================================ + [TestClass] + public class SwapReEodDeleteManualCashRecordTest + { + private static readonly string GoldenDir = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapReEodDeleteCash"); + + /// + /// Step0:探查测试库,列出所有"有手动互换操作"的互换交易,供挑选样本。 + /// + /// 筛选条件(同时满足才是有效复现样本): + /// 1. swap_event 存在 EventType=互换(3) 且 ClientCashId>0 的记录(手动互换且生成了资金记录) + /// 2. 该 ClientCashId 在 ClientCashInCashOut 中真实存在(未被删) + /// 3. 该交易有 eod_swap 记录(已收盘过,才能"重收盘") + /// + /// 连不上测试库时 Inconclusive(CI 无 DB 环境正常跳过)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0_ListManualSwapTrades() + { + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + // 找所有手动互换事件(EventType=互换=3,且关联了资金记录) + var manualEvents = db.swap_event + .Where(x => x.EventType == (int)SwapEventTypeEnum.互换 + && x.ClientCashId > 0 + && !x.Invalid) + .ToList(); + + Console.WriteLine($"=== 手动互换事件(EventType=3, ClientCashId>0)数: {manualEvents.Count} ===\n"); + + // 按交易分组,附加资金记录和 eod 信息 + var byTrade = manualEvents + .GroupBy(x => x.SwapTradeId) + .Select(g => + { + var cashIds = g.Select(x => x.ClientCashId).Distinct().ToList(); + var cashRecords = db.ClientCashInCashOut + .Where(c => cashIds.Contains(c.id)).ToList(); + var eodCount = db.eod_swap.Count(e => e.SwapTradeId == g.Key); + var autoEvents = db.swap_event.Count(x => x.SwapTradeId == g.Key + && x.EventType == (int)SwapEventTypeEnum.自动互换 && !x.Invalid); + return new + { + SwapTradeId = g.Key, + ManualEventCount = g.Count(), + CashIds = cashIds, + CashRecordsFound = cashRecords.Count, + CashAction = cashRecords.Select(c => c.Action).Distinct().ToList(), + CashAmounts = cashRecords.Select(c => c.Money).ToList(), + CashHappenDates = cashRecords.Select(c => c.HappenDate).ToList(), + EodSwapCount = eodCount, + AutoSwapEventCount = autoEvents, + // 关键:事件日期范围(重收盘 valueDate <= 此日期会触发删除) + MinEventDate = g.Min(x => x.ValueDate), + MaxEventDate = g.Max(x => x.ValueDate) + }; + }) + .OrderByDescending(t => t.EodSwapCount > 0) // 优先有eod的(可重收盘) + .ThenByDescending(t => t.ManualEventCount) + .ToList(); + + Console.WriteLine($"{"TradeId",8} {"手动事件",8} {"资金记录",8} {"Action",-20} {"金额",14} {"eod",6} {"自动互换",8} {"事件日期范围",-24} {"可复现",6}"); + int reproducible = 0; + foreach (var t in byTrade.Take(30)) + { + bool canReproduce = t.EodSwapCount > 0 && t.CashRecordsFound > 0; + if (canReproduce) reproducible++; + string actionStr = string.Join("|", t.CashAction); + string amountStr = t.CashAmounts.Any() ? string.Join("|", t.CashAmounts.Select(m => $"{m:F2}")) : "-"; + string dateRange = $"{t.MinEventDate:yyyy-MM-dd}~{t.MaxEventDate:yyyy-MM-dd}"; + Console.WriteLine($"{t.SwapTradeId,8} {t.ManualEventCount,8} {t.CashRecordsFound,8} {actionStr,-20} {amountStr,14} {t.EodSwapCount,6} {t.AutoSwapEventCount,8} {dateRange,-24} {(canReproduce ? "✓" : "✗"),6}"); + } + + Console.WriteLine($"\n=== 可复现样本数(有eod+有资金记录): {reproducible} ==="); + if (reproducible == 0) + { + Assert.Inconclusive("无可复现样本(需要有 eod + 手动互换资金记录的交易)。请先在测试库构造数据。"); + } + Assert.IsTrue(reproducible > 0, "应存在可复现样本"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// Step0b:对单个候选样本做详细诊断,确认"重收盘删除条件"确实会命中手动互换资金记录。 + /// + /// 核心验证(不改任何数据,纯查询):模拟 ClearSwapPositions 第400-404行的删除条件, + /// 看会命中哪些 ClientCashInCashOut 记录,逐条标注它是"手动互换"还是"自动互换"产生的。 + /// 如果命中列表里有手动互换的记录 → bug 坐实(红灯前置证据)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step0b_DiagnoseSingleTradeDeleteCondition() + { + // 候选样本(从 Step0 输出中挑选):1903=最新,有自动互换,结构完整 + int tradeId = SampleTradeId; + + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + + try + { + Console.WriteLine($"===== 诊断 SwapTradeId={tradeId} 的资金记录删除命中情况 =====\n"); + + // 1. 该交易全部资金记录(互换/预付金相关) + var allCashRecords = db.ClientCashInCashOut + .Where(x => x.TradeId == tradeId + && (x.Action == ClientCashInCashOut.系统操作_互换 + || x.Action == ClientCashInCashOut.系统操作_预付金返息)) + .OrderBy(x => x.HappenDate).ThenBy(x => x.id) + .ToList(); + Console.WriteLine($"[1] 该交易全部互换/预付金资金记录: {allCashRecords.Count} 条"); + foreach (var c in allCashRecords) + { + Console.WriteLine($" id={c.id} Action={c.Action} Money={c.Money:F2} HappenDate={c.HappenDate:yyyy-MM-dd} OptName={c.OptName} CreateDate={c.CreateDate:yyyy-MM-dd HH:mm}"); + } + + // 2. 该交易全部 swap_event(区分手动互换 vs 自动互换) + var allEvents = db.swap_event + .Where(x => x.SwapTradeId == tradeId && !x.Invalid) + .OrderBy(x => x.ValueDate).ThenBy(x => x.id) + .ToList(); + Console.WriteLine($"\n[2] 该交易全部 swap_event: {allEvents.Count} 条"); + foreach (var e in allEvents) + { + string etName = ((SwapEventTypeEnum)e.EventType).ToString(); + Console.WriteLine($" id={e.id} EventType={e.EventType}({etName}) ValueDate={e.ValueDate:yyyy-MM-dd} ClientCashId={e.ClientCashId}"); + } + + // 3. 关键:找出"自动互换"事件,确定重收盘的删除起点 valueDate + var autoEvents = allEvents.Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换).ToList(); + if (autoEvents.Count == 0) + { + Console.WriteLine($"\n⚠ 该交易无自动互换事件,重收盘不会触发 ClearSwapPositions 的资金删除逻辑。"); + Console.WriteLine($" 改用 SwapPositionCompose 的合成持仓路径(delAfter=false)也不删资金。"); + Console.WriteLine($" → 此样本不适合复现,需选有自动互换事件的样本。"); + Assert.Inconclusive("此样本无自动互换事件,请换一个有自动互换的交易。"); + return; + } + + // 重收盘时 valueDate 取自动互换事件的最小 ValueDate(重收盘从该日起重算) + var minAutoDate = autoEvents.Min(x => x.ValueDate); + Console.WriteLine($"\n[3] 自动互换事件 {autoEvents.Count} 条,最早 ValueDate={minAutoDate:yyyy-MM-dd}(重收盘 valueDate 起点)"); + + // 4. 模拟 ClearSwapPositions 第399-404行的删除条件 + var swapTradeIds = new List { tradeId }; + var actions = new List { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 }; + var wouldDelete = allCashRecords + .Where(x => x.HappenDate >= minAutoDate && actions.Contains(x.Action)) + .ToList(); + Console.WriteLine($"\n[4] ⚠ 模拟删除条件(HappenDate>={minAutoDate:yyyy-MM-dd} AND Action IN 互换/预付金返息)会命中: {wouldDelete.Count} 条"); + + // 5. 逐条标注命中记录的来源(手动 vs 自动) + var manualCashIds = allEvents + .Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && x.ClientCashId > 0) + .Select(x => x.ClientCashId).ToHashSet(); + var autoCashIds = allEvents + .Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换 && x.ClientCashId > 0) + .Select(x => x.ClientCashId).ToHashSet(); + + int manualHit = 0, autoHit = 0, unknownHit = 0; + Console.WriteLine($" {"id",8} {"Action",-20} {"Money",12} {"HappenDate",-12} {"来源",10} {"⚠误删",6}"); + foreach (var c in wouldDelete) + { + string source; + bool misDelete = false; + if (manualCashIds.Contains(c.id)) { source = "手动互换"; misDelete = true; manualHit++; } + else if (autoCashIds.Contains(c.id)) { source = "自动互换"; autoHit++; } + else { source = "未知(孤儿)"; unknownHit++; } + Console.WriteLine($" {c.id,8} {c.Action,-20} {c.Money,12:F2} {c.HappenDate?.ToString("yyyy-MM-dd"),-12} {source,-10} {(misDelete ? "✓BUG" : ""),6}"); + } + + Console.WriteLine($"\n[结论] 删除命中 {wouldDelete.Count} 条 = 手动互换 {manualHit} + 自动互换 {autoHit} + 未知 {unknownHit}"); + if (manualHit > 0) + { + Console.WriteLine($"⚠⚠⚠ 坐实 BUG:重收盘会误删 {manualHit} 条手动互换资金记录!"); + } + Console.WriteLine($"\n(以上为纯查询诊断,未修改任何数据)"); + + Assert.IsTrue(wouldDelete.Count > 0, "删除条件应至少命中1条"); + } + finally + { + db?.Dispose(); + } + } + + /// + /// 候选样本交易ID。从 Step0 输出中选有自动互换事件 + 有手动互换资金记录的交易。 + /// + private int SampleTradeId => 1903; + + /// + /// Step1:录制样本交易快照 + 模拟删除条件,把"会被误删的手动互换资金记录"固化为 golden。 + /// + /// 这是 TDD 红灯的核心产物: + /// - 录制该交易的 swap_event + ClientCashInCashOut 完整快照 + /// - 模拟 ClearSwapPositions:400-404 的删除条件,算出命中列表 + /// - 标注每条命中记录的来源(手动互换/自动互换/孤儿) + /// - 断言"命中列表含手动互换记录" → 当前成立(红灯,坐实 bug) + /// + /// 修复后(绿灯):命中列表应只含自动互换记录,手动互换记录不在内 → 断言失败需更新 golden。 + /// + /// 为何不直接调 SwapPositionCompose: + /// 那会真删测试库数据且难恢复。录制+模拟条件能等价坐实 bug,又不破坏数据。 + /// + [TestMethod] + [TestCategory("DBRecording")] + // [Ignore] // 有写文件副作用,手动跑时取消注释 + public void Step1_RecordAndDiagnoseDeleteBug() + { + int tradeId = SampleTradeId; + YLContext db; + try { db = DbContextFactory.GetYLDbContext(); } + catch (Exception ex) + { + Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); + return; + } + using (db) + { + Directory.CreateDirectory(GoldenDir); + + Console.WriteLine($"\n========== 录制 SwapTradeId={tradeId}(重收盘误删手动资金记录)==========\n"); + + // 1. 交易主信息 + var trade = db.trade.FirstOrDefault(t => t.id == tradeId); + Assert.IsNotNull(trade, $"trade {tradeId} 不存在"); + + // 2. swap_event 快照(区分手动互换 vs 自动互换) + var allEvents = db.swap_event + .Where(x => x.SwapTradeId == tradeId && !x.Invalid) + .OrderBy(x => x.ValueDate).ThenBy(x => x.id) + .ToList(); + + // 3. ClientCashInCashOut 快照(互换/预付金相关,bug 影响范围) + var allCashRecords = db.ClientCashInCashOut + .Where(x => x.TradeId == tradeId + && (x.Action == ClientCashInCashOut.系统操作_互换 + || x.Action == ClientCashInCashOut.系统操作_预付金返息)) + .OrderBy(x => x.HappenDate).ThenBy(x => x.id) + .ToList(); + + // 4. 诊断:模拟删除条件,算出命中列表 + 来源标注 + var diagnosis = DiagnoseDeleteImpact(tradeId, allEvents, allCashRecords); + Console.WriteLine(diagnosis.Summary); + + // 5. 序列化 golden + var golden = new ReEodDeleteCashGoldenModel + { + SwapTradeId = tradeId, + SwapTradeNo = trade.TradeNumber, + RecordedAt = DateTime.Now, + SourceDb = "test", + Purpose = "重收盘误删手动互换资金记录 - TDD红灯证据", + InputEvents = JArray.FromObject(allEvents, JsonSerializer.Create(JsonSettings)), + InputCashRecords = JArray.FromObject(allCashRecords, JsonSerializer.Create(JsonSettings)), + Diagnosis = JObject.FromObject(diagnosis, JsonSerializer.Create(JsonSettings)) + }; + + string filePath = Path.Combine(GoldenDir, $"reeod_delete_trade_{tradeId}.json"); + File.WriteAllText(filePath, JsonConvert.SerializeObject(golden, JsonSettings)); + Console.WriteLine($"\n✅ golden 已保存: {filePath}"); + + // 红灯断言(旧bug逻辑):按 Action 字符串模糊删会命中手动互换记录 + Assert.IsTrue(diagnosis.手动互换误删记录数 > 0, + $"红灯:旧bug删除条件(Action模糊删)会误删 {diagnosis.手动互换误删记录数} 条手动互换资金记录 " + + $"(ids=[{string.Join(",", diagnosis.手动互换误删CashIds)}])。"); + + // 绿灯断言(修复后逻辑):排除 manualClientCashIds 后,手动互换记录不再被命中 + Assert.AreEqual(0, diagnosis.修复后手动误删数, + $"绿灯:修复后逻辑(排除manualClientCashIds)不应再命中手动互换资金记录," + + $"实际仍命中 {diagnosis.修复后手动误删数} 条。"); + } + } + + /// + /// 模拟 ClearSwapPositions:400-404 的删除条件,诊断命中情况。 + /// + private DeleteDiagnoseResult DiagnoseDeleteImpact( + int tradeId, + List allEvents, + List allCashRecords) + { + var r = new DeleteDiagnoseResult { SwapTradeId = tradeId }; + var lines = new List + { + $"--- 重收盘误删诊断 SwapTradeId={tradeId} ---", + "", + "[swap_event] 手动互换 vs 自动互换:" + }; + + foreach (var e in allEvents) + { + if (e.EventType == (int)SwapEventTypeEnum.互换 || e.EventType == (int)SwapEventTypeEnum.自动互换) + { + lines.Add($" event id={e.id} EventType={((SwapEventTypeEnum)e.EventType).ToString()} " + + $"ValueDate={e.ValueDate:yyyy-MM-dd} ClientCashId={e.ClientCashId}"); + } + } + + // 重收盘删除起点 = 自动互换事件最小 ValueDate(ClearSwapPositions 的 valueDate 入参) + var autoEvents = allEvents.Where(x => x.EventType == (int)SwapEventTypeEnum.自动互换).ToList(); + r.自动互换事件数 = autoEvents.Count; + + if (autoEvents.Count == 0) + { + r.Summary = string.Join("\n", lines) + "\n\n⚠ 无自动互换事件,删除逻辑不触发。"; + return r; + } + + var minAutoDate = autoEvents.Min(x => x.ValueDate); + r.删除起点ValueDate = minAutoDate; + + // 来源标注:按 swap_event.ClientCashId 反查 + var manualCashIds = allEvents + .Where(x => x.EventType == (int)SwapEventTypeEnum.互换 && x.ClientCashId > 0) + .Select(x => x.ClientCashId).ToHashSet(); + var autoCashIds = autoEvents + .Where(x => x.ClientCashId > 0) + .Select(x => x.ClientCashId).ToHashSet(); + + lines.Add(""); + lines.Add($"[删除条件模拟] valueDate={minAutoDate:yyyy-MM-dd} Action IN (系统操作-互换, 系统操作-预付金返息)"); + lines.Add(string.Format(" {0,-8}{1,-22}{2,12}{3,-12}{4,-10}{5,-8}", "id", "Action", "Money", "HappenDate", "来源", "误删?")); + + // 模拟 ClearSwapPositions:400-404 Where 条件 + var actions = new List { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 }; + var wouldDelete = allCashRecords + .Where(x => x.HappenDate >= minAutoDate && actions.Contains(x.Action)) + .ToList(); + + r.删除命中总数 = wouldDelete.Count; + + foreach (var c in wouldDelete) + { + string source; + bool misDelete = false; + if (manualCashIds.Contains(c.id)) { source = "手动互换"; misDelete = true; r.手动互换误删记录数++; r.手动互换误删CashIds.Add(c.id); } + else if (autoCashIds.Contains(c.id)) { source = "自动互换"; r.自动互换命中记录数++; } + else { source = "未知(孤儿)"; r.孤儿命中记录数++; } + r.命中明细.Add(new DeleteHitItem + { + CashId = c.id, Action = c.Action, Money = c.Money ?? 0, + HappenDate = c.HappenDate, 来源 = source, 会被误删 = misDelete + }); + lines.Add(string.Format(" {0,-8}{1,-22}{2,12:F2}{3,-12}{4,-10}{5,-8}", + c.id, c.Action, c.Money, c.HappenDate?.ToString("yyyy-MM-dd"), source, misDelete ? "✓BUG" : "")); + } + + lines.Add(""); + // ===== 修复后逻辑模拟(验证 ClearSwapPositions 新代码不再误删手动记录)===== + // 修复后代码(SwapTradeBaseService.cs:388-421)改为: + // 1. 按 autoSwapEvents.ClientCashId 精准收集 + // 2. GetLegacyAutoSwapClientCashRecords 显式排除 manualClientCashIds(cs:491) + // 模拟这个排除逻辑,看手动记录是否被排除 + lines.Add("[修复后逻辑模拟] 排除 manualClientCashIds 后的命中:"); + var fixedWouldDelete = wouldDelete + .Where(x => !manualCashIds.Contains(x.id)) // 修复后:排除手动互换的资金记录 + .ToList(); + int fixedManualHit = wouldDelete.Count(x => manualCashIds.Contains(x.id)) - fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)); + lines.Add($" 修复前命中手动互换: {r.手动互换误删记录数} 条 (ids=[{string.Join(",", r.手动互换误删CashIds)}])"); + lines.Add($" 修复后命中手动互换: {fixedWouldDelete.Count(x => manualCashIds.Contains(x.id))} 条"); + if (r.手动互换误删记录数 > 0 && fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)) == 0) + { + lines.Add($" ✅ 修复生效:手动互换资金记录被正确排除,不再误删!"); + r.修复后手动误删数 = 0; + } + else + { + lines.Add($" ⚠ 修复未生效或部分生效"); + r.修复后手动误删数 = fixedWouldDelete.Count(x => manualCashIds.Contains(x.id)); + } + + lines.Add(""); + lines.Add("[结论]"); + lines.Add($" 删除命中 {r.删除命中总数} 条 = 手动互换 {r.手动互换误删记录数} + 自动互换 {r.自动互换命中记录数} + 孤儿 {r.孤儿命中记录数}"); + if (r.手动互换误删记录数 > 0) + { + lines.Add($" ⚠⚠⚠ 坐实 BUG:重收盘会误删 {r.手动互换误删记录数} 条手动互换资金记录 (ids=[{string.Join(",", r.手动互换误删CashIds)}])"); + lines.Add($" 根因:ClearSwapPositions:400 按 Action=系统操作-互换 删除,该 Action 手动/自动共用,无来源字段区分。"); + r.结论 = $"坐实BUG:误删 {r.手动互换误删记录数} 条手动互换资金记录"; + r.BUG成立 = true; + } + else + { + lines.Add($" 未检测到误删手动互换记录(可能已修复)。"); + r.结论 = "未检测到误删"; + r.BUG成立 = false; + } + + r.Summary = string.Join("\n", lines); + return r; + } + + /// + /// Step2:离线校验已录制 golden 文件(不连库)。 + /// 确认 json 含完整快照 + 诊断结论能正确反序列化。 + /// 这是唯一不标 [Ignore] 且能进 CI 的测试(纯读文件,无外部依赖)。 + /// + [TestMethod] + [TestCategory("DBRecording")] + public void Step2_VerifyRecordedGoldenFile() + { + if (!Directory.Exists(GoldenDir)) + { + Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Step1_RecordAndDiagnoseDeleteBug)"); + return; + } + var files = Directory.GetFiles(GoldenDir, "reeod_delete_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(json); + + Assert.IsTrue(golden.SwapTradeId > 0, $"{file}: SwapTradeId 无效"); + Assert.IsNotNull(golden.InputEvents, $"{file}: InputEvents 缺失"); + Assert.IsTrue(golden.InputEvents.Count > 0, $"{file}: InputEvents 为空"); + Assert.IsNotNull(golden.InputCashRecords, $"{file}: InputCashRecords 缺失"); + Assert.IsTrue(golden.InputCashRecords.Count > 0, $"{file}: InputCashRecords 为空"); + Assert.IsNotNull(golden.Diagnosis, $"{file}: Diagnosis 缺失"); + + Console.WriteLine($"✅ {Path.GetFileName(file)}: trade={golden.SwapTradeId}, " + + $"events={golden.InputEvents.Count}条, cash={golden.InputCashRecords.Count}条, " + + $"BUG成立={golden.Diagnosis?["BUG成立"]?.Value()}, " + + $"结论={golden.Diagnosis?["结论"]?.Value()}"); + } + } + + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include, + DateFormatString = "yyyy-MM-ddTHH:mm:ss", + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + } + + /// + /// 重收盘误删 golden 模型:swap_event + ClientCashInCashOut 快照 + 删除命中诊断。 + /// + public class ReEodDeleteCashGoldenModel + { + public int SwapTradeId { get; set; } + public string SwapTradeNo { get; set; } + public DateTime RecordedAt { get; set; } + public string SourceDb { get; set; } + public string Purpose { get; set; } + public JArray InputEvents { get; set; } // swap_event + public JArray InputCashRecords { get; set; } // ClientCashInCashOut + public JObject Diagnosis { get; set; } + } + + /// + /// 重收盘删除命中诊断结果。 + /// + public class DeleteDiagnoseResult + { + public int SwapTradeId { get; set; } + public int 自动互换事件数 { get; set; } + public DateTime 删除起点ValueDate { get; set; } + public int 删除命中总数 { get; set; } + public int 手动互换误删记录数 { get; set; } + public int 自动互换命中记录数 { get; set; } + public int 孤儿命中记录数 { get; set; } + /// 修复后逻辑(排除manualClientCashIds)模拟命中手动记录数,应为0。 + public int 修复后手动误删数 { get; set; } + public List 手动互换误删CashIds { get; set; } = new List(); + public List 命中明细 { get; set; } = new List(); + public bool BUG成立 { get; set; } + public string 结论 { get; set; } + public string Summary { get; set; } + } + + public class DeleteHitItem + { + public long CashId { get; set; } + public string Action { get; set; } + public double Money { get; set; } + public DateTime? HappenDate { get; set; } + public string 来源 { get; set; } + public bool 会被误删 { get; set; } + } +} diff --git a/UnitTestProject/Modules/SwapModule/TestableSwapFlowEventService.cs b/UnitTestProject/Modules/SwapModule/TestableSwapFlowEventService.cs new file mode 100644 index 00000000..6ff0c789 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/TestableSwapFlowEventService.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; +using YLErp.Modules.SwapModule; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 可测试的 SwapFlowEventService 子类 + /// override 所有外部依赖方法,用内存数据替代数据库和静态调用 + /// + public class TestableSwapFlowEventService : SwapFlowEventService + { + private readonly trade _trade; + private readonly trade_extend _tradeExtend; + private readonly List _positions; + private readonly Dictionary _underlyings; + private readonly Func _nextBusinessDay; + private readonly long _positionId; + + public List PersistedEvents { get; } = new List(); + + public TestableSwapFlowEventService( + OptUserInfo optUser, + trade trade, + trade_extend tradeExtend, + List positions, + Dictionary underlyings, + Func nextBusinessDay, + long positionId = 999999 + ) : base(optUser) + { + _trade = trade; + _tradeExtend = tradeExtend; + _positions = positions ?? new List(); + _underlyings = underlyings ?? new Dictionary(); + _nextBusinessDay = nextBusinessDay ?? (d => d.AddDays(1)); + _positionId = positionId; + } + + protected override trade FindTrade(int swapTradeId) => _trade; + + protected override trade_extend FindTradeExtend(int swapTradeId) => _tradeExtend; + + protected override List FindPositions(int swapTradeId) => _positions; + + protected override List FindAndInvalidateFutureEvents(int swapTradeId, DateTime tradeDate) + { + // 测试环境:不需要废弃历史事件 + return new List(); + } + + protected override underlying_manager GetUnderlying(string underlyingCode) + { + return _underlyings.TryGetValue(underlyingCode, out var ul) ? ul : new underlying_manager { UnderlyingCode = underlyingCode }; + } + + protected override DateTime GetNextBusinessDay(DateTime date) => _nextBusinessDay(date); + + protected override long ResolvePositionId(swap_flow_merge merge, DateTime maturityDate, int direction, string tradeNumber) => _positionId; + + protected override void PersistEvents(List events) + { + // 不写 DB,收集到列表供验证 + PersistedEvents.AddRange(events); + } + + protected override IDisposable BeginTransaction() => new NoopDisposable(); + + protected override void CommitTransaction(IDisposable transaction) { } + + protected override void RollbackTransaction(IDisposable transaction) { } + + /// 公开调用 protected 的 MergePageEvent,供测试使用 + public List ExecuteMergePageEvent(int swapTradeId, List flowMergeList, DateTime tradeDate, bool needTrans = false) + { + return MergePageEvent(swapTradeId, flowMergeList, tradeDate, needTrans); + } + + public override void UpdateDbOption(DBModelBaseV2 dBModel) + { + // 测试环境不设 Opt 信息,避免依赖 UserInfo + } + + private class NoopDisposable : IDisposable + { + public void Dispose() { } + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/TestableSwapTradeAutoService.cs b/UnitTestProject/Modules/SwapModule/TestableSwapTradeAutoService.cs new file mode 100644 index 00000000..ed4772fa --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/TestableSwapTradeAutoService.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; + +namespace YLErp.Modules.SwapModule +{ + /// + /// SwapTradeAutoService 的可测试子类。 + /// 覆盖所有外部依赖方法,用内存数据替代数据库和静态调用。 + /// + public class TestableSwapTradeAutoService : SwapTradeAutoService + { + private readonly Func _nextBusinessDay; + private readonly Func _nextBusinessDayBefore; + private readonly Func _bondCalc; + + // 注入的数据 + private readonly List _trades; + private readonly List _tradeExtends; + private readonly List _positions; + private readonly Dictionary _clients; + private readonly Dictionary _assets; + private readonly Dictionary _underlyings; + private readonly Func _etradingRuleFactory; + private readonly Func, int, string, SwapFloatRate> _floatRateFactory; + private readonly Func _newSwapTradeFactory; + private readonly Action _autoSwapUnwind; + private readonly List _flowEvents; + private readonly List _validTrades; + + /// 捕获 PersistMerge 写入的所有 merge 记录 + public List PersistedMerges { get; } = new List(); + + /// 捕获 CreateNewSwapTrade 创建的所有交易 + public List CreatedTrades { get; } = new List(); + + /// 捕获 AutoSwapUnwind 调用 + public List<(int tradeId, decimal qty, decimal fee)> UnwindCalls { get; } = new List<(int, decimal, decimal)>(); + + /// SaveChanges 调用次数 + public int SaveChangesCount { get; private set; } + + public TestableSwapTradeAutoService( + OptUserInfo optUser, + Func nextBusinessDay = null, + Func nextBusinessDayBefore = null, + Func bondCalc = null, + List trades = null, + List tradeExtends = null, + List positions = null, + Dictionary clients = null, + Dictionary assets = null, + Dictionary underlyings = null, + Func etradingRuleFactory = null, + Func, int, string, SwapFloatRate> floatRateFactory = null, + Func newSwapTradeFactory = null, + Action autoSwapUnwind = null, + List flowEvents = null, + List validTrades = null + ) : base(optUser) + { + _nextBusinessDay = nextBusinessDay ?? (d => d.AddDays(1)); + _nextBusinessDayBefore = nextBusinessDayBefore ?? (d => d.AddDays(-1)); + _bondCalc = bondCalc ?? ((code, price, date) => null); + _trades = trades ?? new List(); + _tradeExtends = tradeExtends ?? new List(); + _positions = positions ?? new List(); + _clients = clients ?? new Dictionary(); + _assets = assets ?? new Dictionary(); + _underlyings = underlyings ?? new Dictionary(); + _etradingRuleFactory = etradingRuleFactory; + _floatRateFactory = floatRateFactory; + _newSwapTradeFactory = newSwapTradeFactory; + _autoSwapUnwind = autoSwapUnwind; + _flowEvents = flowEvents ?? new List(); + _validTrades = validTrades ?? new List(); + } + + #region Override 可测试化方法 + + protected override DateTime GetNextBusinessDay(DateTime date) => _nextBusinessDay(date); + protected override DateTime GetNextBusinessDayBefore(DateTime date) => _nextBusinessDayBefore(date); + + protected override CalBondResult CalculateBondYtm(string underlyingCode, decimal avgPrice, DateTime settleDate) + => _bondCalc(underlyingCode, avgPrice, settleDate); + + protected override void PersistMerge(swap_flow_merge merge) => PersistedMerges.Add(merge); + + protected override void SetModelOpt(DBModelBaseV2 model) { } + + protected override List FindActiveSwapTrades(DateTime valueDate) => _trades; + protected override List FindTradeExtends(IEnumerable tradeIds) => _tradeExtends; + + protected override List FindActivePositions(IEnumerable tradeIds, int posiDirection) + => _positions.Where(x => x.PosiDirection == posiDirection).ToList(); + + protected override List FindActivePositionsAll(IEnumerable tradeIds) + => _positions; + + protected override IQueryable QueryFloatRates(DateTime valueDate, DateTime matuirityDate) + => new List().AsQueryable(); + + protected override Client FindClient(int clientId) + => _clients.TryGetValue(clientId, out var c) ? c : null; + + protected override AssetUnit FindAssetUnit(string assetAccountName) + => _assets.TryGetValue(assetAccountName ?? "", out var a) ? a : null; + + protected override underlying_manager FindUnderlying(string underlyingCode) + => _underlyings.TryGetValue(underlyingCode ?? "", out var u) ? u : null; + + protected override EtradingRule GetEtradingRule(BoundSideEnum boundSide, string clientNumber) + => _etradingRuleFactory?.Invoke((int)boundSide, clientNumber); + + protected override SwapFloatRate GetSwapFloatRate(IQueryable query, int clientId, string underlyingCode) + => _floatRateFactory?.Invoke(query, clientId, underlyingCode); + + protected override List FindFlowEventsForCashCheck(swap_flow_merge flowMerge) + => _flowEvents; + + protected override List FindValidTrades(IEnumerable tradeIds) + => _validTrades; + + protected override void SaveChanges() => SaveChangesCount++; + + protected override trade CreateNewSwapTrade(swap_flow_merge flowMerge, Client client, AssetUnit asset, underlying_manager underlying, SwapFloatRate floatRate, string clearingAgency, bool cashNeedAfter = false) + { + if (_newSwapTradeFactory != null) + { + var t = _newSwapTradeFactory(flowMerge, client, asset, underlying, floatRate, clearingAgency, cashNeedAfter); + CreatedTrades.Add(t); + return t; + } + var trade = new trade { id = CreatedTrades.Count + 1, TradeNumber = $"TEST-{CreatedTrades.Count + 1}" }; + CreatedTrades.Add(trade); + return trade; + } + + protected override void AutoSwapUnwind(int tradeId, decimal tradingAmountAvg, decimal tradingAmountFeeAvg, decimal tradingAmountNetFeeAvg, decimal tradingAmountNetAvg, DateTime occurTime, decimal tradingQtyAbs, decimal tradingFeePending) + { + UnwindCalls.Add((tradeId, tradingQtyAbs, tradingFeePending)); + _autoSwapUnwind?.Invoke(tradeId, tradingAmountAvg, tradingAmountFeeAvg, tradingAmountNetFeeAvg, tradingAmountNetAvg, occurTime, tradingQtyAbs, tradingFeePending); + } + + #endregion + + /// 公开 SummaryFlow 供测试调用 + public List ExecuteSummaryFlow( + List swapFlows, DateTime valueDate, bool save = true, + Action callback = null) + => SummaryFlow(swapFlows, valueDate, save, callback); + + /// 公开 SummaryFlow 第二个重载 + public List ExecuteSummaryFlowDeal( + List swapFlows1, DateTime tradeDate, List swapFlows) + => SummaryFlow(swapFlows1, tradeDate, swapFlows); + + /// 公开 MergeRestModeCompose 供测试调用 + public void ExecuteMergeRestModeCompose(List mergeList, DateTime valueDate, Action? action = null) + => MergeRestModeCompose(mergeList, valueDate, action); + + /// 公开 MergeAvgModeCompose 供测试调用 + public Dictionary> ExecuteMergeAvgModeCompose(List mergeList, DateTime valueDate, Action? action = null) + => MergeAvgModeCompose(mergeList, valueDate, action); + } +} diff --git a/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_互换结清后待实现归零.json b/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_互换结清后待实现归零.json new file mode 100644 index 00000000..87cf38ba --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_互换结清后待实现归零.json @@ -0,0 +1,35 @@ +{ + "Scenario": "互换结清后待实现归零", + "Description": "攒10天后互换,InterestIncomeSum应≈当天新计", + "Input": { + "Trade": null, + "Positions": null, + "PreEodPositions": null, + "FlowEvents": null, + "PosiLongNotional": 10000.0, + "PosiShortNotional": null, + "CloseNational": null, + "GrossPrice": null, + "OrginPv": 10000.0, + "SettleDate": "2026-05-07T00:00:00" + }, + "Expected": { + "PositionCount": 1, + "EodPositions": [ + { + "PositionId": 1001, + "InterestIncomeSum": 0.821917808219178082191780822, + "InterestProfitSum": 0.821917808219178082191780822, + "TdInterestIncome": 0.8219178082191780821917808219, + "TdCloseInterest": 8.21917808219, + "TdInterestPrincipal": 10000.0, + "RealizedInterest": 8.21917808219, + "RealizedPnl": 0.0, + "SwapPositionValue": 0.821917808219178082191780822, + "InterestFeeSum": 0.0 + } + ] + }, + "Source": "synthetic", + "RecordedAt": null +} \ No newline at end of file diff --git a/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_普通日归档递增.json b/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_普通日归档递增.json new file mode 100644 index 00000000..15fcbc77 --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/DealInterestsGolden/golden_普通日归档递增.json @@ -0,0 +1,24 @@ +{ + "Scenario": "普通日归档递增", + "Description": "第3天收盘,InterestIncomeSum应=2天+1天=3天利息", + "Input": null, + "Expected": { + "PositionCount": 1, + "EodPositions": [ + { + "PositionId": 1001, + "InterestIncomeSum": 1.64383561644, + "InterestProfitSum": 1.64383561644, + "TdInterestIncome": 0.82191780822, + "TdCloseInterest": 0.0, + "TdInterestPrincipal": 10000.0, + "RealizedInterest": 0.0, + "RealizedPnl": 0.0, + "SwapPositionValue": 1.64383561644, + "InterestFeeSum": 0.0 + } + ] + }, + "Source": "synthetic", + "RecordedAt": null +} \ No newline at end of file diff --git a/UnitTestProject/Resources/GoldenFiles/GLMS20260105/golden_GLMS-20260105-0007.json b/UnitTestProject/Resources/GoldenFiles/GLMS20260105/golden_GLMS-20260105-0007.json new file mode 100644 index 00000000..c617a192 --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/GLMS20260105/golden_GLMS-20260105-0007.json @@ -0,0 +1,1280 @@ +{ + "TradeNumber": "GLMS-20260105-0007", + "TradeId": 1924, + "SwapDate": "2026-03-03", + "Description": "分红精度差异:互换前-90400,互换后应为-90000,实际-89999.64", + "KeyFloatEodPositions": [ + { + "ValueDate": "2026-03-02", + "PositionId": 35148, + "PosiDividendSum": -90400.000000, + "TdPosiDividend": -90400.000000, + "TdCloseDividend": 0.0, + "RealizedDividend": 0.0, + "PosiQuantity": 50000000.000000, + "PosiMtmPnL": -500000.000000 + }, + { + "ValueDate": "2026-03-03", + "PositionId": 35148, + "PosiDividendSum": -89999.640000, + "TdPosiDividend": 0.0, + "TdCloseDividend": -400.000000, + "RealizedDividend": -400.000000, + "PosiQuantity": 50000000.000000, + "PosiMtmPnL": -500000.000000 + }, + { + "ValueDate": "2026-03-04", + "PositionId": 35148, + "PosiDividendSum": -89999.640000, + "TdPosiDividend": 0.0, + "TdCloseDividend": 0.0, + "RealizedDividend": -400.000000, + "PosiQuantity": 50000000.000000, + "PosiMtmPnL": -500000.000000 + } + ], + "AllFloatEodDividends": [ + { + "ValueDate": "2026-01-05", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-06", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-07", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-08", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-09", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-10", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-11", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-12", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-13", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-14", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-15", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-16", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-17", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-18", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-19", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-20", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-21", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-22", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-23", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-24", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-25", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-26", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-27", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-28", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-29", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-30", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-01-31", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-01", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-02", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-03", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-04", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-05", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-06", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-07", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-08", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-09", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-10", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-11", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-12", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-13", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-14", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-15", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-16", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-17", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-18", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-19", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-20", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-21", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-22", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-23", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-24", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-25", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-26", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-27", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-02-28", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-03-01", + "PosiDividendSum": 0.0, + "TdPosiDividend": 0.0, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-03-02", + "PosiDividendSum": -90400.000000, + "TdPosiDividend": -90400.000000, + "RealizedDividend": 0.0 + }, + { + "ValueDate": "2026-03-03", + "PosiDividendSum": -89999.640000, + "TdPosiDividend": 0.0, + "RealizedDividend": -400.000000 + }, + { + "ValueDate": "2026-03-04", + "PosiDividendSum": -89999.640000, + "TdPosiDividend": 0.0, + "RealizedDividend": -400.000000 + } + ], + "ReplayInput": { + "Trade": { + "SubTrades": null, + "YesterDayTradeSavedVol": null, + "PV": null, + "RealizedPnl": null, + "PositionPnl": null, + "MaturityWorkDay": 124, + "PairTradeNumber": null, + "ShowNotional": -50000000.0, + "MaturityDay": 182, + "TradeOpenVolatilityString": "0.00%", + "InitialSpotPriceOriginal": null, + "TradeOriginalAmount": null, + "ExerciseDateString": "2026-12-31", + "ActualStrike": null, + "StrikeString": "0.000000000", + "TradeSinglePriceString": "0.00%", + "TradeDateString": "2026-01-05", + "LotsNewInfo": null, + "UnWindPrice": null, + "StockEqvNotionalToShow": 50500000.0, + "SettlementTypeDesc": "收盘价", + "SettlementDesc": null, + "VarietyId": null, + "SyntheticUnderlyingTipsInfo": null, + "StrikeToShow": "0.000000000", + "CurNotional": 0.0, + "LastDayNotional": 0.0, + "Amount": null, + "ContractCode": null, + "InitialMarginRatio": null, + "UnderlyingPrice": null, + "UnderlyingName": null, + "DeltaInLots": null, + "ValueStatus": null, + "UnWindUnderlyingPrice": null, + "UnWindTradePrice": null, + "UnWindTotalAmount": null, + "UnWindFee": null, + "UnWindTimes": 0, + "KnockInOutStatusObservation": null, + "DividendRatio": 0.0, + "InitYtm": 0.025000, + "CalcId": null, + "TTMDays": null, + "UnderlyingInstrumentTypeCn": "利率债", + "ExerciseModeCn": "", + "ClientNumber": null, + "SummaryType": "收益互换", + "QuoteUnitSingle": null, + "QuoteUnit": null, + "trade_forward": { + "id": 0, + "TradeId": 0, + "OpenCommission": 0.0, + "AnnualMarginRate": 0.0, + "AnnualStoragePrice": 0.0, + "ForwardValue": 0.0, + "ObservationStart": null, + "ObservationDates": null, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_asian_option": { + "StrikeType": null, + "PayoffType": null, + "SettleMode": null, + "StrikeGearingFactor": 1.0, + "AveragingPeriodStartDate": null, + "ObservationDates": null, + "EnhancedPrice": 0.0, + "StrikeTypeCn": "", + "PayoffTypeCn": "", + "Fixings": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rainbow_option": { + "UnderlyingAssetCode": null, + "Strike": null, + "UnderlyingMaturityDate": null, + "UnderlyingId1": null, + "UnderlyingId2": null, + "Strike2": null, + "SpotPrice1": null, + "SpotPrice2": null, + "RainbowType": null, + "UnderlyingAssetCode2": null, + "CorRelation": null, + "CashAmount": null, + "Vol2": 0.0, + "UnderlyingCodes": [ + null, + null + ], + "Strikes": [ + 0.0, + 0.0 + ], + "SpotPrices": [ + 0.0, + 0.0 + ], + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_barrier_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "BarrierShift": null, + "BarrierTypeEn": null, + "IsDiscrete": false, + "BarrierPrice": null, + "UpperBarrierPrice": null, + "Discrete": null, + "BarrierType": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateAnnualizedAtKO": false, + "RebateDayCount": null, + "ObservationDates": null, + "LatestObservationDate": null, + "RebateType": null, + "RebateTypeCn": "", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_risky_option": { + "Strike1": null, + "Strike2": null, + "Strike3": null, + "ParticipationRate1": null, + "ParticipationRate2": null, + "ParticipationRate3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_binary_option": { + "RebateType": null, + "RebateTypeCn": "", + "CashOrNothingAmount": null, + "CashOrNothingAmountHigh": null, + "CashOrNothingAmountRate": null, + "CashOrNothingAmountHighRate": null, + "PayoffType": "", + "UpperBarrier": null, + "RebateDayCount": null, + "RebateAnnualizedAtKO": false, + "ObservationDates": null, + "LatestObservationDate": null, + "UpperBarrierRelative": "0", + "MonitorType": null, + "IsDiscreteMonitored": false, + "Offset": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_spread_option": { + "UnderlyingMaturityDate2": null, + "UnderlyingMaturityDate3": null, + "UnderlyingMaturityDate4": null, + "Payoff": "S1-S2", + "UnderlyingId1": null, + "UnderlyingId2": null, + "UnderlyingId3": null, + "UnderlyingId4": null, + "UnderlyingAssetClass1": null, + "UnderlyingAssetClass2": null, + "UnderlyingAssetClass3": null, + "UnderlyingAssetClass4": null, + "UnderlyingAssetCode1": null, + "UnderlyingAssetCode2": null, + "UnderlyingAssetCode3": null, + "UnderlyingAssetCode4": null, + "UnderlyingAssetName1": null, + "UnderlyingAssetName2": null, + "UnderlyingAssetName3": null, + "UnderlyingAssetName4": null, + "TradeCloseVolatility1": null, + "TradeCloseVolatility2": null, + "TradeCloseVolatility3": null, + "TradeCloseVolatility4": null, + "TradeOpenVolatility1": null, + "TradeOpenVolatility2": null, + "TradeOpenVolatility3": null, + "TradeOpenVolatility4": null, + "NumOfSmoothingDays1": null, + "NumOfSmoothingDays2": null, + "NumOfSmoothingDays3": null, + "NumOfSmoothingDays4": null, + "Vol1": null, + "Vol2": null, + "Vol3": null, + "Vol4": null, + "Correlation12": null, + "Correlation13": null, + "Correlation14": null, + "Correlation23": null, + "Correlation24": null, + "Correlation34": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_double_sharkfin_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "IsDiscrete": false, + "BarrierHigh": 0.0, + "BarrierLow": 0.0, + "StrikeHigh": null, + "StrikeLow": null, + "CallParticipationRate": null, + "PutParticipationRate": null, + "Discrete": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateType": null, + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_autocall": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "CouponBarrier": 0.0, + "Coupon": 0.0, + "IsFixedCoupon": false, + "CouponPayType": 0, + "SpreadStrike1": null, + "SpreadStrike": null, + "CouponPayAtMaturity": false, + "IncludeCouponAfterKI": false, + "KOObservationDates": null, + "CouponObservation": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "CouponDayCount": "Act365", + "HappenedObservations": null, + "KIPayoffType": 0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_snowball": { + "IsInitialKnockedIn": false, + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "Coupon": 0.0, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "KORebate": 0.0, + "KORebateType": 0, + "IsFixedCoupon": false, + "SpreadStrikeAtKO": null, + "SpreadStrikeAtKO1": null, + "SpreadStrikeAtMaturity1": null, + "SpreadStrikeAtMaturity": null, + "KOPayoffType": 0, + "KIPayoffType": 0, + "AnnualizedPremiumRate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "InitialAdvance": null, + "PeriodAdvance": null, + "FontEarning": null, + "ConfirmedLine": null, + "ConfirmedFloor": null, + "CouponDayCount": null, + "PrepaymentUsed": false, + "PrepaymentRatio": null, + "PrepaymentInterestRate": null, + "PrepaymentConvertCashRate": null, + "CouponIncludeEndDate": null, + "EnhancedParticipationRate": null, + "KIParticipationRate": null, + "PrincipalProtectionRate": null, + "KIObservationType": 0, + "KOBarrierAdjustStep": 0.0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rangeaccrual": { + "LowerRange": 0.0, + "UpperRange": 0.0, + "BonusRate": 0.0, + "ObservationDates": null, + "LatestObservationDate": null, + "HappenedObservations": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_airbag": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "HighStrike": 0.0, + "HasPayoffLimit": false, + "Barrier": 0.0, + "KIParticipationRate": 0.0, + "NotKIParticipationRate": null, + "IsDiscreteMonitored": false, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_accumulator_option": { + "KOBarrier": null, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "AccumuTradeAmount": 0.0, + "OriginalAccumuTradeAmount": 0.0, + "AccumuType": "", + "PayoffType": null, + "Coupon": 0.0, + "CouponPercent": false, + "IsFixedCoupon": false, + "CouponDayCount": null, + "PutMultiplier": 1.0, + "CallMultiplier": 1.0, + "EarlyTerminate": false, + "SettlementMode": null, + "SettlementMode2": null, + "SettlementMode3": null, + "KnockOutDate": null, + "ForwardDateType": null, + "ForwardPriceType": null, + "AccumulatorStructureType": 0, + "Coupon2": null, + "Strike2": null, + "Strike3": null, + "Multiplier": null, + "Multiplier2": null, + "Multiplier3": null, + "KnockInOutStatusCn": "观察中", + "LatestObservationDate": null, + "StrikeGearingFactor": 1.0, + "LatestKOBarrier": null, + "latestCoupon": null, + "ForwardPriceType2": null, + "ForwardDateType2": null, + "ForwardPriceType3": null, + "ForwardDateType3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_swap": { + "AnnualDays": null, + "IsGetFloatingProfit": false, + "GetVarietyId": null, + "GetUnderlyingId": null, + "GetUnderlyingCode": null, + "GetLongShort": null, + "GetSpotPrice": null, + "GetOpenPrice": null, + "GetTradePrice": null, + "GetSwapTimeAndRate": null, + "GetSwapRate": 0.0, + "GetFixedProfit": null, + "GetSingleFee": null, + "GetUnAnnualRate": null, + "GetMarginRate": 0.0, + "GetFinalPrice": null, + "GetNotional": null, + "GetTradeAmount": null, + "GetLot": null, + "IsPayFloatingProfit": false, + "PayVarietyId": null, + "PayUnderlyingId": null, + "PayUnderlyingCode": null, + "PayLongShort": null, + "PaySpotPrice": null, + "PayOpenPrice": null, + "PayTradePrice": null, + "PaySwapTimeAndRate": null, + "PaySwapRate": 0.0, + "PayFixedProfit": null, + "PaySingleFee": null, + "RateCalcMode": "11", + "IncludeFirstDay": true, + "PayUnAnnualRate": null, + "PayMarginRate": 0.0, + "PayFinalPrice": null, + "PayNotional": null, + "PayTradeAmount": null, + "PayLot": null, + "FlowId": null, + "AnnualVarIncome": false, + "SwapType": null, + "SettlementPayType": 0, + "IsTradePriceWhenOpen": false, + "IsShare": false, + "OriginalTradeId": null, + "GetCountRatio": 0.0, + "PayCountRatio": 0.0, + "GetContractSize": 0.0, + "PayContractSize": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "get_trade_swap_details": [], + "pay_trade_swap_details": [], + "trade_underlying_enhance": { + "AnnualizedEnhanceRate": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_cashflow": { + "ProfitRate": 0.0, + "RateType": 0, + "DepositType": 0, + "PrepayRatio": 0.0, + "ProfitDayCount": "Act365", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_custom": { + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_extend": { + "TradeId": 0, + "ExtendJson": null, + "CompanyJson": null, + "ExtendObj": { + "FlowBookMode": 0, + "FloatingPnlAnnualized": false, + "NeedOpenFee": false, + "OpenFeeType": 0, + "AnnualDays": 365, + "InterestCalcMode": "11", + "Direction": 0, + "SettlementRules": 0, + "DividendPayDate": 1 + } + }, + "swap_positions": [], + "inital_eod_swap_positions": [], + "eod_swap_positions": [], + "trade_Initial_Margin": { + "TradeId": 0, + "Direction": 1, + "MarginType": 0, + "MarginValue": 0.0 + }, + "swap_Flow_Events": [], + "swap_Events": [], + "eod_swaps": [], + "ClientCashInCashOutList": [], + "SalesCommission": null, + "trade_cash": null, + "RemarkInfo": "到期付息(手动付息后次日部分平仓)", + "eod_trade": null, + "SettlementAmount": null, + "ActualExerciseDate": null, + "trade_swap_gj": { + "LongShort": null, + "OpenPrice": null, + "OpenCurrencyRate": null, + "TradeCommission": null, + "PricingOriginalStockEqvNotional": null, + "PricingStockEqvNotional": null, + "SwapTimeAndRate": null + }, + "Tags": null, + "OutputTags": null, + "SaveExt": null, + "TradeType": "收益互换", + "TradeSavedVol": null, + "CreatorId": null, + "CreatorName": null, + "CreateDate": "2026-07-01T20:58:14", + "PairTrade": "", + "TradeSinglePrice": null, + "GroupId": null, + "GroupName": "", + "NumOfSmoothingDays": null, + "ParentTradeId": 0, + "TradeCloseVolatility": null, + "TradeOpenVolatility": null, + "UnWindDate": "2026-03-03T00:00:00", + "UnWindNotional": null, + "CheckStatus": 0, + "CheckTradeUpdate": 1, + "InitialSpotPrice": 1.000004, + "IsMoneynessOption": null, + "TradeAmount": 50000000.0, + "TradeUnit": "", + "StockEqvNotional": 50500000.0, + "StockEqvNotionalMax": null, + "StockEqvNotionalReal": 50500000.0, + "VolType": null, + "UnderlyingInstrumentType": "TBonds", + "ExerciseDate": "2026-12-31T00:00:00", + "TraderName": "唐善洁", + "TraderId": 164, + "Vol": null, + "Day1Pnl": null, + "MidVol": null, + "ExchangeRate": null, + "Strike": null, + "UnderlyingId": 140677, + "BasisUnderlyingId": null, + "BasisUnderlyingCode": null, + "BasisGap": null, + "ExchangeOptionCode": null, + "AssetBookName": "测试簿记", + "AssetId": 40, + "Notional": 50000000.0, + "OptionType": null, + "PricingModel": null, + "ExerciseMode": null, + "NoRiskRate": null, + "SpotPrice": 1.000004, + "TradeNumber": "GLMS-20260105-0007", + "ClientId": 43, + "ClientName": "善洁测试用产品二号", + "UnderlyingCode": "230004.IB", + "UnderlyingAssetClass": "其他市场债券", + "TradeDate": "2026-01-05T00:00:00", + "BuySell": "卖出", + "StartDate": "2026-01-05T00:00:00", + "MaturityDate": null, + "ObservationDateStr": null, + "TradePrice": 0.0, + "AccurateTradePrice": null, + "TradeStatus": "确认成交", + "ProcessStatus": "通过审批", + "ProcessOrderId": -2, + "ProcessOrderBranch": 0, + "ProcessOptDate": "2026-07-01T23:36:20", + "SettlementType": 0, + "ValidState": "Valid", + "Comments": "到期付息(手动付息后次日部分平仓)", + "SentMailCount": null, + "Lots": null, + "HasPartialUnWind": 1, + "OriginalNotional": 0.0, + "TradeSource": "系统交易", + "OriginalStockEqvNotional": 50500000.0, + "OriginalStockEqvNotionalV2": 50500000.0, + "IsUsePremiumRate": true, + "IsTradePricePayType": true, + "IsAnnualized": false, + "AnnualizeFactor": null, + "DividendRate": null, + "ParticipationRate": 1.0, + "PrincipalRate": null, + "OriginalPrincipalSum": null, + "PrincipalRateWrite": null, + "SinglePrincipalWrite": null, + "SettlementDate": null, + "SettlementFlag": 0, + "SettlementFlagStr": "否", + "SettlementFlagDate": null, + "SettlementFlagOptId": null, + "SettlementFlagOptName": null, + "CalcFlag": 0, + "IsSingleContract": null, + "PremiumPayDate": null, + "PremiumRate": null, + "UnderlyingAssetName": "23附息国债04", + "UnderlyingVariety": null, + "ContractVersion": null, + "FinalPrice": null, + "OpponentRole": "乙方", + "DurationDays": null, + "StructureType": "普通债券类收益互换", + "InitialMargin": 0.0, + "StructureIntroduction": null, + "MarginTemplateName": "系统默认", + "MarginType": 0, + "MarginRate": 0.0, + "PositionMarginRate": 0.0, + "QuoteCurrency": null, + "SettlementCurrency": null, + "IsGroup": 0, + "ExtendInfo": null, + "IsNight": false, + "Propertys": [], + "ExtendOptionTypeInfo": null, + "AccumulatorOptionId": null, + "OptId": 164, + "OptName": "唐善洁", + "OptDate": "2026-07-01T23:36:20", + "IsApproval": false, + "IsAutoGenerate": null, + "HTStatus": null, + "Warning": false, + "DividendDate": "2000-01-01T00:00:00", + "CountRatio": 1, + "CallPut": "", + "IsMoneynessOptionData": false, + "MetaDic": {}, + "TradeExtendJson": null, + "TradeMultipleType": "收益互换", + "Fixing": null, + "id": 1924, + "EncryptId": "LKPDwn1OC-Vzkg-aD-cnDA" + }, + "Position": { + "PositionId": 0, + "SwapTradeId": 1924, + "PosiDirection": 2, + "PositionType": 1, + "UnderlyingCode": "230004.IB", + "underlying": null, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 1.01000400000, + "PosiGrossPrice": 1.01000000000, + "PosiNetFeePrice": 1.00000400000, + "PosiNetNoFeePrice": 1.00000000000, + "PosiNotionalValue": 50500000.000000, + "PosiQuantity": 50000000.000000, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-01-05T00:00:00", + "PosiMatuirityDate": "2026-12-31T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 200.000000, + "InterestDirection": 0, + "InterestRateDefault": 0.0, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "InterestMode": 0, + "InterestModeStr": "Unknown", + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": true, + "IsAnnualized": false, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260105-0007-35148", + "UnderlyingInstrumentType": "TBonds", + "InitYtm": 0.025000, + "Invalid": false, + "interest_rest_days": 7, + "interest_rule": null, + "SwapIntervalList": [], + "Obervation": null, + "PositionIdPadding": "00000000", + "id": 35148, + "EncryptId": "bwG1iqm2wVDuqrh3JW4jCg", + "OptId": 164, + "OptName": "唐善洁", + "OptTime": "2026-07-01T21:20:32" + }, + "PreSwapEod": { + "ValueDate": "2026-03-02T00:00:00", + "ClientId": 43, + "SwapTradeId": 1924, + "PositionId": 35148, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "230004.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.01000400000, + "PosiGrossPrice": 1.01000000000, + "PosiQuantity": 50000000.000000, + "PosiNotionalValue": 50500000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-01-05T00:00:00", + "PosiMatuirityDate": "2026-12-31T00:00:00", + "UnderlyingPrice": 1.02000000000, + "UnderlyingMarketValue": 51000000.000000, + "TdPosiDividend": -90400.000000, + "PosiMtmPnL": -500000.000000, + "PosiDividendSum": -90400.000000, + "PosiFeePending": 200.000000, + "PosiProfitSum": -590200.000000, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": -590200.000000, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000400000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 400.000000, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00035148", + "id": 403417, + "EncryptId": "2LJcLhCf_19_0FHneAgs2Q", + "OptId": 164, + "OptName": "唐善洁", + "OptTime": "2026-07-02T00:17:55" + }, + "SwapFlowEvents": [ + { + "EventDate": "2026-03-03T00:00:00", + "SwapTradeId": 1924, + "SwapTradeNo": null, + "SwapPositionIdPadding": "00035148", + "EventType": 3, + "EventTypeStr": "互换", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "230004.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 51005202.000000, + "TradingAmountAvg": 1.01000400000, + "TradingAmountFeeAvg": 1.01000400000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": -400.000000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 35148, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 20929, + "ClientId": 43, + "FloatRate": null, + "PayDate": "2026-03-03T00:00:00", + "TradingAmountNetFeeAvg": 1.00000400000, + "TradingAmountNetAvg": 1.00000000000, + "UnwindDate": "2026-03-03T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "FloatPnlSum": -400.000000, + "InitYtm": null, + "id": 16219, + "EncryptId": "63o4MwWpLrn3ImiMTmCuXQ", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + } + ] + } +} \ No newline at end of file diff --git a/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1875.json b/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1875.json new file mode 100644 index 00000000..cf848f75 --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1875.json @@ -0,0 +1,4070 @@ +{ + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "UnderlyingCode": "173982.IB", + "RecordedAt": "2026-06-26T09:05:16", + "SourceDb": "test", + "Purpose": "分红重复计算验证 + EOD重构回归基线", + "InputTrade": { + "SubTrades": null, + "YesterDayTradeSavedVol": null, + "PV": null, + "RealizedPnl": null, + "PositionPnl": null, + "MaturityWorkDay": 8, + "PairTradeNumber": null, + "ShowNotional": 0.0, + "MaturityDay": 12, + "TradeOpenVolatilityString": "0.00%", + "InitialSpotPriceOriginal": null, + "TradeOriginalAmount": null, + "ExerciseDateString": "2026-07-08", + "ActualStrike": null, + "StrikeString": "0.000000000", + "TradeSinglePriceString": "0.00%", + "TradeDateString": "2026-06-12", + "LotsNewInfo": null, + "UnWindPrice": null, + "StockEqvNotionalToShow": 10000000.0, + "SettlementTypeDesc": "收盘价", + "SettlementDesc": null, + "VarietyId": null, + "SyntheticUnderlyingTipsInfo": null, + "StrikeToShow": "0.000000000", + "CurNotional": 0.0, + "LastDayNotional": 0.0, + "Amount": null, + "ContractCode": null, + "InitialMarginRatio": null, + "UnderlyingPrice": null, + "UnderlyingName": null, + "DeltaInLots": null, + "ValueStatus": null, + "UnWindUnderlyingPrice": null, + "UnWindTradePrice": null, + "UnWindTotalAmount": null, + "UnWindFee": null, + "UnWindTimes": 0, + "KnockInOutStatusObservation": null, + "DividendRatio": 0.0, + "InitYtm": 0.015000, + "CalcId": null, + "TTMDays": null, + "UnderlyingInstrumentTypeCn": "利率债", + "ExerciseModeCn": "", + "ClientNumber": null, + "SummaryType": "收益互换", + "QuoteUnitSingle": null, + "QuoteUnit": null, + "trade_forward": { + "id": 0, + "TradeId": 0, + "OpenCommission": 0.0, + "AnnualMarginRate": 0.0, + "AnnualStoragePrice": 0.0, + "ForwardValue": 0.0, + "ObservationStart": null, + "ObservationDates": null, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_asian_option": { + "StrikeType": null, + "PayoffType": null, + "SettleMode": null, + "StrikeGearingFactor": 1.0, + "AveragingPeriodStartDate": null, + "ObservationDates": null, + "EnhancedPrice": 0.0, + "StrikeTypeCn": "", + "PayoffTypeCn": "", + "Fixings": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rainbow_option": { + "UnderlyingAssetCode": null, + "Strike": null, + "UnderlyingMaturityDate": null, + "UnderlyingId1": null, + "UnderlyingId2": null, + "Strike2": null, + "SpotPrice1": null, + "SpotPrice2": null, + "RainbowType": null, + "UnderlyingAssetCode2": null, + "CorRelation": null, + "CashAmount": null, + "Vol2": 0.0, + "UnderlyingCodes": [ + null, + null + ], + "Strikes": [ + 0.0, + 0.0 + ], + "SpotPrices": [ + 0.0, + 0.0 + ], + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_barrier_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "BarrierShift": null, + "BarrierTypeEn": null, + "IsDiscrete": false, + "BarrierPrice": null, + "UpperBarrierPrice": null, + "Discrete": null, + "BarrierType": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateAnnualizedAtKO": false, + "RebateDayCount": null, + "ObservationDates": null, + "LatestObservationDate": null, + "RebateType": null, + "RebateTypeCn": "", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_risky_option": { + "Strike1": null, + "Strike2": null, + "Strike3": null, + "ParticipationRate1": null, + "ParticipationRate2": null, + "ParticipationRate3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_binary_option": { + "RebateType": null, + "RebateTypeCn": "", + "CashOrNothingAmount": null, + "CashOrNothingAmountHigh": null, + "CashOrNothingAmountRate": null, + "CashOrNothingAmountHighRate": null, + "PayoffType": "", + "UpperBarrier": null, + "RebateDayCount": null, + "RebateAnnualizedAtKO": false, + "ObservationDates": null, + "LatestObservationDate": null, + "UpperBarrierRelative": "0", + "MonitorType": null, + "IsDiscreteMonitored": false, + "Offset": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_spread_option": { + "UnderlyingMaturityDate2": null, + "UnderlyingMaturityDate3": null, + "UnderlyingMaturityDate4": null, + "Payoff": "S1-S2", + "UnderlyingId1": null, + "UnderlyingId2": null, + "UnderlyingId3": null, + "UnderlyingId4": null, + "UnderlyingAssetClass1": null, + "UnderlyingAssetClass2": null, + "UnderlyingAssetClass3": null, + "UnderlyingAssetClass4": null, + "UnderlyingAssetCode1": null, + "UnderlyingAssetCode2": null, + "UnderlyingAssetCode3": null, + "UnderlyingAssetCode4": null, + "UnderlyingAssetName1": null, + "UnderlyingAssetName2": null, + "UnderlyingAssetName3": null, + "UnderlyingAssetName4": null, + "TradeCloseVolatility1": null, + "TradeCloseVolatility2": null, + "TradeCloseVolatility3": null, + "TradeCloseVolatility4": null, + "TradeOpenVolatility1": null, + "TradeOpenVolatility2": null, + "TradeOpenVolatility3": null, + "TradeOpenVolatility4": null, + "NumOfSmoothingDays1": null, + "NumOfSmoothingDays2": null, + "NumOfSmoothingDays3": null, + "NumOfSmoothingDays4": null, + "Vol1": null, + "Vol2": null, + "Vol3": null, + "Vol4": null, + "Correlation12": null, + "Correlation13": null, + "Correlation14": null, + "Correlation23": null, + "Correlation24": null, + "Correlation34": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_double_sharkfin_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "IsDiscrete": false, + "BarrierHigh": 0.0, + "BarrierLow": 0.0, + "StrikeHigh": null, + "StrikeLow": null, + "CallParticipationRate": null, + "PutParticipationRate": null, + "Discrete": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateType": null, + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_autocall": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "CouponBarrier": 0.0, + "Coupon": 0.0, + "IsFixedCoupon": false, + "CouponPayType": 0, + "SpreadStrike1": null, + "SpreadStrike": null, + "CouponPayAtMaturity": false, + "IncludeCouponAfterKI": false, + "KOObservationDates": null, + "CouponObservation": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "CouponDayCount": "Act365", + "HappenedObservations": null, + "KIPayoffType": 0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_snowball": { + "IsInitialKnockedIn": false, + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "Coupon": 0.0, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "KORebate": 0.0, + "KORebateType": 0, + "IsFixedCoupon": false, + "SpreadStrikeAtKO": null, + "SpreadStrikeAtKO1": null, + "SpreadStrikeAtMaturity1": null, + "SpreadStrikeAtMaturity": null, + "KOPayoffType": 0, + "KIPayoffType": 0, + "AnnualizedPremiumRate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "InitialAdvance": null, + "PeriodAdvance": null, + "FontEarning": null, + "ConfirmedLine": null, + "ConfirmedFloor": null, + "CouponDayCount": null, + "PrepaymentUsed": false, + "PrepaymentRatio": null, + "PrepaymentInterestRate": null, + "PrepaymentConvertCashRate": null, + "CouponIncludeEndDate": null, + "EnhancedParticipationRate": null, + "KIParticipationRate": null, + "PrincipalProtectionRate": null, + "KIObservationType": 0, + "KOBarrierAdjustStep": 0.0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rangeaccrual": { + "LowerRange": 0.0, + "UpperRange": 0.0, + "BonusRate": 0.0, + "ObservationDates": null, + "LatestObservationDate": null, + "HappenedObservations": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_airbag": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "HighStrike": 0.0, + "HasPayoffLimit": false, + "Barrier": 0.0, + "KIParticipationRate": 0.0, + "NotKIParticipationRate": null, + "IsDiscreteMonitored": false, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_accumulator_option": { + "KOBarrier": null, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "AccumuTradeAmount": 0.0, + "OriginalAccumuTradeAmount": 0.0, + "AccumuType": "", + "PayoffType": null, + "Coupon": 0.0, + "CouponPercent": false, + "IsFixedCoupon": false, + "CouponDayCount": null, + "PutMultiplier": 1.0, + "CallMultiplier": 1.0, + "EarlyTerminate": false, + "SettlementMode": null, + "SettlementMode2": null, + "SettlementMode3": null, + "KnockOutDate": null, + "ForwardDateType": null, + "ForwardPriceType": null, + "AccumulatorStructureType": 0, + "Coupon2": null, + "Strike2": null, + "Strike3": null, + "Multiplier": null, + "Multiplier2": null, + "Multiplier3": null, + "KnockInOutStatusCn": "观察中", + "LatestObservationDate": null, + "StrikeGearingFactor": 1.0, + "LatestKOBarrier": null, + "latestCoupon": null, + "ForwardPriceType2": null, + "ForwardDateType2": null, + "ForwardPriceType3": null, + "ForwardDateType3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_swap": { + "AnnualDays": null, + "IsGetFloatingProfit": false, + "GetVarietyId": null, + "GetUnderlyingId": null, + "GetUnderlyingCode": null, + "GetLongShort": null, + "GetSpotPrice": null, + "GetOpenPrice": null, + "GetTradePrice": null, + "GetSwapTimeAndRate": null, + "GetSwapRate": 0.0, + "GetFixedProfit": null, + "GetSingleFee": null, + "GetUnAnnualRate": null, + "GetMarginRate": 0.0, + "GetFinalPrice": null, + "GetNotional": null, + "GetTradeAmount": null, + "GetLot": null, + "IsPayFloatingProfit": false, + "PayVarietyId": null, + "PayUnderlyingId": null, + "PayUnderlyingCode": null, + "PayLongShort": null, + "PaySpotPrice": null, + "PayOpenPrice": null, + "PayTradePrice": null, + "PaySwapTimeAndRate": null, + "PaySwapRate": 0.0, + "PayFixedProfit": null, + "PaySingleFee": null, + "RateCalcMode": "11", + "IncludeFirstDay": true, + "PayUnAnnualRate": null, + "PayMarginRate": 0.0, + "PayFinalPrice": null, + "PayNotional": null, + "PayTradeAmount": null, + "PayLot": null, + "FlowId": null, + "AnnualVarIncome": false, + "SwapType": null, + "SettlementPayType": 0, + "IsTradePriceWhenOpen": false, + "IsShare": false, + "OriginalTradeId": null, + "GetCountRatio": 0.0, + "PayCountRatio": 0.0, + "GetContractSize": 0.0, + "PayContractSize": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "get_trade_swap_details": [], + "pay_trade_swap_details": [], + "trade_underlying_enhance": { + "AnnualizedEnhanceRate": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_cashflow": { + "ProfitRate": 0.0, + "RateType": 0, + "DepositType": 0, + "PrepayRatio": 0.0, + "ProfitDayCount": "Act365", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_custom": { + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_extend": { + "TradeId": 0, + "ExtendJson": null, + "CompanyJson": null, + "ExtendObj": { + "FlowBookMode": 0, + "FloatingPnlAnnualized": false, + "NeedOpenFee": false, + "OpenFeeType": 0, + "AnnualDays": 365, + "InterestCalcMode": "11", + "Direction": 0, + "SettlementRules": 0, + "DividendPayDate": 1 + } + }, + "swap_positions": [], + "inital_eod_swap_positions": [], + "eod_swap_positions": [], + "trade_Initial_Margin": { + "TradeId": 0, + "Direction": 1, + "MarginType": 0, + "MarginValue": 0.0 + }, + "swap_Flow_Events": [], + "swap_Events": [], + "eod_swaps": [], + "ClientCashInCashOutList": [], + "SalesCommission": null, + "trade_cash": null, + "RemarkInfo": null, + "eod_trade": null, + "SettlementAmount": null, + "ActualExerciseDate": null, + "trade_swap_gj": { + "LongShort": null, + "OpenPrice": null, + "OpenCurrencyRate": null, + "TradeCommission": null, + "PricingOriginalStockEqvNotional": null, + "PricingStockEqvNotional": null, + "SwapTimeAndRate": null + }, + "Tags": null, + "OutputTags": null, + "SaveExt": null, + "TradeType": "收益互换", + "TradeSavedVol": null, + "CreatorId": null, + "CreatorName": null, + "CreateDate": "2026-06-23T17:18:49", + "PairTrade": "", + "TradeSinglePrice": null, + "GroupId": null, + "GroupName": "", + "NumOfSmoothingDays": null, + "ParentTradeId": 0, + "TradeCloseVolatility": null, + "TradeOpenVolatility": null, + "UnWindDate": "2026-06-25T00:00:00", + "UnWindNotional": null, + "CheckStatus": 0, + "CheckTradeUpdate": 1, + "InitialSpotPrice": 1.0, + "IsMoneynessOption": null, + "TradeAmount": 0.0, + "TradeUnit": "", + "StockEqvNotional": 0.0, + "StockEqvNotionalMax": null, + "StockEqvNotionalReal": 10000000.0, + "VolType": null, + "UnderlyingInstrumentType": "TBonds", + "ExerciseDate": "2026-07-08T00:00:00", + "TraderName": "初始用户", + "TraderId": 1, + "Vol": null, + "Day1Pnl": null, + "MidVol": null, + "ExchangeRate": null, + "Strike": null, + "UnderlyingId": 618857, + "BasisUnderlyingId": null, + "BasisUnderlyingCode": null, + "BasisGap": null, + "ExchangeOptionCode": null, + "AssetBookName": "testjia1", + "AssetId": 38, + "Notional": 0.0, + "OptionType": null, + "PricingModel": null, + "ExerciseMode": null, + "NoRiskRate": null, + "SpotPrice": 1.0, + "TradeNumber": "GLMS-20260612-0003", + "ClientId": 14, + "ClientName": "testjia", + "UnderlyingCode": "173982.IB", + "UnderlyingAssetClass": "其他市场债券", + "TradeDate": "2026-06-12T00:00:00", + "BuySell": "卖出", + "StartDate": "2026-06-15T00:00:00", + "MaturityDate": null, + "ObservationDateStr": null, + "TradePrice": 0.0, + "AccurateTradePrice": null, + "TradeStatus": "已平仓", + "ProcessStatus": "通过审批", + "ProcessOrderId": -2, + "ProcessOrderBranch": 0, + "ProcessOptDate": "2026-06-25T21:00:51", + "SettlementType": 0, + "ValidState": "Valid", + "Comments": null, + "SentMailCount": null, + "Lots": null, + "HasPartialUnWind": null, + "OriginalNotional": 0.0, + "TradeSource": "系统交易", + "OriginalStockEqvNotional": 10000000.0, + "OriginalStockEqvNotionalV2": 10000000.0, + "IsUsePremiumRate": true, + "IsTradePricePayType": true, + "IsAnnualized": false, + "AnnualizeFactor": null, + "DividendRate": null, + "ParticipationRate": 1.0, + "PrincipalRate": null, + "OriginalPrincipalSum": null, + "PrincipalRateWrite": null, + "SinglePrincipalWrite": null, + "SettlementDate": null, + "SettlementFlag": 0, + "SettlementFlagStr": "否", + "SettlementFlagDate": null, + "SettlementFlagOptId": null, + "SettlementFlagOptName": null, + "CalcFlag": 0, + "IsSingleContract": null, + "PremiumPayDate": null, + "PremiumRate": null, + "UnderlyingAssetName": "23湖北03", + "UnderlyingVariety": null, + "ContractVersion": null, + "FinalPrice": null, + "OpponentRole": "乙方", + "DurationDays": null, + "StructureType": "普通债券类收益互换", + "InitialMargin": 0.0, + "StructureIntroduction": null, + "MarginTemplateName": "系统默认", + "MarginType": 0, + "MarginRate": 0.0, + "PositionMarginRate": 0.0, + "QuoteCurrency": null, + "SettlementCurrency": null, + "IsGroup": 0, + "ExtendInfo": null, + "IsNight": false, + "Propertys": [], + "ExtendOptionTypeInfo": null, + "AccumulatorOptionId": null, + "OptId": 166, + "OptName": "贾春梅", + "OptDate": "2026-06-25T21:00:51", + "IsApproval": false, + "IsAutoGenerate": null, + "HTStatus": null, + "Warning": false, + "DividendDate": "2000-01-01T00:00:00", + "CountRatio": 1, + "CallPut": "", + "IsMoneynessOptionData": false, + "MetaDic": {}, + "TradeExtendJson": null, + "TradeMultipleType": "收益互换", + "Fixing": null, + "id": 1875, + "EncryptId": "ZFzhegn1W9Y9Rob7IHpYmg" + }, + "InputPositions": [ + { + "PositionId": 0, + "SwapTradeId": 1875, + "PosiDirection": 0, + "PositionType": 0, + "UnderlyingCode": null, + "underlying": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiNetFeePrice": 0.0, + "PosiNetNoFeePrice": 0.0, + "PosiNotionalValue": 0.0, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 1, + "InterestRateDefault": 0.003000, + "FloatRate": 0.010000, + "FloatRateUnderlyingCode": "FR007", + "InterestMode": 9, + "InterestModeStr": "标的期初全价", + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": true, + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260612-0003-34611", + "UnderlyingInstrumentType": null, + "InitYtm": null, + "Invalid": false, + "interest_rest_days": 1, + "interest_rule": 0, + "SwapIntervalList": [ + { + "Date": "2026-07-07T00:00:00", + "Rate": 0.003, + "Settlement": 0, + "SettlementDate": null + } + ], + "Obervation": null, + "PositionIdPadding": "00000000", + "id": 34611, + "EncryptId": "YgKjgWVpN0h3dvT3Eyur-g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:12" + }, + { + "PositionId": 0, + "SwapTradeId": 1875, + "PosiDirection": 2, + "PositionType": 2, + "UnderlyingCode": "173982.IB", + "underlying": null, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "PosiNotionalValue": 10000000.000000, + "PosiQuantity": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 0, + "InterestRateDefault": 0.0, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "InterestMode": 0, + "InterestModeStr": "Unknown", + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": true, + "IsAnnualized": false, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260612-0003-34612", + "UnderlyingInstrumentType": "TBonds", + "InitYtm": 0.015000, + "Invalid": false, + "interest_rest_days": null, + "interest_rule": null, + "SwapIntervalList": [], + "Obervation": null, + "PositionIdPadding": "00000000", + "id": 34612, + "EncryptId": "JpHsC6ca-d-EN9GQYfYTSw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:12" + }, + { + "PositionId": 34611, + "SwapTradeId": 1875, + "PosiDirection": 0, + "PositionType": 0, + "UnderlyingCode": null, + "underlying": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiNetFeePrice": 0.0, + "PosiNetNoFeePrice": 0.0, + "PosiNotionalValue": 0.0, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 1, + "InterestRateDefault": 0.003000, + "FloatRate": 0.000100, + "FloatRateUnderlyingCode": "FR007", + "InterestMode": 9, + "InterestModeStr": "标的期初全价", + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "InterestAmount": 6007.070000, + "IsInitial": false, + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260612-0003-34611", + "UnderlyingInstrumentType": null, + "InitYtm": null, + "Invalid": false, + "interest_rest_days": 1, + "interest_rule": 0, + "SwapIntervalList": [ + { + "Date": "2026-07-07T00:00:00", + "Rate": 0.003, + "Settlement": 0, + "SettlementDate": null + } + ], + "Obervation": null, + "PositionIdPadding": "00034611", + "id": 34732, + "EncryptId": "5MxbrDljZUb0Kyxym47x6g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:12" + }, + { + "PositionId": 34612, + "SwapTradeId": 1875, + "PosiDirection": 2, + "PositionType": 2, + "UnderlyingCode": "173982.IB", + "underlying": null, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "PosiNotionalValue": 0.0, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 0, + "InterestRateDefault": 0.0, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "InterestMode": 0, + "InterestModeStr": "Unknown", + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": false, + "IsAnnualized": false, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260612-0003-34612", + "UnderlyingInstrumentType": "TBonds", + "InitYtm": 0.015000, + "Invalid": false, + "interest_rest_days": null, + "interest_rule": null, + "SwapIntervalList": [], + "Obervation": null, + "PositionIdPadding": "00034612", + "id": 34733, + "EncryptId": "MbspXF0Mu80IKlE3vLz3uA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:12" + } + ], + "InputFlowEvents": [ + { + "EventDate": "2026-06-12T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034611", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": "2026-07-08T00:00:00", + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34611, + "DataState": 0, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 0.0, + "InterestRate": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07\",\"Rate\":0.003,\"Settlement\":0}]", + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-15T00:00:00", + "TradingAmountNetFeeAvg": 0.0, + "TradingAmountNetAvg": 0.0, + "UnwindDate": "2026-06-15T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [ + { + "Date": "2026-07-07T00:00:00", + "Rate": 0.003, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12402, + "EncryptId": "mJ2Opy6WEn_5ggIxtPMw7g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-23T17:19:06" + }, + { + "EventDate": "2026-06-12T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034612", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "173982.IB", + "MatuirityDate": "2026-07-08T00:00:00", + "Quantity": 10000000.000000, + "PositionQty": 10000000.000000, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 10000000.000000, + "TradingAmountAvg": 1.00000000000, + "TradingAmountFeeAvg": 1.00000000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34612, + "DataState": 0, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-15T00:00:00", + "TradingAmountNetFeeAvg": 1.00000000000, + "TradingAmountNetAvg": 1.00000000000, + "UnwindDate": "2026-06-15T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12403, + "EncryptId": "wf7HDtG5m-A5VS9bCyVqHg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-23T17:19:06" + }, + { + "EventDate": "2026-06-12T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034611", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": "2026-07-08T00:00:00", + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34611, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 0.0, + "InterestRate": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-15T00:00:00", + "TradingAmountNetFeeAvg": 0.0, + "TradingAmountNetAvg": 0.0, + "UnwindDate": "2026-06-15T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [ + { + "Date": "2026-07-07T00:00:00", + "Rate": 0.003, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12833, + "EncryptId": "5ZXliC2vgiujaZoWgkfsog", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:29" + }, + { + "EventDate": "2026-06-12T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034612", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "MatuirityDate": "2026-07-08T00:00:00", + "Quantity": 10000000.000000, + "PositionQty": 10000000.000000, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 10000000.000000, + "TradingAmountAvg": 1.00000000000, + "TradingAmountFeeAvg": 1.00000000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34612, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-15T00:00:00", + "TradingAmountNetFeeAvg": 1.00000000000, + "TradingAmountNetAvg": 1.00000000000, + "UnwindDate": "2026-06-15T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12834, + "EncryptId": "Be-aL5PS8KjDoEou-sztWw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T14:27:29" + }, + { + "EventDate": "2026-06-16T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034612", + "EventType": 4, + "EventTypeStr": "自动互换", + "EventReason": "系统操作-分红", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "173982.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 10000000.000000, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 1.00000000000, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": -100800.000000, + "DividendIn": -100800.000000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34612, + "DataState": 0, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": null, + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17970, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-16T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-16T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": -100800.000000, + "InitYtm": null, + "id": 12805, + "EncryptId": "z4oNoiXTV16C_b6wzvBe4Q", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-16T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034612", + "EventType": 4, + "EventTypeStr": "自动互换", + "EventReason": "系统操作-分红", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 10000000.000000, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 1.00000000000, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 100800.000000, + "DividendIn": 100800.000000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34612, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 18623, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-16T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": 1.00000000000, + "UnwindDate": "2026-06-16T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": 100800.000000, + "InitYtm": null, + "id": 13487, + "EncryptId": "ybacgt9YYQNmG_G-6rwbRQ", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-25T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034611", + "EventType": 2, + "EventTypeStr": "平仓", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34611, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 10005922.089193, + "InterestRate": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestAmount": 6007.070000, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 6007.070000, + "EventId": 18595, + "ClientId": 14, + "FloatRate": 0.000100, + "PayDate": "2026-06-25T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-25T00:00:00", + "OptLog": null, + "SwapIntervalList": [ + { + "Date": "2026-07-07T00:00:00", + "Rate": 0.003, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 13456, + "EncryptId": "CvxbXk8b9JG50zjZtF1imQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:00:32" + }, + { + "EventDate": "2026-06-25T00:00:00", + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "SwapPositionIdPadding": "00034612", + "EventType": 2, + "EventTypeStr": "平仓", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "MatuirityDate": null, + "Quantity": 10000000.000000, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 10000000.000000, + "TradingAmountAvg": 1.00000000000, + "TradingAmountFeeAvg": 1.00000000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 201600.000000, + "DividendIn": 201600.000000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34612, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 18595, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-25T00:00:00", + "TradingAmountNetFeeAvg": 1.00000000000, + "TradingAmountNetAvg": 1.00000000000, + "UnwindDate": "2026-06-25T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": 201600.000000, + "InitYtm": null, + "id": 13457, + "EncryptId": "Yy0iP3gWxy13cBwAMuokag", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T20:58:45" + } + ], + "InputEodPositions": [ + { + "ValueDate": "2026-06-12T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000000.000000, + "TdInterestRate": 0.003000, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.026000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347094, + "EncryptId": "DYZ3pOqgHgsYQQXTtpnzaA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-13T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000000.000000, + "TdInterestRate": 0.003000, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.026000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347118, + "EncryptId": "bxovda2dYdlSz_DJpD_nBQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-14T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000000.000000, + "TdInterestRate": 0.003000, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.026000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347142, + "EncryptId": "KoqQNjotKWKClDDocuBlag", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-15T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000000.000000, + "TdInterestRate": 0.003000, + "TdInterestIncome": 739.726027, + "TdInterestFee": 0.0, + "InterestIncomeSum": 739.726027, + "InterestFeeSum": 0.0, + "InterestProfitSum": 739.726027, + "SwapPositionValue": 739.726027, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347166, + "EncryptId": "XcfIsZUEYNd8K_ngWYEnpw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-16T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000739.726027, + "TdInterestRate": 0.003000, + "TdInterestIncome": 112.337076, + "TdInterestFee": 0.0, + "InterestIncomeSum": 852.063103, + "InterestFeeSum": 0.0, + "InterestProfitSum": 852.063103, + "SwapPositionValue": 852.063103, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.001100, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347190, + "EncryptId": "ZSkkC3H5ndw0UmOi9_mw2Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-17T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10000852.063103, + "TdInterestRate": 0.003000, + "TdInterestIncome": 712.389462, + "TdInterestFee": 0.0, + "InterestIncomeSum": 1564.452565, + "InterestFeeSum": 0.0, + "InterestProfitSum": 1564.452565, + "SwapPositionValue": 1564.452565, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.023000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347218, + "EncryptId": "N7eyGJYkQdYGFhhEb548eA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10001564.452565, + "TdInterestRate": 0.003000, + "TdInterestIncome": 739.841754, + "TdInterestFee": 0.0, + "InterestIncomeSum": 2304.294319, + "InterestFeeSum": 0.0, + "InterestProfitSum": 2304.294319, + "SwapPositionValue": 2304.294319, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347254, + "EncryptId": "YggNW67MGQneix1DHa4cFQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10002304.294319, + "TdInterestRate": 0.003000, + "TdInterestIncome": 739.896482, + "TdInterestFee": 0.0, + "InterestIncomeSum": 3044.190801, + "InterestFeeSum": 0.0, + "InterestProfitSum": 3044.190801, + "SwapPositionValue": 3044.190801, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347290, + "EncryptId": "L89EIkB_rHqrOSFjF_LALQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10003044.190801, + "TdInterestRate": 0.003000, + "TdInterestIncome": 739.951214, + "TdInterestFee": 0.0, + "InterestIncomeSum": 3784.142015, + "InterestFeeSum": 0.0, + "InterestProfitSum": 3784.142015, + "SwapPositionValue": 3784.142015, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347326, + "EncryptId": "vcZD92tss_2STkBJ7wQ9Sw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10003784.142015, + "TdInterestRate": 0.003000, + "TdInterestIncome": 740.005950, + "TdInterestFee": 0.0, + "InterestIncomeSum": 4524.147965, + "InterestFeeSum": 0.0, + "InterestProfitSum": 4524.147965, + "SwapPositionValue": 4524.147965, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347362, + "EncryptId": "OOU25kA-EGL5ddFPllcvVw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10004524.147965, + "TdInterestRate": 0.003000, + "TdInterestIncome": 685.241380, + "TdInterestFee": 0.0, + "InterestIncomeSum": 5209.389345, + "InterestFeeSum": 0.0, + "InterestProfitSum": 5209.389345, + "SwapPositionValue": 5209.389345, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.022000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347398, + "EncryptId": "wLHQb4sQxUFObjB0q9xGIw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10005209.389345, + "TdInterestRate": 0.003000, + "TdInterestIncome": 712.699847, + "TdInterestFee": 0.0, + "InterestIncomeSum": 5922.089192, + "InterestFeeSum": 0.0, + "InterestProfitSum": 5922.089192, + "SwapPositionValue": 5922.089192, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.023000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347440, + "EncryptId": "ZavrxZ6fi7HdosGBMHGGnQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 10005922.089192, + "TdInterestRate": 0.003000, + "TdInterestIncome": 84.981804, + "TdInterestFee": 0.0, + "InterestIncomeSum": 6007.070996, + "InterestFeeSum": 0.0, + "InterestProfitSum": 6007.070996, + "SwapPositionValue": 6007.070996, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.000100, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347484, + "EncryptId": "8eydG5ez3nXCZg3qbcVtQw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34611, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.003000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-07 00:00:00\",\"Rate\":0.003,\"Settlement\":0,\"SettlementDate\":null}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.003000, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 6007.070000, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 6007.070000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 6007.070000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.010000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034611", + "id": 347528, + "EncryptId": "_TVkTrwymT-KqhRc7hkqbQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-12T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347093, + "EncryptId": "UtZUp7SBGDeg71QCx6eDkw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-13T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347117, + "EncryptId": "14G3ANEHrOyO23bvAEjMsg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-14T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347141, + "EncryptId": "ncArS7e6mTos3meyXzMAaw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-15T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347165, + "EncryptId": "4AsooSFWiCPiZ6G9TicV7Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-16T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 100800.000000, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347189, + "EncryptId": "rxg_g1VmE6yz2Zq6KSoQsw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-17T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347217, + "EncryptId": "YInPnMYSK7U0zreP09Zj2A", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347253, + "EncryptId": "xqryfFXmxUFl3Ryx79tP3A", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347289, + "EncryptId": "NibWheeK3swF8Hg0lgzUFQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347325, + "EncryptId": "s4c4AJfZ0DB14CS9wvA2YA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347361, + "EncryptId": "9Ytz3SFpbnUMIm3zsloxhg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347397, + "EncryptId": "tNJf9YIRGfOlBLqnlG_s2w", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347439, + "EncryptId": "zpIW00SYYBVFKU7cUogp9Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 10000000.000000, + "PosiNotionalValue": 10000000.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": -10000000.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 100800.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 100800.000000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347483, + "EncryptId": "WOB5pcS_b2GU1UxCetpceQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "ClientId": 14, + "SwapTradeId": 1875, + "PositionId": 34612, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 2, + "PositionTypeStr": "S", + "UnderlyingCode": "173982.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 1.00000000000, + "PosiGrossPrice": 1.00000000000, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-15T00:00:00", + "PosiMatuirityDate": "2026-07-08T00:00:00", + "UnderlyingPrice": 1.00000000000, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 10000000.000000, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 201600.000000, + "TdCloseDividend": 201600.000000, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 201600.000000, + "RealizedDividend": 302400.000000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 504000.000000, + "TdCurrency": 1.000000, + "PosiStatus": 1, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 1.00000000000, + "PosiNetNoFeePrice": 1.00000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034612", + "id": 347527, + "EncryptId": "GtrjOWcDpz4s0N3n6rnDeg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + } + ], + "InputEodSwaps": [ + { + "ValueDate": "2026-06-12T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161625, + "EncryptId": "38AA9vf21wb_aFSs_5jY_g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-13T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161636, + "EncryptId": "v8ygQnF8yRsPyA5cjGuZBA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:16" + }, + { + "ValueDate": "2026-06-14T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161647, + "EncryptId": "xRaNiM4S7F4l9yIZevA93Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-15T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 739.726027, + "PostionValue": 739.730000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161658, + "EncryptId": "RSMFwMpujriavJe7xvffmw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-16T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 852.063103, + "PostionValue": 852.060000, + "TdRealizedPnL": 100800.000000, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161669, + "EncryptId": "FDDzwHRt-cBwgwn1IZWPLg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:17" + }, + { + "ValueDate": "2026-06-17T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 1564.452565, + "PostionValue": 1564.450000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161682, + "EncryptId": "gKSHOjJpTfojlGdtdSlCYw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 2304.294319, + "PostionValue": 2304.290000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161699, + "EncryptId": "zqvq_21PjalL40HqzYbd-w", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 3044.190801, + "PostionValue": 3044.190801, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161716, + "EncryptId": "tlROQzxMzs6E7rgNbBoK2w", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 3784.142015, + "PostionValue": 3784.142015, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161733, + "EncryptId": "X-u6jhMlMxRCLYEpbE_ziQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 4524.147965, + "PostionValue": 4524.147965, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161750, + "EncryptId": "ZT4pfXnjT8jSTtAI66vRew", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 5209.389345, + "PostionValue": 5209.390000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161767, + "EncryptId": "OgmTmqz9I_RA1QntjOoxEw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 5922.089192, + "PostionValue": 5922.090000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161787, + "EncryptId": "uTDtfY7ZZZ3DRjNfFIyPlA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 10000000.000000, + "NotionalValueLong": 0.0, + "NotionalValueShort": 10000000.000000, + "MarketValueLong": 0.0, + "MarketValueShort": -10000000.000000, + "FloatingPnL": 0.0, + "InterestPnL": 6007.070996, + "PostionValue": 6007.070000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 100800.000000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161808, + "EncryptId": "5HiPn1VXaFqPUYBkYc4JIg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1875, + "SwapTradeNo": "GLMS-20260612-0003", + "StructureType": "普通债券类收益互换", + "NotionalValue": 0.0, + "NotionalValueLong": 0.0, + "NotionalValueShort": 0.0, + "MarketValueLong": 0.0, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 409207.070000, + "RealizedPnL": 510007.070000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 10000000.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161829, + "EncryptId": "8hyFnB9QlQdtutlLzS5S6Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + } + ], + "InputBondPayments": [ + { + "id": 7, + "EncryptId": "y7hK4bnJnRZ0FbfZSvslUg", + "inner_code": -1, + "underlyingCode": "173982.IB", + "security_id": null, + "symbol": null, + "coupon_rate": 2.00000000, + "payment_date_pl": "2026-06-16T00:00:00", + "payment_date": "2026-06-16T00:00:00", + "payment_interest": 1.008000000000, + "payment_parvalue": 1000.000000000000, + "paying_price": 18.000000000000, + "channel_source": "系统", + "jsid": -1, + "create_time": "2026-06-17T17:22:39", + "update_time": "2026-06-17T17:22:41" + } + ], + "Diagnosis": { + "SwapTradeId": 1875, + "交易类型": "纯分红型", + "逐持仓拆解": [ + { + "PositionId": 34611, + "盯市列合计": 0.0, + "盯市价差成分": 0.0, + "盯市费成分": 0.0, + "盯市分红成分": 0.0, + "分红列累计": 0.0, + "重复计入分红": 0.0 + }, + { + "PositionId": 34612, + "盯市列合计": 302400.000000, + "盯市价差成分": 0.0, + "盯市费成分": 0.0, + "盯市分红成分": 302400.000000, + "分红列累计": 302400.000000, + "重复计入分红": 302400.000000 + } + ], + "重复计入分红金额": 302400.000000, + "最终累计盯市已实现": 201600.000000, + "最终累计分红已实现": 302400.000000, + "最终持仓层累计已实现": 510007.070000, + "重复计算成立": true, + "结论": "⚠ 坐实重复计算:盯市列含分红成分 302400.0000(既在 RealizedMtmPnL 又在 RealizedDividend),修复后 RealizedPnl 应减少 302400.0000。类型=纯分红型。", + "Summary": "--- 分红重复计算诊断 SwapTradeId=1875 ---\n\n[流水层] 每条平仓/互换事件拆解 (MarkClosePnl = 价差 + 费CloseFee + 分红DividendIn):\n id EventDate EvType MarkClosePnl DividendIn CloseFee 价差(残差)\n 13487 2026-06-16 自动互换 100800.0000 100800.0000 0.0000 0.0000\n 13456 2026-06-25 平仓 0.0000 0.0000 0.0000 0.0000\n 13457 2026-06-25 平仓 201600.0000 201600.0000 0.0000 0.0000\n\n[EOD层] 按 PositionId 拆解盯市列成分:\n PositionId 盯市列合计 价差成分 费成分 分红成分(重复) 分红列累计 重复计入\n 34611 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n 34612 302400.0000 0.0000 0.0000 302400.0000 302400.0000 302400.0000\n\n[汇总]\n 交易类型: 纯分红型(价差成分合计=0.0000)\n 盯市列里被重复计入的分红成分: 302400.0000\n 最终 RealizedMtmPnL(盯市列): 201600.0000\n 最终 RealizedDividend(分红列): 302400.0000\n 最终 RealizedPnl(持仓层): 510007.0700\n 重复计算成立: True\n [结论] ⚠ 坐实重复计算:盯市列含分红成分 302400.0000(既在 RealizedMtmPnL 又在 RealizedDividend),修复后 RealizedPnl 应减少 302400.0000。类型=纯分红型。" + } +} \ No newline at end of file diff --git a/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1891.json b/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1891.json new file mode 100644 index 00000000..fa8655b6 --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/SwapDividend/dividend_trade_1891.json @@ -0,0 +1,3292 @@ +{ + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "UnderlyingCode": "180205.IB", + "RecordedAt": "2026-06-26T09:05:16", + "SourceDb": "test", + "Purpose": "分红重复计算验证 + EOD重构回归基线", + "InputTrade": { + "SubTrades": null, + "YesterDayTradeSavedVol": null, + "PV": null, + "RealizedPnl": null, + "PositionPnl": null, + "MaturityWorkDay": 10, + "PairTradeNumber": null, + "ShowNotional": 0.0, + "MaturityDay": 14, + "TradeOpenVolatilityString": "0.00%", + "InitialSpotPriceOriginal": null, + "TradeOriginalAmount": null, + "ExerciseDateString": "2026-07-10", + "ActualStrike": null, + "StrikeString": "0.000000000", + "TradeSinglePriceString": "0.00%", + "TradeDateString": "2026-06-17", + "LotsNewInfo": null, + "UnWindPrice": null, + "StockEqvNotionalToShow": 19990.0, + "SettlementTypeDesc": "收盘价", + "SettlementDesc": null, + "VarietyId": null, + "SyntheticUnderlyingTipsInfo": null, + "StrikeToShow": "0.000000000", + "CurNotional": 0.0, + "LastDayNotional": 0.0, + "Amount": null, + "ContractCode": null, + "InitialMarginRatio": null, + "UnderlyingPrice": null, + "UnderlyingName": null, + "DeltaInLots": null, + "ValueStatus": null, + "UnWindUnderlyingPrice": null, + "UnWindTradePrice": null, + "UnWindTotalAmount": null, + "UnWindFee": null, + "UnWindTimes": 0, + "KnockInOutStatusObservation": null, + "DividendRatio": 0.0, + "InitYtm": 0.025000, + "CalcId": null, + "TTMDays": null, + "UnderlyingInstrumentTypeCn": "利率债", + "ExerciseModeCn": "", + "ClientNumber": null, + "SummaryType": "收益互换", + "QuoteUnitSingle": null, + "QuoteUnit": null, + "trade_forward": { + "id": 0, + "TradeId": 0, + "OpenCommission": 0.0, + "AnnualMarginRate": 0.0, + "AnnualStoragePrice": 0.0, + "ForwardValue": 0.0, + "ObservationStart": null, + "ObservationDates": null, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_asian_option": { + "StrikeType": null, + "PayoffType": null, + "SettleMode": null, + "StrikeGearingFactor": 1.0, + "AveragingPeriodStartDate": null, + "ObservationDates": null, + "EnhancedPrice": 0.0, + "StrikeTypeCn": "", + "PayoffTypeCn": "", + "Fixings": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rainbow_option": { + "UnderlyingAssetCode": null, + "Strike": null, + "UnderlyingMaturityDate": null, + "UnderlyingId1": null, + "UnderlyingId2": null, + "Strike2": null, + "SpotPrice1": null, + "SpotPrice2": null, + "RainbowType": null, + "UnderlyingAssetCode2": null, + "CorRelation": null, + "CashAmount": null, + "Vol2": 0.0, + "UnderlyingCodes": [ + null, + null + ], + "Strikes": [ + 0.0, + 0.0 + ], + "SpotPrices": [ + 0.0, + 0.0 + ], + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_barrier_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "BarrierShift": null, + "BarrierTypeEn": null, + "IsDiscrete": false, + "BarrierPrice": null, + "UpperBarrierPrice": null, + "Discrete": null, + "BarrierType": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateAnnualizedAtKO": false, + "RebateDayCount": null, + "ObservationDates": null, + "LatestObservationDate": null, + "RebateType": null, + "RebateTypeCn": "", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_risky_option": { + "Strike1": null, + "Strike2": null, + "Strike3": null, + "ParticipationRate1": null, + "ParticipationRate2": null, + "ParticipationRate3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_binary_option": { + "RebateType": null, + "RebateTypeCn": "", + "CashOrNothingAmount": null, + "CashOrNothingAmountHigh": null, + "CashOrNothingAmountRate": null, + "CashOrNothingAmountHighRate": null, + "PayoffType": "", + "UpperBarrier": null, + "RebateDayCount": null, + "RebateAnnualizedAtKO": false, + "ObservationDates": null, + "LatestObservationDate": null, + "UpperBarrierRelative": "0", + "MonitorType": null, + "IsDiscreteMonitored": false, + "Offset": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_spread_option": { + "UnderlyingMaturityDate2": null, + "UnderlyingMaturityDate3": null, + "UnderlyingMaturityDate4": null, + "Payoff": "S1-S2", + "UnderlyingId1": null, + "UnderlyingId2": null, + "UnderlyingId3": null, + "UnderlyingId4": null, + "UnderlyingAssetClass1": null, + "UnderlyingAssetClass2": null, + "UnderlyingAssetClass3": null, + "UnderlyingAssetClass4": null, + "UnderlyingAssetCode1": null, + "UnderlyingAssetCode2": null, + "UnderlyingAssetCode3": null, + "UnderlyingAssetCode4": null, + "UnderlyingAssetName1": null, + "UnderlyingAssetName2": null, + "UnderlyingAssetName3": null, + "UnderlyingAssetName4": null, + "TradeCloseVolatility1": null, + "TradeCloseVolatility2": null, + "TradeCloseVolatility3": null, + "TradeCloseVolatility4": null, + "TradeOpenVolatility1": null, + "TradeOpenVolatility2": null, + "TradeOpenVolatility3": null, + "TradeOpenVolatility4": null, + "NumOfSmoothingDays1": null, + "NumOfSmoothingDays2": null, + "NumOfSmoothingDays3": null, + "NumOfSmoothingDays4": null, + "Vol1": null, + "Vol2": null, + "Vol3": null, + "Vol4": null, + "Correlation12": null, + "Correlation13": null, + "Correlation14": null, + "Correlation23": null, + "Correlation24": null, + "Correlation34": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_double_sharkfin_option": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "IsDiscrete": false, + "BarrierHigh": 0.0, + "BarrierLow": 0.0, + "StrikeHigh": null, + "StrikeLow": null, + "CallParticipationRate": null, + "PutParticipationRate": null, + "Discrete": null, + "Rebate": null, + "RebateRate": null, + "RebateHigh": null, + "RebateHighRate": null, + "RebateType": null, + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_autocall": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "CouponBarrier": 0.0, + "Coupon": 0.0, + "IsFixedCoupon": false, + "CouponPayType": 0, + "SpreadStrike1": null, + "SpreadStrike": null, + "CouponPayAtMaturity": false, + "IncludeCouponAfterKI": false, + "KOObservationDates": null, + "CouponObservation": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "CouponDayCount": "Act365", + "HappenedObservations": null, + "KIPayoffType": 0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_snowball": { + "IsInitialKnockedIn": false, + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KOBarrier": 0.0, + "KIBarrier": 0.0, + "Coupon": 0.0, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "LatestKOObservationDate": null, + "LatestKOBarrier": null, + "ObservationDates": null, + "LatestObservationDate": null, + "KORebate": 0.0, + "KORebateType": 0, + "IsFixedCoupon": false, + "SpreadStrikeAtKO": null, + "SpreadStrikeAtKO1": null, + "SpreadStrikeAtMaturity1": null, + "SpreadStrikeAtMaturity": null, + "KOPayoffType": 0, + "KIPayoffType": 0, + "AnnualizedPremiumRate": null, + "IsAnnualized2": false, + "AnnualizeFactor2": null, + "InitialAdvance": null, + "PeriodAdvance": null, + "FontEarning": null, + "ConfirmedLine": null, + "ConfirmedFloor": null, + "CouponDayCount": null, + "PrepaymentUsed": false, + "PrepaymentRatio": null, + "PrepaymentInterestRate": null, + "PrepaymentConvertCashRate": null, + "CouponIncludeEndDate": null, + "EnhancedParticipationRate": null, + "KIParticipationRate": null, + "PrincipalProtectionRate": null, + "KIObservationType": 0, + "KOBarrierAdjustStep": 0.0, + "CouponIncludeStartDate": null, + "CouponUsePaymentDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_rangeaccrual": { + "LowerRange": 0.0, + "UpperRange": 0.0, + "BonusRate": 0.0, + "ObservationDates": null, + "LatestObservationDate": null, + "HappenedObservations": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_airbag": { + "KnockInOutStatus": null, + "KnockInOutStatusCn": "观察中", + "KnockInOutDate": null, + "KnockInOutNotional": null, + "HighStrike": 0.0, + "HasPayoffLimit": false, + "Barrier": 0.0, + "KIParticipationRate": 0.0, + "NotKIParticipationRate": null, + "IsDiscreteMonitored": false, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_accumulator_option": { + "KOBarrier": null, + "KOObservationDates": null, + "KOObservationSettleDates": null, + "AccumuTradeAmount": 0.0, + "OriginalAccumuTradeAmount": 0.0, + "AccumuType": "", + "PayoffType": null, + "Coupon": 0.0, + "CouponPercent": false, + "IsFixedCoupon": false, + "CouponDayCount": null, + "PutMultiplier": 1.0, + "CallMultiplier": 1.0, + "EarlyTerminate": false, + "SettlementMode": null, + "SettlementMode2": null, + "SettlementMode3": null, + "KnockOutDate": null, + "ForwardDateType": null, + "ForwardPriceType": null, + "AccumulatorStructureType": 0, + "Coupon2": null, + "Strike2": null, + "Strike3": null, + "Multiplier": null, + "Multiplier2": null, + "Multiplier3": null, + "KnockInOutStatusCn": "观察中", + "LatestObservationDate": null, + "StrikeGearingFactor": 1.0, + "LatestKOBarrier": null, + "latestCoupon": null, + "ForwardPriceType2": null, + "ForwardDateType2": null, + "ForwardPriceType3": null, + "ForwardDateType3": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_swap": { + "AnnualDays": null, + "IsGetFloatingProfit": false, + "GetVarietyId": null, + "GetUnderlyingId": null, + "GetUnderlyingCode": null, + "GetLongShort": null, + "GetSpotPrice": null, + "GetOpenPrice": null, + "GetTradePrice": null, + "GetSwapTimeAndRate": null, + "GetSwapRate": 0.0, + "GetFixedProfit": null, + "GetSingleFee": null, + "GetUnAnnualRate": null, + "GetMarginRate": 0.0, + "GetFinalPrice": null, + "GetNotional": null, + "GetTradeAmount": null, + "GetLot": null, + "IsPayFloatingProfit": false, + "PayVarietyId": null, + "PayUnderlyingId": null, + "PayUnderlyingCode": null, + "PayLongShort": null, + "PaySpotPrice": null, + "PayOpenPrice": null, + "PayTradePrice": null, + "PaySwapTimeAndRate": null, + "PaySwapRate": 0.0, + "PayFixedProfit": null, + "PaySingleFee": null, + "RateCalcMode": "11", + "IncludeFirstDay": true, + "PayUnAnnualRate": null, + "PayMarginRate": 0.0, + "PayFinalPrice": null, + "PayNotional": null, + "PayTradeAmount": null, + "PayLot": null, + "FlowId": null, + "AnnualVarIncome": false, + "SwapType": null, + "SettlementPayType": 0, + "IsTradePriceWhenOpen": false, + "IsShare": false, + "OriginalTradeId": null, + "GetCountRatio": 0.0, + "PayCountRatio": 0.0, + "GetContractSize": 0.0, + "PayContractSize": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "get_trade_swap_details": [], + "pay_trade_swap_details": [], + "trade_underlying_enhance": { + "AnnualizedEnhanceRate": 0.0, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_cashflow": { + "ProfitRate": 0.0, + "RateType": 0, + "DepositType": 0, + "PrepayRatio": 0.0, + "ProfitDayCount": "Act365", + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_custom": { + "ObservationDates": null, + "LatestObservationDate": null, + "id": 0, + "TradeId": 0, + "OptId": null, + "OptName": null, + "OptDate": null, + "EncryptId": "93JNozbumCxdv7dwztjpOg" + }, + "trade_extend": { + "TradeId": 0, + "ExtendJson": null, + "CompanyJson": null, + "ExtendObj": { + "FlowBookMode": 0, + "FloatingPnlAnnualized": false, + "NeedOpenFee": false, + "OpenFeeType": 0, + "AnnualDays": 365, + "InterestCalcMode": "11", + "Direction": 0, + "SettlementRules": 0, + "DividendPayDate": 1 + } + }, + "swap_positions": [], + "inital_eod_swap_positions": [], + "eod_swap_positions": [], + "trade_Initial_Margin": { + "TradeId": 0, + "Direction": 1, + "MarginType": 0, + "MarginValue": 0.0 + }, + "swap_Flow_Events": [], + "swap_Events": [], + "eod_swaps": [], + "ClientCashInCashOutList": [], + "SalesCommission": null, + "trade_cash": null, + "RemarkInfo": null, + "eod_trade": null, + "SettlementAmount": null, + "ActualExerciseDate": null, + "trade_swap_gj": { + "LongShort": null, + "OpenPrice": null, + "OpenCurrencyRate": null, + "TradeCommission": null, + "PricingOriginalStockEqvNotional": null, + "PricingStockEqvNotional": null, + "SwapTimeAndRate": null + }, + "Tags": null, + "OutputTags": null, + "SaveExt": null, + "TradeType": "收益互换", + "TradeSavedVol": null, + "CreatorId": null, + "CreatorName": null, + "CreateDate": "2026-06-25T10:28:00", + "PairTrade": "", + "TradeSinglePrice": null, + "GroupId": null, + "GroupName": "", + "NumOfSmoothingDays": null, + "ParentTradeId": 0, + "TradeCloseVolatility": null, + "TradeOpenVolatility": null, + "UnWindDate": "2026-06-25T00:00:00", + "UnWindNotional": null, + "CheckStatus": 0, + "CheckTradeUpdate": 1, + "InitialSpotPrice": 0.0, + "IsMoneynessOption": null, + "TradeAmount": 0.0, + "TradeUnit": "", + "StockEqvNotional": 0.0, + "StockEqvNotionalMax": null, + "StockEqvNotionalReal": 19990.0, + "VolType": null, + "UnderlyingInstrumentType": "TBonds", + "ExerciseDate": "2026-07-10T00:00:00", + "TraderName": "初始用户", + "TraderId": 1, + "Vol": null, + "Day1Pnl": null, + "MidVol": null, + "ExchangeRate": null, + "Strike": null, + "UnderlyingId": 2393, + "BasisUnderlyingId": null, + "BasisUnderlyingCode": null, + "BasisGap": null, + "ExchangeOptionCode": null, + "AssetBookName": "testjia1", + "AssetId": 38, + "Notional": 0.0, + "OptionType": null, + "PricingModel": null, + "ExerciseMode": null, + "NoRiskRate": null, + "SpotPrice": 0.0, + "TradeNumber": "GLMS-20260617-0002", + "ClientId": 14, + "ClientName": "testjia", + "UnderlyingCode": "180205.IB", + "UnderlyingAssetClass": "其他市场债券", + "TradeDate": "2026-06-17T00:00:00", + "BuySell": "卖出", + "StartDate": "2026-06-18T00:00:00", + "MaturityDate": null, + "ObservationDateStr": null, + "TradePrice": 0.0, + "AccurateTradePrice": null, + "TradeStatus": "已平仓", + "ProcessStatus": "通过审批", + "ProcessOrderId": -2, + "ProcessOrderBranch": 0, + "ProcessOptDate": "2026-06-25T13:51:13", + "SettlementType": 0, + "ValidState": "Valid", + "Comments": null, + "SentMailCount": null, + "Lots": null, + "HasPartialUnWind": 1, + "OriginalNotional": 0.0, + "TradeSource": "系统交易", + "OriginalStockEqvNotional": 19990.0, + "OriginalStockEqvNotionalV2": 19990.0, + "IsUsePremiumRate": true, + "IsTradePricePayType": true, + "IsAnnualized": false, + "AnnualizeFactor": null, + "DividendRate": null, + "ParticipationRate": 1.0, + "PrincipalRate": null, + "OriginalPrincipalSum": null, + "PrincipalRateWrite": null, + "SinglePrincipalWrite": null, + "SettlementDate": null, + "SettlementFlag": 0, + "SettlementFlagStr": "否", + "SettlementFlagDate": null, + "SettlementFlagOptId": null, + "SettlementFlagOptName": null, + "CalcFlag": 0, + "IsSingleContract": null, + "PremiumPayDate": null, + "PremiumRate": null, + "UnderlyingAssetName": "18国开05", + "UnderlyingVariety": null, + "ContractVersion": null, + "FinalPrice": null, + "OpponentRole": "乙方", + "DurationDays": null, + "StructureType": "普通债券类收益互换", + "InitialMargin": 0.0, + "StructureIntroduction": null, + "MarginTemplateName": "系统默认", + "MarginType": 0, + "MarginRate": 0.0, + "PositionMarginRate": 0.0, + "QuoteCurrency": null, + "SettlementCurrency": null, + "IsGroup": 0, + "ExtendInfo": null, + "IsNight": false, + "Propertys": [], + "ExtendOptionTypeInfo": null, + "AccumulatorOptionId": null, + "OptId": 166, + "OptName": "贾春梅", + "OptDate": "2026-06-25T13:51:13", + "IsApproval": false, + "IsAutoGenerate": null, + "HTStatus": null, + "Warning": false, + "DividendDate": "2000-01-01T00:00:00", + "CountRatio": 1, + "CallPut": "", + "IsMoneynessOptionData": false, + "MetaDic": {}, + "TradeExtendJson": null, + "TradeMultipleType": "收益互换", + "Fixing": null, + "id": 1891, + "EncryptId": "cmP2ygz0GGvqtXj55rGpQQ" + }, + "InputPositions": [ + { + "PositionId": 0, + "SwapTradeId": 1891, + "PosiDirection": 0, + "PositionType": 0, + "UnderlyingCode": null, + "underlying": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiNetFeePrice": 0.0, + "PosiNetNoFeePrice": 0.0, + "PosiNotionalValue": 0.0, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 1, + "InterestRateDefault": 0.006000, + "FloatRate": 0.010000, + "FloatRateUnderlyingCode": "FR007", + "InterestMode": 9, + "InterestModeStr": "标的期初全价", + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": true, + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260617-0002-34711", + "UnderlyingInstrumentType": null, + "InitYtm": null, + "Invalid": false, + "interest_rest_days": 1, + "interest_rule": 0, + "SwapIntervalList": [ + { + "Date": "2026-07-09T00:00:00", + "Rate": 0.006, + "Settlement": 0, + "SettlementDate": null + } + ], + "Obervation": null, + "PositionIdPadding": "00000000", + "id": 34711, + "EncryptId": "lcLrIHav6iQfZ6YdUVZRmA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:00" + }, + { + "PositionId": 0, + "SwapTradeId": 1891, + "PosiDirection": 2, + "PositionType": 1, + "UnderlyingCode": "180205.IB", + "underlying": null, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "PosiNotionalValue": 19990.000000, + "PosiQuantity": 1000.000000, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 0, + "InterestRateDefault": 0.0, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "InterestMode": 0, + "InterestModeStr": "Unknown", + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": true, + "IsAnnualized": false, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260617-0002-34712", + "UnderlyingInstrumentType": "TBonds", + "InitYtm": 0.025000, + "Invalid": false, + "interest_rest_days": null, + "interest_rule": null, + "SwapIntervalList": [], + "Obervation": null, + "PositionIdPadding": "00000000", + "id": 34712, + "EncryptId": "w1s9pijgVZe_u7mbD2H8Hw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:00" + }, + { + "PositionId": 34711, + "SwapTradeId": 1891, + "PosiDirection": 0, + "PositionType": 0, + "UnderlyingCode": null, + "underlying": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiNetFeePrice": 0.0, + "PosiNetNoFeePrice": 0.0, + "PosiNotionalValue": 0.0, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 1, + "InterestRateDefault": 0.006000, + "FloatRate": 0.000100, + "FloatRateUnderlyingCode": "FR007", + "InterestMode": 9, + "InterestModeStr": "标的期初全价", + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "InterestAmount": 9.860000, + "IsInitial": false, + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260617-0002-34711", + "UnderlyingInstrumentType": null, + "InitYtm": null, + "Invalid": false, + "interest_rest_days": 1, + "interest_rule": 0, + "SwapIntervalList": [ + { + "Date": "2026-07-09T00:00:00", + "Rate": 0.006, + "Settlement": 0, + "SettlementDate": null + } + ], + "Obervation": null, + "PositionIdPadding": "00034711", + "id": 34715, + "EncryptId": "BtlrfV49E1wCUBAeSzQoXQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:00" + }, + { + "PositionId": 34712, + "SwapTradeId": 1891, + "PosiDirection": 2, + "PositionType": 1, + "UnderlyingCode": "180205.IB", + "underlying": null, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "PosiNotionalValue": 19990.000000, + "PosiQuantity": 0.0, + "PosiTradingFee": 0.0, + "PosiTradingFeeUnit": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "PosiDividendIncome": 0.0, + "PosiTradingFeePending": 0.0, + "InterestDirection": 0, + "InterestRateDefault": 0.0, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "InterestMode": 0, + "InterestModeStr": "Unknown", + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "InterestAmount": 0.0, + "IsInitial": false, + "IsAnnualized": false, + "HappenDate": null, + "Currency": "CNY", + "PosiNumber": "GLMS-20260617-0002-34712", + "UnderlyingInstrumentType": "TBonds", + "InitYtm": 0.025000, + "Invalid": false, + "interest_rest_days": null, + "interest_rule": null, + "SwapIntervalList": [], + "Obervation": null, + "PositionIdPadding": "00034712", + "id": 34716, + "EncryptId": "83tp38yK8JRb2FAKtjuOQg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:00" + } + ], + "InputFlowEvents": [ + { + "EventDate": "2026-06-17T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034711", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": "2026-07-10T00:00:00", + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34711, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 0.0, + "InterestRate": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-18T00:00:00", + "TradingAmountNetFeeAvg": 0.0, + "TradingAmountNetAvg": 0.0, + "UnwindDate": "2026-06-18T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [ + { + "Date": "2026-07-09T00:00:00", + "Rate": 0.006, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12756, + "EncryptId": "8nurr7RFJ_Srd947snnKxw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:10" + }, + { + "EventDate": "2026-06-17T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034712", + "EventType": 1, + "EventTypeStr": "开仓", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": "2026-07-10T00:00:00", + "Quantity": 1000.000000, + "PositionQty": 1000.000000, + "CountRatio": 1.000000, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 19990.000000, + "TradingAmountAvg": 19.99000000000, + "TradingAmountFeeAvg": 19.99000000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": null, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-18T00:00:00", + "TradingAmountNetFeeAvg": 19.99000000000, + "TradingAmountNetAvg": 19.99000000000, + "UnwindDate": "2026-06-18T00:00:00", + "OptLog": "手工操作", + "SwapIntervalList": [], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12757, + "EncryptId": "qLgo5IkwTfM5vYXtH9Gtpg", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:10" + }, + { + "EventDate": "2026-06-20T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034711", + "EventType": 3, + "EventTypeStr": "互换", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34711, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 19991.643014, + "InterestRate": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestAmount": 3.290000, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 3.290000, + "EventId": 17946, + "ClientId": 14, + "FloatRate": 0.024000, + "PayDate": "2026-06-20T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-20T00:00:00", + "OptLog": null, + "SwapIntervalList": [ + { + "Date": "2026-07-09T00:00:00", + "Rate": 0.006, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12773, + "EncryptId": "x9ft9PX9yIer6RmN0m658Q", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:28:49" + }, + { + "EventDate": "2026-06-20T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": null, + "SwapPositionIdPadding": "00034712", + "EventType": 3, + "EventTypeStr": "互换", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 3996.001000, + "TradingAmountAvg": 0.19990000000, + "TradingAmountFeeAvg": 0.19990000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 395594.949000, + "DividendIn": -9.150000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17946, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-20T00:00:00", + "TradingAmountNetFeeAvg": 19.99000000000, + "TradingAmountNetAvg": 19.99000000000, + "UnwindDate": "2026-06-20T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": 395594.949000, + "InitYtm": null, + "id": 12774, + "EncryptId": "Sejdg3W3DAcjVJPmkAfzYw", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-20T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034712", + "EventType": 4, + "EventTypeStr": "自动互换", + "EventReason": "系统操作-分红", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 1000.000000, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 19.99000000000, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": -9.150000, + "DividendIn": -9.150000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 0, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": null, + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17938, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-22T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-20T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": -9.150000, + "InitYtm": null, + "id": 12765, + "EncryptId": "bIsnYfXN3i02BsDVctMC_w", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-22T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034711", + "EventType": 3, + "EventTypeStr": "互换", + "EventReason": "交易", + "PayDirection": 0, + "PayDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": null, + "CountRatio": 0.0, + "ContractSize": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.0, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34711, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 1, + "InterestDirectionStr": "收取", + "InterestMode": 9, + "UnderlyingInstrumentType": null, + "InterestModeStr": "标的期初全价", + "InterestPrincipal": 19994.929446, + "InterestRate": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestAmount": 6.570000, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 6.570000, + "EventId": 17947, + "ClientId": 14, + "FloatRate": 0.024000, + "PayDate": "2026-06-22T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-22T00:00:00", + "OptLog": null, + "SwapIntervalList": [ + { + "Date": "2026-07-09T00:00:00", + "Rate": 0.006, + "Settlement": 0, + "SettlementDate": null + } + ], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12775, + "EncryptId": "if_y_w-7i_bLwimt4k1O6w", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T10:29:22" + }, + { + "EventDate": "2026-06-22T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": null, + "SwapPositionIdPadding": "00034712", + "EventType": 3, + "EventTypeStr": "互换", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 0.19990000000, + "TradingAmountFeeAvg": 0.19990000000, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 0.0, + "DividendIn": 0.0, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17947, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-22T00:00:00", + "TradingAmountNetFeeAvg": 19.99000000000, + "TradingAmountNetAvg": 19.99000000000, + "UnwindDate": "2026-06-22T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12776, + "EncryptId": "aqkYcDFMO31qVBSZTaL5rA", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-23T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034712", + "EventType": 4, + "EventTypeStr": "自动互换", + "EventReason": "系统操作-分红", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 1000.000000, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 19.99000000000, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": -9.510000, + "DividendIn": -9.510000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 0, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": null, + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17943, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-23T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": null, + "UnwindDate": "2026-06-23T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": -9.510000, + "InitYtm": null, + "id": 12771, + "EncryptId": "YsPWR3vVvOg6S3ZP5AZM8w", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-23T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034712", + "EventType": 4, + "EventTypeStr": "自动互换", + "EventReason": "系统操作-分红", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 0.0, + "PositionQty": 1000.000000, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 0.0, + "TradingAmountAvg": 19.99000000000, + "TradingAmountFeeAvg": 0.0, + "TradingFee": 0.0, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": -9.510000, + "DividendIn": -9.510000, + "CloseFee": 0.0, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 18638, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-23T00:00:00", + "TradingAmountNetFeeAvg": null, + "TradingAmountNetAvg": 19.99000000000, + "UnwindDate": "2026-06-23T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": -9.510000, + "InitYtm": null, + "id": 13503, + "EncryptId": "a1QlyMjQBU7RGG4n-JIxnw", + "OptId": 0, + "OptName": null, + "OptTime": "0001-01-01T00:00:00" + }, + { + "EventDate": "2026-06-25T00:00:00", + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "SwapPositionIdPadding": "00034712", + "EventType": 2, + "EventTypeStr": "平仓", + "EventReason": "交易", + "PayDirection": 2, + "PayDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "MatuirityDate": null, + "Quantity": 1000.000000, + "PositionQty": 0.0, + "CountRatio": 0.0, + "ContractSize": 1.000000, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "TradingAmount": 19990.000000, + "TradingAmountAvg": 19.99000000000, + "TradingAmountFeeAvg": 20.10100000000, + "TradingFee": 111.000000, + "TradingFeePending": 0.0, + "DividendPending": 0.0, + "MarkClosePnl": 111.000000, + "DividendIn": 0.0, + "CloseFee": 111.000000, + "BeforeCloseFee": 0.0, + "PositionId": 34712, + "DataState": 100, + "ClientCashId": 0, + "InterestDirection": 0, + "InterestDirectionStr": "", + "InterestMode": 0, + "UnderlyingInstrumentType": "TBonds", + "InterestModeStr": "", + "InterestPrincipal": 0.0, + "InterestRate": 0.0, + "InterestSwapInterval": null, + "InterestAmount": 0.0, + "TdInterestAmount": 0.0, + "InterestFee": 0.0, + "InterestClosePnL": 0.0, + "EventId": 17989, + "ClientId": 14, + "FloatRate": null, + "PayDate": "2026-06-25T00:00:00", + "TradingAmountNetFeeAvg": 19.99000000000, + "TradingAmountNetAvg": 19.99000000000, + "UnwindDate": "2026-06-25T00:00:00", + "OptLog": null, + "SwapIntervalList": [], + "PosiPnl": 0.0, + "InitYtm": null, + "id": 12824, + "EncryptId": "W70dUd2-GzX4uiU0JAoz6g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T13:50:53" + } + ], + "InputEodPositions": [ + { + "ValueDate": "2026-06-17T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19990.000000, + "TdInterestRate": 0.006000, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.023000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347233, + "EncryptId": "Mdk7KX6q2cqYlJRng9oOiQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19990.000000, + "TdInterestRate": 0.006000, + "TdInterestIncome": 1.643014, + "TdInterestFee": 0.0, + "InterestIncomeSum": 1.643014, + "InterestFeeSum": 0.0, + "InterestProfitSum": 1.643014, + "SwapPositionValue": 1.643014, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347269, + "EncryptId": "JwsG7NcNDlzRy12c1Wc3PA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19991.643014, + "TdInterestRate": 0.006000, + "TdInterestIncome": 1.643149, + "TdInterestFee": 0.0, + "InterestIncomeSum": 3.286163, + "InterestFeeSum": 0.0, + "InterestProfitSum": 3.286163, + "SwapPositionValue": 3.286163, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347305, + "EncryptId": "t6v7CSiRfwr2XUg3TYTgfw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": null, + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19991.643014, + "TdInterestRate": 0.006000, + "TdInterestIncome": 3.290000, + "TdInterestFee": 0.0, + "InterestIncomeSum": 3.286163, + "InterestFeeSum": 0.0, + "InterestProfitSum": 3.286163, + "SwapPositionValue": 3.286163, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 3.290000, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 3.290000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347341, + "EncryptId": "JLw3hGy4hBp-b7kXp_7tLQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19993.286163, + "TdInterestRate": 0.006000, + "TdInterestIncome": 1.643284, + "TdInterestFee": 0.0, + "InterestIncomeSum": 4.929447, + "InterestFeeSum": 0.0, + "InterestProfitSum": 4.929447, + "SwapPositionValue": 4.929447, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 3.290000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 3.290000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347377, + "EncryptId": "kBuZrBfJ91q7w44Km0LewA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": null, + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19994.929446, + "TdInterestRate": 0.006000, + "TdInterestIncome": 6.570000, + "TdInterestFee": 0.0, + "InterestIncomeSum": 4.929447, + "InterestFeeSum": 0.0, + "InterestProfitSum": 4.929447, + "SwapPositionValue": 4.929447, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 6.570000, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 9.860000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.024000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347419, + "EncryptId": "azP1-OuKfJ9ojcm8TAkNew", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": null, + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19994.929447, + "TdInterestRate": 0.006000, + "TdInterestIncome": 1.588638, + "TdInterestFee": 0.0, + "InterestIncomeSum": 6.518085, + "InterestFeeSum": 0.0, + "InterestProfitSum": 6.518085, + "SwapPositionValue": 6.518085, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 9.860000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 9.860000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.023000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347463, + "EncryptId": "2rpj9JKjLuVELdBCZLxGwQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": null, + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 19996.518085, + "TdInterestRate": 0.006000, + "TdInterestIncome": 0.334188, + "TdInterestFee": 0.0, + "InterestIncomeSum": 6.852273, + "InterestFeeSum": 0.0, + "InterestProfitSum": 6.852273, + "SwapPositionValue": 6.852273, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 9.860000, + "RealizedInterestFee": 0.0, + "RealizedPnl": 9.860000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.000100, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347507, + "EncryptId": "vZJw32nGH6M5ZvTYpR83CA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34711, + "PosiDirection": 0, + "PosiDirectionStr": "", + "PositionType": 0, + "PositionTypeStr": "", + "UnderlyingCode": null, + "UnderlyingName": null, + "ContractSize": 0.0, + "CountRatio": 0.0, + "PosiNetPrice": 0.0, + "PosiGrossPrice": 0.0, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 0.0, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 1, + "InterestMode": 9, + "InterestType": 1, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.006000, + "InterestSwapInterval": "[{\"Date\":\"2026-07-09\",\"Rate\":0.006,\"Settlement\":0}]", + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.010000, + "FloatRateUnderlyingCode": "FR007", + "IsAnnualized": true, + "HappenDate": null, + "Currency": "CNY", + "UnderlyingInstrumentType": null, + "PosiNetFeePrice": null, + "PosiNetNoFeePrice": null, + "VTradingFee": 0.0, + "interest_rest_days": 1, + "interest_rule": 0, + "dv01": null, + "PositionIdPadding": "00034711", + "id": 347553, + "EncryptId": "EQmUKxQnodQxqrGso1G98A", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-17T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347232, + "EncryptId": "qQTwQci-0xmolo5qy4AZQA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347268, + "EncryptId": "Cqn84s6d4id2TcMzrRm6qw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 0.0, + "RealizedDividend": 0.0, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 0.0, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347304, + "EncryptId": "gcE6wgw1jsmcclZpvBDGHA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": -9.150000, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 395594.949000, + "TdCloseDividend": -9.150000, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395594.949000, + "RealizedDividend": -9.150000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395585.799000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347340, + "EncryptId": "oRvXKUjAlNCaTktdO1o4rw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395594.949000, + "RealizedDividend": -9.150000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395585.799000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347376, + "EncryptId": "AB_XkDU5C5dqJaRLr7_yug", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395594.949000, + "RealizedDividend": -9.150000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395585.799000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347418, + "EncryptId": "hmfuuHhG6-QtbHNF97lAHw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": -9.510000, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395594.949000, + "RealizedDividend": -18.660000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395576.289000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347462, + "EncryptId": "QPgjm-PVKJo8ahfCYK2jww", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 1000.000000, + "PosiNotionalValue": 19990.000000, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 19990.000000, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 0.0, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 0.0, + "TdCloseDividend": 0.0, + "TdCloseFee": 0.0, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395594.949000, + "RealizedDividend": -18.660000, + "RealizedFee": 0.0, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395576.289000, + "TdCurrency": 1.000000, + "PosiStatus": 0, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347506, + "EncryptId": "EP-P16Zbcw3sQAguQYuR-A", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "ClientId": 14, + "SwapTradeId": 1891, + "PositionId": 34712, + "PosiDirection": 2, + "PosiDirectionStr": "支付", + "PositionType": 1, + "PositionTypeStr": "B", + "UnderlyingCode": "180205.IB", + "UnderlyingName": null, + "ContractSize": 1.000000, + "CountRatio": 1.000000, + "PosiNetPrice": 19.99000000000, + "PosiGrossPrice": 19.99000000000, + "PosiQuantity": 0.0, + "PosiNotionalValue": 0.0, + "PosiTradingFee": 0.0, + "PosiStartDate": "2026-06-18T00:00:00", + "PosiMatuirityDate": "2026-07-10T00:00:00", + "UnderlyingPrice": 19.99000000000, + "UnderlyingMarketValue": 0.0, + "TdPosiDividend": 0.0, + "PosiMtmPnL": 0.0, + "PosiDividendSum": 0.0, + "PosiFeePending": 0.0, + "PosiProfitSum": 0.0, + "InterestDirection": 0, + "InterestMode": 0, + "InterestType": 0, + "InterestPrincipalFix": 0.0, + "InterestRateDefault": 0.0, + "InterestSwapInterval": null, + "InterestFeePending": 0.0, + "TdInterestPrincipal": 0.0, + "TdInterestRate": 0.0, + "TdInterestIncome": 0.0, + "TdInterestFee": 0.0, + "InterestIncomeSum": 0.0, + "InterestFeeSum": 0.0, + "InterestProfitSum": 0.0, + "SwapPositionValue": 0.0, + "TdCloseQty": 1000.000000, + "TdChangedQty": 0.0, + "TdCloseMtmPnl": 111.000000, + "TdCloseDividend": 0.0, + "TdCloseFee": 111.000000, + "TdCloseInterest": 0.0, + "TdCloseInterestFee": 0.0, + "RealizedMtmPnL": 395705.949000, + "RealizedDividend": -18.660000, + "RealizedFee": 111.000000, + "RealizedInterest": 0.0, + "RealizedInterestFee": 0.0, + "RealizedPnl": 395687.289000, + "TdCurrency": 1.000000, + "PosiStatus": 1, + "Invalid": false, + "FloatRate": 0.0, + "FloatRateUnderlyingCode": null, + "IsAnnualized": false, + "HappenDate": null, + "Currency": null, + "UnderlyingInstrumentType": "TBonds", + "PosiNetFeePrice": 19.99000000000, + "PosiNetNoFeePrice": 19.99000000000, + "VTradingFee": 0.0, + "interest_rest_days": null, + "interest_rule": null, + "dv01": 0.0, + "PositionIdPadding": "00034712", + "id": 347552, + "EncryptId": "p02AJm2mrSwKTQanMiWHhw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + } + ], + "InputEodSwaps": [ + { + "ValueDate": "2026-06-17T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161689, + "EncryptId": "EAS6UGoXYiLWeaveMZQIJA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-18T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 1.643014, + "PostionValue": 1.640000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161706, + "EncryptId": "uvcMwXlGhlHkafn2xJMp0g", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:18" + }, + { + "ValueDate": "2026-06-19T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 3.286163, + "PostionValue": 3.286163, + "TdRealizedPnL": 0.0, + "RealizedPnL": 0.0, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161723, + "EncryptId": "ky3_d1a_Oe3qADGEgxfHNA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-20T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 3.286163, + "PostionValue": 3.286163, + "TdRealizedPnL": 395585.799000, + "RealizedPnL": 395585.799000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161740, + "EncryptId": "8svPT6DC5Me90jsCRpm5sQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-21T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 4.929447, + "PostionValue": 4.929447, + "TdRealizedPnL": 3.290000, + "RealizedPnL": 395589.089000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161757, + "EncryptId": "dVl5MFmbf3CKcgz8zSGAcQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:19" + }, + { + "ValueDate": "2026-06-22T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 4.929447, + "PostionValue": 4.930000, + "TdRealizedPnL": -3.290000, + "RealizedPnL": 395585.799000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161777, + "EncryptId": "DM-I1CXzbJadiuagAfJSMQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-23T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 6.518085, + "PostionValue": 6.520000, + "TdRealizedPnL": 0.350000, + "RealizedPnL": 395586.149000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161798, + "EncryptId": "OrelzQptEqJDbVIfUM3dxw", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:20" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 19990.000000, + "NotionalValueLong": 19990.000000, + "NotionalValueShort": 0.0, + "MarketValueLong": 19990.000000, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 6.852273, + "PostionValue": 6.850000, + "TdRealizedPnL": 0.0, + "RealizedPnL": 395586.149000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 0.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161819, + "EncryptId": "jfM8CnKDU48Puy6x0RhrpQ", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + }, + { + "ValueDate": "2026-06-25T00:00:00", + "BookId": 38, + "ClientId": 14, + "SwapTradeId": 1891, + "SwapTradeNo": "GLMS-20260617-0002", + "StructureType": "普通债券类收益互换", + "NotionalValue": 0.0, + "NotionalValueLong": 0.0, + "NotionalValueShort": 0.0, + "MarketValueLong": 0.0, + "MarketValueShort": 0.0, + "FloatingPnL": 0.0, + "InterestPnL": 0.0, + "PostionValue": 0.0, + "TdRealizedPnL": 101.140000, + "RealizedPnL": 395687.289000, + "InitMarginGain": 0.0, + "PostionMarginGain": 0.0, + "InitMarginLoss": 0.0, + "PostionMarginLoss": 0.0, + "TdCloseQty": 1000.0, + "dv01": 0.0, + "CloseStartDate": "0001-01-01T00:00:00", + "id": 161841, + "EncryptId": "amHOYrGVVRDdqh1rneWNJA", + "OptId": 166, + "OptName": "贾春梅", + "OptTime": "2026-06-25T21:09:21" + } + ], + "InputBondPayments": [ + { + "id": 6, + "EncryptId": "F8UUkcqqifN9KV2u-6zcbQ", + "inner_code": 100006, + "underlyingCode": "180205.IB", + "security_id": null, + "symbol": null, + "coupon_rate": null, + "payment_date_pl": "2026-06-20T00:00:00", + "payment_date": "2026-06-20T00:00:00", + "payment_interest": 1.011122220000, + "payment_parvalue": null, + "paying_price": null, + "channel_source": "聚源数据库", + "jsid": 1000000006, + "create_time": "2025-09-01T18:55:29", + "update_time": "2025-10-01T18:56:17" + }, + { + "id": 9, + "EncryptId": "l0-qKzO7VpkKdIIkYJG5ww", + "inner_code": 100008, + "underlyingCode": "180205.IB", + "security_id": null, + "symbol": null, + "coupon_rate": null, + "payment_date_pl": "2026-06-23T00:00:00", + "payment_date": "2026-06-23T00:00:00", + "payment_interest": 1.051122220000, + "payment_parvalue": null, + "paying_price": null, + "channel_source": "聚源数据库", + "jsid": 1000000006, + "create_time": "2025-09-01T18:55:29", + "update_time": "2025-10-01T18:56:17" + } + ], + "Diagnosis": { + "SwapTradeId": 1891, + "交易类型": "混合型", + "逐持仓拆解": [ + { + "PositionId": 34711, + "盯市列合计": 0.0, + "盯市价差成分": 0.0, + "盯市费成分": 0.0, + "盯市分红成分": 0.0, + "分红列累计": 0.0, + "重复计入分红": 0.0 + }, + { + "PositionId": 34712, + "盯市列合计": 395696.439000, + "盯市价差成分": 395604.099000, + "盯市费成分": 111.000000, + "盯市分红成分": -18.660000, + "分红列累计": -18.660000, + "重复计入分红": -18.660000 + } + ], + "重复计入分红金额": -18.660000, + "最终累计盯市已实现": 395705.949000, + "最终累计分红已实现": -18.660000, + "最终持仓层累计已实现": 395687.289000, + "重复计算成立": true, + "结论": "⚠ 坐实重复计算:盯市列含分红成分 -18.6600(既在 RealizedMtmPnL 又在 RealizedDividend),修复后 RealizedPnl 应减少 -18.6600。类型=混合型。", + "Summary": "--- 分红重复计算诊断 SwapTradeId=1891 ---\n\n[流水层] 每条平仓/互换事件拆解 (MarkClosePnl = 价差 + 费CloseFee + 分红DividendIn):\n id EventDate EvType MarkClosePnl DividendIn CloseFee 价差(残差)\n 12773 2026-06-20 互换 0.0000 0.0000 0.0000 0.0000\n 12774 2026-06-20 互换 395594.9490 -9.1500 0.0000 395604.0990\n 12775 2026-06-22 互换 0.0000 0.0000 0.0000 0.0000\n 12776 2026-06-22 互换 0.0000 0.0000 0.0000 0.0000\n 13503 2026-06-23 自动互换 -9.5100 -9.5100 0.0000 0.0000\n 12824 2026-06-25 平仓 111.0000 0.0000 111.0000 0.0000\n\n[EOD层] 按 PositionId 拆解盯市列成分:\n PositionId 盯市列合计 价差成分 费成分 分红成分(重复) 分红列累计 重复计入\n 34711 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n 34712 395696.4390 395604.0990 111.0000 -18.6600 -18.6600 -18.6600\n\n[汇总]\n 交易类型: 混合型(价差成分合计=395604.0990)\n 盯市列里被重复计入的分红成分: -18.6600\n 最终 RealizedMtmPnL(盯市列): 395705.9490\n 最终 RealizedDividend(分红列): -18.6600\n 最终 RealizedPnl(持仓层): 395687.2890\n 重复计算成立: True\n [结论] ⚠ 坐实重复计算:盯市列含分红成分 -18.6600(既在 RealizedMtmPnL 又在 RealizedDividend),修复后 RealizedPnl 应减少 -18.6600。类型=混合型。" + } +} \ No newline at end of file diff --git a/UnitTestProject/Resources/GoldenFiles/SwapReEodDeleteCash/reeod_delete_trade_1903.json b/UnitTestProject/Resources/GoldenFiles/SwapReEodDeleteCash/reeod_delete_trade_1903.json new file mode 100644 index 00000000..85aff26b --- /dev/null +++ b/UnitTestProject/Resources/GoldenFiles/SwapReEodDeleteCash/reeod_delete_trade_1903.json @@ -0,0 +1,313 @@ +{ + "SwapTradeId": 1903, + "SwapTradeNo": "GLMS-20260624-0005", + "RecordedAt": "2026-07-01T08:33:12", + "SourceDb": "test", + "Purpose": "重收盘误删手动互换资金记录 - TDD红灯证据", + "InputEvents": [ + { + "ValueDate": "2026-06-24T00:00:00", + "SwapTradeId": 1903, + "EventType": 9, + "EventTypeName": "确认交易", + "EventReason": "确认交易", + "EventData": "", + "Invalid": false, + "ClientCashId": 0, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20625, + "EncryptId": "l4jIqgrZepVZBJ8gLfbu_w", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T17:38:40" + }, + { + "ValueDate": "2026-06-24T00:00:00", + "SwapTradeId": 1903, + "EventType": 5, + "EventTypeName": "回退", + "EventReason": "交易回退至2026年06月24日", + "EventData": "", + "Invalid": false, + "ClientCashId": 0, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20631, + "EncryptId": "lNwpsaP9qzZyFuecPppR4Q", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T17:58:12" + }, + { + "ValueDate": "2026-06-29T00:00:00", + "SwapTradeId": 1903, + "EventType": 4, + "EventTypeName": "自动互换", + "EventReason": "系统操作-自动互换", + "EventData": "{\"SwapTradeId\":1903,\"CloseType\":0,\"CloseQty\":0.0,\"ClosePercent\":0.0,\"CloseNotionalValue\":0.0,\"StartDate\":\"2026-06-25T00:00:00\",\"ValueDate\":\"2026-06-29T00:00:00\",\"UnwindDate\":null,\"NotionalValue\":19990.0,\"NotionalQty\":0.0,\"PosiNotionalValue\":19990.000000,\"PositionQty\":0.0,\"AnnualDays\":0,\"CloseMethod\":0,\"SwapRealizedPnL\":1.86160315679,\"SwapMarginRebatePnl\":-1.36986460274,\"SwapMarginAmount\":0.0,\"SwapCloseAmount\":1.86160315679,\"SwapDividendPnl\":0.0,\"FlowEvents\":[],\"StructureType\":null,\"PayDate\":\"2026-06-29T00:00:00\",\"TradeStartDate\":null}", + "Invalid": false, + "ClientCashId": 13031, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20659, + "EncryptId": "xgIja4GsGIq0qiL3Q-IdbQ", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T18:08:51" + }, + { + "ValueDate": "2026-06-30T00:00:00", + "SwapTradeId": 1903, + "EventType": 3, + "EventTypeName": "互换", + "EventReason": "系统操作-互换", + "EventData": "{\"SwapTradeId\":1903,\"CloseType\":0,\"CloseQty\":0.0,\"ClosePercent\":1.0,\"CloseNotionalValue\":19990.0,\"StartDate\":\"2026-06-24T00:00:00\",\"ValueDate\":\"2026-06-30T00:00:00\",\"UnwindDate\":\"2026-06-30T00:00:00\",\"NotionalValue\":19990.00,\"NotionalQty\":1000.00,\"PosiNotionalValue\":19990.00,\"PositionQty\":1000.0,\"AnnualDays\":365,\"CloseMethod\":0,\"SwapRealizedPnL\":3.23,\"SwapMarginRebatePnl\":0.00,\"SwapMarginAmount\":0.0,\"SwapCloseAmount\":3.23,\"SwapDividendPnl\":0.0,\"FlowEvents\":[],\"StructureType\":\"普通债券类收益互换\",\"PayDate\":\"2026-06-30T00:00:00\",\"TradeStartDate\":\"2026-06-25T00:00:00\"}", + "Invalid": false, + "ClientCashId": 12996, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20632, + "EncryptId": "GLfe6-0kN1kS1j8fwI35Jw", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T18:00:12" + }, + { + "ValueDate": "2026-06-30T00:00:00", + "SwapTradeId": 1903, + "EventType": 3, + "EventTypeName": "互换", + "EventReason": "系统操作-互换", + "EventData": "{\"SwapTradeId\":1903,\"CloseType\":0,\"CloseQty\":0.0,\"ClosePercent\":1.0,\"CloseNotionalValue\":19990.0,\"StartDate\":\"2026-06-30T00:00:00\",\"ValueDate\":\"2026-06-30T00:00:00\",\"UnwindDate\":\"2026-06-30T00:00:00\",\"NotionalValue\":19990.00,\"NotionalQty\":1000.00,\"PosiNotionalValue\":19990.00,\"PositionQty\":1000.0,\"AnnualDays\":365,\"CloseMethod\":0,\"SwapRealizedPnL\":5.00,\"SwapMarginRebatePnl\":0.00,\"SwapMarginAmount\":0.0,\"SwapCloseAmount\":5.00,\"SwapDividendPnl\":0.0,\"FlowEvents\":[],\"StructureType\":\"普通债券类收益互换\",\"PayDate\":\"2026-06-30T00:00:00\",\"TradeStartDate\":\"2026-06-25T00:00:00\"}", + "Invalid": false, + "ClientCashId": 13029, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20658, + "EncryptId": "CGvRD9lBlpTNg44rkLnzvA", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T18:08:07" + }, + { + "ValueDate": "2026-06-30T00:00:00", + "SwapTradeId": 1903, + "EventType": 3, + "EventTypeName": "互换", + "EventReason": "系统操作-互换", + "EventData": "{\"SwapTradeId\":1903,\"CloseType\":0,\"CloseQty\":0.0,\"ClosePercent\":1.0,\"CloseNotionalValue\":19990.0,\"StartDate\":\"2026-06-30T00:00:00\",\"ValueDate\":\"2026-06-30T00:00:00\",\"UnwindDate\":\"2026-06-30T00:00:00\",\"NotionalValue\":19990.00,\"NotionalQty\":1000.00,\"PosiNotionalValue\":19990.00,\"PositionQty\":1000.0,\"AnnualDays\":365,\"CloseMethod\":0,\"SwapRealizedPnL\":5.00,\"SwapMarginRebatePnl\":0.00,\"SwapMarginAmount\":0.0,\"SwapCloseAmount\":5.00,\"SwapDividendPnl\":0.0,\"ClientCashIds\":[],\"FlowEvents\":[],\"StructureType\":\"普通债券类收益互换\",\"PayDate\":\"2026-06-30T00:00:00\",\"TradeStartDate\":\"2026-06-25T00:00:00\"}", + "Invalid": false, + "ClientCashId": 13032, + "BackId": 0, + "extenstionData": null, + "SwapTradeNumber": null, + "unwindData": null, + "id": 20660, + "EncryptId": "Q5Dzi9hzcXG6b8gXbawLHw", + "OptId": 1, + "OptName": "初始用户", + "OptTime": "2026-06-30T18:10:29" + } + ], + "InputCashRecords": [ + { + "DirectionType": "互换支出", + "TradeDirectionType": "互换收入", + "CanGenerateExit": false, + "Explain": null, + "ValidState": "Valid", + "TradeId": 1903, + "Action": "系统操作-互换", + "Direction": "应收", + "Number": "20260630180850883", + "ClientId": 38, + "ClientNumber": "26033ZMRCS", + "ClientName": "张名锐测试", + "Money": -3.23, + "HappenDate": "2026-06-29T00:00:00", + "State": "已结算", + "OpenBank": null, + "OpenBankId": null, + "OpenBankComments": null, + "OpenBankCard": "", + "MoneyStr": "-3.23", + "SerialNumber": null, + "BankFlowId": null, + "AccountBalance": null, + "Uses": null, + "SettleDate": "2026-06-29T00:00:00", + "Comments": null, + "TradeNumber": "GLMS-20260624-0005", + "TradeType": null, + "CashFlag": 0, + "TradeCashId": 0, + "IsGroup": 0, + "CurrencyCode": "", + "ParentTradeNumber": null, + "Deal": 0, + "ApprovalProcess": 0, + "CreatorId": 1, + "CreatorName": "初始用户", + "CreateDate": "2026-06-30T18:08:51", + "ApprovalDate": null, + "cash_type": null, + "TransferOutAccount": null, + "TransferInAccount": null, + "OptId": 1, + "OptName": "初始用户", + "OptDate": "2026-06-30T18:08:51", + "id": 13030, + "EncryptId": "PmbbhBl-gOhCtjzjFAL3MQ" + }, + { + "DirectionType": "预付金返息收入", + "TradeDirectionType": "预付金返息支出", + "CanGenerateExit": false, + "Explain": null, + "ValidState": "Valid", + "TradeId": 1903, + "Action": "系统操作-预付金返息", + "Direction": "应收", + "Number": "20260630180850888", + "ClientId": 38, + "ClientNumber": "26033ZMRCS", + "ClientName": "张名锐测试", + "Money": 1.37, + "HappenDate": "2026-06-29T00:00:00", + "State": "已结算", + "OpenBank": null, + "OpenBankId": null, + "OpenBankComments": null, + "OpenBankCard": "", + "MoneyStr": "1.37", + "SerialNumber": null, + "BankFlowId": null, + "AccountBalance": null, + "Uses": null, + "SettleDate": "2026-06-29T00:00:00", + "Comments": null, + "TradeNumber": "GLMS-20260624-0005", + "TradeType": null, + "CashFlag": 0, + "TradeCashId": 0, + "IsGroup": 0, + "CurrencyCode": "", + "ParentTradeNumber": null, + "Deal": 0, + "ApprovalProcess": 0, + "CreatorId": 1, + "CreatorName": "初始用户", + "CreateDate": "2026-06-30T18:08:51", + "ApprovalDate": null, + "cash_type": null, + "TransferOutAccount": null, + "TransferInAccount": null, + "OptId": 1, + "OptName": "初始用户", + "OptDate": "2026-06-30T18:08:51", + "id": 13031, + "EncryptId": "cR8xg27rKifLQECWC61RFg" + }, + { + "DirectionType": "互换支出", + "TradeDirectionType": "互换收入", + "CanGenerateExit": false, + "Explain": null, + "ValidState": "Valid", + "TradeId": 1903, + "Action": "系统操作-互换", + "Direction": "应收", + "Number": "20260630181033576", + "ClientId": 38, + "ClientNumber": "26033ZMRCS", + "ClientName": "张名锐测试", + "Money": -5.0, + "HappenDate": "2026-06-30T00:00:00", + "State": "已结算", + "OpenBank": null, + "OpenBankId": null, + "OpenBankComments": null, + "OpenBankCard": "", + "MoneyStr": "-5.00", + "SerialNumber": null, + "BankFlowId": null, + "AccountBalance": null, + "Uses": null, + "SettleDate": "2026-06-30T00:00:00", + "Comments": null, + "TradeNumber": "GLMS-20260624-0005", + "TradeType": null, + "CashFlag": 0, + "TradeCashId": 0, + "IsGroup": 0, + "CurrencyCode": "", + "ParentTradeNumber": null, + "Deal": 0, + "ApprovalProcess": 0, + "CreatorId": 1, + "CreatorName": "初始用户", + "CreateDate": "2026-06-30T18:10:34", + "ApprovalDate": null, + "cash_type": null, + "TransferOutAccount": null, + "TransferInAccount": null, + "OptId": 1, + "OptName": "初始用户", + "OptDate": "2026-06-30T18:10:34", + "id": 13032, + "EncryptId": "NSMb8Sa0Q0Xx-5oOCr_UAw" + } + ], + "Diagnosis": { + "SwapTradeId": 1903, + "自动互换事件数": 1, + "删除起点ValueDate": "2026-06-29T00:00:00", + "删除命中总数": 3, + "手动互换误删记录数": 1, + "自动互换命中记录数": 1, + "孤儿命中记录数": 1, + "手动互换误删CashIds": [ + 13032 + ], + "命中明细": [ + { + "CashId": 13030, + "Action": "系统操作-互换", + "Money": -3.23, + "HappenDate": "2026-06-29T00:00:00", + "来源": "未知(孤儿)", + "会被误删": false + }, + { + "CashId": 13031, + "Action": "系统操作-预付金返息", + "Money": 1.37, + "HappenDate": "2026-06-29T00:00:00", + "来源": "自动互换", + "会被误删": false + }, + { + "CashId": 13032, + "Action": "系统操作-互换", + "Money": -5.0, + "HappenDate": "2026-06-30T00:00:00", + "来源": "手动互换", + "会被误删": true + } + ], + "BUG成立": true, + "结论": "坐实BUG:误删 1 条手动互换资金记录", + "Summary": "--- 重收盘误删诊断 SwapTradeId=1903 ---\n\n[swap_event] 手动互换 vs 自动互换:\n event id=20659 EventType=自动互换 ValueDate=2026-06-29 ClientCashId=13031\n event id=20632 EventType=互换 ValueDate=2026-06-30 ClientCashId=12996\n event id=20658 EventType=互换 ValueDate=2026-06-30 ClientCashId=13029\n event id=20660 EventType=互换 ValueDate=2026-06-30 ClientCashId=13032\n\n[删除条件模拟] valueDate=2026-06-29 Action IN (系统操作-互换, 系统操作-预付金返息)\n id Action MoneyHappenDate 来源 误删? \n 13030 系统操作-互换 -3.232026-06-29 未知(孤儿) \n 13031 系统操作-预付金返息 1.372026-06-29 自动互换 \n 13032 系统操作-互换 -5.002026-06-30 手动互换 ✓BUG \n\n[结论]\n 删除命中 3 条 = 手动互换 1 + 自动互换 1 + 孤儿 1\n ⚠⚠⚠ 坐实 BUG:重收盘会误删 1 条手动互换资金记录 (ids=[13032])\n 根因:ClearSwapPositions:400 按 Action=系统操作-互换 删除,该 Action 手动/自动共用,无来源字段区分。" + } +} \ No newline at end of file diff --git a/UnitTestProject/UnitTestProject.csproj b/UnitTestProject/UnitTestProject.csproj index a5198648..2ab816f7 100644 --- a/UnitTestProject/UnitTestProject.csproj +++ b/UnitTestProject/UnitTestProject.csproj @@ -49,6 +49,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/UnitTestProject/appsettings.json b/UnitTestProject/appsettings.json index 166c0e8e..7505ceff 100644 --- a/UnitTestProject/appsettings.json +++ b/UnitTestProject/appsettings.json @@ -1,8 +1,8 @@ { "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;", - "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;", - "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;", + "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=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=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;" }, "LibreOffice": { diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs index e60beb81..2d16d4dd 100644 --- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs +++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs @@ -12,6 +12,7 @@ using YLErp.Commons; using YLErp.DataBase; using YLErp.DBModels; using YLErp.DBModels.Enums; +using YLErp.Helpers; using YLErp.Model; using YLErp.Model.Enum; using YLErp.Models; @@ -1334,13 +1335,13 @@ namespace YLErp.BLL.Eod } unwindPercent = posiQty == 0 ? 0 : unwindQty / posiQty; } - var tdRealizedPnL = flowEvents.Where(x => x.EventDate > lastSettletDate).ToList().Sum(s => s.InterestClosePnL + s.MarkClosePnl); + var tdRealizedPnL = flowEvents.Where(x => x.EventDate > lastSettletDate).ToList().Sum(s => s.InterestClosePnL + s.FloatPnlSum); var tdRealizedInterestPnL = flowEvents.Where(x => x.EventDate > lastSettletDate).ToList().Sum(s => s.InterestClosePnL); var tradeFee= flowEvents.Where(x => x.EventDate > lastSettletDate).ToList().Sum(s => s.TradingFee+s.TradingFeePending); balance.InterestPnl += Convert.ToDouble(tdRealizedInterestPnL)*-1; balance.TradeFee += Convert.ToDouble(tradeFee) *-1; var currentEvents = flowEvents.Where(x => x.EventDate == startDate).ToList(); - var currentRealizedPnl = currentEvents.Sum(s => s.InterestClosePnL + s.MarkClosePnl); + var currentRealizedPnl = currentEvents.Sum(s => s.InterestClosePnL + s.FloatPnlSum); //潜在行权收益等于实值额 balance.PotentialSurpluses += -Convert.ToDouble(pnl); //持仓市值 @@ -1385,8 +1386,8 @@ namespace YLErp.BLL.Eod ClientId = item.client_id ?? 0, ClientName = item.client_name, TradingQty = (item.order_qty ?? 0) - (item.last_shares ?? 0), - TradingAmountAvg = (item.full_price ?? 0)*0.01m, - TradingAmountFeeAvg = (item.full_price ?? 0) * 0.01m, + TradingAmountAvg = BondPriceConverter.ToStorage(item.full_price ?? 0), + TradingAmountFeeAvg = BondPriceConverter.ToStorage(item.full_price ?? 0), TradingFee = 0 }; // clientOrder中数量单位为万 diff --git a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs index 862b0e49..54e9a178 100644 --- a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs +++ b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs @@ -598,8 +598,10 @@ namespace YLErp.BLL.Eod } clientPosition.update_user = 0; SetClientPositionPrice(clientPosition); - clientPosition.swap_market_value = clientPosition.full_price_now * clientPosition.position_qty * (clientPosition.side == 0 ? 1 : -1) * 100; - clientPosition.position_profit_loss = (clientPosition.full_price_now - clientPosition.deal_full_price_avg) * 0.01m * (clientPosition.position_qty * 10000) * (clientPosition.side == 0 ? 1 : -1) - clientPosition.commission; + // full_price_now 为债券报价(面值百分比,×100形式);×100 还原市值数量级 + clientPosition.swap_market_value = clientPosition.full_price_now * clientPosition.position_qty * (clientPosition.side == 0 ? 1 : -1) * ConsGlobal.bondShowPriceMultiple; + // 盈亏 = (现价−成本) × 价差系数(÷100,bondPriceMultiple) × 数量(万手×10000) × 方向。此处 0.01m 与 10000 分属价格/数量两个维度,不宜合并为 BondPriceConverter + clientPosition.position_profit_loss = (clientPosition.full_price_now - clientPosition.deal_full_price_avg) * ConsGlobal.bondPriceMultiple * (clientPosition.position_qty * 10000) * (clientPosition.side == 0 ? 1 : -1) - clientPosition.commission; clientPosition.position_profit_loss = Math.Round(clientPosition.position_profit_loss ?? 0, 2, MidpointRounding.AwayFromZero); clientPosition.today_profit_loss = clientPosition.swap_market_value - lastPv; if (clientPosition.deal_full_price_avg > 0 && enableCalcBongd)//发kafka 获取成交收益率 diff --git a/YLErpDAL/DataBase/YLContext.cs b/YLErpDAL/DataBase/YLContext.cs index ce383a6d..b6ed6e6d 100644 --- a/YLErpDAL/DataBase/YLContext.cs +++ b/YLErpDAL/DataBase/YLContext.cs @@ -1,4 +1,4 @@ -using BaseOUDAL; +using BaseOUDAL; using YLErp.Core.DBModels; using YLErp.Model; @@ -408,10 +408,5 @@ namespace YLErp.BLL public DbSet tradeContractOaResult { get; set; } public DbSet bondPayment { get; set; } - public DbSet glms_risk_rule { get; set; } - public DbSet glms_risk_rule_application { get; set; } - public DbSet glms_risk_rule_audit_log { get; set; } - public DbSet glms_risk_variable { get; set; } - } } \ No newline at end of file diff --git a/YLErpDAL/Helpers/BondPriceConverter.cs b/YLErpDAL/Helpers/BondPriceConverter.cs new file mode 100644 index 00000000..2e113748 --- /dev/null +++ b/YLErpDAL/Helpers/BondPriceConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YLErp.Helpers +{ + /// + /// 债券价格展示态 ↔ 入库态 统一转换入口。 + /// ============================================================================ + /// 背景:债券价格习惯用"面值的百分比"报价(如全价 99.50 = 面值 100 的 99.5%)。 + /// 入库时统一存为小数形式(0.995),展示时再 ×100 还原为 99.5。 + /// + /// 过去这组转换散落在 11+ 个文件、87 处,多数用 ConsGlobal.bondPriceMultiple/ + /// bondShowPriceMultiple,但有 4 处用字面量 *0.01m / *100 绕过常量(改常量值时不会跟随, + /// 极易引入 Bug)。本类收敛所有调用入口,杜绝散落。 + /// + /// 命名口径(见《互换价格字段命名规范决策文档》): + /// ToStorage = 展示态(99.5) → 入库态(0.995) 即 × bondPriceMultiple(0.01) + /// ToDisplay = 入库态(0.995) → 展示态(99.5) 即 × bondShowPriceMultiple(100) + /// ============================================================================ + /// + public static class BondPriceConverter + { + /// + /// 展示态 → 入库态。债券报价(如 99.5)转为库内小数(0.995)。 + /// 用于:成交流水导入、EOD 价格缓存、债券付息计算等入库/计算场景。 + /// + /// 展示态价格(面值百分比形式,如 99.5) + /// 入库态价格(小数形式,如 0.995) + public static decimal ToStorage(decimal displayPrice) + { + return displayPrice * ConsGlobal.bondPriceMultiple; + } + + /// + /// 入库态 → 展示态。库内小数(0.995)转为债券报价(99.5)。 + /// 用于:列表查询、详情展示、报表导出等展示场景。 + /// + /// 入库态价格(小数形式,如 0.995) + /// 展示态价格(面值百分比形式,如 99.5) + public static decimal ToDisplay(decimal storagePrice) + { + return storagePrice * ConsGlobal.bondShowPriceMultiple; + } + + /// 可空重载:null 保持 null 语义(与原 *= ConsGlobal.bondPriceMultiple 行为一致) + public static decimal? ToStorage(decimal? displayPrice) + { + return displayPrice.HasValue ? displayPrice.Value * ConsGlobal.bondPriceMultiple : displayPrice; + } + + /// 可空重载:null 保持 null 语义(与原 *= ConsGlobal.bondShowPriceMultiple 行为一致) + public static decimal? ToDisplay(decimal? storagePrice) + { + return storagePrice.HasValue ? storagePrice.Value * ConsGlobal.bondShowPriceMultiple : storagePrice; + } + } +} diff --git a/YLErpDAL/Helpers/Dv01Helper.cs b/YLErpDAL/Helpers/Dv01Helper.cs new file mode 100644 index 00000000..408bdd2c --- /dev/null +++ b/YLErpDAL/Helpers/Dv01Helper.cs @@ -0,0 +1,54 @@ +using System; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Modules; + +namespace YLErp.Helpers +{ + /// + /// DV01计算工具类 + /// 公式: DV01(万元) = 方向 * (持仓面值(万元) / 单位面值) * 中债基点价值 + /// 等价业界标准: DV01(元) = 持仓面值(元) / 100 * vobp,本方法输出为该值的 1/10000(即万元)。 + /// 注意: PosiQuantity 入参须为"元面值"单位(由 PosiNotionalValue/PosiGrossPrice 反推); + /// 单位面值默认 100,取自 UnderlyingBond.Price;vobp 为"每100元面值1bp的价格变动"。 + /// + public static class Dv01Helper + { + /// + /// 计算DV01 + /// + /// 标的代码 + /// 持仓数量,须为"元面值"单位(如1000000=100万元面值) + /// 收支方向: 1=收取, 2=支付 + /// 多空方向: 1=多头, 2=空头 + /// 中债基点价值(每100元面值,收益率变动1bp的价格变动) + /// DV01值,单位为"万元" + public static decimal CalcDv01(string underlyingCode, decimal posiQuantity, int posiDirection, int positionType, decimal vobp) + { + if (vobp == 0 || posiQuantity == 0) return 0; + + var um = DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + if (um == null || !um.IsBond()) return 0; + + // 方向: 支付多头=-1, 收取多头=1, 支付空头=1, 收取空头=-1 + // = (PosiDirection==收取?1:-1) * (PositionType==多头?1:-1) + int direction = (posiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1) + * (positionType == (int)PositionTypeFlag.Long ? 1 : -1); + + // 券面总额(万元) = 交易数量(元面值) / 10000 + double faceValueWan = Convert.ToDouble(posiQuantity) / 10000d; + + // 单位面值, 默认100 + decimal unitFaceValue = 100m; + if (!string.IsNullOrEmpty(um.ExJson)) + { + var bond = JsonHelper.Deserialize(um.ExJson); + if (bond?.Price != null && bond.Price != 0) + unitFaceValue = bond.Price.Value; + } + + decimal result = Convert.ToDecimal(direction * (faceValueWan / Convert.ToDouble(unitFaceValue)) * Convert.ToDouble(vobp)); + return Math.Round(result, 4); + } + } +} diff --git a/YLErpDAL/Helpers/FrontendCalcReference.cs b/YLErpDAL/Helpers/FrontendCalcReference.cs new file mode 100644 index 00000000..cd2585e8 --- /dev/null +++ b/YLErpDAL/Helpers/FrontendCalcReference.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; + +namespace YLErp.Helpers +{ + /// + /// 前端互换计算公式的后端参考实现(忠实重写 unwindSwapTrade.js / incomeSwapTrade.js)。 + /// ============================================================================ + /// 用途: + /// 1. 特征化测试的金标准(FrontendCalcCharacterizationTest)—— 冻结前端行为 + /// 2. 后端只读校验(SwapDealService.ValidateFrontendPnL)—— 重算盈亏与前端传值比对 + /// + /// 注:前端保持快速反馈(用户改输入立即算),本类不替代前端,仅作后端校验兜底。 + /// 命名规范(见《互换价格字段命名规范决策文档》): + /// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice + /// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice + /// ============================================================================ + /// + public static class FrontendCalcReference + { + /// 模拟 otcformat.trading.StockEqvNotional(金额类,2位小数) + private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + + /// + /// 计算平仓页(unwind)的盯市盈亏与汇总。 + /// 对应 unwindSwapTrade.js:196-261(calcFloatClosePnl + calcCloseAmount)。 + /// + public static UnwindResult CalcUnwind(UnwindInput input) + { + // 规范名映射:initPosiNetPrice(前端变量) = PosiGrossPrice = EntryDirtyPrice(期初全价不含费) + decimal entryPrice = input.PosiGrossPrice; + // scale = getPriceScale():债券(multiplier=100)→0.01,非债券→1 + decimal scale = input.Multiplier == 100 ? 0.01m : 1m; + // 方向因子:PayDirection(1=收取)→+1;PositionType(1=多头)→+1 + decimal floatRatio = input.PayDirection == 1 ? 1 : -1; + decimal longRatio = input.PositionType == 1 ? 1 : -1; + + decimal tradingFee = ParseOrZero(input.TradingFee); + decimal tradingFeePending = ParseOrZero(input.TradingFeePending); + decimal dividendIn = ParseOrZero(input.DividendIn); + + // MarkClosePnl = round(CloseQty × (TradingAmountAvg × scale − EntryPrice) × floatRatio × longRatio × 10000)/10000 + decimal markClosePnl = Math.Round( + input.CloseQty * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio * 10000) / 10000; + // toFixed(2) → StockEqvNotional + markClosePnl = Math.Round(markClosePnl, 2, MidpointRounding.AwayFromZero); + markClosePnl = StockEqvNotional(markClosePnl); + + // FloatPnlSum = (MarkClosePnl + TradingFee + TradingFeePending + DividendIn).toFixed(2) + decimal floatPnlSum = decimal.Parse( + (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); + + // calcCloseAmount:SwapRealizedPnL/SwapCloseAmount = FloatPnlSum + Σ利息腿 + Σ预付金腿 + decimal swapCloseAmount = floatPnlSum; + decimal swapRealizedPnL = floatPnlSum; + decimal swapMarginRebatePnl = 0m; + foreach (var interest in input.InterestLegs) + { + swapCloseAmount += interest.InterestClosePnL; + swapRealizedPnL += interest.InterestClosePnL; + } + foreach (var margin in input.MarginLegs) + { + swapCloseAmount += margin.InterestClosePnL; + swapMarginRebatePnl += margin.InterestClosePnL; + swapRealizedPnL += margin.InterestClosePnL; + } + swapRealizedPnL = StockEqvNotional(swapRealizedPnL); + swapCloseAmount = StockEqvNotional(swapCloseAmount); + swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); + + // TradingAmountFeeAvg = CloseQty==0 ? 0 : (TradingAmountAvg×scale + TradingFee/CloseQty × ratio) + // 注:unwind 的 ratio = PositionType?1:-1(calcCloseAmount 内重新定义) + decimal ratio = input.PositionType == 1 ? 1 : -1; + decimal tradingAmountFeeAvg = input.CloseQty == 0 ? 0 : + input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * ratio; + + return new UnwindResult + { + MarkClosePnl = markClosePnl, + FloatPnlSum = floatPnlSum, + SwapRealizedPnL = swapRealizedPnL, + SwapCloseAmount = swapCloseAmount, + SwapMarginRebatePnl = swapMarginRebatePnl, + TradingAmountFeeAvg = tradingAmountFeeAvg + }; + } + + /// + /// 计算结息页(income)的盯市盈亏与汇总。 + /// 对应 incomeSwapTrade.js:128-178。 + /// 差异:用 CloseNotionalValue(非 CloseQty)作量纲,无 longRatio,无 Math.round/10000。 + /// + public static UnwindResult CalcIncome(UnwindInput input) + { + // income 页 initPosiGrossPrice = PosiGrossPrice = EntryDirtyPrice + decimal entryPrice = input.PosiGrossPrice; + decimal scale = input.Multiplier == 100 ? 0.01m : 1m; + decimal floatRatio = input.PayDirection == 1 ? 1 : -1; + + decimal tradingFee = ParseOrZero(input.TradingFee); + decimal tradingFeePending = ParseOrZero(input.TradingFeePending); + decimal dividendIn = ParseOrZero(input.DividendIn); + + // MarkClosePnl = CloseNotionalValue × (TradingAmountAvg × scale − EntryPrice) × floatRatio + // (无 longRatio、无 Math.round/10000) + decimal markClosePnl = input.CloseNotionalValue * (input.TradingAmountAvg * scale - entryPrice) * floatRatio; + markClosePnl = StockEqvNotional(markClosePnl); + + decimal floatPnlSum = decimal.Parse( + (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); + + decimal swapCloseAmount = floatPnlSum; + decimal swapRealizedPnL = floatPnlSum; + decimal swapMarginRebatePnl = 0m; + foreach (var interest in input.InterestLegs) + { + swapCloseAmount += interest.InterestClosePnL; + swapRealizedPnL += interest.InterestClosePnL; + } + foreach (var margin in input.MarginLegs) + { + swapCloseAmount += margin.InterestClosePnL; + swapMarginRebatePnl += margin.InterestClosePnL; + swapRealizedPnL += margin.InterestClosePnL; + } + // income 页无 SwapMarginAmount 计算(恒为0) + swapRealizedPnL = StockEqvNotional(swapRealizedPnL); + swapCloseAmount = StockEqvNotional(swapCloseAmount); + swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); + + // TradingAmountFeeAvg = CloseQty>0 ? (TradingAmountAvg×scale + TradingFee/CloseQty × floatRatio) : TradingAmountAvg×scale + // 注:income 用 floatRatio(PayDirection),与 unwind 的 ratio(PositionType) 不同 + decimal tradingAmountFeeAvg = input.CloseQty > 0 + ? input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * floatRatio + : input.TradingAmountAvg * scale; + + return new UnwindResult + { + MarkClosePnl = markClosePnl, + FloatPnlSum = floatPnlSum, + SwapRealizedPnL = swapRealizedPnL, + SwapCloseAmount = swapCloseAmount, + SwapMarginRebatePnl = swapMarginRebatePnl, + TradingAmountFeeAvg = tradingAmountFeeAvg + }; + } + + private static decimal ParseOrZero(string s) => string.IsNullOrEmpty(s) ? 0m : decimal.Parse(s); + } + + /// 前端计算输入模型(对应前端可见的原始字段) + public class UnwindInput + { + public int Multiplier; // 债券=100,非债券=1 + public decimal PosiGrossPrice; // EntryDirtyPrice(期初全价不含费) + public decimal TradingAmountAvg; // 用户可改的期末标的价格(界面×multiplier形态) + public decimal CloseQty; // 平仓数量 + public decimal CloseNotionalValue;// 平仓名义本金(income 用) + public int PayDirection; // 1=收取,-1=支付 + public int PositionType; // 1=多头,2=空头 + public string TradingFee; // 交易费用(前端是字符串) + public string TradingFeePending; // 待结算费用 + public string DividendIn; // 分红 + public List InterestLegs = new(); + public List MarginLegs = new(); + } + + /// 利息腿/预付金腿输入 + public class LegInput + { + public decimal InterestClosePnL; // 利息腿平仓盈亏(已含方向) + } + + /// 前端计算输出模型(前端算出的衍生字段) + public class UnwindResult + { + public decimal MarkClosePnl; + public decimal FloatPnlSum; + public decimal SwapRealizedPnL; + public decimal SwapCloseAmount; + public decimal SwapMarginRebatePnl; + public decimal TradingAmountFeeAvg; + } +} diff --git a/YLErpDAL/Model/Trade_Self.cs b/YLErpDAL/Model/Trade_Self.cs index 237a7b44..8f2e69a6 100644 --- a/YLErpDAL/Model/Trade_Self.cs +++ b/YLErpDAL/Model/Trade_Self.cs @@ -199,8 +199,14 @@ namespace YLErp.DBModels } public bool SwapCanEdit() { + var hasSwapDealEvent = swap_Events.Any(x => + !x.Invalid + && (x.EventType == (int)SwapEventTypeEnum.平仓 + || x.EventType == (int)SwapEventTypeEnum.互换 + || x.EventType == (int)SwapEventTypeEnum.自动互换)); + //确认成交 - return !(ConsTrade.TradeStatusAfterAndWithOutConfirmed.Contains(TradeStatus) || ConsTrade.审批中.Equals(TradeStatus) || UnWindDate.HasValue || (!PS.Config.TradeElement.IsAmendableAfterConfirm && TradeDate < valuedateBLL.ValueDate && ConsTrade.确认成交.Equals(TradeStatus))); + return !(ConsTrade.TradeStatusAfterAndWithOutConfirmed.Contains(TradeStatus) || ConsTrade.审批中.Equals(TradeStatus) || UnWindDate.HasValue || hasSwapDealEvent || (!PS.Config.TradeElement.IsAmendableAfterConfirm && TradeDate < valuedateBLL.ValueDate && ConsTrade.确认成交.Equals(TradeStatus))); } public bool CanDelete() { diff --git a/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs b/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs index 9ed78cef..18b31de5 100644 --- a/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs +++ b/YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs @@ -1,4 +1,5 @@ using YLErp.Abstract.DataProviders; +using YLErp.Helpers; using YLErp.Models; using YLErp.Modules.TradeModule.DealModule; using YLErp.QdpModule; @@ -139,9 +140,9 @@ namespace YLErp.Modules.DataProviderModule { if (item.UnderlyingInstrumentType == "Bonds") { - item.SettlePrice = Convert.ToDouble(item.DeciSettlePrice * ConsGlobal.bondPriceMultiple); - item.ClosePrice = Convert.ToDouble(item.DeciClosePrice * ConsGlobal.bondPriceMultiple); - item.ReferencePrice = Convert.ToDouble(item.DeciReferencePrice * ConsGlobal.bondPriceMultiple); + item.SettlePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciSettlePrice)); + item.ClosePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciClosePrice)); + item.ReferencePrice = Convert.ToDouble(BondPriceConverter.ToStorage(item.DeciReferencePrice)); } _priceDic[item.UnderlyingCode] = item; } diff --git a/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs b/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs index 5c9f6b6f..02e7787b 100644 --- a/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs +++ b/YLErpDAL/Modules/DataProviderModule/EodPriceQueryService.cs @@ -1,5 +1,6 @@ using DocumentFormat.OpenXml.Drawing.Charts; using System.Linq.Expressions; +using YLErp.Helpers; using YLErp.Models; using YLErp.QdpModule; @@ -228,9 +229,9 @@ namespace YLErp.Modules.DataProviderModule Vobp = bondPrice.vobp, ValueDate = valueDate, UnderlyingCode = underlyingCode, - ClosePrice = Convert.ToDouble(bondPrice.dirty_price_close * ConsGlobal.bondPriceMultiple), - SettlePrice = Convert.ToDouble(bondPrice.net_price * ConsGlobal.bondPriceMultiple), - ReferencePrice = Convert.ToDouble(bondPrice.yield * ConsGlobal.bondPriceMultiple) + ClosePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.dirty_price_close)), + SettlePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.net_price)), + ReferencePrice = Convert.ToDouble(BondPriceConverter.ToStorage(bondPrice.yield)) }; } /// diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index eda780f8..a10caab3 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -135,7 +135,8 @@ namespace YLErp.Modules.EodModule public decimal CalcPayment(List payments, decimal qty, decimal longRatio, decimal payDirection) { var interest = payments.Sum(s => s.payment_interest ?? 0); - return interest * qty * 0.01m * longRatio * payDirection; + // interest 为每 100 元面值的票息,×qty 后需 ÷100 转为实际金额(与入库价格 bondPriceMultiple 同口径) + return BondPriceConverter.ToStorage(interest * qty) * longRatio * payDirection; } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index fe89dd14..8c557d4b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using YLErp.BLL; using YLErp.BLL.Eod; using YLErp.DBModels.Enums; +using YLErp.Helpers; using YLErp.Modules.DataProviderModule; using YLErp.Modules.EodModule; using YLErp.Modules.TradeModule; @@ -15,11 +16,105 @@ namespace YLErp.Modules.SwapModule { public class SwapDealService : SwapTradeBaseService { + private static readonly IYcLogger Logger = LogFactory.GetLogger(nameof(SwapDealService)); protected virtual bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate); } + #region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变 + + // FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复) + + /// 添加资金记录(生产: AddClientCashInCashOut;测试: 计数并记录金额) + protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate) + { + return AddClientCashInCashOut(td, amount, action, valueDate); + } + + /// 保存互换/平仓事件(生产: 落库+建事件;测试: 收集 unwindData 入内存列表)。 + /// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。 + protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) + { + return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve); + } + + /// 保存所有变更(生产: DbContext.SaveChanges;测试: 空操作) + protected virtual void SaveAllChanges() + { + DbContext.SaveChanges(); + } + + /// 在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务) + protected virtual void ExecuteInTransaction(Action action) + { + var trans = DbContext.Database.BeginTransaction(); + try + { + action(); + trans.Commit(); + } + catch + { + trans.Rollback(); + throw; + } + finally + { + trans.Dispose(); + } + } + + /// 保存互换交易资金记录(生产: new ClientCashInCashOutService;测试: 空操作)。 + /// 仅 SwapUnwind 全平仓且 NeedOpenFee=false 时调用。 + protected virtual void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) + { + td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id); + if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee) + { + new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, valueDate, 0); + } + } + + /// 触发互换实时持仓计算(生产: Task.Run 异步 RealtimePnlCalc;测试: 空操作)。 + /// 仅 SwapUnwind 成功后调用。 + protected virtual void TriggerRealtimeSwapPosition() + { + Task.Run(() => + { + try + { + RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service)); + } + catch (Exception ex) + { + LogFactory.GetLogger().Error("互换实时持仓服务计算失败", ex); + } + }); + } + + /// 查找待审核的互换/平仓事件(生产: DbContext.swap_event 查询;测试: 返回内存对象) + protected virtual swap_event FindSwapEvent(int tradeId, int eventType) + { + return DbContext.swap_event + .Where(x => x.SwapTradeId == tradeId && !x.Invalid && x.EventType == eventType) + .OrderByDescending(o => o.id).FirstOrDefault(); + } + + /// 查找事件关联的流水事件(生产: DbContext.swap_flow_event 查询;测试: 返回内存列表) + protected virtual List FindFlowEventsByEventId(long eventId) + { + return DbContext.swap_flow_event.Where(x => x.EventId == eventId).ToList(); + } + + /// 平仓/互换审核的前置校验与状态设置(生产: new TradeUnwindService;测试: 空操作或计数) + protected virtual void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck) + { + new TradeUnwindService(this).CloseReCheck_SetTrade(swapTradeId, isSwap, needCheck); + } + + #endregion + public SwapDealService(OptUserInfo optUser) : base(optUser) { @@ -28,6 +123,85 @@ namespace YLErp.Modules.SwapModule { } + + #region 前端盈亏只读校验(不阻断交易) + + /// + /// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对, + /// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。 + /// + /// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底, + /// 为将来公式统一积累"前后端差异"数据。 + /// + /// 前端算好传入的结算数据 + /// true=结息页(income公式),false=平仓页(unwind公式) + private void ValidateFrontendPnL(UnwindData unwindData, bool isIncome) + { + try + { + // 取浮动腿(有 UnderlyingCode 的),与前端 initDeal 取法一致 + var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode)); + // PosiGrossPrice 是 [NotMapped],前端可能没传;为空/0 时跳过(避免误报) + if (floatLeg == null || floatLeg.PosiGrossPrice == 0) + { + return; + } + + // 用 UnderlyingInstrumentType 推 Multiplier(债券=100,否则1) + bool isBond = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType); + int multiplier = isBond ? 100 : 1; + + // 分类利息腿/预付金腿(InterestMode 初始预付金/追加预付金→Margin,否则→Interest) + var input = new UnwindInput + { + Multiplier = multiplier, + PosiGrossPrice = floatLeg.PosiGrossPrice, // EntryDirtyPrice + TradingAmountAvg = floatLeg.TradingAmountAvg, // ExitDirtyPrice(界面×multiplier形态) + CloseQty = unwindData.CloseQty, + CloseNotionalValue = unwindData.CloseNotionalValue, + PayDirection = floatLeg.PayDirection, + PositionType = floatLeg.PositionType, + TradingFee = floatLeg.TradingFee.ToString(), + TradingFeePending = floatLeg.TradingFeePending.ToString(), + DividendIn = floatLeg.DividendIn.ToString(), + }; + foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode))) + { + var target = (leg.InterestMode == (int)InterestModeEnum.初始预付金 + || leg.InterestMode == (int)InterestModeEnum.追加预付金) + ? input.MarginLegs : input.InterestLegs; + target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL }); + } + + var recalc = isIncome + ? FrontendCalcReference.CalcIncome(input) + : FrontendCalcReference.CalcUnwind(input); + + // 逐字段比对,差异 > 0.01 告警 + const decimal threshold = 0.01m; + CheckDiff(nameof(recalc.SwapRealizedPnL), unwindData.SwapRealizedPnL, recalc.SwapRealizedPnL, threshold, unwindData.SwapTradeId, floatLeg); + CheckDiff(nameof(recalc.SwapCloseAmount), unwindData.SwapCloseAmount, recalc.SwapCloseAmount, threshold, unwindData.SwapTradeId, floatLeg); + CheckDiff("MarkClosePnl", floatLeg.MarkClosePnl, recalc.MarkClosePnl, threshold, unwindData.SwapTradeId, floatLeg); + } + catch (Exception ex) + { + // 校验自身错误绝不阻断交易 + Logger.Error($"[互换盈亏校验异常] tradeId={unwindData.SwapTradeId} isIncome={isIncome}", ex); + } + } + + private void CheckDiff(string field, decimal frontendVal, decimal backendVal, decimal threshold, int tradeId, swap_flow_event floatLeg) + { + decimal diff = frontendVal - backendVal; + if (Math.Abs(diff) > threshold) + { + Logger.Error($"[互换盈亏校验分歧] tradeId={tradeId} field={field} frontend={frontendVal} backend={backendVal} diff={diff} " + + $"floatLeg=[gross={floatLeg.PosiGrossPrice} avg={floatLeg.TradingAmountAvg} qty={floatLeg.Quantity} payDir={floatLeg.PayDirection} posType={floatLeg.PositionType}]"); + } + } + + #endregion + /// /// 平仓初始化 /// @@ -246,6 +420,7 @@ namespace YLErp.Modules.SwapModule { unwindData.StartDate = preDealDate.Value; } + unwindData.TradeStartDate = td.StartDate; unwindData.ValueDate = dealDate; unwindData.UnwindDate = dealDate; floatEvent.UnwindDate = unwindData.UnwindDate; @@ -253,6 +428,9 @@ namespace YLErp.Modules.SwapModule unwindData.PayDate = QdpCalendarHelper.GetNonHoliday(unwindData.UnwindDate.Value.AddDays(td.trade_extend.ExtendObj.SettlementRules)); floatEvent.PayDate = unwindData.PayDate; floatEvent.SwapTradeId = tradeId; + floatEvent.SwapTradeNo = td.TradeNumber; + floatEvent.EventType = (int)SwapFlowEventTypeEnum.互换; + floatEvent.EventReason = "交易"; unwindData.SwapTradeId = tradeId; unwindData.StructureType = td.StructureType; unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); @@ -264,8 +442,6 @@ namespace YLErp.Modules.SwapModule unwindData.CloseNotionalValue = unwindData.PosiNotionalValue; if (position != null) { - floatEvent.EventType = (int)SwapEventTypeEnum.互换; - floatEvent.EventReason = "交易"; floatEvent.PositionId = position.PositionId; floatEvent.DividendIn = 0; floatEvent.UnderlyingCode = position.UnderlyingCode; @@ -275,6 +451,7 @@ namespace YLErp.Modules.SwapModule floatEvent.PayDirection = position.PosiDirection; floatEvent.PosiGrossPrice = position.PosiGrossPrice; floatEvent.PosiNetPrice = position.PosiNetPrice; + // 注意:TradingAmountNetAvg 字段名为"成交净价(期末语义)",但收益结算/平仓初始化时装入的是期初净价(PosiNetNoFeePrice),前端展示期初净价时取此字段 floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice; floatEvent.TradingAmountNetFeeAvg = position.PosiNetFeePrice; floatEvent.PositionType = position.PositionType; @@ -336,6 +513,27 @@ namespace YLErp.Modules.SwapModule interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false,false, closeList); return interests; } + + /// + /// 获取利息腿"已通过历史互换结出的累计利息"(用于复利重算时扣除,类比分红的 CalcConsumedDividend)。 + /// 数据源为事件级 swap_flow_event.InterestAmount(互换/自动互换 完成态事件,互换当时即落库,不依赖日终归档)。 + /// + /// 交易id + /// 利息腿id + /// 结算日(不含,仅汇总此日之前的历史已结利息;当日事件由 closeList 去重逻辑单独处理) + /// 历史已结利息累计金额(绝对值) + public virtual decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) + { + List swapEventTypes = new List() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; + var consumed = DbContext.swap_flow_event + .Where(x => x.SwapTradeId == tradeId && x.PositionId == positionId + && swapEventTypes.Contains(x.EventType) + && x.DataState == (int)SwapFlowDateStateEnum.完成 + && x.EventDate < beforeDate) + .Sum(s => (decimal?)s.InterestAmount) ?? 0m; + return consumed; + } + /// /// 计算利息腿计息详细 /// @@ -410,7 +608,11 @@ namespace YLErp.Modules.SwapModule else { // 盘中互换场景,使用 CalcUnwindInterest - interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast||newCalcLast)); + // 取历史已结利息(事件级,互换当时落库),供复利重算扣除(仅复利需要;单利基于日终快照自带状态) + var consumedInterest = position.InterestType == (int)InterestTypeEnum.复利 + ? GetConsumedInterest(td.id, position.id, endDate) + : 0m; + interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast||newCalcLast, consumedInterest)); } } //当日有平仓或互换记录时,避免重复结算 @@ -421,8 +623,9 @@ namespace YLErp.Modules.SwapModule var closeEvent = closeList.Where(x => x.PositionId == item.PositionId); if (eventType == (int)SwapEventTypeEnum.互换 || eventType == (int)SwapEventTypeEnum.自动互换) { - // 互换:该仓位当天已有完成事件,直接归0 - if (closeEvent.Any()) + // 互换:该仓位当天有同类型的完成事件,直接归0 + var swapCloseEvent = closeEvent.Where(x => x.EventType == eventType); + if (swapCloseEvent.Any()) { item.InterestAmount = 0; item.TdInterestAmount = 0; @@ -589,7 +792,7 @@ namespace YLErp.Modules.SwapModule /// /// 计算盘中利息(平仓/互换) /// - private swap_flow_event CalcUnwindInterest(trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate, decimal floatRate, decimal posiPrincipal, decimal closePrincipal, decimal closePercent, int annualDays, eod_swap_position preEod, int eventType, bool add, bool swap, decimal orginPv, bool calcFirst, bool calcLast) + private swap_flow_event CalcUnwindInterest(trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate, decimal floatRate, decimal posiPrincipal, decimal closePrincipal, decimal closePercent, int annualDays, eod_swap_position preEod, int eventType, bool add, bool swap, decimal orginPv, bool calcFirst, bool calcLast, decimal consumedInterest = 0m) { if (preEod.id == 0) { @@ -603,7 +806,7 @@ namespace YLErp.Modules.SwapModule } } - return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, closePrincipal, closePercent, annualDays, eventType, preEod, false, orginPv, calcFirst, calcLast); + return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, closePrincipal, closePercent, annualDays, eventType, preEod, false, orginPv, calcFirst, calcLast, consumedInterest); } /// /// 初始化利息腿信息 @@ -635,8 +838,9 @@ namespace YLErp.Modules.SwapModule eod_swap_position preEodPosition, bool needPrice, decimal orginPv, - bool calcFirst, - bool calcLast + bool calcFirst, + bool calcLast, + decimal consumedInterest = 0m ) { decimal interestProfitSum = preEodPosition.InterestProfitSum; @@ -671,7 +875,7 @@ namespace YLErp.Modules.SwapModule var floateRate = preEodPosition.FloatRate; if (position.InterestType == (int)InterestTypeEnum.复利) { - CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); + CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest); } else { @@ -699,7 +903,7 @@ namespace YLErp.Modules.SwapModule /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterest( DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailyCompoundInterest( DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m) { var startDate = position.PosiStartDate; decimal interestProfitSum = 0; @@ -758,6 +962,11 @@ namespace YLErp.Modules.SwapModule tdinterest += tdinterest1; } } + // 复利从头重放得到的是"假设从未结出"的整段总利息,需扣除历史已通过互换结出的利息, + // 否则已结部分会重复计息(类比分红 PosiDividendSum = totalToDate − RealizedDividend)。 + // consumedInterest 为绝对值口径(swap_flow_event.InterestAmount 之和),与 interest 口径一致。 + interest -= consumedInterest; + tdinterest -= consumedInterest; InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } @@ -765,7 +974,7 @@ namespace YLErp.Modules.SwapModule /// /// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率) /// - public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m) { var startDate = position.PosiStartDate; decimal interestProfitSum = preEodPosition.InterestProfitSum; @@ -955,24 +1164,24 @@ namespace YLErp.Modules.SwapModule /// public void SwapUnwind(UnwindData unwindData) { - var td = DbContext.trade.Find(unwindData.SwapTradeId); + var td = FindTrade(unwindData.SwapTradeId); if (td == null) { throw new ServiceException("未找到交易信息"); } //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 - var trans = DbContext.Database.BeginTransaction(); + ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易 bool cofirm = false; - try + ExecuteInTransaction(() => { - int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate); + int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate); if (unwindData.SwapMarginAmount != 0) { - AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate); + AddClientCash(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate); } //if (unwindData.SwapMarginRebatePnl != 0) //{ - // AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); + // AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); //} DealFloatPosition(unwindData); var flowList = new List(unwindData.FlowEvents); @@ -980,11 +1189,7 @@ namespace YLErp.Modules.SwapModule if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓 || unwindData.ClosePercent == 1) { td.TradeStatus = "已平仓"; - td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id); - if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee) - { - new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, unwindData.ValueDate, 0); - } + CallSaveSwapTradeClientCash(td, unwindData.ValueDate); } else { @@ -993,32 +1198,12 @@ namespace YLErp.Modules.SwapModule td.UnWindDate = unwindData.UnwindDate; td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue); td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty); - DbContext.SaveChanges(); - trans.Commit(); + SaveAllChanges(); cofirm = true; - } - catch (Exception ex) - { - trans.Rollback(); - throw new Exception(ex.Message, ex); - } - finally - { - trans.Dispose(); - } + }); if (cofirm) { - Task.Run(() => - { - try - { - RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service)); - } - catch (Exception ex) - { - LogFactory.GetLogger().Error("互换实时持仓服务计算失败", ex); - } - }); + TriggerRealtimeSwapPosition(); } } /// @@ -1096,7 +1281,8 @@ namespace YLErp.Modules.SwapModule floatEvent.TradingFeePending = Math.Round(floatEvent.TradingFeePending, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); floatEvent.TradingFee = closeFee; floatEvent.MarkClosePnl = (unwindPrice - position.PosiGrossPrice) * unwindQty * floatRatio * longRatio; - floatEvent.MarkClosePnl = Math.Round(floatEvent.MarkClosePnl + ((floatEvent.TradingFeePending + closeFee) * floatRatio * -1) + floatEvent.DividendIn, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + //MarkClosePnl 纯盯市不要计算交易费用和分红 + floatEvent.MarkClosePnl = Math.Round(floatEvent.MarkClosePnl, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); floatEvent.TradingAmount = unwindPrice * floatEvent.Quantity * floatEvent.ContractSize; floatEvent.TradingAmount = Math.Round(floatEvent.TradingAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); floatEvent.OptLog = "流水自动"; @@ -1343,7 +1529,7 @@ namespace YLErp.Modules.SwapModule var floatPosition = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode)); var interestList = unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)); decimal floatRatio = floatPosition.PayDirection == 1 ? 1m : -1m; - var pnl = floatPosition.MarkClosePnl; + var pnl = floatPosition.FloatPnlSum; unwindData.SwapCloseAmount = pnl; unwindData.SwapRealizedPnL = pnl; unwindData.SwapMarginRebatePnl = 0; @@ -1454,20 +1640,19 @@ namespace YLErp.Modules.SwapModule /// public void SwapIncome(UnwindData unwindData) { - var td = DbContext.trade.Find(unwindData.SwapTradeId); + var td = FindTrade(unwindData.SwapTradeId); if (td == null) { throw new ServiceException("未找到交易信息"); } //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 - var trans = DbContext.Database.BeginTransaction(); - bool confirm = false; - try + ValidateFrontendPnL(unwindData, isIncome: true); // 只读校验告警,不阻断交易 + ExecuteInTransaction(() => { - int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate); + int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate); if (unwindData.SwapMarginRebatePnl != 0) { - AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); + AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); } foreach (var item in unwindData.FlowEvents) { @@ -1481,19 +1666,8 @@ namespace YLErp.Modules.SwapModule td.TradeStatus = "已到期"; } td.UnWindDate = unwindData.UnwindDate; - DbContext.SaveChanges(); - trans.Commit(); - confirm = true; - } - catch (Exception ex) - { - trans.Rollback(); - throw; - } - finally - { - trans.Dispose(); - } + SaveAllChanges(); + }); } /// /// 互换/平仓审核通过 @@ -1503,29 +1677,24 @@ namespace YLErp.Modules.SwapModule /// public void ApproveSwapTrade(trade td, int eventType) { - Expression> eventExpression = x => x.SwapTradeId == td.id && !x.Invalid && x.EventType == eventType; - var swapEvent = DbContext.swap_event.Where(eventExpression).OrderByDescending(o => o.id).FirstOrDefault(); + var swapEvent = FindSwapEvent(td.id, eventType); if (swapEvent == null) { throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效"); } swapEvent.unwindData = JsonConvert.DeserializeObject(swapEvent.EventData); - var flowList = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList(); + var flowList = FindFlowEventsByEventId(swapEvent.id); string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费; - int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate); + int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate); if (swapEvent.unwindData.SwapMarginAmount != 0) { - AddClientCashInCashOut(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate); + AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate); } swapEvent.ClientCashId = clientCashId; if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓) { td.TradeStatus = "已平仓"; - td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id); - if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee) - { - new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, swapEvent.unwindData.ValueDate, 0); - } + CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate); } else { @@ -1533,11 +1702,15 @@ namespace YLErp.Modules.SwapModule td.HasPartialUnWind = 1; } td.UnWindDate = swapEvent.unwindData.UnwindDate; - td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue); - td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty); + if (eventType != (int)SwapEventTypeEnum.互换) + { + td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue); + td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty); + } + td.Notional = td.TradeAmount; UpdateInitalPosition(flowList, swapEvent.unwindData, eventType); - DbContext.SaveChanges(); + SaveAllChanges(); } /// @@ -1548,38 +1721,32 @@ namespace YLErp.Modules.SwapModule /// public void ApplySwapTrade(UnwindData unwindData, int eventType) { - var td = DbContext.trade.Find(unwindData.SwapTradeId); + var td = FindTrade(unwindData.SwapTradeId); if (td == null) { throw new ServiceException("未找到交易信息"); } unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount; - var trans = DbContext.Database.BeginTransaction(); string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费; - try + ExecuteInTransaction(() => { - new TradeUnwindService(this).CloseReCheck_SetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true); + CloseReCheckSetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true); SaveSwapDeal(unwindData, eventType, 0, action, true); - DbContext.SaveChanges(); - trans.Commit(); - } - catch (Exception ex) - { - trans.Rollback(); - throw; - } - finally - { - trans.Dispose(); - } + SaveAllChanges(); + }); } /// /// 保存平仓/互换事件 /// /// /// - private long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) + private long SaveSwapDealInternal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) { + var td = DbContext.trade.Find(unwindData.SwapTradeId); + if (td == null) + { + throw new ServiceException("未找到交易信息"); + } var flowList = new List(unwindData.FlowEvents); unwindData.FlowEvents.Clear(); string data = JsonConvert.SerializeObject(unwindData); @@ -1594,7 +1761,7 @@ namespace YLErp.Modules.SwapModule item.PayDate = unwindData.PayDate; item.UnwindDate = unwindData.UnwindDate; item.EventDate = unwindData.ValueDate; - item.EventId = swapEvent.id; + FillSwapFlowEvent(item, unwindData, td, eventType, swapEvent.id); DbContext.swap_flow_event.Add(item); } if (!approve) @@ -1602,8 +1769,25 @@ namespace YLErp.Modules.SwapModule UpdateInitalPosition(flowList, unwindData, eventType); } DbContext.SaveChanges(); + var savedFlowEvents = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList(); + foreach (var item in savedFlowEvents) + { + FillSwapFlowEvent(item, unwindData, td, eventType, swapEvent.id); + } + DbContext.SaveChanges(); return swapEvent.id; } + + private void FillSwapFlowEvent(swap_flow_event item, UnwindData unwindData, trade td, int eventType, long eventId) + { + item.EventId = eventId; + item.SwapTradeId = unwindData.SwapTradeId; + item.SwapTradeNo = td.TradeNumber; + item.EventType = eventType; + item.EventReason = string.IsNullOrWhiteSpace(item.EventReason) ? "交易" : item.EventReason; + UpdateDbOption(item); + } + /// /// 计算平仓数据 /// diff --git a/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs b/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs index 8ddb7786..c8786ab1 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEndConfirmService.cs @@ -78,10 +78,10 @@ namespace YLErp.Modules.SwapModule var um = underlyings.FirstOrDefault(x => x.UnderlyingCode == swapEventFlow.UnderlyingCode); if (um != null && um.IsBond()) { - x.swap_flow_event.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - x.swap_flow_event.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - x.swap_flow_event.TradingAmountNetAvg *= ConsGlobal.bondShowPriceMultiple; - x.swap_flow_event.TradingAmountNetFeeAvg *= ConsGlobal.bondShowPriceMultiple; + x.swap_flow_event.TradingAmountAvg = BondPriceConverter.ToDisplay(x.swap_flow_event.TradingAmountAvg); + x.swap_flow_event.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(x.swap_flow_event.TradingAmountFeeAvg); + x.swap_flow_event.TradingAmountNetAvg = x.swap_flow_event.TradingAmountNetAvg.HasValue ? BondPriceConverter.ToDisplay(x.swap_flow_event.TradingAmountNetAvg.Value) : x.swap_flow_event.TradingAmountNetAvg; + x.swap_flow_event.TradingAmountNetFeeAvg = x.swap_flow_event.TradingAmountNetFeeAvg.HasValue ? BondPriceConverter.ToDisplay(x.swap_flow_event.TradingAmountNetFeeAvg.Value) : x.swap_flow_event.TradingAmountNetFeeAvg; } if (x.trade_contract_r != null) { @@ -93,7 +93,7 @@ namespace YLErp.Modules.SwapModule if (x.trade.StructureType == "多空组合") { UnwindData unwindData = new UnwindData(); - unwindData.SwapCloseAmount = x.swap_flow_event.MarkClosePnl; + unwindData.SwapCloseAmount = x.swap_flow_event.FloatPnlSum; x.swap_event.unwindData = unwindData; } } diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 888e067f..9007286c 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1,8 +1,11 @@ using BaseOUDAL; using Newtonsoft.Json; +using NPOI.POIFS.Properties; using System.Linq.Expressions; +using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; +using YLErp.Helpers; using YLErp.Model; using YLErp.Model.Enum; using YLErp.Models; @@ -27,6 +30,145 @@ namespace YLErp.Modules.SwapModule { } + + #region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变 + + /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) + protected virtual void PersistEodSwapPosition(eod_swap_position position) + { + if (position.id == 0) + { + DbContext.eod_swap_position.Add(position); + } + else + { + UpdateDbOption(position); + } + } + + /// 保存所有变更(生产: DbContext.SaveChanges;测试: 计数) + protected virtual void SaveAllChanges() + { + DbContext.SaveChanges(); + } + + /// 获取汇率(生产: EodCurrencyRateService;测试: 返回固定值) + protected virtual double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) + { + return new EodCurrencyRateService(UserInfo).GetCurrencyRate(quoteCurrency, settlementCurrency, valueDate, seekPreday, currencyRateType); + } + + /// + /// 计算利息腿利息明细(生产: new SwapDealService(this).GetInterests;测试: 用StubSwapDealService内存算) + /// 参数与 SwapDealService.GetInterests 完全一致,保证行为不变。 + /// + protected virtual List CalcSwapInterests( + trade td, trade_extend tradeExtend, + DateTime valueDate, DateTime unwindDate, + List eodPositions, List positions, + decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue, + decimal closePosiNotionalValue, decimal closePrecent, + int eventType, bool tdClose, bool needPrice, + decimal grossPrice, decimal orginPv, + bool add = false, bool settment = true, bool newCalcLast = false, + List closeList = null) + { + return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate, + eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue, + closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice, + grossPrice, orginPv, add, settment, newCalcLast, closeList); + } + + // FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复) + + /// 查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典) + protected virtual trade_extend FindTradeExtend(int tradeId) + { + return DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); + } + + /// 查找指定日期范围的 eod 持仓(生产: DbContext.eod_swap_position.Where;测试: 内存列表) + protected virtual List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) + { + return DbContext.eod_swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList(); + } + + /// 查找交易持仓(生产: DbContext.swap_position.Where;测试: 内存列表) + protected virtual List FindSwapPositions(int swapTradeId) + { + return DbContext.swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList(); + } + + /// 查找框架合约日终汇总(生产: DbContext.eod_swap.FirstOrDefault;测试: 内存字典) + protected virtual eod_swap FindEodSwap(int swapTradeId, DateTime valueDate) + { + return DbContext.eod_swap.FirstOrDefault(x => x.SwapTradeId == swapTradeId && x.ValueDate == valueDate); + } + + /// 添加互换事件(生产: new SwapEventService(this).AddSwapEventDate;测试: 收集到列表) + protected virtual swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) + { + return new SwapEventService(this).AddSwapEventDate(tradeDate, swapTradeId, eventType, data, clientCashId, save, reason); + } + + /// 在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务) + protected virtual void ExecuteInTransaction(Action action) + { + var trans = DbContext.Database.BeginTransaction(); + try + { + action(); + trans.Commit(); + } + catch + { + trans.Rollback(); + throw; + } + finally + { + trans.Dispose(); + } + } + + /// 添加资金记录(生产: AddClientCashInCashOut;测试: 收集到计数器) + protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate) + { + return AddClientCashInCashOut(td, amount, action, valueDate); + } + + /// 保存框架合约日终汇总(生产: SaveEodSwap私有方法;测试: 收集到列表) + protected virtual void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) + { + SaveEodSwap(td, settleDate, preSettleDate); + } + + /// 清理旧持仓事件(生产: ClearSwapPositions;测试: 空操作) + protected virtual void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) + { + ClearSwapPositions(td, tradeDate, eventTypes, false); + } + + /// 获取标的市场价格(生产: UnderlyingCodePrice查缓存+中债估值;测试: 返回固定值) + protected virtual decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) + { + return UnderlyingCodePrice(code, settleDate, out vobp); + } + + /// 获取标的缓存数据(生产: DataCacheProvider;测试: 返回内存对象) + protected virtual underlying_manager GetUnderlyingData(string underlyingCode) + { + return DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + } + + /// 计算债券付息(生产: BondPaymentService;测试: 返回固定值) + protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) + { + return new BondPaymentService(UserInfo).CalcPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); + } + + #endregion + /// /// 多空组合 互换流水合成持仓 /// @@ -144,7 +286,10 @@ namespace YLErp.Modules.SwapModule if (autoInterval != null) break; } + // 自动互换(仅利息/预付金,不含分红) DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval); + // 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关 + DealDividends(curEodPosis, td, settleDate, tradeExtend); //多空组合判断是否已到到期日且无持仓信息 if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty == 0) { @@ -206,7 +351,7 @@ namespace YLErp.Modules.SwapModule /// 自动互换集合 /// 多空组合平仓利息腿信息 /// 上一日终框架合约 - private void DealInterests(List interestList, + protected void DealInterests(List interestList, List eodPositions, List todyEodPositions, DateTime settleDate, @@ -314,7 +459,7 @@ namespace YLErp.Modules.SwapModule /// /// /// - private List DealFloatPositions(List posiList, + protected List DealFloatPositions(List posiList, List realPosiList, List eodPositions, List todyEodPositions, @@ -370,90 +515,188 @@ namespace YLErp.Modules.SwapModule /// /// /// 自动互换观察日信息,用于获取结算日期 + /// 当日浮动端EOD持仓 + /// 交易扩展信息 + /// + /// 自动互换(仅利息/预付金,不含分红) + /// private void DealAutoInterests(List autoInterests, trade td, DateTime settleDate, DateTime? preDealDate, decimal StockEqvNotional, IntervalModel interval) { - if (autoInterests.Count == 0) - { - return; - } - //生成自动互换事件 + if (autoInterests.Count == 0) return; + UnwindData unwindData = new UnwindData(); unwindData.SwapTradeId = td.id; unwindData.ValueDate = settleDate; - if (preDealDate.HasValue) - { - unwindData.StartDate = preDealDate.Value; - } - else - { - unwindData.StartDate = td.StartDate.Value; - } + unwindData.StartDate = preDealDate ?? td.StartDate.Value; unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); unwindData.PosiNotionalValue = StockEqvNotional; + unwindData.PayDate = settleDate; + + autoInterests.ForEach(x => x.PayDate = settleDate); - // 预付金腿类型列表:初始预付金、追加预付金 var premiumModes = new List() { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; - - // 分别计算预付金腿和利息腿的金额 var premiumInterests = autoInterests.Where(x => premiumModes.Contains(x.InterestMode)).ToList(); var interestLegs = autoInterests.Where(x => !premiumModes.Contains(x.InterestMode)).ToList(); - // 预付金腿金额 decimal premiumTotal = 0; premiumInterests.ForEach(x => { var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1; premiumTotal += x.InterestClosePnL * ratio; }); - unwindData.SwapMarginRebatePnl = premiumTotal; // 预付金腿金额 + unwindData.SwapMarginRebatePnl = premiumTotal; - // 利息腿金额(总金额减去预付金腿金额) decimal interestTotal = 0; interestLegs.ForEach(x => { var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; interestTotal += x.InterestClosePnL * ratio; }); - unwindData.SwapCloseAmount = interestTotal; // 利息腿金额 - - // 总实现盈亏 + unwindData.SwapCloseAmount = interestTotal ; + unwindData.SwapDividendPnl = 0; unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount + unwindData.SwapMarginRebatePnl; SaveAutoSwapDeal(td, autoInterests, unwindData, interval); } /// + /// 分红独立处理:当天有债券需要分红时,生成独立的分红自动互换事件 + /// + private void DealDividends(List curEodPositions, trade td, DateTime settleDate, trade_extend tradeExtend) + { + if (curEodPositions == null) return; + var hasDividend = curEodPositions.Any(x => x.PosiDividendSum != 0); + if (!hasDividend) return; + + var dividendPayDateOffset = tradeExtend?.ExtendObj?.DividendPayDate ?? 1; + if (dividendPayDateOffset <= 0) return; + + var payDays = dividendPayDateOffset - 1; + var dividendPayDate = QdpCalendarHelper.GetNonHoliday(settleDate.AddDays(payDays)); + + List dividendEvents = new List(); + decimal dividendTotal = 0; + + foreach (var eodPosi in curEodPositions.Where(x => x.PosiDividendSum != 0)) + { + var dividendEvent = new swap_flow_event + { + SwapTradeId = td.id, + SwapTradeNo = td.TradeNumber, + EventType = (int)SwapEventTypeEnum.自动互换, + EventReason = "系统操作-分红", + EventDate = settleDate, + UnwindDate = settleDate, + PayDate = dividendPayDate, + PositionId = eodPosi.PositionId, + UnderlyingCode = eodPosi.UnderlyingCode, + UnderlyingInstrumentType = eodPosi.UnderlyingInstrumentType, + PayDirection = eodPosi.PosiDirection, + PositionType = eodPosi.PositionType, + PositionQty = eodPosi.PosiQuantity, + Quantity = 0, + ContractSize = eodPosi.ContractSize, + TradingAmountAvg = eodPosi.PosiGrossPrice, + TradingAmountNetAvg = eodPosi.PosiNetNoFeePrice, + PosiGrossPrice = eodPosi.PosiGrossPrice, + PosiNetPrice = eodPosi.PosiNetPrice, + MarkClosePnl = 0,//当日盯市不要计算分红 + DividendIn = eodPosi.PosiDividendSum, + CloseFee = 0, + TradingFee = 0, + TradingFeePending = 0, + ClientId = td.ClientId, + DataState = (int)SwapFlowDateStateEnum.完成, + }; + dividendEvents.Add(dividendEvent); + dividendTotal += eodPosi.PosiDividendSum; + + eodPosi.TdCloseDividend += eodPosi.PosiDividendSum; + //当日也要展示 eodPosi.TdPosiDividend = 0; + eodPosi.RealizedDividend += eodPosi.PosiDividendSum; + //互换持仓价值要去掉已实现的 + eodPosi.SwapPositionValue -= eodPosi.PosiDividendSum; + //已实现盈亏要加上已实现的 + eodPosi.RealizedPnl += eodPosi.PosiDividendSum; + eodPosi.PosiDividendSum = 0; + eodPosi.PosiProfitSum -= eodPosi.TdCloseDividend; + + } + + UnwindData unwindData = new UnwindData(); + unwindData.SwapTradeId = td.id; + unwindData.ValueDate = settleDate; + unwindData.StartDate = td.StartDate.Value; + unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); + unwindData.PosiNotionalValue = curEodPositions.Sum(x => x.PosiNotionalValue); + unwindData.PayDate = dividendPayDate; + unwindData.SwapDividendPnl = dividendTotal; + unwindData.SwapCloseAmount = 0; + unwindData.SwapMarginRebatePnl = 0; + unwindData.SwapRealizedPnL = dividendTotal; + + SaveAutoSwapDeal(td,null , unwindData,null, dividendEvents:dividendEvents); + } + /// /// 保存自动互换数据信息 /// /// /// /// 自动互换观察日信息,用于获取结算日期 - private long SaveAutoSwapDeal(trade td, List flowEvents, UnwindData unwindData, IntervalModel interval) + private long SaveAutoSwapDeal(trade td, List flowEvents, UnwindData unwindData, IntervalModel interval, List dividendEvents = null) { //td.UnWindDate = unwindData.ValueDate; //优先使用 interval.SettlementDate 作为资金记录发生日期,如果没有则使用 ValueDate var cashHappenDate = interval?.SettlementDate ?? unwindData.ValueDate; int clientCashId = 0; + var clientCashIds = new List(); // 利息腿:插入资金记录(使用系统操作_互换) if (unwindData.SwapCloseAmount != 0) { clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, cashHappenDate); + clientCashIds.Add(clientCashId); } - + // 预付金腿:单独插入一条资金记录(系统操作_预付金返息) if (unwindData.SwapMarginRebatePnl != 0) { - AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); + clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); + clientCashIds.Add(clientCashId); + } + unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL;//需要算上预付金利息 和 分红; 只是不算预付金返还 + // 分红:使用派息支付日偏移记录资金记录 + if (unwindData.SwapDividendPnl != 0) + { + var dividendPayDate = (dividendEvents != null && dividendEvents.Count > 0) + ? dividendEvents.First().PayDate.Value + : unwindData.ValueDate; + clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapDividendPnl), ClientCashInCashOut.系统操作_互换, dividendPayDate); + clientCashIds.Add(clientCashId); } + unwindData.ClientCashIds = clientCashIds; string data = JsonConvert.SerializeObject(unwindData); var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件 - flowEvents.ForEach(x => + if (flowEvents!=null) { - x.EventId = swapEvent.id; - DbContext.swap_flow_event.Add(x); - }); - UpdateInitalPostion(flowEvents, td.id); + flowEvents.ForEach(x => + { + x.EventId = swapEvent.id; + DbContext.swap_flow_event.Add(x); + }); + UpdateInitalPostion(flowEvents, td.id); + } + + // 保存分红事件 + if (dividendEvents != null) + { + dividendEvents.ForEach(x => + { + x.EventId = swapEvent.id; + DbContext.swap_flow_event.Add(x); + }); + UpdateInitalPostion(dividendEvents, td.id); + } return swapEvent.id; } /// @@ -478,7 +721,7 @@ namespace YLErp.Modules.SwapModule /// /// 互换交易id /// 清算日期 - private void ComposePage(int swapTradeId, List flowEvents, DateTime tradeDate, bool needTrans = true) + protected void ComposePage(int swapTradeId, List flowEvents, DateTime tradeDate, bool needTrans = true) { // 同一标的 事件编码一致 var eventQuery = flowEvents.OrderBy(o => o.PositionId).ToList(); @@ -492,21 +735,16 @@ namespace YLErp.Modules.SwapModule { UnwindData unwindData = new UnwindData(); unwindData.SwapTradeId = swapTradeId; - var swapEvent = new SwapEventService(this).AddSwapEventDate(tradeDate, swapTradeId, (int)SwapEventTypeEnum.合成持仓, string.Empty, 0, true, "系统操作-自动合成持仓");//不用回退,回退时过滤该事件类型 - var td = DbContext.trade.Find(swapTradeId); + var swapEvent = AddSwapEvent(tradeDate, swapTradeId, (int)SwapEventTypeEnum.合成持仓, string.Empty, 0, true, "系统操作-自动合成持仓"); + var td = FindTrade(swapTradeId); var preSettleDate = GetPreValueDate(tradeDate);//上一交易日期 List eventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; List removeEventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; - ClearSwapPositions(td, tradeDate, removeEventTyps, false); - td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == swapTradeId); - var allEodPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate); - //var curentInvalidEodPostions = allEodPositions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= tradeDate);//废弃当前清算日期之后的归档数据 - //curentInvalidEodPostions.ForEach(x => - //{ - // x.Invalid = true; - //}); + ClearSwapPositionsForCompose(td, tradeDate, removeEventTyps); + td.trade_extend = FindTradeExtend(swapTradeId); + var allEodPositions = FindEodSwapPositions(swapTradeId, preSettleDate); var eodPositions = allEodPositions.Where(x => x.ValueDate == preSettleDate).ToList();//上一日终持仓信息 - var positions = DbContext.swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList(); + var positions = FindSwapPositions(swapTradeId); var oriPositions = positions.Where(x => x.IsInitial).ToList(); var realPositions = positions.Where(x => !x.IsInitial).ToList(); var fpositions = positions.Where(x => x.PosiDirection > 0).ToList(); @@ -514,7 +752,7 @@ namespace YLErp.Modules.SwapModule decimal totalPosiNotionalValue = 0;//总剩余名义本金 decimal tdCloseNotionalValue = 0;//当日平仓名义本金 var preDealDate = GetPreDealDate(swapTradeId, tradeDate, eventTyps);//上一次平仓/互换/自动互换事件日期 - var lastEodSwap = DbContext.eod_swap.FirstOrDefault(x => x.SwapTradeId == swapTradeId && x.ValueDate == preSettleDate); + var lastEodSwap = FindEodSwap(swapTradeId, preSettleDate); decimal stockEqvNotional = lastEodSwap == null ? Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0) : lastEodSwap.NotionalValue;//上一日名义本金 unwindData.NotionalValue = stockEqvNotional; List longshortCloseInterests = new List(); @@ -583,7 +821,7 @@ namespace YLErp.Modules.SwapModule //记录资金记录 if (amount != 0) { - int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(x.MarkClosePnl), ClientCashInCashOut.系统操作_平仓费, x.UnwindDate.Value); + int clientCashId = AddClientCash(td, Convert.ToDouble(x.MarkClosePnl), ClientCashInCashOut.系统操作_平仓费, x.UnwindDate.Value); x.ClientCashId = clientCashId; } payQty = payQty - x.Quantity; @@ -640,9 +878,9 @@ namespace YLErp.Modules.SwapModule unwindData.PosiNotionalValue = allPosiNotionalValue; unwindData.CloseNotionalValue = tdCloseNotionalValue; swapEvent.EventData = JsonHelper.Serialize(unwindData); - DbContext.SaveChanges(); - SaveEodSwap(td, tradeDate, preSettleDate); - DbContext.SaveChanges(); + SaveAllChanges(); + SaveEodSwapRecord(td, tradeDate, preSettleDate); + SaveAllChanges(); trans?.Commit(); } catch (Exception ex) @@ -665,7 +903,7 @@ namespace YLErp.Modules.SwapModule /// 计息开始日 /// 计息结束日 /// 平仓金额 - private void SaveEodInterestPosition(eod_swap_position eodPayPosition, + protected void SaveEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, @@ -726,8 +964,13 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; //持仓内容-利息腿-损益统计(本方视角) - newEodPayPosition.TdInterestIncome = newEodPayPosition.TdCloseInterest; - newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome - newEodPayPosition.TdCloseInterest; + var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); + if (position.IsAnnualized) + { + intersetAcmount /= tradeExtend.AnnualDays; + } + newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息 + newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome - newEodPayPosition.TdCloseInterest; //上一天待实现 + 当天产生的利息 - flowEvents的利息 newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 @@ -736,13 +979,10 @@ namespace YLErp.Modules.SwapModule //累计已实现 newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; - var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate - , seekPreday: true, currencyRateType: position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, + position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); - if (newEodPayPosition.id == 0) - { - DbContext.eod_swap_position.Add(newEodPayPosition); - } + PersistEodSwapPosition(newEodPayPosition); } /// /// 自动互换用,当日无互换,当日无平仓 @@ -756,7 +996,7 @@ namespace YLErp.Modules.SwapModule /// 上一平仓/互换日期 /// 当日平仓金额 /// 上一日终框架合约估值 - private List SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiLongNotional, decimal posiShortNational, decimal grossPrice, decimal orginPv) + protected List SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiLongNotional, decimal posiShortNational, decimal grossPrice, decimal orginPv) { Log.Info($"[SaveAutoEodInterestPosition] 开始执行 - valueDate: {valueDate:yyyy-MM-dd}, td.id: {td?.id}, position.id: {position?.id}"); @@ -830,7 +1070,7 @@ namespace YLErp.Modules.SwapModule { orginPv = eodPayPosition.InterestPrincipalFix; } - var interests = new SwapDealService(this).GetInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, true, grossPrice, orginPv, true); + var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, true, grossPrice, orginPv, true); decimal InterestAmount = interests.Sum(x => x.InterestAmount); decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount); @@ -873,13 +1113,10 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; newEodPayPosition.RealizedPnl = newEodPayPosition.RealizedInterest + newEodPayPosition.RealizedInterestFee; - var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate - , seekPreday: true, currencyRateType: position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, + position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); - if (newEodPayPosition.id == 0) - { - DbContext.eod_swap_position.Add(newEodPayPosition); - } + PersistEodSwapPosition(newEodPayPosition); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); return interests; } @@ -897,7 +1134,7 @@ namespace YLErp.Modules.SwapModule /// 当日平仓金额 /// 上一日终框架合约估值 /// 平仓主信息 - private List SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiLongNotional, decimal posiShortNational, List flowEvents, decimal closeNational, bool autoSwap, decimal grossPrice, decimal orginPv) + protected List SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiLongNotional, decimal posiShortNational, List flowEvents, decimal closeNational, bool autoSwap, decimal grossPrice, decimal orginPv) { Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); var tradeExtend = td.trade_extend.ExtendObj; @@ -943,7 +1180,7 @@ namespace YLErp.Modules.SwapModule positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); - var interests = new SwapDealService(this).GetInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: true); + var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: true); decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount); decimal InterestAmount = interests.Sum(x => x.InterestAmount); newEodPayPosition.ValueDate = valueDate; @@ -979,7 +1216,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; newEodPayPosition.TdCloseInterest = flowEvents.Sum(s => s.InterestClosePnL); - var intersetAcmount = newEodPayPosition.TdInterestPrincipal*(newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); + var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); if (position.IsAnnualized) { intersetAcmount /= tradeExtend.AnnualDays; @@ -1011,14 +1248,11 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; newEodPayPosition.RealizedPnl = newEodPayPosition.RealizedInterest + newEodPayPosition.RealizedInterestFee; ; - var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate - , seekPreday: true, currencyRateType: position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, + position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); - if (newEodPayPosition.id == 0) - { - DbContext.eod_swap_position.Add(newEodPayPosition); - } + PersistEodSwapPosition(newEodPayPosition); return interests; } @@ -1030,7 +1264,7 @@ namespace YLErp.Modules.SwapModule /// 上一交易日 /// 当前结算日 /// 互换交易主干 - private void SaveEodInterestPositionCopy(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, DateTime valueDate, trade td, swap_position position, eod_swap lastEodSwap, bool needPrice, decimal posiLongNational, decimal posiShortNational, decimal grossPrice, decimal orginPv) + protected void SaveEodInterestPositionCopy(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, DateTime valueDate, trade td, swap_position position, eod_swap lastEodSwap, bool needPrice, decimal posiLongNational, decimal posiShortNational, decimal grossPrice, decimal orginPv) { Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); List intervals = position.SwapIntervalList; @@ -1104,7 +1338,7 @@ namespace YLErp.Modules.SwapModule { preEodPositions.Add(eodPayPosition); } - var interests = new SwapDealService(this).GetInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNational, posiShortNational, posiNotionalValue, closePercent, 0, false, needPrice, grossPrice, orginPv); + var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNational, posiShortNational, posiNotionalValue, closePercent, 0, false, needPrice, grossPrice, orginPv); UpdateDbOption(newEodPayPosition); newEodPayPosition.PosiStatus = 0; @@ -1136,14 +1370,11 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; newEodPayPosition.RealizedPnl = newEodPayPosition.RealizedInterest + newEodPayPosition.RealizedInterestFee; - var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate - , seekPreday: true, currencyRateType: eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, + eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); - if (newEodPayPosition.id == 0) - { - DbContext.eod_swap_position.Add(newEodPayPosition); - } + PersistEodSwapPosition(newEodPayPosition); } /// @@ -1162,7 +1393,7 @@ namespace YLErp.Modules.SwapModule /// 当日平仓数量 /// 当日平仓费用 /// 当日浮动盈亏 - private decimal SaveEodPosition(eod_swap_position newEodPayPosition, + protected virtual decimal SaveEodPosition(eod_swap_position newEodPayPosition, trade td, swap_flow_event eventFlow, decimal netPrice, @@ -1213,6 +1444,7 @@ namespace YLErp.Modules.SwapModule //浮动端估值用信息 newEodPayPosition.UnderlyingPrice = UnderlyingCodePrice(newEodPayPosition.UnderlyingCode, eventFlow.EventDate, out decimal vobp); + newEodPayPosition.dv01 = Dv01Helper.CalcDv01(newEodPayPosition.UnderlyingCode, newEodPayPosition.PosiQuantity, newEodPayPosition.PosiDirection, newEodPayPosition.PositionType, vobp); newEodPayPosition.UnderlyingMarketValue = newEodPayPosition.UnderlyingPrice * newEodPayPosition.PosiQuantity * newEodPayPosition.ContractSize * shortRatio; //当日已实现 newEodPayPosition.TdCloseQty = closeQty; @@ -1222,9 +1454,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseFee = closeFee * ratio; //持仓内容-浮动收益腿-损益统计(本方视角 - newEodPayPosition.TdPosiDividend = dividendIn * ratio; + newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2); newEodPayPosition.PosiMtmPnL = (newEodPayPosition.UnderlyingPrice - newEodPayPosition.PosiGrossPrice) * newEodPayPosition.PosiQuantity * newEodPayPosition.ContractSize * shortRatio * ratio; - newEodPayPosition.PosiDividendSum = newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend; + newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2); newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending; //持仓价值 @@ -1252,10 +1484,7 @@ namespace YLErp.Modules.SwapModule UpdateSwapPosition(newEodPayPosition, td.TradeNumber); } UpdateSwapPositionWithRealTime(newEodPayPosition); - if (newEodPayPosition.id == 0) - { - DbContext.eod_swap_position.Add(newEodPayPosition); - } + PersistEodSwapPosition(newEodPayPosition); return posiTradingFee; } /// @@ -1265,7 +1494,7 @@ namespace YLErp.Modules.SwapModule /// 当日日终归档信息 /// 当日平仓/互换事件信息 /// 交易信息 - private eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate) + protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate) { if (curretEod == null) { @@ -1273,7 +1502,7 @@ namespace YLErp.Modules.SwapModule curretEod.id = 0; curretEod.ValueDate = valueDate; } - var um = DataCacheProvider.GetUnderlyingDataSource().GetData(eod.UnderlyingCode); + var um = GetUnderlyingData(eod.UnderlyingCode); if (um == null) { return curretEod; @@ -1282,16 +1511,16 @@ namespace YLErp.Modules.SwapModule int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; - var price = UnderlyingCodePrice(eod.UnderlyingCode, dealDate, out decimal vobp); + var price = GetUnderlyingPrice(eod.UnderlyingCode, dealDate, out decimal vobp); + curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); decimal tax = um.ValueAddedTax ?? 0; - BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0) { - decimal payment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); // 考虑增值税 - curretEod.TdPosiDividend = payment / (1 + tax) * (1 - tax); + curretEod.TdPosiDividend = Math.Round(payment / (1 + tax) * (1 - tax), 2); } - curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? (eod.PosiDividendSum + curretEod.TdPosiDividend) : 0; + curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0; curretEod.PosiQuantity = eod.PosiQuantity; if (curretEod.PosiStatus == 1) { @@ -1344,7 +1573,7 @@ namespace YLErp.Modules.SwapModule /// /// /// - private eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents) + protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents) { if (curretEod == null) { @@ -1354,7 +1583,7 @@ namespace YLErp.Modules.SwapModule curretEod.ValueDate = valueDate; } - var um = DataCacheProvider.GetUnderlyingDataSource().GetData(eod.UnderlyingCode); + var um = GetUnderlyingData(eod.UnderlyingCode); if (um == null) { return curretEod; @@ -1362,32 +1591,48 @@ namespace YLErp.Modules.SwapModule var dealDate = curretEod.ValueDate; int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; - var price = UnderlyingCodePrice(eod.UnderlyingCode, dealDate, out decimal vobp); + var price = GetUnderlyingPrice(eod.UnderlyingCode, dealDate, out decimal vobp); + var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents); + var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice; + decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio); + decimal tax = um.ValueAddedTax ?? 0; + decimal totalInterest = totalPayment / (1 + tax) * (1 - tax); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition); + curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); curretEod.UnderlyingPrice = price; curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio; curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio; curretEod.TdPosiDividend = 0; + // 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红 + // 修改,互换事件会影响待实现的分红的,现在要算上 if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0)) { - decimal tax = um.ValueAddedTax ?? 0; - BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); - decimal payment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); - // 考虑增值税 - curretEod.TdPosiDividend = payment / (1 + tax) * (1 - tax); + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); + curretEod.TdPosiDividend = Math.Round(payment / (1 + tax) * (1 - tax), 2); } curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; - + // 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红) + curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn); curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; curretEod.RealizedPnl = eod.RealizedPnl + curretEod.TdCloseMtmPnl; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var closeQty = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList().Sum(s => s.Quantity); - // 当日浮动端平仓盈亏·分红 = 平仓数量/昨天剩余平仓数量 * 昨日浮动端待实现收益·分红 - curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn); + curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend; - // 浮动端待实现收益·分红 = 昨日 + 当日浮动端分红 - 当日浮动端平仓盈亏·分红 - curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend - curretEod.TdCloseDividend; + // 分红与互换解耦:持仓>0时待实现分红用递增模式(前日+当天新计-当天实现), + // 与 CopyEodPosition 的逐天递增口径一致,避免从头重算的舍入累积差异。 + if (curretEod.PosiQuantity > 0) + { + curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend - curretEod.TdCloseDividend; + } + else + { + curretEod.PosiDividendSum = 0; + } + curretEod.RealizedPnl += curretEod.TdCloseDividend; + curretEod.SwapPositionValue -= curretEod.TdCloseDividend; + curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; if (curretEod.PosiStatus == 1) { @@ -1406,6 +1651,21 @@ namespace YLErp.Modules.SwapModule } return curretEod; } + + private decimal CalcConsumedDividend(eod_swap_position curretEod, List events) + { + decimal consumedDividend = 0; + + List swapEventTypes = new List() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; + //这里要剔除掉平仓产生的分红 + consumedDividend = events + .Where(x => x.SwapTradeId == curretEod.SwapTradeId + && swapEventTypes.Contains(x.EventType) + && x.DataState == (int)SwapFlowDateStateEnum.完成) + .Sum(s => s.DividendIn); + return consumedDividend; + } + /// /// 根据开平仓事件算价格及后付费用 /// @@ -1464,7 +1724,7 @@ namespace YLErp.Modules.SwapModule } curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize; curretEod.PosiNotionalValue = Math.Round(curretEod.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - curretEod.TdPosiDividend = unwindEvents.Sum(x => x.DividendIn) * directionRatio; + curretEod.TdCloseDividend = unwindEvents.Sum(x => x.DividendIn); curretEod.TdCloseFee = unwindFlowEvents.Sum(x => x.TradingFee + x.TradingFeePending); curretEod.TdCloseQty = unwindQty; curretEod.TdCloseMtmPnl = unwindEvents.Sum(x => x.MarkClosePnl); @@ -1482,10 +1742,10 @@ namespace YLErp.Modules.SwapModule /// /// /// - private eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List unwindEvents) + protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List unwindEvents) { eod_swap_position curretEod = new eod_swap_position(); - var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode); + var um = GetUnderlyingData(position.UnderlyingCode); if (um == null) { return curretEod; @@ -1518,15 +1778,30 @@ namespace YLErp.Modules.SwapModule curretEod.ContractSize = position.ContractSize; curretEod.CountRatio = position.CountRatio; curretEod.PosiTradingFee = position.PosiTradingFee; - curretEod.UnderlyingPrice = UnderlyingCodePrice(position.UnderlyingCode, dealDate, out decimal vobp); + curretEod.UnderlyingPrice = GetUnderlyingPrice(position.UnderlyingCode, dealDate, out decimal vobp); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, position); + curretEod.dv01 = Dv01Helper.CalcDv01(curretEod.UnderlyingCode, curretEod.PosiQuantity, curretEod.PosiDirection, curretEod.PositionType, vobp); //if (settleDate == td.TradeDate) //{ // curretEod.UnderlyingPrice = curretEod.PosiGrossPrice; // //curretEod.TdCloseMtmPnl = 0; // //curretEod.TdCloseFee = 0; //} - curretEod.TdCloseDividend = curretEod.TdPosiDividend; + // TdCloseDividend 已由 SetPriceInfoByFlowEvent 设置 + + // 当日新增分红及待实现分红(有互换全量归0,开仓首日两者相同) + curretEod.TdPosiDividend = 0; + var hasSwapEvent = unwindEvents.Any(e => e.EventType == (int)SwapFlowEventTypeEnum.互换 || e.EventType == (int)SwapFlowEventTypeEnum.自动互换); + if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0) + { + decimal tax = um.ValueAddedTax ?? 0; + decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio); + payment = Math.Round(payment / (1 + tax) * (1 - tax), 2); + //var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红 + curretEod.TdPosiDividend = payment; + curretEod.PosiDividendSum = payment; + } + curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio; curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio; curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; @@ -1625,6 +1900,7 @@ namespace YLErp.Modules.SwapModule eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); + eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); decimal interestPnL = 0; interestPositions.ForEach(x => { @@ -1689,6 +1965,7 @@ namespace YLErp.Modules.SwapModule eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); + eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); interestPositions.ForEach(x => { decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负 @@ -1877,6 +2154,9 @@ namespace YLErp.Modules.SwapModule var client = DataCacheProvider.GetClientDataSource().GetData(item.ClientId); item.SwapTradeTypeStr = client?.SwapTradeTypeStr; } + + var dv01 = query.Sum(O => O.position.dv01??0); + retListResult.Sum = new {DV = dv01 }; return retListResult; } diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs index 3bad869c..39eb73e4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs @@ -31,6 +31,61 @@ namespace YLErp.Modules.SwapModule } + #region 可测试化接缝(Seams)——借鉴 refactor-swap-event-testable 分支,override 可在测试中替换 DB/外部调用,生产代码行为不变 + + // FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复) + + protected virtual trade_extend FindTradeExtend(int swapTradeId) + => DbContext.trade_extend.First(x => x.TradeId == swapTradeId); + + protected virtual List FindPositions(int swapTradeId) + => DbContext.swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial && !x.Invalid).AsNoTracking().ToList(); + + protected virtual List FindAndInvalidateFutureEvents(int swapTradeId, DateTime tradeDate) + { + var olds = DbContext.swap_flow_event.Where(x => x.EventDate > tradeDate && x.SwapTradeId == swapTradeId && x.DataState > 0); + olds.ForEach(x => x.DataState = (int)SwapFlowDateStateEnum.废弃); + return olds.ToList(); + } + + protected virtual underlying_manager GetUnderlying(string underlyingCode) + => DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + + protected virtual DateTime GetNextBusinessDay(DateTime date) + => QdpCalendarHelper.GetNonHoliday(date); + + protected virtual long ResolvePositionId(swap_flow_merge merge, DateTime maturityDate, int direction, string tradeNumber) + => GetMaxPositionId(merge, maturityDate, direction, tradeNumber); + + protected virtual void PersistEvents(List events) + { + foreach (var evt in events) + { + DbContext.swap_flow_event.Add(evt); + } + DbContext.SaveChanges(); + } + + protected virtual IDisposable BeginTransaction() + => DbContext.Database.BeginTransaction(); + + protected virtual void CommitTransaction(IDisposable transaction) + => (transaction as Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction)?.Commit(); + + protected virtual void RollbackTransaction(IDisposable transaction) + => (transaction as Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction)?.Rollback(); + + /// MergePageEvent 业务逻辑执行前钩子(默认空,录制golden用)。在DB查询完成、合成逻辑执行前触发 + protected virtual void OnBeforeMergePageEvent(int swapTradeId, DateTime tradeDate, + trade trade, trade_extend tradeExtend, + List merges, List positions) { } + + /// MergePageEvent 业务逻辑执行后钩子(默认空,录制golden用)。在PersistEvents之后触发,捕获输出事件 + protected virtual void OnAfterMergePageEvent(int swapTradeId, DateTime tradeDate, + List resultEvents) { } + + #endregion + /// /// 互换流水开平仓事件 /// @@ -70,7 +125,7 @@ namespace YLErp.Modules.SwapModule /// 分页处理互换流水开平仓事件,暂时只按加权平均处理 /// /// - private List MergePageEvent(int swapTradeId, List flowMergeList, DateTime tradeDate, bool needTrans = true) + protected virtual List MergePageEvent(int swapTradeId, List flowMergeList, DateTime tradeDate, bool needTrans = true) { List flowEvents = new List(); //按照同一互换编码、标的、买卖方向排序,一条买,一条卖//会存在买卖不在同一页 @@ -79,26 +134,23 @@ namespace YLErp.Modules.SwapModule { return flowEvents; } - var trade = DbContext.trade.Find(swapTradeId); - var tradeExtend = DbContext.trade_extend.First(x => x.TradeId == swapTradeId); - var trans = needTrans ? DbContext.Database.BeginTransaction() : null; + var trade = FindTrade(swapTradeId); + var tradeExtend = FindTradeExtend(swapTradeId); + var trans = needTrans ? BeginTransaction() : null; try { - var eodPositions = DbContext.swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial && !x.Invalid).AsNoTracking().ToList();//上一日终持仓信息 - var swapFlowEventOlds = DbContext.swap_flow_event.Where(x => x.EventDate > tradeDate && x.SwapTradeId == swapTradeId && x.DataState > 0);//废弃当前清算日期及之后的开平仓事件 - swapFlowEventOlds.ForEach(x => - { - x.DataState = (int)SwapFlowDateStateEnum.废弃; - }); + var eodPositions = FindPositions(swapTradeId);//上一日终持仓信息 + FindAndInvalidateFutureEvents(swapTradeId, tradeDate);//废弃当前清算日期及之后的开平仓事件 + OnBeforeMergePageEvent(swapTradeId, tradeDate, trade, tradeExtend, flowquery, eodPositions); var mergeUnderlyingGroup = flowquery.GroupBy(g => g.UnderlyingCode); int direction = tradeExtend.ExtendObj.Direction; foreach (var underlyingGroup in mergeUnderlyingGroup) { var mergeList = underlyingGroup.OrderByDescending(o => o.TradingQty).ToList();//先按数量最大的排序 var flowMerge = mergeList.First(); - var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(flowMerge.UnderlyingCode); + var underlying = GetUnderlying(flowMerge.UnderlyingCode); var matuirityDate = trade.ExerciseDate; - var positionId = GetMaxPositionId(flowMerge, matuirityDate.Value, direction, trade.TradeNumber); + var positionId = ResolvePositionId(flowMerge, matuirityDate.Value, direction, trade.TradeNumber); var payPosition = eodPositions.FirstOrDefault(x => x.PositionId == positionId);//浮动腿 日终持仓信息 bool hasPayPosition = payPosition != null;//是否存在日终持仓 if (mergeList.Count == 1)//只有一条流水 @@ -208,12 +260,13 @@ namespace YLErp.Modules.SwapModule } } - DbContext.SaveChanges(); - trans?.Commit(); + PersistEvents(flowEvents); + OnAfterMergePageEvent(swapTradeId, tradeDate, flowEvents); + if (trans != null) CommitTransaction(trans); } catch (Exception ex) { - trans?.Rollback(); + if (trans != null) RollbackTransaction(trans); throw new Exception(ex.Message, ex); } finally @@ -238,7 +291,7 @@ namespace YLErp.Modules.SwapModule /// 平仓浮动费用 /// 是否完全平仓 /// 0 T+0 1 T+1 - private swap_flow_event InitEvent( + protected virtual swap_flow_event InitEvent( int eventType, swap_flow_merge flow_merge, int direction, @@ -287,7 +340,6 @@ namespace YLErp.Modules.SwapModule flow_Event.MarkClosePnl = PayMarkUnwindPnl; flow_Event.CloseFee = PayFeeUnwindPnl; flow_Event.DataState = (int)SwapFlowDateStateEnum.等待完成; - DbContext.swap_flow_event.Add(flow_Event); return flow_Event; } @@ -525,8 +577,8 @@ namespace YLErp.Modules.SwapModule { item.TradeFee = item.FlowEvent.TradingFee; item.TradingFee = item.FlowEvent.TradingFeePending; - item.PosiPnl= item.FlowEvent.MarkClosePnl- item.TradeFee-item.FlowEvent.DividendIn - item.TradingFee; - item.NetSettmentAmount = item.FlowEvent.MarkClosePnl + item.FlowEvent.InterestClosePnL; + item.PosiPnl= item.FlowEvent.MarkClosePnl;//MarkClosePnl 纯盯市不计算交易费用和分红 + item.NetSettmentAmount = item.FlowEvent.FloatPnlSum + item.FlowEvent.InterestClosePnL; item.Days = (item.FlowEvent.UnwindDate - item.StartDate).Value.Days; } diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs index 3246277b..fb65eff8 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs @@ -120,10 +120,14 @@ namespace YLErp.Modules.SwapModule swap_flow.TradingAmountNetFee = TradeFeeHelper.CalcPriceWithFee(swap_flow.TradingFee, swap_flow.TradingAmountNet??0, swap_flow.TradingQty, swap_flow.BsType); if (underlying != null && underlying.IsBond()) { - swap_flow.TradingAmountAvg *= ConsGlobal.bondPriceMultiple; - swap_flow.TradingAmountFeeAvg *= ConsGlobal.bondPriceMultiple; - swap_flow.TradingAmountNet *= ConsGlobal.bondPriceMultiple; - swap_flow.TradingAmountNetFee *= ConsGlobal.bondPriceMultiple; + // 成交流水债券报价(×100形式)转入库小数(×0.01),统一走 BondPriceConverter + swap_flow.TradingAmountAvg = BondPriceConverter.ToStorage(swap_flow.TradingAmountAvg); + swap_flow.TradingAmountFeeAvg = BondPriceConverter.ToStorage(swap_flow.TradingAmountFeeAvg); + // TradingAmountNet/NetFee 可空,null 时保持 null 语义(与原 *= 一致) + if (swap_flow.TradingAmountNet.HasValue) + swap_flow.TradingAmountNet = BondPriceConverter.ToStorage(swap_flow.TradingAmountNet.Value); + if (swap_flow.TradingAmountNetFee.HasValue) + swap_flow.TradingAmountNetFee = BondPriceConverter.ToStorage(swap_flow.TradingAmountNetFee.Value); } if (!string.IsNullOrEmpty(clientName)) { diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs index 089a79fc..2641648a 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs @@ -167,10 +167,12 @@ namespace YLErp.Modules.SwapModule var um = underlyings.FirstOrDefault(x=>x.UnderlyingCode==item.UnderlyingCode); if (um!=null&&um.IsBond()) { - item.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountNet *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountNetFee *= ConsGlobal.bondShowPriceMultiple; + // 入库小数(0.995)→展示报价(99.5),价格字段统一走 BondPriceConverter + item.TradingAmountAvg = BondPriceConverter.ToDisplay(item.TradingAmountAvg); + item.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(item.TradingAmountFeeAvg); + item.TradingAmountNet = item.TradingAmountNet.HasValue ? BondPriceConverter.ToDisplay(item.TradingAmountNet.Value) : item.TradingAmountNet; + item.TradingAmountNetFee = item.TradingAmountNetFee.HasValue ? BondPriceConverter.ToDisplay(item.TradingAmountNetFee.Value) : item.TradingAmountNetFee; + // 数量÷100(手→万手展示),与价格维度无关,保留常量 item.TradingQty /= ConsGlobal.bondShowPriceMultiple; } } @@ -228,9 +230,9 @@ namespace YLErp.Modules.SwapModule var um = underlyings.FirstOrDefault(x => x.UnderlyingCode == item.UnderlyingCode); if (um != null && um.IsBond()) { - item.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountNet *= ConsGlobal.bondShowPriceMultiple; + item.TradingAmountAvg = BondPriceConverter.ToDisplay(item.TradingAmountAvg); + item.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(item.TradingAmountFeeAvg); + item.TradingAmountNet = BondPriceConverter.ToDisplay(item.TradingAmountNet); item.TradingQty /= ConsGlobal.bondShowPriceMultiple; } } @@ -304,10 +306,10 @@ namespace YLErp.Modules.SwapModule item.InitYtm = ytmMap.GetValueOrDefault(item.SwapTradeId); if (ConsGlobal.InstrumentType.IsBond(item.UnderlyingInstrumentType)) { - item.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountNetFeeAvg *= ConsGlobal.bondShowPriceMultiple; - item.TradingAmountNetAvg *= ConsGlobal.bondShowPriceMultiple; + item.TradingAmountAvg = BondPriceConverter.ToDisplay(item.TradingAmountAvg); + item.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(item.TradingAmountFeeAvg); + item.TradingAmountNetFeeAvg = item.TradingAmountNetFeeAvg.HasValue ? BondPriceConverter.ToDisplay(item.TradingAmountNetFeeAvg.Value) : item.TradingAmountNetFeeAvg; + item.TradingAmountNetAvg = item.TradingAmountNetAvg.HasValue ? BondPriceConverter.ToDisplay(item.TradingAmountNetAvg.Value) : item.TradingAmountNetAvg; item.Quantity /= ConsGlobal.bondShowPriceMultiple; } if (item.PayDate == null || item.EventType != (int)SwapEventTypeEnum.平仓) @@ -440,10 +442,10 @@ namespace YLErp.Modules.SwapModule { if (ConsGlobal.InstrumentType.IsBond(item.position.UnderlyingInstrumentType)) { - item.position.PosiNetPrice *= ConsGlobal.bondShowPriceMultiple; - item.position.PosiGrossPrice *= ConsGlobal.bondShowPriceMultiple; - item.position.PosiNetNoFeePrice *= ConsGlobal.bondShowPriceMultiple; - item.position.PosiNetFeePrice *= ConsGlobal.bondShowPriceMultiple; + item.position.PosiNetPrice = BondPriceConverter.ToDisplay(item.position.PosiNetPrice); + item.position.PosiGrossPrice = BondPriceConverter.ToDisplay(item.position.PosiGrossPrice); + item.position.PosiNetNoFeePrice = item.position.PosiNetNoFeePrice.HasValue ? BondPriceConverter.ToDisplay(item.position.PosiNetNoFeePrice.Value) : item.position.PosiNetNoFeePrice; + item.position.PosiNetFeePrice = item.position.PosiNetFeePrice.HasValue ? BondPriceConverter.ToDisplay(item.position.PosiNetFeePrice.Value) : item.position.PosiNetFeePrice; item.position.PosiQuantity /= ConsGlobal.bondShowPriceMultiple; } } @@ -599,10 +601,10 @@ namespace YLErp.Modules.SwapModule var um = DataCacheProvider.GetUnderlyingDataSource().GetData(swapFlow.UnderlyingCode); if (um != null && um.IsBond()) { - swapFlow.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountNet *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountNetFee *= ConsGlobal.bondShowPriceMultiple; + swapFlow.TradingAmountAvg = BondPriceConverter.ToDisplay(swapFlow.TradingAmountAvg); + swapFlow.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(swapFlow.TradingAmountFeeAvg); + swapFlow.TradingAmountNet = swapFlow.TradingAmountNet.HasValue ? BondPriceConverter.ToDisplay(swapFlow.TradingAmountNet.Value) : swapFlow.TradingAmountNet; + swapFlow.TradingAmountNetFee = swapFlow.TradingAmountNetFee.HasValue ? BondPriceConverter.ToDisplay(swapFlow.TradingAmountNetFee.Value) : swapFlow.TradingAmountNetFee; swapFlow.TradingQty /= ConsGlobal.bondShowPriceMultiple; } return swapFlow; @@ -617,10 +619,10 @@ namespace YLErp.Modules.SwapModule var um = DataCacheProvider.GetUnderlyingDataSource().GetData(swapFlow.UnderlyingCode); if (um != null && um.IsBond()) { - swapFlow.TradingAmountAvg *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountFeeAvg *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountNetAvg *= ConsGlobal.bondShowPriceMultiple; - swapFlow.TradingAmountNetFeeAvg *= ConsGlobal.bondShowPriceMultiple; + swapFlow.TradingAmountAvg = BondPriceConverter.ToDisplay(swapFlow.TradingAmountAvg); + swapFlow.TradingAmountFeeAvg = BondPriceConverter.ToDisplay(swapFlow.TradingAmountFeeAvg); + swapFlow.TradingAmountNetAvg = swapFlow.TradingAmountNetAvg.HasValue ? BondPriceConverter.ToDisplay(swapFlow.TradingAmountNetAvg.Value) : swapFlow.TradingAmountNetAvg; + swapFlow.TradingAmountNetFeeAvg = swapFlow.TradingAmountNetFeeAvg.HasValue ? BondPriceConverter.ToDisplay(swapFlow.TradingAmountNetFeeAvg.Value) : swapFlow.TradingAmountNetFeeAvg; swapFlow.TradingQty /= ConsGlobal.bondShowPriceMultiple; } return swapFlow; @@ -752,10 +754,14 @@ namespace YLErp.Modules.SwapModule } if (underlying != null && underlying.IsBond()) { - req.TradingAmountAvg *= ConsGlobal.bondPriceMultiple; - req.TradingAmountFeeAvg *= ConsGlobal.bondPriceMultiple; - req.TradingAmountNet *= ConsGlobal.bondPriceMultiple; - req.TradingAmountNetFee *= ConsGlobal.bondPriceMultiple; + // 债券报价(×100)转入库小数(×0.01),价格字段统一走 BondPriceConverter + req.TradingAmountAvg = BondPriceConverter.ToStorage(req.TradingAmountAvg); + req.TradingAmountFeeAvg = BondPriceConverter.ToStorage(req.TradingAmountFeeAvg); + if (req.TradingAmountNet.HasValue) + req.TradingAmountNet = BondPriceConverter.ToStorage(req.TradingAmountNet.Value); + if (req.TradingAmountNetFee.HasValue) + req.TradingAmountNetFee = BondPriceConverter.ToStorage(req.TradingAmountNetFee.Value); + // 数量×100(万手→手),与价格维度无关,保留常量 req.TradingQty *= ConsGlobal.bondShowPriceMultiple; } } diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs index f3c1a530..75af7ea2 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs @@ -47,6 +47,74 @@ namespace YLErp.Modules.SwapModule { } + + #region 可测试化接缝(Seams)——借鉴 refactor-swap-event-testable 分支,override 可在测试中替换 DB/外部调用,生产代码行为不变 + + protected virtual DateTime GetNextBusinessDay(DateTime date) + => QdpCalendarHelper.GetNonHoliday(date); + + protected virtual DateTime GetNextBusinessDayBefore(DateTime date) + => QdpCalendarHelper.GetNonHolidayDefore(date); + + protected virtual CalBondResult CalculateBondYtm(string underlyingCode, decimal avgPrice, DateTime settleDate) + => BondCalcHepler.BondCalcByDate(underlyingCode, avgPrice * ConsGlobal.bondShowPriceMultiple, settleDate.ToString("yyyy-MM-dd")); + + protected virtual void PersistMerge(swap_flow_merge merge) + => DbContext.swap_flow_merge.Add(merge); + + protected virtual void SetModelOpt(DBModelBaseV2 model) + => model.SetOpt(UserInfo); + + protected virtual List FindActiveSwapTrades(DateTime valueDate) + => DbContext.trade.Where(t => t.TradeType == "收益互换" && t.StructureType == "普通债券类收益互换" + && t.TradeDate <= valueDate + && t.ValidState != ConsGlobal.InValid + && !ConsTrade.TradeCompleteStatus.Contains(t.TradeStatus)).ToList(); + + protected virtual List FindTradeExtends(IEnumerable tradeIds) + => DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList(); + + protected virtual List FindActivePositions(IEnumerable tradeIds, int posiDirection) + => DbContext.swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && !x.IsInitial && x.PosiQuantity > 0 && !x.Invalid && x.PosiDirection == posiDirection).ToList(); + + protected virtual List FindActivePositionsAll(IEnumerable tradeIds) + => DbContext.swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && x.PosiDirection > 0 && !x.IsInitial && x.PosiQuantity > 0 && !x.Invalid).ToList(); + + protected virtual IQueryable QueryFloatRates(DateTime valueDate, DateTime matuirityDate) + => DbContext.swap_float_rate.Where(x => x.StartDate <= valueDate && x.EndDate >= matuirityDate); + + protected virtual Client FindClient(int clientId) + => DataCacheProvider.GetClientDataSource().GetData(clientId); + + protected virtual AssetUnit FindAssetUnit(string assetAccountName) + => DataCacheProvider.GetAssetUnitDataSource().AsQueryable(x => x.Name == assetAccountName).FirstOrDefault(); + + protected virtual underlying_manager FindUnderlying(string underlyingCode) + => DataCacheProvider.GetUnderlyingDataSource().AsQueryable(x => x.UnderlyingCode == underlyingCode).FirstOrDefault(); + + protected virtual EtradingRule GetEtradingRule(BoundSideEnum boundSide, string clientNumber) + => new EtradingRuleService(UserInfo).GetEtradingRuleAccont(boundSide, clientNumber); + + protected virtual SwapFloatRate GetSwapFloatRate(IQueryable query, int clientId, string underlyingCode) + => new SwapFloatRateService(UserInfo).GetSwapFloatRate(query, clientId, underlyingCode); + + protected virtual List FindFlowEventsForCashCheck(swap_flow_merge flowMerge) + => DbContext.swap_flow_event.Where(x => x.EventDate == flowMerge.OccurTime && x.PayDate > x.UnwindDate && x.EventType == (int)SwapFlowEventTypeEnum.平仓 && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.ClientId == flowMerge.ClientId && x.PayDirection > 0).ToList(); + + protected virtual List FindValidTrades(IEnumerable tradeIds) + => DbContext.trade.Where(x => tradeIds.Contains(x.id) && x.ValidState != ConsGlobal.InValid).ToList(); + + protected virtual void SaveChanges() + => DbContext.SaveChanges(); + + protected virtual trade CreateNewSwapTrade(swap_flow_merge flowMerge, Client client, AssetUnit asset, underlying_manager underlying, SwapFloatRate floatRate, string clearingAgency, bool cashNeedAfter = false) + => new SwapTradeService(UserInfo).NewSwapTrade(flowMerge, client, asset, underlying, floatRate, clearingAgency, cashNeedAfter: cashNeedAfter); + + protected virtual void AutoSwapUnwind(int tradeId, decimal tradingAmountAvg, decimal tradingAmountFeeAvg, decimal tradingAmountNetFeeAvg, decimal tradingAmountNetAvg, DateTime occurTime, decimal tradingQtyAbs, decimal tradingFeePending) + => new SwapDealService(UserInfo).AuotoSwapUnwind(tradeId, tradingAmountAvg, tradingAmountFeeAvg, tradingAmountNetFeeAvg, tradingAmountNetAvg, occurTime, tradingQtyAbs, tradingFeePending); + + #endregion + /// /// 从db获取流水自动簿记 /// @@ -85,13 +153,14 @@ namespace YLErp.Modules.SwapModule swapFlow.SwapTradeNo = null; swapFlow.DataState = (int)SwapFlowDateStateEnum.等待完成; } - swapFlow.TradingAmountAvg = (item.deal_full_price ?? 0) * 0.01m; - swapFlow.TradingAmountFeeAvg = (item.deal_full_price_include_fee ?? 0) * 0.01m; + // 债券报价(×100)转入库小数(×0.01),统一走 BondPriceConverter + swapFlow.TradingAmountAvg = BondPriceConverter.ToStorage(item.deal_full_price ?? 0); + swapFlow.TradingAmountFeeAvg = BondPriceConverter.ToStorage(item.deal_full_price_include_fee ?? 0); swapFlow.TradingAmount = swapFlow.TradingQty * swapFlow.ContractSize * swapFlow.TradingAmountAvg; swapFlow.ClientId = Convert.ToInt32(item.client_id ?? 0); - swapFlow.ytm = (item.ytm ?? 0) * 0.01m; - swapFlow.TradingAmountNet = (item.deal_price ?? 0) * 0.01m; - swapFlow.TradingAmountNetFee = (item.deal_price_include_fee ?? 0) * 0.01m; + swapFlow.ytm = BondPriceConverter.ToStorage(item.ytm ?? 0); + swapFlow.TradingAmountNet = BondPriceConverter.ToStorage(item.deal_price ?? 0); + swapFlow.TradingAmountNetFee = BondPriceConverter.ToStorage(item.deal_price_include_fee ?? 0); swapFlow.ClientName = item.client_name; swapFlow.SetOpt(UserInfo); swapFlow.OptTime = item.create_time.HasValue ? item.create_time.Value : DateTime.Now; @@ -460,12 +529,9 @@ namespace YLErp.Modules.SwapModule { return; } - var swaptrades = DbContext.trade.Where(t => t.TradeType == "收益互换" && t.StructureType == "普通债券类收益互换" - && t.TradeDate <= valueDate - && t.ValidState != ConsGlobal.InValid - && !ConsTrade.TradeCompleteStatus.Contains(t.TradeStatus)).ToList(); + var swaptrades = FindActiveSwapTrades(valueDate); var swapTradeIds = swaptrades.Select(s => s.id); - var tradeExtends = DbContext.trade_extend.Where(x => swapTradeIds.Contains(x.TradeId)); + var tradeExtends = FindTradeExtends(swapTradeIds); var restSwapTrades = new List(); foreach (var swaptrade in swaptrades) { @@ -475,10 +541,9 @@ namespace YLErp.Modules.SwapModule restSwapTrades.Add(swaptrade); } } - var swapPositions = DbContext.swap_position.Where(x => swapTradeIds.Contains(x.SwapTradeId) && !x.IsInitial && x.PosiQuantity > 0 && !x.Invalid && x.PosiDirection == (int)SwapDirectionEnum.支付).ToList(); - var matuirityDate = QdpCalendarHelper.GetNonHolidayDefore(valueDate.AddDays(14)); - var floatRatePredicate = PredicateBuilder.Create(x => x.StartDate <= valueDate && x.EndDate >= matuirityDate); - var floatRateQuery = DbContext.swap_float_rate.Where(floatRatePredicate); + var swapPositions = FindActivePositions(swapTradeIds, (int)SwapDirectionEnum.支付); + var matuirityDate = GetNextBusinessDayBefore(valueDate.AddDays(14)); + var floatRateQuery = QueryFloatRates(valueDate, matuirityDate); int dealCount = 0; foreach (var groupItem in flowquery) { @@ -545,7 +610,7 @@ namespace YLErp.Modules.SwapModule IQueryable floatRateQuery, ref int dealCount, Action? action) { var clientId = groupItem.Key; - var client = DataCacheProvider.GetClientDataSource().GetData(clientId ?? 0); + var client = FindClient(clientId ?? 0); if (client == null) { throw new ServiceException($"找不到id为{clientId}的客户信息"); @@ -554,13 +619,13 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException($"客户:{client.Name}未设置交易种类“场外互换”,无法生成互换交易!"); } - var etradeRule = new EtradingRuleService(UserInfo).GetEtradingRuleAccont(client.BoundSide, client.Number); + var etradeRule = GetEtradingRule(client.BoundSide, client.Number); if (etradeRule == null || string.IsNullOrEmpty(etradeRule.AssetAccount_0)) { throw new ServiceException($"{client.Number}未设置TRS对客簿记账户"); } string clearingAgency = etradeRule.ClearingAgency_0; - var asset = DataCacheProvider.GetAssetUnitDataSource().AsQueryable(x => x.Name == etradeRule.AssetAccount_0).FirstOrDefault();//取对客簿记账户 + var asset = FindAssetUnit(etradeRule.AssetAccount_0);//取对客簿记账户 if (asset == null) { throw new ServiceException($"找不到名为{etradeRule.AssetAccount_0}的簿记账户信息"); @@ -575,16 +640,15 @@ namespace YLErp.Modules.SwapModule var clientSwapPositions = swapPositions.Where(x => clientSwapTradeIds.Contains(x.SwapTradeId));//现有客户持仓 var underlyingCodes = underlyingGroup.Select(s => s.Key).ToList(); - var underlyings = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(x => underlyingCodes.Contains(x.UnderlyingCode)); foreach (var underlyingGroupItem in underlyingGroup) { var underlyingCode = underlyingGroupItem.Key; - var underlying = underlyings.FirstOrDefault(x => x.UnderlyingCode == underlyingCode); + var underlying = FindUnderlying(underlyingCode); if (underlying == null) { throw new ServiceException($"找不到标的代码为{underlyingCode}的标的信息"); } - var floatRate = new SwapFloatRateService(UserInfo).GetSwapFloatRate(floatRateQuery, clientId ?? 0, underlyingCode); + var floatRate = GetSwapFloatRate(floatRateQuery, clientId ?? 0, underlyingCode); var clientSwapPositionList = clientSwapPositions.Where(x => x.UnderlyingCode == underlyingCode).ToList();//现有标的持仓 var hasPayPosition = clientSwapPositionList.Any(); var mergeList = underlyingGroupItem.OrderByDescending(o => o.TradingQty).ToList(); @@ -605,9 +669,9 @@ namespace YLErp.Modules.SwapModule } if (cashNeedAfter) { - var flowEvents = DbContext.swap_flow_event.Where(x => x.EventDate == flowMerge.OccurTime && x.PayDate > x.UnwindDate && x.EventType == (int)SwapFlowEventTypeEnum.平仓 && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.ClientId == flowMerge.ClientId && x.PayDirection > 0); + var flowEvents = FindFlowEventsForCashCheck(flowMerge); var tradeIds = flowEvents.Select(s => s.SwapTradeId).Distinct(); - var trades = DbContext.trade.Where(x => tradeIds.Contains(x.id) && x.ValidState != ConsGlobal.InValid); + var trades = FindValidTrades(tradeIds); cashNeedAfter = !trades.Any(); } if (!hasPayPosition)//没有持仓 @@ -620,7 +684,7 @@ namespace YLErp.Modules.SwapModule } } - DbContext.SaveChanges(); + SaveChanges(); } /// @@ -648,16 +712,15 @@ namespace YLErp.Modules.SwapModule var clientSwapTradeIds = clientSwapTrades.Select(s => s.id); var clientSwapPositions = swapPositions.Where(x => clientSwapTradeIds.Contains(x.SwapTradeId));//现有客户持仓 var underlyingCodes = underlyingGroup.Select(s => s.Key).ToList(); - var underlyings = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(x => underlyingCodes.Contains(x.UnderlyingCode)); foreach (var underlyingGroupItem in underlyingGroup) { var underlyingCode = underlyingGroupItem.Key; - var underlying = underlyings.FirstOrDefault(x => x.UnderlyingCode == underlyingCode); + var underlying = FindUnderlying(underlyingCode); if (underlying == null) { throw new ServiceException($"找不到标的代码为{underlyingCode}的标的信息"); } - var floatRate = new SwapFloatRateService(UserInfo).GetSwapFloatRate(floatRateQuery, clientId ?? 0, underlyingCode); + var floatRate = GetSwapFloatRate(floatRateQuery, clientId ?? 0, underlyingCode); var clientSwapPositionList = clientSwapPositions.Where(x => x.UnderlyingCode == underlyingCode).ToList();//现有标的持仓 var hasPayPosition = clientSwapPositionList.Any(); var mergeList = underlyingGroupItem.OrderBy(o => o.OptTime).ToList(); @@ -696,14 +759,13 @@ namespace YLErp.Modules.SwapModule var mergeOrderList = mergeList.OrderBy(o => o.FirstFlowTime); swap_flow_merge flowMergeMax = mergeOrderList.First();//先开最早的一条 swap_flow_merge flowMergeMin = mergeOrderList.Last(); - var swapTradeService = new SwapTradeService(UserInfo); - var trade = swapTradeService.NewSwapTrade(flowMergeMax, client, asset, underlying, floatRate, clearingAgency, cashNeedAfter: cashNeedAfter); + var trade = CreateNewSwapTrade(flowMergeMax, client, asset, underlying, floatRate, clearingAgency, cashNeedAfter: cashNeedAfter); flowMergeMax.SwapTradeNo = trade.TradeNumber; flowMergeMin.SwapTradeNo = trade.TradeNumber; if (mergeList.Count == 2)//有两条流水 { var qty = flowMergeMax.TradingQtyAbs - flowMergeMin.TradingQtyAbs;//平仓剩余数量 - new SwapDealService(UserInfo).AuotoSwapUnwind(trade.id, + AutoSwapUnwind(trade.id, flowMergeMin.TradingAmountAvg, flowMergeMin.TradingAmountFeeAvg, flowMergeMin.TradingAmountNetFeeAvg ?? 0, @@ -729,7 +791,7 @@ namespace YLErp.Modules.SwapModule var posi = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == trade.id && x.PosiDirection > 0 && !x.IsInitial); SetNewOpenData(flowMergeMin, flowMergeClone, posi); } - var trade2 = swapTradeService.NewSwapTrade(flowMergeClone, client, asset, underlying, floatRate, clearingAgency); + var trade2 = CreateNewSwapTrade(flowMergeClone, client, asset, underlying, floatRate, clearingAgency); flowMergeMax.SwapTradeNo = trade2.TradeNumber; flowMergeMin.SwapTradeNo = trade2.TradeNumber; } @@ -793,7 +855,7 @@ namespace YLErp.Modules.SwapModule } var trade = NewSwapTrade(sameFlowClone, client, asset, underlying, floatRate, clearingAgency); // 平仓 - new SwapDealService(UserInfo).AuotoSwapUnwind(trade.id, + AutoSwapUnwind(trade.id, negaFlowClone.TradingAmountAvg, negaFlowClone.TradingAmountFeeAvg, negaFlowClone.TradingAmountNetFeeAvg ?? 0, diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs index 0fe0981a..d1a54850 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs @@ -19,6 +19,18 @@ namespace YLErp.Modules.SwapModule { } + + #region 可测试化接缝(Seams)——子类共用,override 可在测试中替换 DB/外部调用 + + /// 查找交易(生产: DbContext.trade.Find;测试: 返回内存对象)。 + /// SwapDealService/SwapEodPositionService/SwapFlowEventService 三处实现完全一致,上提基类消除重复。 + protected virtual trade FindTrade(int tradeId) + { + return DbContext.trade.Find(tradeId); + } + + #endregion + /// /// 校验标的是否存在 /// @@ -282,6 +294,34 @@ namespace YLErp.Modules.SwapModule } return swapEvent.ValueDate; } + + #region 可测试化接缝(ClearSwapPositions 相关) + + /// 查找该交易的 flow_event(生产: DbContext.swap_flow_event;测试: 内存列表) + protected virtual List FindFlowEventsByEventIds(List eventIds) + { + return DbContext.swap_flow_event.Where(x => x.EventId.HasValue && eventIds.Contains(x.EventId.Value)).ToList(); + } + + /// 查找手动互换的 ClientCashId(生产: DbContext.swap_event;测试: 内存列表) + protected virtual List FindManualClientCashIds(int swapTradeId) + { + return DbContext.swap_event + .Where(x => x.SwapTradeId == swapTradeId + && x.ClientCashId > 0 + && x.EventType != (int)SwapEventTypeEnum.自动互换) + .Select(x => x.ClientCashId) + .ToList(); + } + + /// 查找该交易的资金记录(生产: DbContext.ClientCashInCashOut;测试: 内存列表) + protected virtual List FindClientCashRecords(int tradeId) + { + return DbContext.ClientCashInCashOut.Where(x => x.TradeId == tradeId).ToList(); + } + + #endregion + public int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate) { var cl = DataCacheProvider.GetClientDataSource().GetData(td.ClientId); @@ -360,7 +400,7 @@ namespace YLErp.Modules.SwapModule return false; } - public void UpdateDbOption(DBModelBaseV2 dBModel) + public virtual void UpdateDbOption(DBModelBaseV2 dBModel) { dBModel.OptTime = DateTime.Now; dBModel.OptName = UserName; @@ -384,28 +424,40 @@ namespace YLErp.Modules.SwapModule DbContext.swap_flow_event.RemoveRange(swapFlowEvents); // 删除自动互换产生的资金记录(client_cash_in_out) - var swapEventIds = swapEvents.Select(s => s.id).ToList(); - if (swapEventIds.Any()) + var autoSwapEvents = swapEvents.ToList(); + if (autoSwapEvents.Any()) { // 通过 swap_event 的 ClientCashId 删除对应的资金记录(利息腿) - var clientCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId).ToList(); + var clientCashIds = autoSwapEvents + .Where(s => s.ClientCashId > 0) + .Select(s => s.ClientCashId) + .ToList(); + + var legacyAutoEvents = new List(); + foreach (var swapEvent in autoSwapEvents) + { + var eventCashIds = GetAutoSwapClientCashIds(swapEvent); + if (eventCashIds.Any()) + { + clientCashIds.AddRange(eventCashIds); + } + else + { + legacyAutoEvents.Add(swapEvent); + } + } + + clientCashIds = clientCashIds.Distinct().ToList(); if (clientCashIds.Any()) { var clientCashRecords = DbContext.ClientCashInCashOut.Where(x => clientCashIds.Contains(x.id)).ToList(); DbContext.ClientCashInCashOut.RemoveRange(clientCashRecords); } - - // 删除预付金腿的资金记录(通过交易ID、日期和Action类型查找,一次性查询避免MySQL连接重用问题) - var swapTradeIds = swapEvents.Select(s => s.SwapTradeId).Distinct().ToList(); - var actions = new List() { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 }; - var premiumCashRecords = DbContext.ClientCashInCashOut - .Where(x => swapTradeIds.Contains(x.TradeId ?? 0) - && x.HappenDate>=valueDate - && actions.Contains(x.Action)) - .ToList(); - if (premiumCashRecords.Any()) + + var legacyCashRecords = GetLegacyAutoSwapClientCashRecords(legacyAutoEvents, clientCashIds); + if (legacyCashRecords.Any()) { - DbContext.ClientCashInCashOut.RemoveRange(premiumCashRecords); + DbContext.ClientCashInCashOut.RemoveRange(legacyCashRecords); } } } @@ -413,6 +465,98 @@ namespace YLErp.Modules.SwapModule DbContext.eod_swap.RemoveRange(eodSwaps); DbContext.SaveChanges(); } + + private List GetAutoSwapClientCashIds(swap_event swapEvent) + { + if (swapEvent == null || string.IsNullOrWhiteSpace(swapEvent.EventData)) + { + return new List(); + } + + try + { + var unwindData = JsonConvert.DeserializeObject(swapEvent.EventData); + return unwindData?.ClientCashIds?.Where(x => x > 0).Distinct().ToList() ?? new List(); + } + catch + { + return new List(); + } + } + + protected virtual List GetLegacyAutoSwapClientCashRecords(List swapEvents, List excludedClientCashIds) + { + if (swapEvents == null || !swapEvents.Any()) + { + return new List(); + } + + var eventIds = swapEvents.Select(x => x.id).ToList(); + var flowEvents = FindFlowEventsByEventIds(eventIds); + var manualClientCashIds = FindManualClientCashIds(swapEvents.First().SwapTradeId); + var records = new List(); + + foreach (var swapEvent in swapEvents) + { + UnwindData unwindData = null; + if (!string.IsNullOrWhiteSpace(swapEvent.EventData)) + { + try + { + unwindData = JsonConvert.DeserializeObject(swapEvent.EventData); + } + catch + { + } + } + + var eventFlowEvents = flowEvents.Where(x => x.EventId == swapEvent.id).ToList(); + var candidateDates = new HashSet { swapEvent.ValueDate.Date }; + if (unwindData?.PayDate != null) + { + candidateDates.Add(unwindData.PayDate.Value.Date); + } + eventFlowEvents.Where(x => x.PayDate.HasValue).ToList().ForEach(x => candidateDates.Add(x.PayDate.Value.Date)); + + var allCashRecords = FindClientCashRecords(swapEvent.SwapTradeId); + var eventRecords = allCashRecords + .Where(x => !excludedClientCashIds.Contains(x.id) + && !manualClientCashIds.Contains(x.id) + && (x.Action == ClientCashInCashOut.系统操作_预付金返息 || x.Action == ClientCashInCashOut.系统操作_互换)) + .ToList() + .Where(x => x.HappenDate.HasValue && candidateDates.Contains(x.HappenDate.Value.Date)) + .Where(x => IsLegacyAutoSwapClientCashRecord(x, unwindData)) + .ToList(); + + records.AddRange(eventRecords); + } + + return records.GroupBy(x => x.id).Select(x => x.First()).ToList(); + } + + private bool IsLegacyAutoSwapClientCashRecord(ClientCashInCashOut cashRecord, UnwindData unwindData) + { + if (unwindData == null) + { + return true; + } + + var amount = Convert.ToDecimal(cashRecord.Money ?? 0); + if (cashRecord.Action == ClientCashInCashOut.系统操作_预付金返息) + { + return unwindData.SwapMarginRebatePnl != 0 + && amount == -unwindData.SwapMarginRebatePnl; + } + + if (cashRecord.Action == ClientCashInCashOut.系统操作_互换) + { + return (unwindData.SwapCloseAmount != 0 && amount == -unwindData.SwapCloseAmount) + || (unwindData.SwapDividendPnl != 0 && amount == -unwindData.SwapDividendPnl) + || (unwindData.SwapRealizedPnL != 0 && amount == -unwindData.SwapRealizedPnL); + } + + return false; + } /// /// 获取上一交易日 /// diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs index fb20c420..67901f31 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs @@ -8,9 +8,9 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule /// /// 国贸交易确认书编号生成器 /// 规则: - /// - 对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{当日第N笔对客交易(数字递增)}-{标的代码},从01开始 + /// - 对客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔对客交易(数字递增)}-{标的代码},从01开始 /// 例:GLMS-IS-20260424-01-250210IB - /// - 非对客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{当日第N笔非对客交易使用字母递增}-{标的代码},从A开始 + /// - 非对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔非对客交易使用字母递增}-{标的代码},从A开始 /// 例:GLMS-IS-20260424-A-250210IB /// public class GuolianContractNoGenerator @@ -29,12 +29,12 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule trade trade, string clientCode) { - // 根据trade的OpponentRole判断是否为对客交易(乙方=对客) - var isClientTrade = trade.OpponentRole == "乙方"; + // 根据trade的OpponentRole判断是否为对客交易(甲方=对客)。glms客户规定甲方=对客 + var isClientTrade = trade.OpponentRole == "甲方"; var underlyingCode = (trade.UnderlyingCode ?? "").Replace(".", ""); // 去掉标的代码中的点号 var tradeDate = trade.TradeDate ?? DateTime.MinValue; var tradeDateStr = tradeDate.ToString("yyyyMMdd"); - var prefix = $"GLMS-{clientCode}-{tradeDateStr}-"; + var prefix = $"GLMS-{clientCode}-{tradeDateStr}-FICC-"; lock (_syncLock) { diff --git a/YLErpDAL/YLErpDAL.csproj b/YLErpDAL/YLErpDAL.csproj index e9d409ac..83cf8334 100644 --- a/YLErpDAL/YLErpDAL.csproj +++ b/YLErpDAL/YLErpDAL.csproj @@ -30,8 +30,6 @@ - - @@ -42,7 +40,7 @@ - + diff --git a/YLErpWeb/App/HostedTaskService.cs b/YLErpWeb/App/HostedTaskService.cs index 28660c10..75bf1339 100644 --- a/YLErpWeb/App/HostedTaskService.cs +++ b/YLErpWeb/App/HostedTaskService.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Hosting.Server.Features; using YLErp.DBModels.Consts; using YLErp.Modules.AppModule; using YLErp.Modules.EodModule.SettlementModule; -using YLErp.Modules.RiskEngine; using YLErp.Modules.RiskModule; using YLErp.Modules.SuperviseReportModule.SAC.Service; using YLErp.Modules.TradeRiskCalcModule; @@ -125,18 +124,6 @@ namespace YLErp.Web.App PS.SetConfig(ConsAppConfig.YLErpWebUrlConfig, address); } - // 预热风控引擎:加载规则与应用到内存,并预编译所有规则 - Task.Run(() => - { - try - { - RiskEngineService.GetInstance().Preload(); - } - catch (Exception ex) - { - LogFactory.GetLogger("HostedTaskService").Error("[风控引擎] 启动预热失败", ex); - } - }); }); return Task.CompletedTask; @@ -157,4 +144,4 @@ namespace YLErp.Web.App return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/YLErpWeb/App_Data/FunctionRight.xml b/YLErpWeb/App_Data/FunctionRight.xml index ed07a284..7962c464 100644 --- a/YLErpWeb/App_Data/FunctionRight.xml +++ b/YLErpWeb/App_Data/FunctionRight.xml @@ -1,29 +1,28 @@ - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -31,13 +30,13 @@ - - - - - - - + + + + + + + @@ -53,31 +52,14 @@ - + - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -89,39 +71,39 @@ - + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + @@ -129,62 +111,62 @@ - - - - + + + + - - + + - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - + - + - - - - - - - + + + + + + + @@ -212,28 +194,46 @@ - + + + + + + + + + + + + + + + + - + + + + - - - - - - - + + + + + + + - - + + - + diff --git a/YLErpWeb/App_Data/Menus.txt b/YLErpWeb/App_Data/Menus.txt index 506f2a79..a8aea562 100644 --- a/YLErpWeb/App_Data/Menus.txt +++ b/YLErpWeb/App_Data/Menus.txt @@ -25,7 +25,6 @@ {Name:"资金监控",Rights:["风险控制-资金监控"],Url:"client/clientRiskMonitor"}, {Name:"市场风险",Rights:["风险控制-市场风险"],Url:"risk/RiskExposureReport"}, {Name:"限额监控",Rights:["风险控制-限额监控"],Url:"risk/quotaMonitor"}, - {Name:"异常交易监控",Rights:["风险控制-异常交易监控"],Url:"v3/risk/risk-engine-config"}, {Name:"白名单券池",Rights:["风险控制-白名单券池"],Url:"v3/data/underlying-pool"}, {Name:"日终持仓风险",Rights:["风险控制-日终持仓风险"],Url:"trade/EodPositionRisks"}, {Name:"日终持仓风险_互换",Rights:["风险控制-日终持仓风险_互换"],Url:"swaptrade2/EodPositionRisks"}, diff --git a/YLErpWeb/Common/UserInfo.cs b/YLErpWeb/Common/UserInfo.cs index e5071920..d9c507eb 100644 --- a/YLErpWeb/Common/UserInfo.cs +++ b/YLErpWeb/Common/UserInfo.cs @@ -1,4 +1,4 @@ -using YLErp.DBModels.Consts; +using YLErp.DBModels.Consts; using YLErp.Model.Enum; using YLErp.Modules.ClientModule; using YLErp.Modules.ClientModule.Models; @@ -280,7 +280,7 @@ namespace YLErp.Web if (user.Roles.Any(n => n.Name == "权限管理员")) { - var rightStrs = new string[] { "部门管理", "系统管理", "角色管理", "角色新增", "角色修改", "用户管理", "用户新增", "用户修改", "用户禁用", "基础缓存", "风控规则查看", "风控规则新增", "风控规则编辑", "风控规则删除", "风控规则启停", "风控应用查看", "风控应用新增", "风控应用编辑", "风控应用删除", "风控应用启停", "风控变量查看", "风控变量新增", "风控变量编辑", "风控变量删除", "风控日志查看", "风控日志导出" }; + var rightStrs = new string[] { "部门管理", "系统管理", "角色管理", "角色新增", "角色修改", "用户管理", "用户新增", "用户修改", "用户禁用", "基础缓存" }; var query1 = db.Functions.Where(o => rightStrs.Contains(o.Name)).Select(n => n.Id); var query2 = from roleFunc in db.RoleFunctions join roleUsr in db.RoleUsers on roleFunc.RoleId equals roleUsr.RoleId diff --git a/YLErpWeb/Controllers/BondPaymentController.cs b/YLErpWeb/Controllers/BondPaymentController.cs index d0b08dbe..fa4f2ef2 100644 --- a/YLErpWeb/Controllers/BondPaymentController.cs +++ b/YLErpWeb/Controllers/BondPaymentController.cs @@ -78,17 +78,39 @@ namespace YLErp.Web.Controllers return JsonSuccess("删除成功"); } /// - /// 获取某债券期间付息 + /// 获取某债券期间付息,扣除当天已消费部分 /// /// /// /// + /// 交易ID(可选,用于扣除已消费分红) + /// 结算日期(可选) /// - public JsonResult GetBondPayMentInterest(DateTime startDate, DateTime endDate, string underlyingCode) + public JsonResult GetBondPayMentInterest(DateTime startDate, DateTime endDate, string underlyingCode, int? tradeId = null, DateTime? unwindDate = null) { var payments = new BondPaymentService(CurUser).GetBondPayments(underlyingCode, startDate, endDate); - decimal interest = payments.Sum(s => s.payment_interest ?? 0) * 0.01m; - return JsonSuccess("", interest); + decimal totalInterest = payments.Sum(s => s.payment_interest ?? 0) * 0.01m; + + // 考虑增值税,与 EOD UpdateEodPosition 口径一致 + var um = yldb.underlying_manager.FirstOrDefault(x => x.UnderlyingCode == underlyingCode); + if (um != null) + { + decimal tax = um.ValueAddedTax ?? 0; + totalInterest = totalInterest / (1 + tax) * (1 - tax); + } + + decimal consumedDividend = 0; + if (tradeId.HasValue) + { + List swapEventTypes = new List() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换,(int)SwapEventTypeEnum.平仓 }; + consumedDividend = yldb.swap_flow_event + .Where(x => x.SwapTradeId == tradeId.Value + && swapEventTypes.Contains(x.EventType) + && x.DataState == (int)SwapFlowDateStateEnum.完成) + .Sum(s => s.DividendIn); + } + + return JsonSuccess("", new { totalInterest, consumedDividend }); } } } diff --git a/YLErpWeb/Controllers/SwapTrade2Controller.cs b/YLErpWeb/Controllers/SwapTrade2Controller.cs index 6b174297..52d4ae46 100644 --- a/YLErpWeb/Controllers/SwapTrade2Controller.cs +++ b/YLErpWeb/Controllers/SwapTrade2Controller.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using System.Collections.Concurrent; using System.Linq; using System.Web.Mvc; +using YLErp.BLL; using YLErp.BLL.Eod; using YLErp.Configuration; using YLErp.DBModels; @@ -29,6 +30,14 @@ namespace YLErp.Web.Controllers public class SwapTrade2Controller : BaseController { #region 框架合约 + //按年份判断是否有IB日历,优先用IB,没有则用CHN + private string GetBestCountry(int year) + { + var hasIB = CalendarBLL.GetAllcalendarModel() + .Any(c => c.Year == year && "ib".Equals(c.Country, StringComparison.OrdinalIgnoreCase)); + return hasIB ? "ib" : "chn"; + } + [MyAuthorize("交易管理-互换交易")] public ActionResult TradeList(string settleDate = null, string observationDate = null) { @@ -51,13 +60,15 @@ namespace YLErp.Web.Controllers OpenFeeType = 0, InterestCalcMode = "10", SettlementRules=0, + DividendPayDate=0 }; + var tradeDateCountry = GetBestCountry(valuedateBLL.ValueDate.Year); r = new trade() { TradeType = "收益互换", UnderlyingInstrumentType = "Stock", - StartDate = QdpCalendarHelper.GetNonHoliday(valuedateBLL.ValueDate.AddDays(1)), - TradeDate = valuedateBLL.ValueDate, + StartDate = valuedateBLL.ValueDate, + TradeDate = QdpCalendarHelper.GetNonHolidayDefore(valuedateBLL.ValueDate.AddDays(-1), tradeDateCountry), TraderId = CurUser.UserId, TraderName = CurUser.UserName, MarginTemplateName = "系统默认", @@ -67,7 +78,6 @@ namespace YLErp.Web.Controllers InitialMargin = 0 }; r.StructureType = "普通债券类收益互换"; - r.ExerciseDate = QdpCalendarHelper.GetNonHolidayDefore(r.StartDate.Value.AddDays(14)); tradeExtendJson.FlowBookMode = (int)FlowBookModeEnum.先进先出; r.trade_extend = new trade_extend() { diff --git a/YLErpWeb/Controllers/underlying_managerController.cs b/YLErpWeb/Controllers/underlying_managerController.cs index 85e08a03..851b449b 100644 --- a/YLErpWeb/Controllers/underlying_managerController.cs +++ b/YLErpWeb/Controllers/underlying_managerController.cs @@ -307,6 +307,7 @@ namespace YLErp.Web.Controllers if (EodPriceQueryService.TryGetEodPrice(valuedate.Value, code, out var eodPrice)) { price = eodPrice.GetPrice((SettlementTypeEnum)settlementType); + return JsonSuccessData(price); } } diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml index 14e12e6b..8084253a 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml @@ -1,4 +1,4 @@ -@model UnwindData +@model UnwindData @{ ViewBag.Title = "交易 | 收益结算"; Layout = "~/Views/Shared/_InfoLayout.cshtml"; @@ -48,10 +48,16 @@ + 支付日期 + 年化天数 {{deal.AnnualDays}} + + 平仓总额 {{deal.SwapCloseAmount}} + + @@ -127,10 +133,12 @@ 收支方向 多空方向 标的代码 - 期初标的交割全价% - 期初标的交割净价% - 标的价格% - 持仓数量 + 期初标的交割全价% + 期初标的交割净价% + 期初标的价格 + 期末标的交割全价% + 期末标的价格 + 数量 交易费用(平仓) 分红收益 浮动端平仓盈亏 @@ -144,10 +152,10 @@ {{floatPosition.UnderlyingCode}} {{priceFormat(floatPosition.PosiGrossPrice)}} - {{priceFormat(floatPosition.TradingAmountNetAvg)}} + + {{priceFormat(floatPosition.TradingAmountNetAvg > 0 ? floatPosition.TradingAmountNetAvg : floatPosition.PosiNetPrice)}} - @@ -157,8 +165,8 @@
我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用
- - {{floatPosition.MarkClosePnl}} + + {{floatPosition.FloatPnlSum}} diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml index 612995b4..9eb4bd3b 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml @@ -1,4 +1,4 @@ -@model UnwindData +@model UnwindData @{ ViewBag.Title = "交易 | 交易平仓"; Layout = "~/Views/Shared/_InfoLayout.cshtml"; @@ -179,9 +179,11 @@ 收支方向 多空方向 标的代码 - 期初标的交割全价% - 期末标的交割全价% - 持仓数量 + 期初标的交割全价% + 期初标的价格 + 期末标的交割全价% + 期末标的价格 + 数量 交易费用(平仓) 交易费用(待结算) 分红收益 @@ -202,7 +204,7 @@ - {{floatPosition.Quantity}} + {{deal.CloseQty}}
我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用
@@ -211,7 +213,7 @@ {{floatPosition.DividendIn}} - {{floatPosition.MarkClosePnl}} + {{floatPosition.FloatPnlSum}} diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index 4133bbc4..3e8660bc 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -1,4 +1,5 @@ @using YLErp.Web.Models.JsModels; +@using YLErp.Commons; @model trade @{ ViewBag.Title = "交易信息 | 编辑"; @@ -30,6 +31,24 @@ { agencys.Add(item.Key); } + var otcFormatConfig = new + { + StockEqvNotional = new + { + precision = OtcFormatHelper.FormatModel.trading.StockEqvNotional.precision, + minDecimals = OtcFormatHelper.FormatModel.trading.StockEqvNotional.minDecimals, + maxDecimals = OtcFormatHelper.FormatModel.trading.StockEqvNotional.maxDecimals + }, + // 如果需要其他格式,可以继续添加 + notional = new + { + precision = OtcFormatHelper.FormatModel.trading.notional.precision + }, + tradePrice = new + { + precision = OtcFormatHelper.FormatModel.trading.tradePrice.precision + } + }; var pageObj = new { isAdd, @@ -56,7 +75,8 @@ securitiesEnvironment = PS.Config.ErpElement.SecuritiesEnvironment, tradingPlaceMap = places, clearingAgencyMap = agencys, - timeUnits = GlobalData.AllTimeUnits() + timeUnits = GlobalData.AllTimeUnits(), + otcFormatConfig = otcFormatConfig }; //pageObj.clearingAgencyMap.Remove("甲方"); //pageObj.clearingAgencyMap.Remove("已方"); @@ -120,15 +140,15 @@
- +
- +
- +
@@ -236,6 +256,15 @@
+
+ + +
@@ -394,9 +423,10 @@ 收支方向 多空方向 标的代码 - 期初标的交割全价% - 期初标的交割净价% - 期初标的成交收益率% + 期初标的交割全价% + 期初标的交割净价% + 期初标的成交收益率% + 期初标的价格 数量 交易费用后付 @@ -414,19 +444,23 @@ - + + - - + + - - + + - - + + + + + {{item.underlying!=null?item.underlying.QuoteUnitString:''}} diff --git a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml index 093dd18f..310b7897 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml @@ -22,9 +22,10 @@ var swap_deals = trade.swap_Events.Where(x => x.EventType == (int)SwapEventTypeEnum.互换 || x.EventType == (int)SwapEventTypeEnum.自动互换).ToList();//单标的/多空组合互换 互换记录 var closeSwapDeals = trade.swap_Events.Where(x => x.EventType == (int)SwapEventTypeEnum.平仓).ToList();//单标的互换 平仓记录 bool isBond = trade.StructureType == "普通债券类收益互换"; - string spotPriceTitle = "期初标的交割全价%"; - string netSportPriceTitle = "期初标的交割净价%"; - string initYtmTitle = "期初标的成交收益率%"; + string spotPriceTitle = isBond ? "期初标的交割全价%" : "期初标的价格"; + string netSportPriceTitle = isBond ? "期初标的交割净价%" : string.Empty; + string initYtmTitle = isBond ? "期初标的成交收益率%" : string.Empty; + string endSpotPriceTitle = isBond ? "期末标的交割全价%" : "期末标的价格"; var positions = trade.swap_positions.Where(x=>x.PosiDirection>0&&x.IsInitial).ToList(); var realPositions = trade.swap_positions.Where(x => x.PosiDirection > 0 && !x.IsInitial).ToList(); var sr = trade.trade_extend.ExtendObj.SettlementRules; @@ -70,18 +71,21 @@ 提前终止 } + @if (swap_deals.Count > 0) { } + @if (trade.ClientCashInCashOutList.Count > 0) { } +
@@ -228,6 +232,10 @@ 结算规则 @(trade.trade_extend.ExtendObj.SettlementRules == 0 ? "T+0" : "T+1") + + 派息金额支付日 + @(trade.trade_extend.ExtendObj.DividendPayDate == 0 ? "到期结算日" : "派息日+" + (trade.trade_extend.ExtendObj.DividendPayDate - 1)) +
@@ -364,8 +372,11 @@ 多空方向 标的代码 @spotPriceTitle - @netSportPriceTitle - @initYtmTitle + @if (isBond) + { + @netSportPriceTitle + @initYtmTitle + } 数量 交易费用后付 @@ -380,12 +391,15 @@ @((item.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) - - @(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice)) - - - @((item.InitYtm * multiplier).OtcFormat(OtcFormatFlag.umprice)) - + @if (isBond) + { + + @(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice)) + + + @((item.InitYtm * multiplier).OtcFormat(OtcFormatFlag.umprice)) + + } @item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional) @@ -557,8 +571,11 @@ 多空方向 标的代码 @spotPriceTitle - @netSportPriceTitle - 持仓数量 + @if (isBond) + { + @netSportPriceTitle + } + 数量 持仓名义本金 交易费用后付 起始日期 @@ -579,9 +596,12 @@ @((item.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) - - @(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice)) - + @if (isBond) + { + + @(((item.PosiNetNoFeePrice ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice)) + + } @item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional) @@ -757,9 +777,17 @@ 收支方向 多空方向 标的代码 - 期初标的交割全价% - 期末标的交割全价% - 平仓数量 + @if (isBond) + { + 期初标的交割全价% + 期末标的交割全价% + } + else + { + 期初标的价格 + 期末标的价格 + } + 数量 交易费用 交易费用(待结算) 分红收益 @@ -769,13 +797,21 @@ @((SwapDirectionEnum)closeFloat.PayDirection) @(closeFloat.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头") @closeFloat.UnderlyingCode - @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) - @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) + @if (isBond) + { + @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) + @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) + } + else + { + @(closeFloat.PosiGrossPrice.OtcFormat(OtcFormatFlag.umprice)) + @(closeFloat.TradingAmountAvg.OtcFormat(OtcFormatFlag.umprice)) + } @(closeFloat.Quantity.OtcFormat(OtcFormatFlag.StockEqvNotional)) @(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) @(closeFloat.TradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)) @(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional)) - @(closeFloat.MarkClosePnl.OtcFormat(OtcFormatFlag.StockEqvNotional)) + @(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional)) @@ -787,6 +823,7 @@ } } + @if (!singleTrade && closeSwapDeals.Count > 0) { var index = 1; @@ -816,6 +853,7 @@ } } + @if (swap_deals.Count > 0) { var index = 1; @@ -840,6 +878,12 @@ 互换日期 @(tc.ValueDate.OtcFormatDate()) + + 支付日期 + @(tc.PayDate?.OtcFormatDate() ?? tc.ValueDate.OtcFormatDate()) + + + 平仓总额 @(tc.SwapCloseAmount.OtcFormat(OtcFormatFlag.StockEqvNotional)) @@ -880,6 +924,7 @@ } + @if (swapInterests.Count > 0) {
@@ -928,10 +973,18 @@ 收支方向 多空方向 标的代码 - @spotPriceTitle - @netSportPriceTitle - 标的价格% - 持仓数量 + @if (isBond) + { + @spotPriceTitle + @netSportPriceTitle + + } + else + { + 标的价格 + } + @endSpotPriceTitle + 数量 交易费用 分红收益 浮动端平仓盈亏 @@ -940,13 +993,20 @@ @((SwapDirectionEnum)closeFloat.PayDirection) @(closeFloat.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头") @closeFloat.UnderlyingCode - @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umpriceP)) - @(((closeFloat.TradingAmountNetAvg ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umpriceP)) - @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umpriceP)) + @if (isBond) + { + @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) + @(((closeFloat.TradingAmountNetAvg ?? 0) * multiplier).OtcFormat(OtcFormatFlag.umprice)) + } + else + { + @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) + } + @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) @((closeFloat.PositionQty??0).OtcFormat(OtcFormatFlag.StockEqvNotional)) @(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) @(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional)) - @(closeFloat.MarkClosePnl.OtcFormat(OtcFormatFlag.StockEqvNotional)) + @(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional)) @@ -960,6 +1020,7 @@ }
} + @if (trade.ClientCashInCashOutList.Count > 0) {
@@ -994,6 +1055,7 @@
} + @@ -1047,4 +1109,4 @@ @if (!string.IsNullOrWhiteSpace(Context.Request.Query["abstract"])) { @await Html.PartialAsync("/Views/trade/_part/TradeAbstractInfo.cshtml", Model.Trade) -} \ No newline at end of file +} diff --git a/YLErpWeb/appsettings.dev.json b/YLErpWeb/appsettings.dev.json index f53778f2..57e00050 100644 --- a/YLErpWeb/appsettings.dev.json +++ b/YLErpWeb/appsettings.dev.json @@ -15,7 +15,7 @@ "AppSettings": { "VirtualPathRoot": "", "UseRightAligned": "", - "PluginFolder": "D:\\workspace\\onederiv\\trs\\Plugins\\YLErp.Plugins.GuoLian\\obj\\Debug\\net6.0" // 改为本地目录地址 + "PluginFolder": "D:\\workspace\\onederiv\\trs\\Plugins\\YLErp.Plugins.GuoLian\\obj\\Debug\\net6.0" }, "LibreOffice": { "ExePath": "", @@ -104,4 +104,4 @@ "appId": "" } } -} +} \ No newline at end of file diff --git a/YLErpWeb/appsettings.local.json b/YLErpWeb/appsettings.local.json new file mode 100644 index 00000000..01547973 --- /dev/null +++ b/YLErpWeb/appsettings.local.json @@ -0,0 +1,104 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "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=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=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=glms_bond_oms;charset=utf8;Allow User Variables=True;SslMode=none;Connection Timeout=30;IgnoreCommandTransaction=true;" + }, + "AppSettings": { + "VirtualPathRoot": "", + "UseRightAligned": "", + "PluginFolder": "C:\\Users\\Administrator\\Desktop\\workCode\\zszq-trs\\Plugins\\build\\GuoLian\\Debug\\net6.0" + }, + "LibreOffice": { + "ExePath": "", + "UserInstallation": "" + }, + "EPPlus": { + "ExcelPackage": { + "LicenseContext": "NonCommercial" //The license context used + } + }, + "KafkaConfig": { + "BootstrapServers": "192.168.2.96:10112", // Kafka 集群的地址 + "Acks": -1, // 消息确认方式,可以是 All(-1)、Leader(1)、None(0) 中的一种 + "EnableIdempotence": false, // 开启幂等性,确保消息只被发送一次 + "MaxInFlight": 5, // 控制生产者在同一时间最多可以发送的未确认消息数 + "CompressionType": 0, // 消息压缩方式,可以是 None(0)、Gzip(1)、Snappy(2)、Lz4(3)、Zstd(4) 中的一种, + "MessageTimeoutMs": 3000, // 控制生产者等待消息确认的时间,单位是毫秒 + "ClientRateTopic": "ylClientRateTopic", //客户互换费率生产topic + "HedgingAccountTopic": "ylHedgingAccountTopic", //对冲账户生产topic + "ReqAccountCapitalTopic": "ReqAccountCapital", //账户资金请求topic + "OnRspAccountCapitalTopic": "OnRspAccountCapital", //账户资金请求返回topic + "AccountCapitalTopicGroupId": "YiLian_OnRspAccountCapitalConsumer", //账户资金消费组 + "AutoOffsetReset": 1, //Latest(0),Earliest(1),Error(2) + "EnableCalcBongd": false, //是否启用kafka计算 + "ReqClientRiskCheckTopic": "YiLian_ReqClientRiskCheck", //客户风控检查 + "ReqClientRiskCheckConsumerTopic": "YiLian_OnRspClientRiskCheck", //客户风控检查消费者 + "ReqClientRiskCheckTopicGroupId": "YiLian_ReqClientRiskCheckConsumer", //客户风控检查消费者 + "ReqClientMonitorTopic": "YiLian_ReqClientMonitor", //客户资金监控 + "OnRspClientMonitorConsumerTopic": "YiLian_OnRspClientMonitor", //客户资金监控消费者 + "ReqClientMonitorTopicGroupId": "YiLian_ReqClientMonitorConsumer", //客户资金监控消费组 + "ReqClientMonitorSingleTopic": "YiLian_ReqClientMonitorSingle", //单个客户资金监控 + "OnRspClientMonitorSingleConsumerTopic": "YiLian_OnRspClientMonitorSingle", //单个客户资金监控 + "ReqClientMonitorSingleTopicGroupId": "YiLian_ReqClientMonitorSingleConsumer", //单个客户资金监控 + "TrsExchangeTradeConsumerTopic": "YiLian_HedgingOrder", //TRS对冲交易消费 topic + "TrsExchangeTradeConsumerTopicGroupId": "YiLian_HedgingOrderGroup", //TRS对冲交易消费 Group + "YiLian_ReqHedgingOrderCancelTopic": "YiLian_ReqHedgingOrderCancel", //TRS对冲交易撤单请求消费 topic + "YiLian_ReqHedgingOrderCancelTopicGroupId": "YiLian_ReqHedgingOrderCancelGroup", //TRS对冲交易撤单请求消费 Group + "YiLian_RespHedgingOrderCancelTopic": "YiLian_RespHedgingOrderCancel", //TRS对冲交易撤单请求回应 topic + "YiLian_ClientInfoChangeTopic": "YiLian_ClientInfoChangeTopic", //客户信息变更通知客户端 topic + "YiLian_CashCalcConsumerTopic": "YiLian_CashCalc", //客户资金计算 topic + "YiLian_CashCalcConsumerGroup": "YiLian_CashCalcGroup", //客户资金计算消费组 topic + "YiLian_CashNoticeConsumerTopic": "YiLian_CashNotice", //客户资金通知 topic + "YiLian_CashNoticeConsumerGroup": "YiLian_CashNoticeGroup", //客户资金通知消费组 topic + "YiLian_HolidayResetTopic": "YiLian_HolidayReset", //交易日历缓存重新加载 topic + "YiLian_HolidayResetGroupId": "YiLian_HolidayResetGroup", //交易日历缓存重新加载消费 Group + "YiLian_SwapFlowTopic": "YiLian_SwapFlow", //交易端同步流水 + "YiLian_SwapFlowGroup": "YiLian_SwapFlowGroup", //交易端同步流水消费组 + "YiLian_SysUserInfoChangeTopic": "YiLian_SysUserInfoChangeTopic" //管理端系统用户变更 + }, + "BondOmsInterface": { + "BaseUrl": "http://192.168.2.96:10114" + }, + "Reg_Report_Url": "http://192.168.2.96:10019", + "OrcaleDatabaseConfig": { + "Schema": "APEX_040000" + }, + "SendKafakaCron": "10 0 * * *", + "MaxThreadPercent": 0.6, //占系统资源最多60%的线程数量 + "oa_confg": { + "BaseUrl": "http://ip:port", + "SubUrl": "", + "UploadUrl": "/api/oa/upload", // OA附件上传接口路径 + "SystemToken": "your-system-token", // OA系统鉴权Token + "SystemName": "OTC_TRADE", // 调用方系统标识 + "IsNextFlow": 0 + }, + "GeneralSSO": { + "enable": true, //是否启用通用SSO登录 + "auth_url": "http://192.168.2.96:10117/sso/login", //SSO授权地址 + "callback_url": "http://localhost:49462/Account/Callback", //回调处理地址 + "get_user_url": "http://192.168.2.96:10117/sso/getSsoUser" //获取用户信息接口地址 + }, + "Tracking": { + "sensors": { + "serverUrl": "", + "_platformType": "", + "_systemVersion": "", + "_techStack": "" + }, + "bonree": { + "uploadAddrHttp": "", + "uploadAddrHttps": "", + "appId": "" + } + } +} diff --git a/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js b/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js index bcb2bea8..4c159554 100644 --- a/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js +++ b/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js @@ -1,4 +1,4 @@ -var varieties = [{ id: 0, Name: '全部' }].concat(ylotc.varieties); +var varieties = [{ id: 0, Name: '全部' }].concat(ylotc.varieties); var remarkDict = {}; var valueDateShowStatus = true; $(function () { @@ -1587,6 +1587,33 @@ var colModel_trade = [ var style = "style='" + checkQuota(rowObject, 'SwapPercent') + "'"; return style; } + }, { + name: 'Quota_DV_Upper', + label: 'DV限额', + index: 'Quota_DV_Upper', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + return formatQuotaAbs(rowObject, 'DV'); + } + }, { + name: 'DV', + label: 'DV', + index: 'DV', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + if (!cellvalue && cellvalue !== 0 || cellvalue == "NaN") return ""; + var abs = Math.abs(cellvalue); + if (abs < 0.0001) return "0"; + return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = "style='" + checkQuota(rowObject, 'DV') + "'"; + return style; + } } ]; @@ -1676,6 +1703,51 @@ var colModel_global = [ formatter: function (cellvalue, options, rowObject) { return !cellvalue || cellvalue == "NaN" ? "" : cellvalue; } + }, { + name: 'Quota_DV_Upper', + label: 'DV限额', + index: 'Quota_DV_Upper', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + return formatQuotaAbs(rowObject, 'DV'); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = ''; + if (!rowObject.ParentKey) { + style += 'font-weight: bold;' + if (rowObject.BusinessType == "合计") { + style += 'font-size: 1rem;' + } + } + return "style='" + style + "'"; + } + }, { + name: 'DV', + label: 'DV', + index: 'DV', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + if (rowObject.BusinessType == "场内业务") { + return '
'; + } + if (!cellvalue && cellvalue !== 0 || cellvalue == "NaN") return ""; + if (Math.abs(cellvalue) < 0.0001) return "0"; + return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = checkQuota(rowObject, 'DV'); + if (!rowObject.ParentKey) { + style += 'font-weight: bold;' + if (rowObject.BusinessType == "合计") { + style += 'font-size: 1rem;' + } + } + return "style='" + style + "'"; + } } ]; diff --git a/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js b/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js index d0ad962e..543e8caa 100644 --- a/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js +++ b/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js @@ -430,6 +430,7 @@ var colModel = [ case "非权益类业务规模": case "本年累计盈亏": case "浮动盈亏": + case "DV": // DV是绝对值指标,不允许百分比模式 break; default: html += "onclick='changePercent(this)' "; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js index 3818a18f..5abf3e14 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js @@ -38,7 +38,7 @@ $(function () { pagerpos: 'left', rowNum: 25, rowList: [25, 50, 100, 200, 10000], - footerrow: false, + footerrow: page.tabIndex == 2, loadComplete: gridComplete, onPaging: onJqgridPaging, grouping: true @@ -610,6 +610,18 @@ function colModelGridEodSwap() { width: 150, align: 'center', formatter: StockEqvNotionalFormat, + }, { + name: 'position.dv01', + label: 'DV', + index: 'position.dv01', + width: 120, + align: 'center', + formatter: function (cellvalue, options, rowObject) { + if (Number.isNaN(cellvalue)) return "0"; + var abs = Math.abs(Number(cellvalue)); + if (abs < 0.0001) return "0"; + return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); + }, }, { name: 'SwapTradeTypeStr', label: '互换类型', @@ -626,6 +638,9 @@ function colModelGridEodSwap() { function gridComplete() { var jgrid = $(this); + if (arguments[0].Sum) { + jgrid.footerData("set", { 'position.dv01': arguments[0].Sum["DV"] }); + } main.setcolumnChooser(jgrid, page.configcolumn_data); $(window).resize(); } diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 9846f8f8..019e302f 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -1,10 +1,11 @@ -//otcformat禁止千分位分组 +//otcformat禁止千分位分组 window.otcformat.options.disableGrouping = true; const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, append: '%' }); const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' }); const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' }); const inputFormatEqvNotional = Object.freeze({ precision: otcformat.trading.StockEqvNotional.precision, append: '', negative: true }); +const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative: true }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true }); let ValueDate = model.ValueDate; @@ -16,7 +17,8 @@ const vue = new Vue({ interestList: [], marginList: [], multiplier: 1, - initPosiNetPrice: 0 + initPosiNetPrice: 0, + initPosiGrossPrice:0 }, computed: { maxUnwindDate() { @@ -27,25 +29,31 @@ const vue = new Vue({ } }, created() { + this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1; this.initDeal(); this.setValueDate(); - if (this.deal.StructureType != '普通收益互换') { - this.multiplier = 100; - } }, methods: { + getPriceScale() { + return this.multiplier == 100 ? 0.01 : 1; + }, initDeal() { var positions = model.FlowEvents.filter((item) => { return item.UnderlyingCode; }); this.floatPosition = positions[0]; this.initPosiNetPrice = this.floatPosition.PosiNetPrice; + this.initPosiGrossPrice = this.floatPosition.PosiGrossPrice; + // 互换标的价格固定为期初净价,与平仓不同不需要用户填写 + // 期初净价入库为相对价(如1.02),需转换为界面百分比形态(102),与平仓页保持一致 + this.floatPosition.TradingAmountAvg = this.initPosiGrossPrice * this.multiplier; this.interestList = model.FlowEvents.filter((item) => { return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9; }); this.marginList = model.FlowEvents.filter((item) => { return item.InterestMode == 5 || item.InterestMode == 6; }); + this.TradeStartDate = model.TradeStartDate; }, IsBond(instType) { return tradeHelper.IsBond(instType); @@ -66,7 +74,7 @@ const vue = new Vue({ //this.floatPosition.PosiNetPrice = otcformat.trading.umprice(this.floatPosition.PosiNetPrice); //this.floatPosition.PosiGrossPrice = otcformat.trading.umprice(this.floatPosition.PosiGrossPrice); this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee); - this.floatPosition.DividendIn = otcformat.trading.StockEqvNotional(this.floatPosition.DividendIn); + this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2); this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl); this.interestList.forEach(x => { //x.Principal = otcformat.trading.StockEqvNotional(x.Principal); @@ -91,7 +99,6 @@ const vue = new Vue({ } if (!isUseApproval) { this.getInterestList(); - //this.refreshUnderlyingPrice(); } else { this.dataFormat(); } @@ -102,10 +109,10 @@ const vue = new Vue({ refreshUnderlyingPrice() {//刷新标的价格 var thisObj = this; main.post("/underlying_manager/GetUnderlyingPriceByCode", - { code: thisObj.floatPosition.UnderlyingCode, ValueDate: thisObj.deal.ValueDate }) + { code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate }) .done(function (res) { - res.obj = res.obj * 100; - thisObj.floatPosition.TradingAmountAvg = otcformat.trading.umprice(res.obj); + res.obj = res.obj * thisObj.multiplier; + thisObj.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(res.obj); thisObj.calcFloatClosePnl(); }); }, @@ -124,9 +131,11 @@ const vue = new Vue({ let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending); let DividendIn = thisObj.floatPosition.DividendIn == "" ? 0 : parseFloat(thisObj.floatPosition.DividendIn ?? 0); - thisObj.floatPosition.MarkClosePnl = thisObj.deal.CloseNotionalValue * (thisObj.floatPosition.TradingAmountAvg*0.01 - thisObj.initPosiNetPrice) * floatRatio; - thisObj.floatPosition.MarkClosePnl = thisObj.floatPosition.MarkClosePnl + TradingFee + TradingFeePending + DividendIn; - thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl); + let scale = thisObj.getPriceScale(); + //thisObj.floatPosition.MarkClosePnl = thisObj.deal.CloseNotionalValue * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiNetPrice) * floatRatio; + thisObj.floatPosition.MarkClosePnl = thisObj.deal.CloseNotionalValue * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiGrossPrice) * floatRatio; + thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红 + thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + DividendIn).toFixed(2); thisObj.calcCloseAmount(); }, //calcClosePnL() {//计算浮动端平仓盈亏 @@ -137,18 +146,19 @@ const vue = new Vue({ calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付 var floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; var thisObj = this; - let pnl = parseFloat(this.floatPosition.MarkClosePnl); + let pnl = parseFloat(this.floatPosition.FloatPnlSum); let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); thisObj.deal.SwapCloseAmount = pnl; thisObj.deal.SwapRealizedPnL = pnl; thisObj.deal.SwapMarginRebatePnl = 0; thisObj.deal.SwapMarginAmount = 0; - thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseNotionalValue) * 0.01; + let scale = thisObj.getPriceScale(); + thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseNotionalValue) * scale; thisObj.floatPosition.CloseFee = TradingFee; if (thisObj.deal.CloseQty > 0) { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * 0.01 + (TradingFee / thisObj.deal.CloseQty) * floatRatio; + thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale + (TradingFee / thisObj.deal.CloseQty) * floatRatio; } else { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * 0.01; + thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale; } this.interestList.forEach(x => { //let interestRatio = x.InterestDirection == 1 ? 1 : -1; @@ -178,6 +188,22 @@ const vue = new Vue({ }); thisObj.calcCloseAmount(); thisObj.dataFormat(); + thisObj.getDivindIn(); + }); + }, + getDivindIn() { + var thisObj = this; + let ratio = this.floatPosition.PositionType == 1 ? 1 : -1; + let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; + var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate } + main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) { + let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio;//总的 + let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的 + // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0 + thisObj.floatPosition.DividendIn = Math.abs(consumedDividend) > 0 ? parseFloat((totalDividend - consumedDividend).toFixed(2)) : parseFloat(totalDividend.toFixed(2)); + thisObj.floatPosition.DividendPending = 0; + thisObj.calcFloatClosePnl(); + thisObj.dataFormat(); }); }, incomeTrade() {//互换 @@ -195,7 +221,7 @@ const vue = new Vue({ thisObj.floatPosition.EventDate = thisObj.deal.ValueDate; let floatPosition = _.cloneDeep(thisObj.floatPosition); floatPosition.Quantity = 0; - floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * 0.01; + floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * thisObj.getPriceScale(); reqObj.FlowEvents.push(floatPosition); var postData = { unwindData: reqObj }; var msg = "确认提交收益结算?"; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js index e71b9f5a..0b3f630b 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js @@ -11,14 +11,29 @@ const inputFormatEqvNotional = Object.freeze({ precision: otcformat.trading.Stoc const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' }); const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, negative: true, append: '%' }); const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, negative: true, append: '' }); -const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '' }); +const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '', percent: false }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent:true }); -const consUnderlyingFlag = (function () { +const consUnderlyingFlagBase = (function () { let unSelFlag = tradeHelper.UnderlyingSelectFlag; return unSelFlag.UseForTrading | unSelFlag.IncludeMatured | unSelFlag.UsePinYinFilter | unSelFlag.IncludeBasket | unSelFlag.IncludeSynthetic | unSelFlag.CheckLaunch; }()); +const consUnderlyingFlagNonBond = (function () { + let unSelFlag = tradeHelper.UnderlyingSelectFlag; + //临时注释,保证业务不受影响 + //return consUnderlyingFlagBase | unSelFlag.Stock | unSelFlag.StockIndex | unSelFlag.StockIF | unSelFlag.Fund | unSelFlag.CommodityFutures | unSelFlag.CommoditySpot; + return consUnderlyingFlagBase; +}()); +const consUnderlyingFlagBond = (function () { + let unSelFlag = tradeHelper.UnderlyingSelectFlag; + const consUnderlyingFlagNonBond = (function () { + let unSelFlag = tradeHelper.UnderlyingSelectFlag; + //临时注释,保证业务不受影响 + //return consUnderlyingFlagBase | unSelFlag.Stock | unSelFlag.StockIndex | unSelFlag.StockIF | unSelFlag.Fund | unSelFlag.CommodityFutures | unSelFlag.CommoditySpot; + return consUnderlyingFlagBase; + }()); +}()); const inputFormatDouble2 = Object.freeze({ precision: 2, append: '' }); var autoMarginTemplateName; @@ -42,8 +57,7 @@ const consNumberFormat = Object.freeze(new function () { return this; }()); -//标的选择组件 -const vueUnderlying = function () { +const createVueUnderlying = function (selectFlag) { return { props: ['value', 'index'], data() { @@ -51,7 +65,12 @@ const vueUnderlying = function () { }, mounted() { var selFlag = tradeHelper.UnderlyingSelectFlag; - this.autoUnderlying = tradeHelper.UnderlyingAutoComplete(this.$el, { SelectFlag: selFlag.UsePinYinFilter | selFlag.UseForTrading | selFlag.IncludeSynthetic | selFlag.IncludeMatured | selFlag.IncludeBasket | selFlag.CheckLaunch, BlackLimit: 2, }); + this.autoUnderlying = tradeHelper.UnderlyingAutoComplete(this.$el, { SelectFlag: selFlag.UsePinYinFilter | + selFlag.UseForTrading | + selFlag.IncludeSynthetic | + selFlag.IncludeMatured | + selFlag.IncludeBasket | + selFlag.CheckLaunch, BlackLimit: 2, }); this.autoUnderlying.onSelect(this.onchange); this.value && this.autoUnderlying.selectByCode(this.value); }, @@ -67,6 +86,12 @@ const vueUnderlying = function () { template: '' }; }; +const vueUnderlyingNonBond = function () { + return createVueUnderlying(consUnderlyingFlagNonBond); +}; +const vueUnderlyingBond = function () { + return createVueUnderlying(consUnderlyingFlagBond); +}; //标的选择组件 银行间回购定盘&其他利率 const vueUnderlyingRate = function () { @@ -78,7 +103,13 @@ const vueUnderlyingRate = function () { mounted() { var selFlag = tradeHelper.UnderlyingSelectFlag; var instrumentTypes = ["FixingRepoRate", "OtherRate"]; - this.autoUnderlying = tradeHelper.UnderlyingAutoComplete(this.$el, { SelectFlag: selFlag.UsePinYinFilter | selFlag.UseForTrading | selFlag.IncludeSynthetic | selFlag.IncludeMatured | selFlag.IncludeBasket | selFlag.CheckLaunch, BlackLimit: 2, InstrumentTypes: instrumentTypes, UseAll: true, UseSignle: true }); + this.autoUnderlying = tradeHelper.UnderlyingAutoComplete(this.$el, + { SelectFlag: selFlag.UsePinYinFilter | + selFlag.UseForTrading | + selFlag.IncludeSynthetic | + selFlag.IncludeMatured | + selFlag.IncludeBasket | + selFlag.CheckLaunch, BlackLimit: 2, InstrumentTypes: instrumentTypes, UseAll: true, UseSignle: true }); this.autoUnderlying.onSelect(this.onchange); this.value && this.autoUnderlying.selectByCode(this.value); }, @@ -167,21 +198,27 @@ const vue = new Vue({ if (this.trade.MetaDic['组合标的']) { this.viewState.synthetic = JSON.parse(this.trade.MetaDic['组合标的']); } - this.viewState.Extend.TradingPlace = this.trade.MetaDic["交易场所"] || ""; - this.viewState.Extend.ClearingAgency = this.trade.MetaDic["清算机构"] || ""; + if (this.trade.MetaDic["交易场所"]) { + this.viewState.Extend.TradingPlace = this.trade.MetaDic["交易场所"]; + } + if (this.trade.MetaDic["清算机构"]) { + this.viewState.Extend.ClearingAgency = this.trade.MetaDic["清算机构"]; + } }, mounted() { __init(this); $("#MainProtocolCode").val(this.trade.MetaDic["主协议编号"]); $("#SupProtocolCode").val(this.trade.MetaDic["补充协议编号"]); this.initSwapRateList(); + // 初始化到期日期选择器下限 + this.$nextTick(() => { + if (this.$refs.exerciseDatePicker) { + var minDate = this.trade.StartDate > this.trade.TradeDate ? this.trade.StartDate : this.trade.TradeDate; + this.$refs.exerciseDatePicker.refresh(minDate, null); + } + }); }, methods: { - getPosiPriceFormat(item) { - return tradeHelper.IsBond(item && item.UnderlyingInstrumentType) - ? inputFormatMarginRateNoPercent - : inputFormatTradeSinglePrice; - }, getPosiPriceFormatKey(item, field) { const index = item && item.index != null ? item.index : ''; const isBond = tradeHelper.IsBond(item && item.UnderlyingInstrumentType); @@ -189,6 +226,13 @@ const vue = new Vue({ }, changeStructureType() { this.trade.StockEqvNotional = 0; + let direction = this.trade.trade_extend.ExtendObj.Direction; + if (direction !== 1 && direction !== 2) { + direction = 2; + this.trade.trade_extend.ExtendObj.Direction = direction; + } + this.paySwapList = []; + this.addSwapFloat(direction); //this.trade.trade_extend.ExtendObj.FlowBookMode = 0; }, //变更初始预付金收取方向 @@ -234,7 +278,7 @@ const vue = new Vue({ if (this.paySwapList.length > 0) { var item = this.paySwapList[0]; var notional = item.PosiGrossPrice * item.ContractSize; - item.PosiQuantity = notional == 0 ? 0 : this.trade.StockEqvNotional / notional; + item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision); this.calcNotional(); } @@ -250,6 +294,41 @@ const vue = new Vue({ this.trade.trade_Initial_Margin.MarginValue = this.trade.StockEqvNotional == 0 ? 0 : this.trade.trade_Initial_Margin.MarginValue / this.trade.StockEqvNotional; }, + //成交日期变更时自动修正联动日期 + onTradeDateChange() { + if (this.trade.StartDate < this.trade.TradeDate) { + // 开始日期 = 成交日期 + 1天,跳过节假日 + var d = new Date(this.trade.TradeDate); + d.setDate(d.getDate() + 1); + while (ylotc.isHoliday(d)) { + d.setDate(d.getDate() + 1); + } + this.trade.StartDate = this.formatDate(d); + } + // 到期日期必须 ≥ 成交日期 且 ≥ 开始日期 + if (this.trade.ExerciseDate < this.trade.TradeDate || this.trade.ExerciseDate < this.trade.StartDate) { + // 到期日期 = 开始日期 + 14天,跳过节假日 + var sd = new Date(this.trade.StartDate); + sd.setDate(sd.getDate() + 14); + while (ylotc.isHoliday(sd)) { + sd.setDate(sd.getDate() + 1); + } + this.trade.ExerciseDate = this.formatDate(sd); + } + this.changeTradeDate(); + }, + //开始日期变更时检查到期日期 + onStartDateChange() { + if (this.trade.ExerciseDate < this.trade.StartDate) { + var sd = new Date(this.trade.StartDate); + sd.setDate(sd.getDate() + 14); + while (ylotc.isHoliday(sd)) { + sd.setDate(sd.getDate() + 1); + } + this.trade.ExerciseDate = this.formatDate(sd); + } + this.changeTradeDate(); + }, //变更交易日期 changeTradeDate(force) { this.changeObservationStart(); @@ -259,6 +338,16 @@ const vue = new Vue({ this.calcNotional(); this.refreshDatepicker(); this.changeMarginDate(); + // 动态更新开始日期和到期日期的下限 + this.$nextTick(() => { + if (this.$refs.startDatePicker) { + this.$refs.startDatePicker.refresh(this.trade.TradeDate, null); + } + if (this.$refs.exerciseDatePicker) { + var minExerciseDate = this.trade.StartDate > this.trade.TradeDate ? this.trade.StartDate : this.trade.TradeDate; + this.$refs.exerciseDatePicker.refresh(minExerciseDate, null); + } + }); //this.initMarginRate(); }, calcNotional(calcPrice) { @@ -474,7 +563,7 @@ const vue = new Vue({ }, changeInterestType(item) { if (!item.interest_rest_days) { - item.interest_rest_days = 1; + item.interest_rest_days = 7; } }, setUnderlyingCode(data, item) { @@ -505,8 +594,8 @@ const vue = new Vue({ item.FloatRateUnderlyingCode = data.Code; if (data.Code.length > 0 && data.Code != '--') { item.IsAnnualized = true; - item.interest_rest_days = 1; - item.interest_rule = 0; + item.interest_rest_days = 7; + item.interest_rule = data.Code === 'FR007' ? -1 : 0; } else { item.interest_rule = null; } @@ -973,6 +1062,14 @@ const vue = new Vue({ main.message("请输入正确的开始日期格式"); return false; } + if (StartDate < TradeDate) { + main.message("开始日期不能早于成交日期"); + return false; + } + if (StartDate > ExerciseDate) { + main.message("到期日期不能早于开始日期"); + return false; + } if (!this.trade.AssetId) { main.message("请设置簿记账户"); return false; @@ -1269,7 +1366,7 @@ const vue = new Vue({ IsAnnualized: true,//是否年化 HappenDate: null,//发生日期, Currency: 'CNY',//币种 - interest_rest_days: 1,//重置频率 + interest_rest_days: 7,//重置频率 interest_rule: null//利率准则 } thisObj.getSwapList.push(getSwap); @@ -1307,7 +1404,7 @@ const vue = new Vue({ IsAnnualized: true,//是否年化, HappenDate: thisObj.trade.TradeDate,//发生日期, Currency: 'CNY',//币种 - interest_rest_days: null,//重置频率 + interest_rest_days: 7,//重置频率 interest_rule: null//利率准则 } thisObj.marginSwapList.push(getSwap); @@ -1355,7 +1452,7 @@ const vue = new Vue({ IsAnnualized: false,//是否年化 HappenDate: null,//发生日期, Currency: 'CNY',//币种 - interest_rest_days: null,//重置频率 + interest_rest_days: 7,//重置频率 interest_rule: null//利率准则 } thisObj.paySwapList.push(petSwap); @@ -1427,12 +1524,13 @@ const vue = new Vue({ maxTradeDate() { let exerciseDate = this.trade.ExerciseDate; return exerciseDate && exerciseDate <= page.valuedate ? exerciseDate : page.valuedate; - } + }, }, components: { 'vue-datepicker': FastVue.vueDatePicker(), 'vue-number-input': FastVue.vueNumberInput(), - 'vue-underlying': vueUnderlying(), + 'vue-underlying-nonbond': vueUnderlyingNonBond(), + 'vue-underlying-bond': vueUnderlyingBond(), 'vue-underlying-rate': vueUnderlyingRate() }, destroyed() { diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index eb16694c..b97414cf 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -1,4 +1,4 @@ -//otcformat禁止千分位分组 +//otcformat禁止千分位分组 window.otcformat.options.disableGrouping = true; const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, append: '%' }); @@ -30,13 +30,14 @@ const vue = new Vue({ } }, created() { - if (this.deal.StructureType != '普通收益互换') { - this.multiplier = 100; - } + this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1; this.initDeal(); this.setUnwindDate(); }, methods: { + getPriceScale() { + return this.multiplier == 100 ? 0.01 : 1; + }, initDeal() { var positions = model.FlowEvents.filter((item) => { return item.UnderlyingCode; @@ -81,7 +82,7 @@ const vue = new Vue({ this.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(this.floatPosition.TradingAmountAvg); this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee); this.floatPosition.TradingFeePending = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFeePending); - this.floatPosition.DividendIn = otcformat.trading.StockEqvNotional(this.floatPosition.DividendIn); + this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2); this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl); this.interestList.forEach(x => { //x.Principal = otcformat.trading.StockEqvNotional(x.Principal); @@ -187,7 +188,7 @@ const vue = new Vue({ main.post("/underlying_manager/GetUnderlyingPriceByCode", { code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate }) .done(function (res) { - res.obj = res.obj * 100; + res.obj = res.obj * thisObj.multiplier; thisObj.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(res.obj); thisObj.calcFloatClosePnl(); }); @@ -198,10 +199,11 @@ const vue = new Vue({ let longRatio = thisObj.floatPosition.PositionType == 1 ? 1 : -1; let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending); - thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (thisObj.floatPosition.TradingAmountAvg*0.01 - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000; - thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2)); - thisObj.floatPosition.MarkClosePnl = thisObj.floatPosition.MarkClosePnl + (TradingFee + TradingFeePending) * floatRatio * -1 + parseFloat(thisObj.floatPosition.DividendIn); + let scale = thisObj.getPriceScale(); + thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000; + thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2));//MarkClosePnl 纯盯市不要计算交易费用和分红 thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl); + thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + parseFloat(thisObj.floatPosition.DividendIn)).toFixed(2); thisObj.calcCloseAmount(); }, @@ -222,18 +224,19 @@ const vue = new Vue({ let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; let ratio = this.floatPosition.PositionType == 1 ? 1 : -1; let thisObj = this; - let pnl = parseFloat(this.floatPosition.MarkClosePnl); + let pnl = parseFloat(this.floatPosition.FloatPnlSum); let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); thisObj.deal.SwapCloseAmount = pnl; thisObj.deal.SwapRealizedPnL = pnl; thisObj.deal.SwapMarginRebatePnl = 0; thisObj.deal.SwapMarginAmount = 0; - thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseQty) * 0.01; + let scale = thisObj.getPriceScale(); + thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseQty) * scale; thisObj.floatPosition.CloseFee = TradingFee; if (thisObj.deal.CloseQty == 0) { thisObj.floatPosition.TradingAmountFeeAvg = 0; } else { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * 0.01 + (TradingFee / thisObj.deal.CloseQty) * ratio; + thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale + (TradingFee / thisObj.deal.CloseQty) * ratio; } this.interestList.forEach(x => { /*let interestRatio = x.InterestDirection == 1 ? 1 : -1;*/ @@ -275,11 +278,22 @@ const vue = new Vue({ var thisObj = this; let ratio = this.floatPosition.PositionType == 1 ? 1 : -1; let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; - var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode } + var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate } main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) { - thisObj.floatPosition.DividendIn = parseFloat(thisObj.deal.CloseQty) * resp.obj * ratio * floatRatio; + let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的 + let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio; + let remainDividend = totalDividend - consumedDividend; + let dividendIn = 0; + if (remainDividend != 0) { + dividendIn = parseFloat(thisObj.deal.CloseQty) / parseFloat(thisObj.floatPosition.Quantity) * remainDividend + } + // 这里计算已经分红的利息,从totalInterest里扣除 + + // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0 + // 现在可能做了纯分红的互换结算,所以不能直接归0 + thisObj.floatPosition.DividendIn = parseFloat(dividendIn.toFixed(2)); var posiQty = parseFloat(thisObj.floatPosition.Quantity) - parseFloat(thisObj.deal.CloseQty); - thisObj.floatPosition.DividendPending = posiQty * resp.obj * ratio * floatRatio; + thisObj.floatPosition.DividendPending = parseFloat((posiQty * resp.obj.totalInterest * ratio * floatRatio).toFixed(2)); thisObj.calcFloatClosePnl(); thisObj.dataFormat(); }); @@ -322,7 +336,7 @@ const vue = new Vue({ thisObj.floatPosition.EventDate = thisObj.deal.ValueDate; let floatPosition = _.cloneDeep(thisObj.floatPosition); floatPosition.Quantity = reqObj.CloseQty; - floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * 0.01; + floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * thisObj.getPriceScale(); reqObj.FlowEvents.push(floatPosition); var postData = { unwindData: reqObj }; var msg = "确认提交平仓?"; diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/forwardTradeEditV2.js b/YLErpWeb/wwwroot/Scripts/app/trade/forwardTradeEditV2.js index fbe333b7..e93a9764 100644 --- a/YLErpWeb/wwwroot/Scripts/app/trade/forwardTradeEditV2.js +++ b/YLErpWeb/wwwroot/Scripts/app/trade/forwardTradeEditV2.js @@ -606,15 +606,15 @@ var vue = new Vue({ }, //变更交易手数 changeLots() { - this.trade.Notional = this.trade.Lots * this.viewState.underlying.ContractSize; + this.trade.Notional = consFormat.notional(this.trade.Lots * this.viewState.underlying.ContractSize); this.trade.OriginalNotional = this.trade.Notional; - this.trade.TradeAmount = this.trade.Notional / this.viewState.underlying.CountRatio; + this.trade.TradeAmount = consFormat.notional(this.trade.Lots * this.viewState.underlying.ContractSize / this.viewState.underlying.CountRatio); this.resetTradePrice(true); }, //变更交易数量 changeTradeAmount() { - this.trade.Lots = this.trade.TradeAmount * this.viewState.underlying.CountRatio / this.viewState.underlying.ContractSize; - this.trade.Notional = this.trade.Lots * this.viewState.underlying.ContractSize; + this.trade.Lots = _.round(this.trade.TradeAmount * this.viewState.underlying.CountRatio / this.viewState.underlying.ContractSize, InputFormatLots.precision); + this.trade.Notional = consFormat.notional(this.trade.TradeAmount * this.viewState.underlying.CountRatio); this.trade.OriginalNotional = this.trade.Notional; this.resetTradePrice(true); }, @@ -711,9 +711,9 @@ var vue = new Vue({ let SpotPrice1 = parseFloat(this.trade.SpotPrice1 || 0); let SpotPrice2 = parseFloat(this.trade.SpotPrice2 || 0); if (SpotPrice1 > SpotPrice2) { - this.trade.StockEqvNotional = this.trade.SpotPrice1 * this.trade.Notional; + this.trade.StockEqvNotional = consFormat.StockEqvNotional(this.trade.SpotPrice1 * this.trade.Notional); } else { - this.trade.StockEqvNotional = this.trade.SpotPrice2 * this.trade.Notional; + this.trade.StockEqvNotional = consFormat.StockEqvNotional(this.trade.SpotPrice2 * this.trade.Notional); } } }, diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/tradeApproval.js b/YLErpWeb/wwwroot/Scripts/app/trade/tradeApproval.js index 51dd8d14..ecbd7a7d 100644 --- a/YLErpWeb/wwwroot/Scripts/app/trade/tradeApproval.js +++ b/YLErpWeb/wwwroot/Scripts/app/trade/tradeApproval.js @@ -747,7 +747,7 @@ function passorreinfo(eid, tradeType, status, isGroup, StructureType) { if (StructureType == "多空组合") { url = "/swaptrade2/SwapLongShortSwap/?enid=" + eid + "&isUseApproval=" + true; } - height = "720px"; + height = "800px"; } else if (status === "行权待复核") { title = "行权审批"; diff --git a/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js b/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js index 564d5697..75701fcb 100644 --- a/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js +++ b/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js @@ -1,4 +1,4 @@ -//tradeHelper +//tradeHelper (function () { function tradeHelper() { } @@ -311,7 +311,8 @@ IncludeMatured: 1 << (i++), UseForTrading: 1 << (i++), IncludeBasket: 1 << (i++), - OnlyBasket: 1 << (i++) + OnlyBasket: 1 << (i++), + Fund: 1 << (i++) }; flags.OtcTrade = flags.UsePinYinFilter | flags.IncludeMatured | flags.IncludeSynthetic | flags.IncludeBasket; flags.OtcForward = flags.UsePinYinFilter | flags.IncludeMatured | flags.IncludeBasket | flags.CommodityFutures | flags.StockIF | flags.CommoditySpot; @@ -362,22 +363,24 @@ function __getPostData() { let options = this.options; let sf = options.SelectFlag; - let InstrumentTypes = options.InstrumentTypes; - (sf & SelectFlag.Stock) > 0 && (InstrumentTypes.push('Stock')); - (sf & SelectFlag.StockIndex) > 0 && (InstrumentTypes.push('StockIndex')); - (sf & SelectFlag.CommodityFutures) > 0 && (InstrumentTypes.push('CommodityFutures')); - (sf & SelectFlag.CommoditySpot) > 0 && (InstrumentTypes.push('CommoditySpot')); - (sf & SelectFlag.CreditBonds) > 0 && (InstrumentTypes.push('CreditBonds')); - (sf & SelectFlag.Bonds) > 0 && (InstrumentTypes.push('Bonds')); - (sf & SelectFlag.TBonds) > 0 && (InstrumentTypes.push('TBonds')); - (sf & SelectFlag.OtherBonds) > 0 && (InstrumentTypes.push('OtherBonds')); + let instrumentTypes = Array.isArray(options.InstrumentTypes) ? options.InstrumentTypes.slice() : []; + (sf & SelectFlag.Stock) > 0 && (instrumentTypes.push('Stock')); + (sf & SelectFlag.StockIndex) > 0 && (instrumentTypes.push('StockIndex')); + (sf & SelectFlag.StockIF) > 0 && (instrumentTypes.push('StockIF')); + (sf & SelectFlag.CommodityFutures) > 0 && (instrumentTypes.push('CommodityFutures')); + (sf & SelectFlag.CommoditySpot) > 0 && (instrumentTypes.push('CommoditySpot')); + (sf & SelectFlag.CreditBonds) > 0 && (instrumentTypes.push('CreditBonds')); + (sf & SelectFlag.Bonds) > 0 && (instrumentTypes.push('Bonds')); + (sf & SelectFlag.TBonds) > 0 && (instrumentTypes.push('TBonds')); + (sf & SelectFlag.OtherBonds) > 0 && (instrumentTypes.push('OtherBonds')); + (sf & SelectFlag.Fund) > 0 && (instrumentTypes.push('Fund')); return { FilterCode: '{{{q}}}', ClientId: options.ClientId, VarietyId: options.VarietyId, BlackLimit: options.BlackLimit, MaxShowLength: options.MaxShowLength, - InstrumentTypes: InstrumentTypes, + InstrumentTypes: instrumentTypes, UsePinYinFilter: (sf & (SelectFlag.UsePinYinFilter | SelectFlag.Stock | SelectFlag.StockIndex)) > 0, IncludeMatured: (sf & SelectFlag.IncludeMatured) > 0, OnlySynthetic: (sf & SelectFlag.OnlySynthetic) > 0, @@ -728,4 +731,4 @@ }(window.tradeHelper)); -Object.freeze(window.tradeHelper); \ No newline at end of file +Object.freeze(window.tradeHelper); diff --git a/YLErpWeb/wwwroot/Scripts/fast/fastVue.components.js b/YLErpWeb/wwwroot/Scripts/fast/fastVue.components.js index 7b1d84a2..7d0b0fd2 100644 --- a/YLErpWeb/wwwroot/Scripts/fast/fastVue.components.js +++ b/YLErpWeb/wwwroot/Scripts/fast/fastVue.components.js @@ -24,9 +24,9 @@ } }).datepicker({ - minDate: self.mindate, + minDate: self.mindate ? new Date(self.mindate) : null, beforeShowDay(date) { - return [(!self.maxdate || date <= new Date(self.maxdate)) && (self.noholiday || !ylotc.isHoliday(date))]; + return [(self.noholiday || !ylotc.isHoliday(date))]; }, changeMonth: true, changeYear: true, diff --git a/项目文档/MarkClosePnl字段语义统一决策文档.md b/项目文档/MarkClosePnl字段语义统一决策文档.md new file mode 100644 index 00000000..363eb618 --- /dev/null +++ b/项目文档/MarkClosePnl字段语义统一决策文档.md @@ -0,0 +1,159 @@ +# MarkClosePnl 字段语义统一:决策文档 + +> **状态**:待团队决策 | **日期**:2026-06-26 | **前置**:[互换分红损益字段语义与重复计算分析.md](./互换分红损益字段语义与重复计算分析.md)、修复提交 `ab70531f`(2026-06-26,MarkClosePnl 不再含分红) + +--- + +## 一、问题背景:一个字段,三种语义 + +`swap_flow_event.MarkClosePnl`(浮动端平仓盈亏·盯市)在历史上**同时承担了三个互斥的职责**,导致各处使用公式不一致。2026-06-26 的修复(`ab70531f`)让它"不再含分红",但**消费端的公式没有同步调整**,新数据下会产生新的偏差。 + +### MarkClosePnl 的三种历史语义 + +| 语义 | 含义 | 谁在用 | +|---|---|---| +| **A. 纯价差盯市** | `MarkClosePnl = (平仓价-成本价)×量` | 修复后(`ab70531f`)的目标语义 | +| **B. 价差+费+分红 总和** | 历史上前端 `calcFloatClosePnl` 把费和分红加进去 | 界面"浮动"列(用户误以为是总盈亏)| +| **C. 收益计算基数** | 各处用不同公式减/不减分红得到 PosiPnl | 结算单、收益列表 | + +**根因**:2024-05-09 从山证 v2.3.0 拷贝(`f9d8a256`)时,`MarkClosePnl` 入库的就是 B(总和),但后续不同开发者按各自理解写了 C 的多种公式,造成不一致。 + +--- + +## 二、各处使用现状与不一致清单(含引入时间) + +### 2.1 写入点(MarkClosePnl 怎么生成) + +| 位置 | 公式 | 引入 | 语义 | +|---|---|---|---| +| `SwapDealService.cs:1101` 自动平仓 | `(平仓价-成本价)×量` | `f9d8a256` 2024-05-09 | **修复后=纯价差** | +| `SwapDealService.cs:1103` 自动平仓(加费) | 上面 `+费×-1` | `f9d8a256` | 修复后=价差-费 | +| `SwapEodPositionService.cs:463` 自动互换分红 | `=0`(修复前=`PosiDividendSum`)| `ab70531f` 2026-06-26 | **修复后=0(不含分红)** | +| 前端 `incomeSwapTrade.js:133` 手动互换 | `calcFloatClosePnl` 去 `+DividendIn` | `ab70531f` | **修复后=纯价差** | +| 前端 `unwindSwapTrade.js:203` 手动平仓 | 同上 | `ab70531f` | **修复后=纯价差** | + +> **关键**:`ab70531f` 已把 4 个写入点统一为"纯价差/价差-费",不含分红。 + +### 2.2 消费点(PosiPnl / 收益公式)—— ⚠ 不一致集中区 + +| # | 位置 | 公式 | 减分红? | 引入 | 修复后影响 | +|---|---|---|---|---|---| +| ① | `SwapFlowEvent.cs:384` | `PosiPnl = MarkClosePnl - TradingFee` | **否** | `f9d8a256` 2024-05-09 | 新数据偏大(没减分红)| +| ② | `TradeSettleBillGenerator.cs:110` 结算单 | `PosiPnl = -(MarkClosePnl - tradingFee - DividendIn)` | **是** | `48cf0592` 2026-05-06 | **新数据多减一份分红** ⚠ | +| ③ | `SwapFlowEventService.cs:528` 收益列表 | `PosiPnl = MarkClosePnl - TradeFee - DividendIn - TradingFee` | **是** | `829334a4` 2025-05-30 | **新数据多减一份分红** ⚠ | + +**核心风险(②③)**:修复前 MarkClosePnl 含分红,减去 DividendIn 正好还原纯价差;修复后 MarkClosePnl **不含**分红了,再减 DividendIn 就**多减了一份**,结算单和收益列表金额偏小。 + +### 2.3 当"总和"用的地方 + +| 位置 | 公式 | 引入 | 含义 | +|---|---|---|---| +| `SwapEodPositionService.cs:681` | `amount = MarkClosePnl + CloseFee + DividendIn` | `f9d8a256` 2024-05-09 | 判是否产生资金记录(三者总和)| +| `SwapFlowEventService.cs:529` | `NetSettmentAmount = MarkClosePnl + InterestClosePnL` | `083848fb` 2025-05-28 | 净结算额 | +| `SwapEodPositionService.cs:1610` | `TdCloseMtmPnl = Σ MarkClosePnl` | `f9d8a256` 2024-05-09 | eod 盯市列累加 | + +### 2.4 界面展示(eventlist/SwapflowList/EodPositionRisks) + +"浮动端平仓盈亏"在界面**拆成 3 个独立列**,没有"总和"列: +- `MarkClosePnl` → 列名"浮动端平仓盈亏·**浮动**" +- `DividendIn` → 列名"浮动端平仓盈亏·**分红**" +- `CloseFee` → 列名"浮动端平仓盈亏·**费用**" + +> 用户历史上看到的"浮动列"= 总盈亏,是 buggy 的 MarkClosePnl 凑出来的;修复后该列=纯价差,会变小。**"三者总和"从未有独立列**。 + +--- + +## 三、历史数据问题 + +已入库的 `swap_flow_event.MarkClosePnl` 不可逆——**历史记录含分红,新记录不含**。这意味着任何消费点公式,对历史和新数据只能选一种正确: + +| 公式策略 | 历史数据(含分红) | 新数据(纯价差) | +|---|---|---| +| 不减 DividendIn(如 ①)| 偏大 | 偏大 | +| 减 DividendIn(如 ②③)| 正确(抵消)| **偏小(多减)** | + +**没有一种公式能同时让历史和新数据都对**,除非: +- (a) 数据迁移:刷新历史 MarkClosePnl 还原为纯价差,或 +- (b) 加版本标记:区分"修复前/后"的事件,分别用不同公式 + +--- + +## 四、三种改法方案(供团队选择) + +### 方案一:修正消费点公式(最小改动,推荐作为第一步) + +**做法**:把 MarkClosePnl 的语义**正式定为"纯价差盯市"**,修正 ②③ 去掉 `-DividendIn`: + +| 位置 | 改动 | +|---|---| +| ② `TradeSettleBillGenerator.cs:110` | `-(MarkClosePnl - tradingFee)`(去掉 -DividendIn)| +| ③ `SwapFlowEventService.cs:528` | `MarkClosePnl - TradeFee - TradingFee`(去掉 -DividendIn)| +| ① `SwapFlowEvent.cs:384` | 保持,或补 `-DividendIn` 与 ③ 统一(需业务确认收益口径)| + +- ✅ **优点**:改动小(3 行),新数据全部正确,语义立即清晰 +- ⚠ **缺点**:**历史数据仍不一致**(见第三节),需配合数据迁移或接受历史偏差 +- 🎯 **适合**:快速止血,让修复(ab70531f)真正生效,不让新数据出错 + +### 方案二:数据迁移 + 方案一(彻底统一) + +**做法**:在方案一基础上,对历史 `swap_flow_event` 执行数据迁移: + +```sql +-- 把历史 MarkClosePnl 还原为纯价差(减去当时混入的分红和费) +UPDATE swap_flow_event +SET MarkClosePnl = MarkClosePnl - DividendIn - CloseFee +WHERE EventType IN (2,3,4) AND DataState = 100 + AND EventDate < '2026-06-26'; -- 修复提交前的事件 +``` +(实际迁移需先 SELECT 验证范围、备份、分批执行) + +- ✅ **优点**:历史与新数据语义完全统一,所有公式一套即可 +- ⚠ **缺点**:改库有风险,需备份+回滚预案;eod 历史表(RealizedMtmPnL 等)是否也需重算需评估 +- 🎯 **适合**:团队接受改历史数据,追求长期干净 + +### 方案三:新增 TotalClosePnl 字段,职责分离(最长远正确) + +**做法**:让每个字段职责单一: +1. `MarkClosePnl` 永远=纯价差(修复已做到,语义固定) +2. 新增 `[NotMapped] decimal TotalClosePnl => MarkClosePnl + DividendIn + CloseFee`,专给"总和"场景(结算单、资金记录判断、界面总和列) +3. 界面如需"总盈亏"列,显式绑定 `TotalClosePnl`,而非靠 MarkClosePnl 隐式承担 + +- ✅ **优点**:根除"一字段三语义"债务,未来不会再出现"改一处引发另一处偏差" +- ⚠ **缺点**:改动面最大(涉及结算单/资金记录/界面多处),需排期 +- 🎯 **适合**:作为技术债治理的长期目标,可与方案一/二分阶段实施 + +--- + +## 五、建议路径 + +**推荐分阶段推进**,兼顾止血与长期: + +1. **立即(方案一)**:修正 ②③ 消费点公式,避免修复后新数据出错(结算单/收益金额偏小)。这是 `ab70531f` 修复的必要补全,不做的话修复只做了一半。 +2. **近期(方案二)**:评估数据迁移可行性,统一历史数据语义。 +3. **中期(方案三)**:引入 TotalClosePnl,彻底消除语义债务。 + +--- + +## 六、需团队决策的问题 + +1. **收益(PosiPnl)口径**:① 不减分红、②③ 减分红——哪个是业务正确的"收益"定义?(决定公式统一方向) +2. **历史数据**:是否接受数据迁移(方案二)?还是接受历史偏差、只保证新数据正确? +3. **界面"总盈亏"列**:是否需要显式新增一列展示 `MarkClosePnl+DividendIn+CloseFee` 总和?(决定是否走方案三) +4. **实施优先级**:方案一是否立即执行(它阻塞 ab70531f 修复的完整生效)? + +--- + +## 附录:字段引用全景(便于实施时定位) + +| 文件:行 | 用法 | 语义 | +|---|---|---| +| `SwapFlowEvent.cs:384` | `PosiPnl = MarkClosePnl - TradingFee` | 收益(不减分红) | +| `SwapFlowEvent.cs:211` | 字段定义 | 入库列 | +| `TradeSettleBillGenerator.cs:110,112,116` | 减 DividendIn / 取负 / 净结算 | 收益+结算 | +| `SwapFlowEventService.cs:287,378,429,509,528,529` | 赋值/取负/收益/净结算 | 多用途 | +| `SwapEodPositionService.cs:463,681,694,1610` | 赋0/总和/累加/eod盯市 | 写入+累加 | +| `SwapDealService.cs:101,275,1101,1103,1219,1350` | 赋值 | 写入 | +| `SwapEndConfirmService.cs:96` | `SwapCloseAmount = MarkClosePnl` | 展示 | +| `RealTimeClientBanlanceService.cs:1337,1343` | `+ MarkClosePnl` 累加 | 收益累加 | +| `SwapFlowService.cs:381` | 导出格式化 | 导出 | +| 前端 `eventlist.js:247`/`SwapflowList.js:556` | 界面"浮动"列 | 展示 | diff --git a/项目文档/互换交易价格字段存储与显示规范.md b/项目文档/互换交易价格字段存储与显示规范.md new file mode 100644 index 00000000..aab923ef --- /dev/null +++ b/项目文档/互换交易价格字段存储与显示规范.md @@ -0,0 +1,308 @@ +# 互换交易价格字段存储与显示规范 + +> 本文档总结互换交易(收益互换)中"标的价格"类字段在**录入、入库、展示**三个环节的处理规则,以及债券类与普通(非债券)类的差异。 +> 该规则是 2026-06 一系列价格显示 Bug 排查后总结的结论,供后续开发维护参考,避免再次出现"多除/少乘 100"类问题。 + +--- + +## 一、核心规则速查 + +| 标的类型 | 录入存库 | 入库基准 | 展示规则 | +|---------|---------|---------|---------| +| **普通债券类收益互换**(债券标的,如 180205.IB) | 用户输入值 ÷ 100 存库(**相对价**) | 相对价,如 `1.02`(对应输入 102) | 相对价 × 100 展示,如 `102.00000%` | +| **普通收益互换**(非债券标的,如 ETF、商品) | 用户输入值原样存库 | 绝对价,如 `100`(对应输入 100) | 原样展示,如 `100.00000` | + +**一句话**:债券类的价格,库里存"相对价(÷100)",界面显示时"×100 还原"。普通非债券类输入什么就存什么、显示什么。 + +--- + +## 二、为什么债券类要 ÷100 / ×100 + +债券价格习惯用"面值的百分比"表达(全价 102 元 = 面值 100 的 102%)。 +为了与其他利率/收益率类字段在库里保持"小数形式(1.02)"统一,录入时由 `vue-number-input` 组件自动 ÷100 转成相对价存库;展示时再 ×100 还原成用户习惯的百分比形式。 + +--- + +## 三、关键字段与所在表 + +互换涉及的价格字段命名容易混淆(有的字段名是"期末语义"但实际装的是"期初值"),现梳理如下: + +### swap_position 表(期初持仓,`IsInitial=1`) + +| 字段 | 含义 | 备注 | +|------|------|------| +| `PosiGrossPrice` | 期初全价(不含费) | 相对价基准(债券为 1.02 形式) | +| `PosiNetPrice` | 期初净价(含费) | 相对价基准 | +| `PosiNetNoFeePrice` | 成交净价(不含费),**可空** | 收益结算页"期初净价"列取此字段;为空时需回退到 `PosiNetPrice` | +| `PosiNetFeePrice` | 成交净价(含费),**可空** | | + +### swap_flow_event 表(互换/平仓/自动互换事件) + +| 字段 | 含义 | 是否入库 | 备注 | +|------|------|---------|------| +| `TradingAmountAvg` | 成交均价(期末全价) | ✅ 入库 | 债券类存相对价 | +| `TradingAmountNetAvg` | 成交净价(不含费) | ✅ 入库(可空) | 字段名像"期末",但收益结算初始化时装的是**期初净价** | +| `TradingAmountNetFeeAvg` | 成交净价(含费) | ✅ 入库 | | +| `TradingAmount` | 成交金额 | ✅ 入库 | = `TradingAmountAvg × Quantity × ContractSize`(后端计算) | +| `PosiGrossPrice` | 期初全价 | ❌ `[NotMapped]` 不入库 | 运行时从 `swap_position` 拷贝 | +| `PosiNetPrice` | 期初净价 | ❌ `[NotMapped]` 不入库 | 运行时从 `swap_position` 拷贝 | +| `UnderlyingInstrumentType` | 标的类型 | ✅ 入库 | **展示时判断是否债券(决定 ×100)的关键字段,生成事件时必须赋值** | + +> ⚠️ `PosiGrossPrice` / `PosiNetPrice` 在 `swap_flow_event` 里是 `[NotMapped]`,**数据库查不到这两列**,只在运行时从 `swap_position` 拷贝。所以"期初价"展示的实际数据源是 `swap_position`,不是 `swap_flow_event`。 + +### eod_swap_position 表(日终持仓) + +`PosiDividendSum`(浮动端未实现分红收益)≠ 0 时,会触发分红型自动互换(`DealDividends`)。 + +--- + +## 四、各环节的代码实现位置 + +### 1. 录入(TradeEdit.cshtml + swapTradeEdit.js) + +债券类与普通类用**不同的输入格式器**(只是配置开关),真正的 ÷100 转换在 `FastVue.parseNumber` 里执行: + +```html + + + + + +``` + +格式器定义(swapTradeEdit.js,仅配置开关): +- `inputFormatMarginRateNoPercent`:`percent: true`(注意:名字里的 "NoPercent" 指"显示时不带%号",而非"不做换算",容易误解) +- `inputFormatTradeSinglePrice`:`percent: false` + +**÷100 的真正实现**(`statics/bundles/bundle.js` 的 `FastVue.parseNumber`): +```js +FastVue.parseNumber = function (val, percent) { + // ... + let isPercent = val.endsWith('%') || percent == true; // format.percent=true 时 + let number = new Number(val.replace(/%+$/, '')); + return isPercent ? number / 100 : number; // ← ÷100 在这里执行 +}; +``` +即:组件本身基于第三方库(打包进 bundle.js),但 `percent:true` 时 `number / 100` 的换算是项目自定义的 `FastVue.parseNumber` 实现的,不是第三方库原生行为。 + +### 2. 展示(TradeView.cshtml) + +互换 Tab(浮动收益端)对期初/期末价的处理,**依赖 `UnderlyingInstrumentType` 判断是否债券**: + +```cshtml +@{ + // 关键: multiplier 由标的类型决定, 债券=100, 非债券=1 + var multiplier = ConsGlobal.InstrumentType.IsBond(closeFloat.UnderlyingInstrumentType) ? 100 : 1; +} +@if (isBond) { + @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) + @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) +} else { + @(closeFloat.PosiGrossPrice.OtcFormat(OtcFormatFlag.umprice)) + @(closeFloat.TradingAmountAvg.OtcFormat(OtcFormatFlag.umprice)) +} +``` + +> 🔑 **`UnderlyingInstrumentType` 是展示层判断的关键**。所有生成 `swap_flow_event` 的地方都必须赋值这个字段,否则 `IsBond` 判断失败,债券标的会走非债券分支(不 ×100),导致显示成相对价(1.00 而非 100.00)。 + +### 3. 收益结算页(SwapIncome.cshtml + incomeSwapTrade.js) + +前端 `priceFormat()` 同样 `× multiplier`: + +```js +priceFormat(price) { + price = price * this.multiplier; // 债券类 ×100 + return otcformat.trading.umprice(price); +} +``` + +--- + +## 五、互换事件的生成路径与价格赋值 + +互换 Tab 会按时间序列展示所有互换事件(EventType=互换/自动互换),手动与自动用**同一套渲染代码**,所以入库基准必须一致。 + +### 手动互换(EventType=3) +- 入口:`SwapDealService.SwapUnwind` / `InitIncome` +- 价格赋值:`floatEvent.TradingAmountAvg = unwindPrice`(前端用户输入,已 ÷100) +- **已赋值 `UnderlyingInstrumentType`**(来自 position) + +### 自动互换-分红型(EventType=4)⚠️ 重点 +- 入口:`SwapEodPositionService.DealDividends`(`PosiDividendSum != 0` 触发) +- 价格赋值:`TradingAmountAvg = eodPosi.PosiNetPrice`(相对价,来自 eod 持仓) +- **必须赋值 `UnderlyingInstrumentType` 和 `TradingAmountNetAvg`**(曾经遗漏导致 Bug) + +### 自动互换-利息端(EventType=4) +- 入口:`DealAutoInterests` → `SaveAutoEodWithCloseInterestPosition` +- `UnderlyingCode` 为空(利息腿),不出现在互换 Tab 浮动端 + +--- + +## 六、已修复的 Bug 与教训 + +| Bug | 根因 | 修复提交 | +|-----|------|---------| +| 收益结算期末价缩放/显示错误 | `incomeSwapTrade.js` initDeal 合并冲突丢 ×multiplier | 已修 | +| 收益结算期初净价显示 0 | `PosiNetNoFeePrice` 可空,为 0 时未回退 `PosiNetPrice` | 已修 | +| 收益结算期末价不可编辑/无刷新图标 | `ab09cbc9` 误删输入框与方法 | 已修 | +| **分红型自动互换显示未 ×100** | `DealDividends` 漏赋 `UnderlyingInstrumentType` | 已修(d9c5ce4c)| +| **分红型自动互换期初净价显示 0** | `DealDividends` 漏赋 `TradingAmountNetAvg` | 已修(d9c5ce4c)| + +### 教训 + +1. **生成 `swap_flow_event` 时必须赋全字段**,尤其是 `UnderlyingInstrumentType`——它是展示层判断债券/非债券(决定 ×100)的关键,遗漏会导致显示数量级错误。 +2. **手动互换与自动互换共用同一套展示代码**,入库基准(相对价)必须保持一致。 +3. **字段命名误导**:`TradingAmountNetAvg` 名字像"期末",但收益结算初始化时装的是期初净价,改代码时务必核对实际数据流向。 +4. **可空字段要兜底**:`PosiNetNoFeePrice`、`TradingAmountNetAvg` 可空,展示时为 0/空要有回退值。 + +--- + +## 七、历史脏数据说明 + +排查过程中发现,测试环境里同一笔手动互换的 `swap_flow_event` 可能存在多条记录,`TradingAmountAvg` 取值不一致(有的相对价如 1.0011,有的被 ÷100 如 0.010011),成交金额相应相差 100 倍。这是早期代码版本更迭残留的脏数据。 + +判断脏数据的依据:成交金额 `TradingAmount = TradingAmountAvg × Quantity × ContractSize`(后端 `SwapDealService:1103` 计算)。若同一交易的两条互换记录成交金额相差正好 100 倍,则其中一条的 `TradingAmountAvg` 被多 ÷100。 + +这类历史数据问题用 SQL 单独修复,**不应在代码里迁就脏数据而保留错误的逻辑**。排查与修复 SQL 见文末附录。 + +--- + +## 附录 A:排查工具箱 SQL + +> 以下 SQL 是 2026-06 价格显示 Bug 排查过程中验证有效的查询,数据库为 MySQL,库名 `glms_yltrs_ylcms`(ylcms 连接)。后续遇到类似"显示数量级不对"问题可直接复用。 + +### A.1 查互换交易期初持仓的 4 个价格字段(确认入库基准) + +```sql +SELECT + t.TradeNumber AS trade_no, + t.StructureType AS 结构类型, + sp.UnderlyingCode AS 标的代码, + sp.PosiGrossPrice AS 期初全价, + sp.PosiNetPrice AS 期初净价, + sp.PosiNetNoFeePrice AS 期初净价不含费, + sp.PosiNetFeePrice AS 期初净价含费 +FROM trade t +JOIN swap_position sp ON sp.SwapTradeId = t.id + AND sp.IsInitial = 1 + AND sp.UnderlyingCode IS NOT NULL -- 过滤利息腿/预付金腿 +WHERE t.TradeType = '收益互换' + AND t.StructureType IN ('普通债券类收益互换', '普通收益互换') + AND t.ValidState = 'valid' +ORDER BY t.id DESC +LIMIT 20; +``` + +### A.2 查互换/自动互换事件入库值(区分手动/自动、用成交金额反推正确基准) + +⚠️ `swap_flow_event` 表的 `PosiGrossPrice`/`PosiNetPrice` 是 `[NotMapped]`,**数据库查不到这两列**;只能查真正入库的 `TradingAmountAvg`、`TradingAmountNetAvg`、`TradingAmount`。 + +```sql +SELECT + t.TradeNumber AS trade_no, + fe.id AS 事件ID, + fe.EventType AS 事件类型, + CASE fe.EventType WHEN 3 THEN '手动互换' WHEN 4 THEN '自动互换' END AS 类型, + fe.EventReason AS 事件原因, + fe.EventDate AS 业务日期, + fe.UnderlyingCode AS 标的, + fe.PayDirection AS 收支方向, + fe.PositionType AS 多空方向, + fe.UnderlyingInstrumentType AS 标的类型_判断债券用, + fe.TradingAmountAvg AS 期末全价_入库, + fe.TradingAmountNetAvg AS 期末净价_入库, + fe.TradingAmount AS 成交金额, + fe.DividendIn AS 分红金额 +FROM trade t +JOIN swap_flow_event fe ON fe.SwapTradeId = t.id +WHERE t.id IN (/*填trade id*/) + AND t.ValidState = 'valid' + AND fe.EventType IN (3, 4) + AND fe.UnderlyingCode IS NOT NULL -- 只要浮动端事件,排除利息腿 +ORDER BY t.id, fe.EventDate DESC, fe.id DESC +LIMIT 30; +``` + +**判断要点**: +- `UnderlyingInstrumentType` 为空 → 该事件的展示会走"非债券分支(不×100)",债券标的显示会错(这就是自动互换的 Bug 根因)。 +- 同一交易同一腿出现 `TradingAmountAvg` 相差 100 倍的两条记录 → 其中一条是被多 ÷100 的脏数据。 + +### A.3 查分红型自动互换的数据源(eod_swap_position,确认 PosiDividendSum 触发条件) + +`PosiDividendSum != 0` 时触发 `DealDividends` 生成自动互换事件。注意测试数据可能为 0 查不到,需去掉该条件排查。 + +```sql +SELECT + esp.SwapTradeId AS trade_id, + esp.ValueDate AS 持仓日期, + esp.UnderlyingCode AS 标的代码, + esp.UnderlyingInstrumentType AS 标的类型, + esp.PosiDividendSum AS 浮动端未实现分红_触发条件, + esp.TdPosiDividend AS 当日浮动端分红, + esp.PosiNetPrice AS 持仓净价_自动互换TradingAmountAvg来源, + esp.PosiGrossPrice AS 持仓全价, + esp.PosiNetNoFeePrice AS 持仓净价不含费_期初净价来源 +FROM eod_swap_position esp +WHERE esp.SwapTradeId IN (/*填trade id*/) +ORDER BY esp.ValueDate DESC +LIMIT 20; +``` + +### A.4 查中债估值数据源(确认录入时默认读取的价格) + +簿记交易时标的默认价格来自 `china_bond_valuation`(中债估值)表。⚠️ **该表的字段命名与业务常用名差异很大**,特别注意:债券代码列是 `bond_id`(不是 UnderlyingCode),价格列是 `dirty_price_close`(日终估值全价)、`net_price`(估价净值)、`yield`(收益率%)、`vobp`(基点价值)。 + +```sql +SELECT + cbv.bond_id AS 债券代码, + cbv.valuation_date AS 估值日期, + cbv.dirty_price_close AS 日终估值全价, -- 录入默认读取的"全价"来源 + cbv.net_price AS 估价净值, + cbv.yield AS 估价收益率_pct, + cbv.vobp AS 估价基点价值, -- DV01 计算用此字段 + cbv.acc_rint_close AS 日终应计利息, + cbv.credibility AS 可信度 -- GetBondPrice 按可信度降序取最新 +FROM china_bond_valuation cbv +WHERE cbv.bond_id = '180205.IB' +ORDER BY cbv.credibility DESC, cbv.valuation_date DESC +LIMIT 10; +``` + +> 后端 `EodPriceQueryService.GetBondPrice` 取数逻辑:按 `bond_id` 匹配,`valuation_date <= 指定日`,按 `credibility` 降序、`valuation_date` 降序取第一条。返回的 `ClosePrice = dirty_price_close × bondPriceMultiple`(默认乘数通常为 1)。测试环境可能被维护成离谱值(如全价 1999)。 + +### A.5 历史脏数据修复(手动互换被多 ÷100 的记录) + +⚠️ **执行前务必先用 A.2 核对,并备份**。以下仅作为模板,需根据实际范围调整 WHERE: + +```sql +-- 备份 +CREATE TABLE swap_flow_event_bak_yyyymmdd AS +SELECT * FROM swap_flow_event WHERE /*同下方WHERE条件*/; + +-- 修复: 被多÷100的TradingAmountAvg ×100还原, 成交金额同步×100 +UPDATE swap_flow_event fe +JOIN trade t ON t.id = fe.SwapTradeId +SET fe.TradingAmountAvg = fe.TradingAmountAvg * 100, + fe.TradingAmount = fe.TradingAmount * 100 +WHERE t.StructureType = '普通债券类收益互换' + AND fe.EventType = 3 -- 手动互换 + AND fe.UnderlyingCode IS NOT NULL + AND fe.TradingAmountAvg > 0 + AND fe.TradingAmountAvg < 0.5; -- 相对价合理范围判断(债券相对价通常0.5~2) +``` + +--- + +## 附录 B:字段命名与数据流向速查 + +容易混淆的字段对应关系(排查时务必核对实际数据流向,不要被字段名误导): + +| 界面列(表头) | 取值字段 | 字段所在表 | 实际数据来源 | 备注 | +|---|---|---|---|---| +| 期初标的交割全价% | `PosiGrossPrice` | swap_position(入库)/ swap_flow_event([NotMapped]运行时拷贝) | `swap_position.PosiGrossPrice` | 相对价,展示×100 | +| 期初标的交割净价% | `TradingAmountNetAvg` | swap_flow_event(入库) | `swap_position.PosiNetNoFeePrice`(收益结算初始化时) | **字段名像期末,实装期初值**;可空需兜底 | +| 期末标的交割全价% | `TradingAmountAvg` | swap_flow_event(入库) | 手动互换=用户输入;自动互换=`eod.PosiNetPrice` | 相对价,展示×100 | +| 标的类型判断 | `UnderlyingInstrumentType` | swap_flow_event(入库) | 生成事件时赋值 | **展示层判断债券(×100)的关键,必须赋值** | +| 成交金额 | `TradingAmount` | swap_flow_event(入库) | `TradingAmountAvg × Quantity × ContractSize` | 反推正确基准的依据 | + +**关键提醒**:`swap_flow_event.PosiGrossPrice` 和 `PosiNetPrice` 是 `[NotMapped]`,数据库**没有这两列**,查库会报 `Unknown column`。它们只在运行时从 `swap_position` 拷贝过来用于展示。 diff --git a/项目文档/互换价格字段命名规范决策文档.md b/项目文档/互换价格字段命名规范决策文档.md new file mode 100644 index 00000000..79618e67 --- /dev/null +++ b/项目文档/互换价格字段命名规范决策文档.md @@ -0,0 +1,291 @@ +# 互换价格字段命名规范决策 + +> 本文档是**命名规范的决策与论证**,回答"应该叫什么、为什么这么叫"。 +> 现状梳理(字段流向、存储显示规则)见姊妹文档 [互换交易价格字段存储与显示规范.md](互换交易价格字段存储与显示规范.md)。 +> 适用范围:互换业务(收益互换)涉及的价格、方向、时点字段。期权/远期业务字段不在本文档范围。 + +--- + +## 一、为什么命名必须先定下来 + +现状是**同一概念多套名字、名字与含义颠倒**,已经反复造成 Bug。三条最严重的证据: + +1. **`PosiNetPrice` 名为"Net",实为"含费全价"**。真正的净价是 `PosiNetNoFeePrice`。连现状文档(《互换交易价格字段存储与显示规范》)都把它误标成"期初净价"——文档作者都被字段名骗了。 +2. **两套并行命名无对应关系**:持仓表 `PosiNetPrice` ↔ 流水表 `TradingAmountFeeAvg`,持仓表 `PosiGrossPrice` ↔ 流水表 `TradingAmountAvg`,名字里没有一处对得上。 +3. **前端同一中文标签绑不同字段**:`成交净价` 一处绑 `TradingAmountNet`,另一处绑 `TradingAmountNetAvg`;`期初标的价格` 录入页绑 `PosiGrossPrice`,平仓明细页绑 `PosiNetPrice`。 + +命名不定下来,后续每个改动都要反复核对数据流向,且极易引入新 Bug。本文档的目的是**把每个维度的命名一次性钉死,给出对比理由**。 + +--- + +## 二、三个维度的决策 + +互换价格有三个正交维度,必须分别命名: + +| 维度 | 取值 | 现状混乱 | +|---|---|---| +| **A. 价格种类** | 全价 / 净价 | Net/Gross 颠倒;Posi vs TradingAmount 两套 | +| **B. 是否含交易费** | 含费 / 不含费 | Fee 标记位置在两套体系里相反 | +| **C. 时点** | 期初 / 期末 / 盯市 | Start/Begin/End/Initial/Final/Spot/Open 散布,无统一规则 | + +外加一个**方向维度**(收取方/支付方),目前用中文枚举,trade_swap 老表用 Get/Pay。 + +下面逐项决策。 + +--- + +## 三、维度 A:价格种类 —— 采用 Clean / Dirty + +### 候选对比 + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Clean / Dirty`** | 债券行业标准术语;歧义最小;与中债估值表 `dirty_price_close`/`net_price`、量化库常见命名一致 | 非 OTDer 可能需要学习成本 | **✅ 推荐** | +| `Net / Gross`(现状持仓表) | 简短 | **本项目已颠倒**(PosiNetPrice 是含费全价);Net/Gross 在金融里本身就有"净值/毛额"歧义,不专指债券净价全价 | ❌ 已证明不可行 | +| `Clean / Full` | Full 含义尚可 | 不如 Dirty 标准;中债表用 dirty,不统一 | △ | +| `净价 / 全价`(中文直译) | 业务人员熟悉 | C# 字段用中文不合规;与现有英文体系混搭更乱 | ❌ | + +### 决策 + +**全价用 `Dirty`,净价用 `Clean`。** + +理由: +1. **行业术语,零歧义**。Clean/Dirty 是债券领域全世界通用的无歧义术语(clean price = 不含应计利息,dirty price = 含应计利息)。 +2. **与数据源一致**。录入默认价格来源 `china_bond_valuation` 表用的就是 `dirty_price_close`(日终估值全价)和 `net_price`(估价净值)。字段命名与数据源对齐,减少心智负担。 +3. **现状 Net/Gross 已被证明失败**。它在本项目里语义颠倒,且 Net 在不同字段里含义不同(PosiNetPrice 的 Net = 含费,PosiNetNoFeePrice 的 Net = 净价),是混乱根源,必须废弃。 + +### 与"费"维度组合后的完整 4 象限见第五节。 + +--- + +## 四、维度 B:费用 —— 采用 Fee / NoFee + +### 先澄清:"费"是什么 + +**"费" = 交易费用 / 佣金(TradingFee)**,由 `TradeFeeHelper.CalcPriceWithFee(price, fee, qty)` 计算:`含费价 = 不含费价 + 单位佣金`。它**不是**应计利息(应计利息体现在 Clean/Dirty 维度),也**不是**利息腿费用。 + +### 候选对比 + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Fee / NoFee`(后缀)** | 现状已在用(PosiNetNoFeePrice/TradingAmountFeeAvg);直白 | — | **✅ 推荐**(沿用现有约定,零迁移成本) | +| `Gross / Net`(用 Net 表示含费) | — | 与维度 A 的 Clean/Dirty 严重冲突,两套词打架 | ❌ | +| `WithFee / WithoutFee` | 更明确 | 太长;后缀冗余 | △ | +| `PreFee / PostFee` | 金融衍生品常见 | 偏衍生品语境,互换收益端用着别扭 | △ | + +### 决策 + +**含费用 `Fee` 后缀,不含费用 `NoFee` 后缀。** + +理由:现状流水表已经是这套(`TradingAmountAvg` 不含费 / `TradingAmountFeeAvg` 含费 / `TradingAmountNetAvg` 净价不含费 / `TradingAmountNetFeeAvg` 净价含费),**这个约定本身没问题**,问题是持仓表用了另一套颠倒的 Posi 命名。统一到 Fee/NoFee 即可。 + +--- + +## 五、维度 A+B 组合:价格 4 象限最终命名 + +### 决策表 + +| 业务含义 | **规范命名** | 现状持仓表 | 现状流水表 | +|---|---|---|---| +| 全价 · 不含费 | `XxxDirtyPrice` | `PosiGrossPrice` | `TradingAmountAvg` | +| 全价 · 含费 | `XxxDirtyFeePrice` | `PosiNetPrice` ⚠️ | `TradingAmountFeeAvg` | +| 净价 · 不含费 | `XxxCleanPrice` | `PosiNetNoFeePrice` | `TradingAmountNetAvg` | +| 净价 · 含费 | `XxxCleanFeePrice` | `PosiNetFeePrice` | `TradingAmountNetFeeAvg` | + +其中 `Xxx` 是时点前缀(见第六节)。 + +### 命名规则一句话 + +> **`{时点}{Dirty|Clean}{Fee|}{Price}`** —— Dirty/Clean 选价格种类,Fee 出现表示含费、不出现表示不含费。 + +例:`EntryDirtyPrice`(期初全价不含费)、`EntryDirtyFeePrice`(期初全价含费)、`ExitCleanPrice`(期末净价不含费)。 + +--- + +## 六、维度 C:时点 —— 采用 Entry / Exit(+ Market) + +这是你重点要求对比的部分。先看现状,再逐词对比。 + +### 现状:时点词散布(统计自实体字段) + +| 现状词 | 出现次数(实体字段) | 用在什么字段 | +|---|---|---| +| `Start` | 36 | StartDate(起始日) | +| `Spot` | 28+4 | SpotPrice(期初价,期权/远期)、GetSpotPrice/PaySpotPrice(trade_swap) | +| `Open` | 3 | OpenPrice、OpenTradePrice(开仓价) | +| `Init/Initial` | 4 | InitYtm、InitialSpotPrice([NotMapped]) | +| `Final` | 10 | FinalPrice(期末价)、GetFinalPrice/PayFinalPrice | +| `End` | 12 | EndDate(结束日) | +| `Close` | 21 | ClosePrice(收盘价,行情侧) | +| `Maturity` | 14 | MaturityDate(到期日) | +| `Entry/Exit` | **0** | 未使用 | + +**现状问题**:表达"期初价"用了 `Spot`、`Open`、`Init`、`Initial` 四种;表达"期末价"用了 `Final`、`End`、`Close` 三种;且 `Close` 还和"行情收盘价"语义重叠。 + +### 候选词逐个对比 + +#### 期初价(开仓成本价)候选 + +| 方案 | 含义直觉 | 优点 | 缺点 | 评价 | +|---|---|---|---|---| +| **`Entry`** | 入场/建仓 | 交易术语,明确"进入持仓那一刻";与 `Exit` 对称;**不与现有任何日期/价格词冲突** | 全新词,需迁移 | **✅ 推荐** | +| `Start` | 开始 | 与 `StartDate`(起始日)一致,延续性最好 | **与日期混淆**:`StartPrice` 容易被误读成"起始日当天的价格"而非"建仓成本价";价格 vs 日期语义纠缠 | △ | +| `Begin` | 开始 | 同 Start | 同 Start;且与 Start 二选一造成二次混乱(现已有 Start,再加 Begin 是雪上加霜) | ❌ | +| `Initial` / `Init` | 初始 | 语义尚可 | `Init` 是缩写不规整;现状 `InitYtm` 已用,但 `InitialPrice` 太长;**Init/Initial 两种并存**(现状就有 `InitYtm` 和 `InitialSpotPrice`)本身就是混乱证据 | ❌ | +| `Spot` | 即期 | 现状 trade_swap 在用 | **语义错误**:Spot 在金融里指"即期(spot vs forward)",不是"期初";互换里强行用 Spot 表示期初是误用;且与"即期价格"概念冲突 | ❌ | +| `Open` | 开仓 | 有"开仓"意味 | `OpenPrice` 在期权语境指"开盘价"(open=开盘 vs close=收盘),互换用 Open 会与行情术语打架 | ❌ | + +#### 期末价(平仓/到期价)候选 + +| 方案 | 含义直觉 | 优点 | 缺点 | 评价 | +|---|---|---|---|---| +| **`Exit`** | 出场/了结 | 与 Entry 对称;明确"退出持仓那一刻";**不与 Close/Maturity 冲突** | 全新词,需迁移 | **✅ 推荐** | +| `Final` | 最终 | 现状在用(FinalPrice) | `Final` 在期权语境常指"到期(final/expiration)",与互换"平仓了结"语义有偏差;且 Final 给人"永远结束"的感觉,但互换可能多次部分平仓(每次都是 Exit) | △ | +| `End` | 结束 | 简短 | 与 `EndDate`(结束日)混淆,同 Start 的问题 | ❌ | +| `Close` | 收盘 | 行情侧在用(ClosePrice) | **语义已被占用**:Close = 收盘行情价;互换平仓价用 Close 会和"每日盯市收盘价"混淆。这是现状混乱之一 | ❌ | +| `Maturity` | 到期 | 债券标准术语 | 只适用于"持有到期",不适用于"提前平仓";互换平仓≠到期 | ❌(仅 MaturityDate 用于到期日) | + +### 决策 + +**期初用 `Entry`,期末用 `Exit`,每日盯市用 `Market`。** + +### 为什么是 Entry/Exit 而不是其他 —— 三条核心理由 + +**理由 1:Entry/Exit 是唯一一组"互相对称、且不与现有词冲突"的方案。** + +| 对称组 | 冲突情况 | +|---|---| +| **Entry / Exit** | ✅ 零冲突,现有代码 0 处使用 | +| Start / End | ❌ 与 StartDate/EndDate 日期字段冲突 | +| Open / Close | ❌ 与行情开盘价/收盘价冲突 | +| Initial / Final | △ Initial 太长,Final 偏"到期"语义 | +| Begin / End | ❌ 与 Start 重复造词 | + +互换价格的核心心智模型是"**Entry(建仓成本)vs Exit(了结价格)**"——盈亏 = Exit − Entry。Entry/Exit 直接表达这个对偶关系,其他词都带着别的语义包袱。 + +**理由 2:互换"多次部分平仓"语义下,Entry/Exit 比 Start/End/Final 更准确。** + +互换持仓期间可以有多次部分平仓(每次部分平仓都是一次 Exit),不是"一次性 Start→End"。`Final`(最终)暗示"只有最后一次",`Start`/`End` 暗示"单一起止点",都不符合"多次 Exit"的现实。`Exit` 天然支持"多次出场"语义(每次平仓 = 一次 Exit)。 + +**理由 3:与"盯市价"清晰分层。** + +| 时点 | 命名 | 含义 | +|---|---|---| +| 建仓时 | `Entry*` | 成本价(固定) | +| 平仓时 | `Exit*` | 了结成交价(每次平仓一个) | +| 持仓期间每日 | `Market*` | 盯市价(随行情变动) | + +三者职责清晰:Entry 是成本基线,Exit 是实现盈亏的时点,Market 是浮动盈亏的基准。现状 `UnderlyingPrice`(盯市价)改为 `MarketDirtyPrice` 后,与 Entry/Exit 形成 `Entry / Market / Exit` 完整时点链。 + +### 收益率字段同理 + +| 含义 | 规范命名 | 现状 | +|---|---|---| +| 期初 YTM | `EntryYtm` | InitYtm | +| 平仓 YTM | `ExitYtm` | ytm / Ytm | + +--- + +## 七、维度 D:方向 —— 收取/支付方的命名 + +### 现状 + +| 位置 | 现状命名 | 问题 | +|---|---|---| +| `SwapDirectionEnum`(V2 在用) | `收取 / 支付`(中文) | C# 字段用中文不合规;与英文代码混排突兀 | +| `trade_swap`(老表,已弃用互换主流程) | `Get* / Pay*`(如 GetSpotPrice/PaySpotPrice) | Get 语义弱,不像金融术语;你已指出这是老互换遗留 | +| `PositionTypeFlag`(多空) | `Long / Short` | ✅ 这个没问题,标准术语 | +| 业务文档/前端 | "收取方/支付方"、"固定端/浮动端" | 中文展示 OK | + +### 候选对比(英文方向词) + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Rcv / Pay`** | 金融工程标准(IRS 收付固定方叫 Receive/Pay);FINCAD/Bloomberg/量化库通用;3字母缩写简洁 | Rcv 需要新人熟悉"是 receive 缩写" | **✅ 推荐** | +| `Receive / Pay` | 不缩写,更明确 | Receive 较长,字段名 `ReceiveDirtyPrice` 偏长 | △ | +| `Get / Pay`(现状老表) | 现状 | Get 不是金融术语,语义弱("获得"太泛);与 Pay 不对称(Pay 是金融词,Get 是口语) | ❌ | +| `Long / Short` | 标准术语 | **语义不同**:Long/Short 是多头/空头(PositionType),不是收取/支付方(SwapDirection);互换里收取方可能是空头。**两者不能混用** | ❌(维度不同) | +| `Buy / Sell` | 通用 | 互换不是买卖关系,是交换现金流;Buy/Sell 易误导 | ❌ | + +### 决策 + +**英文方向词用 `Rcv / Pay`,仅用于代码字段名(如 `RcvEntryDirtyPrice`);枚举值和前端展示保持中文"收取/支付"。** + +理由: +1. **金融标准**。利率互换(IRS)行业里固定方/浮动方标准叫法就是 Receive fixed / Pay fixed,FINCAD、Bloomberg、主流量化库都用 Rcv/Pay。 +2. **纠正老表 Get 的错误**。trade_swap 的 `Get/Pay` 里 Get 不是金融词,是早期随手起的,正好借命名规范统一为 Rcv/Pay。 +3. **不与 Long/Short 冲突**。Rcv/Pay 是 SwapDirection(收取/支付哪一端现金流),Long/Short 是 PositionType(多空),两个维度独立,不能互相替代。 +4. **中文枚举保留**。`SwapDirectionEnum.收取/支付` 已被业务文档、前端、监管报表广泛使用,强行改英文枚举值成本高且无收益。只在新英文字段名里用 Rcv/Pay。 + +> 注:当前 V2 互换主流程(swap_position/eod_swap_position)已经不按"收取/支付腿"存价格(价格存在不分腿的持仓记录里,方向用 `PosiDirection` 字段标记),所以 `Rcv/Pay` 前缀**只在需要显式区分双腿的场景**(如 trade_swap 录入、双边互换)使用,不是所有字段都加。 + +--- + +## 八、最终命名总表(一锤定音) + +### 8.1 价格 4 象限(含时点前缀) + +| 业务含义 | 规范命名 | 现状(持仓/流水) | +|---|---|---| +| 期初全价·不含费 | `EntryDirtyPrice` | PosiGrossPrice / — | +| 期初全价·含费 | `EntryDirtyFeePrice` | PosiNetPrice ⚠️ / — | +| 期初净价·不含费 | `EntryCleanPrice` | PosiNetNoFeePrice / — | +| 期初净价·含费 | `EntryCleanFeePrice` | PosiNetFeePrice / — | +| 期末/平仓全价·不含费 | `ExitDirtyPrice` | — / TradingAmountAvg | +| 期末/平仓全价·含费 | `ExitDirtyFeePrice` | — / TradingAmountFeeAvg | +| 期末/平仓净价·不含费 | `ExitCleanPrice` | — / TradingAmountNetAvg | +| 期末/平仓净价·含费 | `ExitCleanFeePrice` | — / TradingAmountNetFeeAvg | +| 盯市全价 | `MarketDirtyPrice` | UnderlyingPrice | +| 期初收益率 | `EntryYtm` | InitYtm | +| 期末收益率 | `ExitYtm` | ytm | + +### 8.2 方向 + +| 业务含义 | 代码字段名 | 枚举/展示值 | +|---|---|---| +| 收取方 | `Rcv*`(如需区分腿) | 收取(SwapDirectionEnum.收取) | +| 支付方 | `Pay*`(如需区分腿) | 支付(SwapDirectionEnum.支付) | +| 多头 | `Long`(PositionTypeFlag.Long) | 多头 | +| 空头 | `Short`(PositionTypeFlag.Short) | 空头 | + +### 8.3 命名规则速记 + +> **价格 = `{Entry|Exit|Market}{Dirty|Clean}{Fee|}Price`** +> **方向 = `Rcv|Pay`(SwapDirection)与 `Long|Short`(PositionType)是两个独立维度,不混用** +> **收益率 = `{Entry|Exit}Ytm`** + +--- + +## 九、落地策略(渐进式,不一次性改) + +直接全局重命名风险极高(DB 列 + EF 实体 + ~15 服务类 + ~20 前端文件 + 交易确认书 + Excel 导入)。建议分阶段: + +| 阶段 | 动作 | 风险 | 产出 | +|---|---|---|---| +| **0. 文档定调(本文档)** | 命名规则钉死,团队达成共识 | 零 | 本文档 | +| **1. 注释止血** | 在现状最混乱的字段(PosiNetPrice/PosiGrossPrice)加 `[Obsolete]` + XML 注释,写明真实含义与规范名 | 零 | 防止再被字段名误导 | +| **2. 前端统一取值封装** | 新增 `swapPriceHelper.js`,把分散的 `item.PosiGrossPrice` 等收敛成 `getEntryDirtyPrice(item)`;先修"同标签绑不同字段"的混乱 | 低 | 前端混乱点消除 | +| **3. 新代码强制规范** | 新增字段/新功能必须用规范命名;对外 API 新增 DTO 用规范名,内部旧字段靠 Mapper 转换 | 低 | 增量规范化 | +| **4. 大重构(慎重)** | DB 列重命名 + EF 映射 + 全量替换,配合数据迁移。建议趁大版本升级做 | 高 | 彻底统一 | + +**阶段 1 最该立刻做**:给 `PosiNetPrice`(含费全价,非净价)和 `PosiGrossPrice`(不含费全价)加注释,因为连现状文档都被它们误导过。 + +--- + +## 附录:现状混乱全证据清单(决策依据) + +> 以下每条都有代码/DDL/前端文件佐证,是上述决策的事实基础。 + +| # | 混乱点 | 关键证据位置 | +|---|---|---| +| 1 | PosiNetPrice 名为 Net 实为含费全价 | `SwapTradeService.cs:372-373` 赋值:PosiNetPrice←TradingAmountFeeAvg(含费均价) | +| 2 | PosiGrossPrice 名为 Gross 实为不含费全价 | 同上:PosiGrossPrice←TradingAmountAvg(不含费均价) | +| 3 | 同字段两表 DDL 注释矛盾 | PosiNetPrice:swap_position 注释"持仓净价",eod_swap_position 注释"期初标的价格" | +| 4 | 持仓表/流水表两套命名无对应 | Posi*(Net/Gross)vs TradingAmount*(Avg/FeeAvg),名字对不上 | +| 5 | 前端同标签绑不同字段 | `成交净价` 绑 TradingAmountNet(303) 也绑 TradingAmountNetAvg(508),见 SwapflowList.js | +| 6 | 作者自曝字段语义错位 | `SwapIncome.cshtml:155`、`SwapDealService.cs:279` 注释:"TradingAmountNetAvg 字段名为期末语义,实装期初值" | +| 7 | trade_swap 第三套命名 | GetSpotPrice/PaySpotPrice/GetFinalPrice/PayFinalPrice,与 Posi* 平行无对应 | +| 8 | 时点词散布 | 期初价用了 Spot/Open/Init/Initial 四种;期末价用 Final/End/Close 三种 | +| 9 | Close 语义被占用 | ClosePrice=行情收盘价 vs 互换平仓价混用 | +| 10 | Get 不是金融术语 | trade_swap 的 Get/Pay,Get 语义弱,与 Pay 不对称 | +| 11 | 现状文档自身被误导 | 《互换交易价格字段存储与显示规范》把 PosiNetPrice 标为"期初净价",实为含费全价 | diff --git a/项目文档/互换分红损益字段语义与重复计算分析.md b/项目文档/互换分红损益字段语义与重复计算分析.md new file mode 100644 index 00000000..7509ee27 --- /dev/null +++ b/项目文档/互换分红损益字段语义与重复计算分析.md @@ -0,0 +1,355 @@ +# 互换分红损益:字段语义与重复计算分析 + +> 本文档分析互换(收益互换)"分红/付息损益"核算链路中存在的字段语义混乱与分红重复计算问题。 +> 用真实交易数据佐证,供团队对齐字段语义、确定修复方案。 +> 成文于 2026-06,排查范围覆盖 `f9d8a256`(山证基线)至 `1c9b28ff`(最新)。 + +--- + +## 一、问题概述 + +互换的"已实现盈亏"在多个代码路径下**会把同一笔分红计算恰好 2 次**,导致已实现盈亏虚高(分红翻倍)。 + +根本原因:**`MarkClosePnl`(盯市盈亏)字段把"价差盈亏"和"分红"揉在了一起**,而分红又有独立的字段 `DividendIn`,导致同一笔分红在两个字段里各存了一份,汇总时被重复累加。 + +> 经过逐行确认代码路径 + 真实数据验证,**确定**是 2 次(非 2~3 次)。证明见第三、四章。 + +--- + +## 二、字段分类:客观源头 vs 派生计算 + +理解整块的关键是分清"客观源头"(不可改的事实)和"派生计算"(基于源头算出来的)。分三层: + +### 第一层:客观源头(事实,所有计算的基石) + +| 字段 | 所在表 | 来源 | 含义 | 示例值 | +|------|--------|------|------|--------| +| `payment_interest` | `bond_payment` | 外部付息公告 | 每张面值付息额(如每100元付3元)| 3 | +| `PosiQuantity` | `swap_position` | 用户录入/中债估值 | 持仓数量 | 1000万 | +| `PosiGrossPrice` | `swap_position` | 用户录入/中债估值 | 期初全价(相对价) | 1.00 | +| `TradingAmountAvg` | `swap_flow_event` | 用户互换/平仓时输入 | 期末全价(相对价) | 1.00 | + +> 这些是"真实发生的事",不随计算方式改变。 + +### `totalInterest` 的来源(payment_interest → totalInterest → DividendIn 链路) + +`totalInterest` **不是** `payment_interest` 本身,而是经过换算的"每张分红率(小数)"。链路: + +``` +payment_interest (bond_payment, 每张付息额, 如 3) + │ GetBondPaymentService 在区间内求和 + ▼ +Σ payment_interest (如 3+3+3=9) + │ × 0.01 (从"每100元"转成"小数比率") + ▼ +0.09 + │ ÷ (1+税) × (1-税) (增值税调整, 与 EOD 口径一致) + ▼ +totalInterest ≈ 0.077 (每张面值的分红率, 小数) + │ 前端 getDivindIn: PosiQuantity × totalInterest × 方向 + ▼ +DividendIn = 1000万 × 0.077 × 方向 = -770000 (本次互换的分红金额) +``` + +代码位置:后端 `BondPaymentController.GetBondPayMentInterest`(`BondPaymentController.cs:91-100`),前端 `getDivindIn`(`incomeSwapTrade.js:197` / `unwindSwapTrade.js:286`)。 + +### 第二层:一次计算(基于源头直接算) + +| 字段/中间值 | 所在表 | 算法 | 示例 | +|------------|--------|------|------| +| `DividendIn` | `swap_flow_event` | `PosiQuantity × totalInterest × 方向`(后端算每张率,前端算金额)| -9148.25 | +| 价差盈亏(临时值) | 无独立字段 | `CloseNotionalValue × (期末价 - 期初价) × 方向` | 0(期末=期初时)| +| `TradingFee` | `swap_flow_event` | 用户填的费用 | 0 | + +### 第三层:入库字段(⚠️ 问题所在) + +| 字段 | 所在表 | 算法 | ⚠️ 问题 | +|------|--------|------|---------| +| **`MarkClosePnl`** | `swap_flow_event` | `价差盈亏 + 交易费 + DividendIn` | **把分红揉进来了!** | +| `DividendIn` | `swap_flow_event` | 直接存第二层的值 | 和上面 MarkClosePnl 里的分红是**同一笔** | + +**核心矛盾**:同一笔分红(如 -9148.25)同时存在 `MarkClosePnl` 和 `DividendIn` 两个字段里。 + +--- + +## 三、MarkClosePnl 含分红:四个场景对照表(代码 + 数据佐证) + +`MarkClosePnl` 在四个场景下的计算来源不同,但**全部含分红**: + +| 场景 | EventType | MarkClosePnl 计算位置 | 算法 | 含分红 | +|------|-----------|---------------------|------|--------| +| 手动互换 | 3 | 前端 `calcFloatClosePnl:140`(后端 `SwapIncome` 直接存)| `价差 + 费 + DividendIn` | **含** | +| 手动平仓 | 2 | 前端 `calcFloatClosePnl`(后端 `SwapUnwind` 直接存)| `价差 + 费 + DividendIn` | **含** | +| 自动互换(分红型) | 4 | 后端 `DealDividends:463` | `= PosiDividendSum`(分红本身)| **含**(就是分红本身)| +| 自动全平仓 | 2 | 后端 `AuotoSwapUnwind:1102`(`SwapDealService` 类)| `价差 + 费 + DividendIn` | **含** | + +> 注意:后端 `SwapDealService:1102` 在 `AuotoSwapUnwind` 方法内,仅被 `SwapTradeAutoService` 调用(自动全平仓,如到期自动平仓)。**手动互换/平仓的 MarkClosePnl 由前端算好后传入,后端直接存库不重算。** + +### 真实数据佐证(2026-06 查询实测) + +查询 swap_flow_event,对有分红(DividendIn!=0)的事件看 `MarkClosePnl`、`DividendIn`、`价差(期末-期初)`: + +**A组:MarkClosePnl == DividendIn(差值=0,纯分红,无价差)** + +| trade | EventType | 标的 | MarkClosePnl | DividendIn | 差值 | 价差(期末-期初) | +|-------|-----------|------|--------------|------------|------|----------------| +| 1854 | 4(自动互换) | 180205.IB | -18.95 | -18.95 | **0** | **0** | +| 1854 | 3(互换) | 180205.IB | 18.00 | 18.00 | **0** | **0** | +| 1868 | 3(互换) | 210210.IB | 103.346 | 103.346 | **0** | **0** | +| 1877 | 3(互换) | 210210.IB | -1033.46 | -1033.46 | **0** | **0** | +| 1885 | 3(互换) | 180205.IB | -9148.25 | -9148.25 | **0** | **0** | +| 1891 | 4(自动互换) | 180205.IB | -9.51 | -9.51 | **0** | **0** | + +**结论**:当价差=0(期末价=期初价)时,`MarkClosePnl` 完全等于 `DividendIn`。这证明 `MarkClosePnl = 价差(0) + 费(0) + 分红 = 分红`,**MarkClosePnl 含分红,铁证成立**。自动互换(EventType=4)同样成立。 + +> B组(差值≠0,如 1891-3互换 MarkClosePnl=395594.949)的差值是"价差+费"成分,但这些是历史脏数据(期末价被多除100导致价差异常),不影响"MarkClosePnl 含分红"的结论——只是这部分价差也是脏的。 + +--- + +## 四、重复次数:确定 2 次(非 2~3 次) + +### 数据流图(以 trade 1885 纯分红互换为例,分红 = -9148.25) + +``` +【客观源头】 + PosiQuantity = 100万, totalInterest = 每张分红率 + 期末价 = 期初价 = 1.00 (价差=0) + │ + ▼ +【第二层计算】 + DividendIn = 100万 × totalInterest × 方向 = -9148.25 + 价差盈亏 = 0 + 交易费 = 0 + │ + ▼ +【第三层入库 swap_flow_event】 ⚠️ 问题在这里 + ┌──────────────────────────────────────────────┐ + │ MarkClosePnl = 价差(0) + 费(0) + 分红(-9148.25) │ ◄── 分红第①次存入 + │ = -9148.25 │ + │ DividendIn = -9148.25 │ ◄── 分红第②次存入(同一笔!) + └──────────────────────────────────────────────┘ + │ 日终汇总 + ▼ +【eod_swap_position】 (实测确认) + TdCloseMtmPnl = Σ MarkClosePnl = -9148.25 (含分红) + TdCloseDividend = Σ DividendIn = -9148.25 (分红) + │ + ▼ +【算 RealizedPnl 已实现盈亏】 ⚠️⚠️ 重复累加 +``` + +### 各层级重复次数(确定 2 次) + +3 处代码分属**两个不同层级**,每个层级各自独立计算,但**每个层级分红都是 2 次**。不是叠加成 3 次。 + +| 聚合对象 | 代码位置 | 公式 | 分红算几次 | +|---------|---------|------|-----------| +| 单持仓 `eod_swap_position.RealizedPnl` | `1490` + `1512` | `RealizedPnl = eod.RealizedPnl + TdCloseMtmPnl`;`if(PosiDividendSum==0) RealizedPnl += TdCloseDividend` | **2 次**(Mtm含1次 + Dividend加1次)| +| 框架合约 `eod_swap.TdRealizedPnL`(当日) | `1866` | `TdRealizedPnL += TdCloseMtmPnl + TdCloseDividend` | **2 次**(Mtm + Dividend)| +| 框架合约 `eod_swap.RealizedPnL`(累计) | `1869` | `RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + RealizedFee + ...)` | **2 次**(RealizedMtmPnL + RealizedDividend)| + +**为什么不是 3 次**:第1869行的 `eod_swap.RealizedPnL` **不读取**单持仓的 `eod_swap_position.RealizedPnl`,而是用 `RealizedMtmPnL + RealizedDividend` **独立重新聚合**。所以它与1490/1512行不是叠加关系——它们是**两个不同层级的对象**(`eod_swap` 是 `eod_swap_position` 的上层聚合),各自都把分红算了 2 次。 + +``` +层级关系: + eod_swap_position (单持仓, 1490/1512算RealizedPnl) ← 第1层,分红2次 + │ 聚合 + ▼ + eod_swap (框架合约, 1866/1869重新聚合) ← 第2层,分红2次(不读第1层的RealizedPnl) +``` + +**根本原因只有一个**:`MarkClosePnl`(→ `TdCloseMtmPnl`/`RealizedMtmPnL`)含分红,而 `DividendIn`(→ `TdCloseDividend`/`RealizedDividend`)又是同一笔分红。无论哪个层级,只要把"Mtm"和"Dividend"相加,分红就翻倍。 + +--- + +## 五、为什么会变成这样(演进过程,用 hash + 日期) + +分红核算经历了一长串反复修改,每次补丁都在加剧或修正重复: + +| 时间 | 提交 | 改动 | 效果 | +|------|------|------|------| +| 2024-05-09 | `f9d8a256` | 山证基线,MarkClosePnl 含分红 | 埋下根源 | +| 2025-05-28 | `083848fb` | 审核流程重构,丢 eventType 分支 | 引入互换扣本金bug | +| 2026-06-25 | `1aba5cca` | DealDividends 加 SwapPositionValue/RealizedPnl 调整 | 语句顺序错(EQD-6290) | +| 2026-06-25 | `90b66922` | 修语句顺序 + 改全量重算口径 | 守恒修复✓ | +| 2026-06-25 | `2140a97f` | 加 `RealizedPnl += TdCloseDividend` | **加剧重复!**(以为分红漏算,其实Mtm已含)| +| 2026-06-26 | `1c9b28ff` | 互换审核不扣本金 | 修互换扣本金bug ✓ | + +--- + +## 六、修复方向 + +判断标准:**让每个字段语义单一、职责不重叠**。 + +### 方向A(推荐,治本):让 MarkClosePnl 不含分红 + +让字段职责分离——Mtm 只管价差,Dividend 只管分红: + +| 改动点 | 改前 | 改后 | +|--------|------|------| +| 前端 `calcFloatClosePnl:140`(手动互换/平仓)| `MarkClosePnl = 价差 + 费 + DividendIn` | `MarkClosePnl = 价差 + 费`(去掉 DividendIn)| +| 后端 `AuotoSwapUnwind:1102`(自动平仓)| `MarkClosePnl = 价差 + 费 + DividendIn` | 去掉 `+ DividendIn` | +| 后端 `DealDividends:463`(自动互换)| `MarkClosePnl = PosiDividendSum` | `MarkClosePnl = 0`(自动互换无价差,分红走 DividendIn)| +| `2140a97f` 新增(1512行)| `RealizedPnl += TdCloseDividend` | **保留**(此时 Mtm 不含分红,加 Dividend 才正确)| +| 1490行 | `RealizedPnl = eod.RealizedPnl + TdCloseMtmPnl` | 保留(Mtm 现在是纯价差)| +| 1866/1869行 | `TdCloseMtmPnl + TdCloseDividend` | 保留(现在 Mtm 不含分红,相加正确)| + +**效果**: +``` +MarkClosePnl = 纯价差(0) ← 只管价差 +DividendIn = 分红(-9148.25) ← 只管分红 +RealizedPnl = TdCloseMtmPnl(0) + TdCloseDividend(-9148.25) = -9148.25 ← 各加1次,不重复 ✓ +``` + +**风险**:要排查"前端盯市盈亏展示列"是否期望含分红(如果某处展示 MarkClosePnl 给用户看,去掉分红后显示会变)。需要回归测试前端展示。 + +### 方向B(保守):保留 MarkClosePnl 含分红,改汇总公式 + +| 改动点 | 改法 | +|--------|------| +| 1866行 | `TdRealizedPnL += TdCloseMtmPnl`(去掉 `+ TdCloseDividend`,因为 Mtm 已含)| +| 1869行 | `RealizedPnL = Σ(RealizedMtmPnL + RealizedFee + ...)`(去掉 `+ RealizedDividend`)| +| `2140a97f`(1512行)| **撤销** `RealizedPnl += TdCloseDividend`(1490 的 Mtm 已含)| + +**风险**:`TdCloseDividend`/`RealizedDividend` 可能被分红明细报表单独展示,去掉后那部分变0。 + +--- + +## 七、建议 + +1. **优先对齐"MarkClosePnl 该不该含分红"这个根本问题**。这是所有重复的根源。 +2. 达成共识后按**方向A**改(字段职责分离)。方向A改完后,`2140a97f` 这类补丁就不再需要,逻辑能稳定下来。 +3. 改完后用"守恒检验"验证:`ΔSwapPositionValue + ΔRealizedPnl == 0`(见附录 SQL)。 +4. **这块缺少自动化测试**(前端无测试、后端 Swap 单测只覆盖 EodPositionService)。建议补一个"分红守恒"后端单测兜底,避免反复打补丁。 + +--- + +## 附录A:验证 SQL + +### A.1 验证 MarkClosePnl 含分红(含价差佐证) + +```sql +SELECT + fe.SwapTradeId AS trade_id, + fe.EventType AS 事件类型, -- 2=平仓, 3=互换, 4=自动互换 + fe.UnderlyingCode AS 标的, + fe.MarkClosePnl AS 盯市盈亏_入库, + fe.DividendIn AS 分红_入库, + fe.MarkClosePnl - fe.DividendIn AS 差值_纯价差和费, + sp.PosiGrossPrice AS 期初全价, + fe.TradingAmountAvg AS 期末全价, + fe.TradingAmountAvg - sp.PosiGrossPrice AS 价差_期末减期初, + fe.TradingFee AS 交易费 +FROM swap_flow_event fe +JOIN swap_position sp ON sp.SwapTradeId = fe.SwapTradeId AND sp.IsInitial = 1 AND sp.UnderlyingCode = fe.UnderlyingCode +WHERE fe.DividendIn != 0 +ORDER BY fe.SwapTradeId, fe.id DESC +LIMIT 20; +``` +**判断**:若某行 `差值=0` 且 `价差=0` → 证明无价差互换时 MarkClosePnl 完全等于分红。 + +### A.2 验证分红互换守恒(修复后用) + +```sql +SELECT + cur.SwapTradeId AS trade_id, + cur.ValueDate AS 当日, + cur.SwapPositionValue AS 当日持仓价值, + pre.SwapPositionValue AS 前日持仓价值, + cur.SwapPositionValue - pre.SwapPositionValue AS Δ持仓价值, + cur.RealizedPnl AS 当日已实现盈亏, + pre.RealizedPnl AS 前日已实现盈亏, + cur.RealizedPnl - pre.RealizedPnl AS Δ已实现盈亏, + (cur.SwapPositionValue - pre.SwapPositionValue) + (cur.RealizedPnl - pre.RealizedPnl) AS 守恒检验_应接近0, + cur.TdCloseDividend AS 当日已实现分红_参考 +FROM eod_swap_position cur +JOIN eod_swap_position pre + ON pre.SwapTradeId = cur.SwapTradeId AND pre.UnderlyingCode = cur.UnderlyingCode + AND pre.ValueDate < cur.ValueDate + AND pre.id = (SELECT MAX(id) FROM eod_swap_position + WHERE SwapTradeId = cur.SwapTradeId AND UnderlyingCode = cur.UnderlyingCode AND ValueDate < cur.ValueDate) +WHERE cur.SwapTradeId IN ( + SELECT DISTINCT fe.SwapTradeId FROM swap_flow_event fe + WHERE fe.EventType = 4 AND fe.DividendIn != 0 AND fe.UnderlyingCode IS NOT NULL +) +AND cur.UnderlyingCode IS NOT NULL +ORDER BY cur.SwapTradeId DESC, cur.ValueDate DESC LIMIT 15; +``` +**判断**:守恒检验列接近0 → 修复成功。 + +--- + +## 附录B:关键代码位置索引(精确到类:行号) + +### B.1 MarkClosePnl 含分红(重复根源) + +| 场景 | 文件:类:行号 | 算法 | 含分红 | +|------|-------------|------|--------| +| 手动互换 | `incomeSwapTrade.js`:`calcFloatClosePnl`:133-134 | `价差(CloseNotionalValue×...) + 费 + DividendIn` | **含** | +| 手动平仓 | `unwindSwapTrade.js`:`calcFloatClosePnl`:203-205 | `价差(CloseQty×...) + 费×floatRatio×-1 + DividendIn` | **含** | +| 自动互换(分红型) | `SwapEodPositionService.cs`:`DealDividends`:463 | `= PosiDividendSum`(分红本身)| **含** | +| 自动全平仓 | `SwapDealService.cs`:`AuotoSwapUnwind`:1101-1102 | `价差 + 费 + DividendIn` | **含** | +| 多空组合合并事件 | `SwapFlowEventService.cs`:`MergePageEvent`:287 | `= PayMarkUnwindPnl`(待确认来源)| 待确认 | + +### B.2 已实现盈亏累加点(重复发生处) + +| 聚合对象 | 文件:类:行号 | 公式 | 分红次数 | +|---------|-------------|------|---------| +| 单持仓 `RealizedPnl` | `SwapEodPositionService.cs`:`UpdateEodPosition`:1490 | `= eod.RealizedPnl + TdCloseMtmPnl` | 含分红(Mtm) | +| 单持仓 `RealizedPnl`(加分红) | `SwapEodPositionService.cs`:`UpdateEodPosition`:1512(`2140a97f`)| `if(PosiDividendSum==0) += TdCloseDividend` | 又加分红 | +| 框架合约 `TdRealizedPnL` | `SwapEodPositionService.cs`:`SaveEodSwap`:1866 | `+= TdCloseMtmPnl + TdCloseDividend` | 翻倍 | +| 框架合约 `RealizedPnL` | `SwapEodPositionService.cs`:`SaveEodSwap`:1869 | `= Σ(RealizedMtmPnL + RealizedDividend + ...)` | 翻倍 | + +### B.3 totalInterest 计算链路 + +| 环节 | 文件:类:行号 | 算法 | +|------|-------------|------| +| totalInterest | `BondPaymentController.cs`:`GetBondPayMentInterest`:91-100 | `Σ payment_interest × 0.01 / (1+税) × (1-税)` | +| DividendIn(互换) | `incomeSwapTrade.js`:`getDivindIn`:197 | `PositionQty × totalInterest × 方向` | +| DividendIn(平仓) | `unwindSwapTrade.js`:`getDivindIn`:286 | `CloseQty × (totalInterest - consumedDividendInterest) × 方向` | + +--- + +## 附录C:calcFloatClosePnl 两处不一致(技术债,待确认) + +平仓页和互换页的 `calcFloatClosePnl` 存在 **4 处不一致**,属于重复代码各自实现导致,哪个对需业务确认: + +```js +// floatRatio / longRatio 含义: +// floatRatio = (PayDirection == 1) ? 1 : -1; // 收取=1, 支付=-1 (本方收支方向) +// longRatio = (PositionType == 1) ? 1 : -1; // 多头=1, 空头=-1 (本方多空方向) +``` + +| 维度 | 平仓 `unwindSwapTrade.js:203-205` | 互换 `incomeSwapTrade.js:133-134` | 说明 | +|------|----------------------------------|----------------------------------|------| +| 价差基数 | `CloseQty`(数量)| `CloseNotionalValue`(名义本金)| 量纲不同 | +| 价差方向 | `× floatRatio × longRatio` | `× floatRatio`(**无 longRatio**)| ⚠️ 互换若支持空头可能算错 | +| 四舍五入 | `Math.round×10000/10000` + `toFixed(2)` | 无 | ⚠️ 互换无取整,精度风险 | +| 费用方向 | `(费) × floatRatio × -1` | `费`(**无方向**)| ⚠️ 互换支付方费用方向可能没反转 | +| 分红 | `+ DividendIn` | `+ DividendIn` | 一致(都含分红)| + +**建议**:统一为公共函数(参考 `changeUnderlyingPrice` 两页一字不差的做法),方向处理对齐到更完整的平仓版本(含 `longRatio` + `floatRatio*-1`)。但需业务确认互换场景下 `PayDirection` 是否已隐含多空方向。 + +--- + +## 附录D:测试策略 + +### 现状 +- 后端有测试基础设施:`UnitTestProject/Modules/SwapModule/SwapEodPositionServiceIntegrationTest.cs`(MSTest,集成测试) +- **但现有测试基础薄弱**:现有测试(如 `TestDealInterests_ViaSwapPositionCompose`)虽调用了真实业务代码 `service.SwapPositionCompose(...)`,但 catch 异常后 `Assert.IsTrue(true)` 就算通过,**没有对输出的 eod 持仓字段做任何断言**,属于"能跑不崩就算过"的烟雾测试,无法验证业务正确性。 +- 前端无测试框架(无 jest/mocha/playwright) + +### 教训:测试必须调用真实业务代码 +> 曾尝试写"构造字段对象自己断言"的纯逻辑测试(如 `TestDividendConservation_*`),但这类测试**自己造数据自己测**,不调用 `SwapEodPositionService` 的任何方法,是**无意义的伪测试**——业务代码写错也会通过。已删除。真正的测试必须:调用真实业务方法 → 查回输出字段 → 断言字段关系。 + +### 后续测试方向 +- **参考 golden source 分支**:`glms/feature/refactor-swap-event-testable`(该分支有可复用的 Swap 事件测试数据构造,后续补分红守恒测试时以此为基础) +- 真正有意义的守恒测试需要: + 1. 用 golden source 的测试数据工厂构造一笔带分红的互换交易(`trade` + `swap_position` + `bond_payment` + `swap_flow_event`,多张表自洽) + 2. 调用真实的 `SwapPositionCompose` 或 `DealDividends` + 3. 查回 `eod_swap_position`,断言 `ΔSwapPositionValue + ΔRealizedPnl == 0` +- 这是独立的工程任务,需要专门投入时间,不适合在排查任务里匆忙做 + +### 当前验证手段(测试补全前) +- **数据验证SQL**(附录A.1/A.2):纯查询,不改代码,可快速验证线上数据是否重复/守恒 +- **修复后回归**:用附录A的SQL在修复前后各跑一次,对比 `TdCloseMtmPnl + TdCloseDividend` 是否还有翻倍 diff --git a/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md b/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md new file mode 100644 index 00000000..37c0ddf9 --- /dev/null +++ b/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md @@ -0,0 +1,129 @@ +# 互换收益结算审核后状态卡死、阻止收盘问题分析 + +> 本文分析到期互换"收益结算(互换)审核通过后,交易状态不流转,导致收盘到期检查一直阻止"的问题。 +> 成文于 2026-07-01。 + +--- + +## 一、问题概述 + +**复现场景**:一笔互换交易到期日(如 2026-06-29),用户在界面做"收益结算(互换)"并审核通过,但: +- 交易状态没变成"已到期"或"已平仓",仍是"确认成交" +- 不管做多少次收益结算+审核,状态依旧不变 +- 收盘报错:`[检查当日到期交易]当日到期的场外交易未全部操作完成,请操作完成后再执行收盘操作:收益互换 GLMS-20260427-0001` + +--- + +## 二、根因:审核路径 ApproveSwapTrade 漏了到期日判断 + +### 代码对照(铁证) + +**免审核路径 `SwapIncome`(正确,基线就有)** — `SwapDealService.cs:1481-1485`: +```csharp +if (td.ExerciseDate <= unwindData.ValueDate) // 判断到期 +{ + td.Notional = 0; + td.StockEqvNotional = 0; + td.TradeStatus = "已到期"; // ✅ 到期 → 已到期 +} +``` + +**审核路径 `ApproveSwapTrade`(漏了)** — `SwapDealService.cs:1525-1538`: +```csharp +if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓) + td.TradeStatus = "已平仓"; +else +{ + td.TradeStatus = ConsTrade.确认成交; // ❌ 到期互换落到这里!没有 ExerciseDate 判断 + td.HasPartialUnWind = 1; +} +``` + +`ApproveSwapTrade` **完全没有 `ExerciseDate` 到期判断**,而互换操作的 `CloseMethod` 又是默认值 0(`InitIncome` cs:216 从不设它)→ 0 ≠ 全部平仓(1) → 走 else → 状态退回"确认成交"。 + +### 为什么互换必然走 else 分支 + +- `CloseMethodEnum`:Unknown=0 / 全部平仓=1 / 部分平仓=2 +- 互换初始化 `InitIncome`(cs:216-292)**从不给 `unwindData.CloseMethod` 赋值** → 默认 0 +- 只有平仓路径(`InitUnwind`/`InitLongShortUnwind`)才设 `CloseMethod=全部平仓` +- 0 ≠ 1 → 必走 else → 退回"确认成交" + +### 收盘检查为什么一直阻止 + +`EodCheckMaturityTrade.cs:27-33`(到期检查): +```csharp +var predicate = PredicateBuilder.Create(t => + t.ExerciseDate >= startDate && t.ExerciseDate <= settleDate // 今日到期 + && t.TradeStatus == ConsTrade.确认成交 // ★ 状态还是"确认成交" + && t.SettlementFlag != 1); +``` + +判断口径只看 `TradeStatus`。`TradeCompleteStatus = { 已到期, 已执行, 已平仓 }`(ConsTrade.cs:33)。审核后状态停在"确认成交"(不在完成列表里)→ 永远命中 → 永远阻止。 + +--- + +## 三、关于"提交数量但审核时数量0" + +用户还反馈"提交了数量但审核时候数量0"。经查: + +1. `InitIncome`(cs:216-292)**从没给 `unwindData.CloseQty` 赋值**(对比 `InitUnwind` cs:90 有赋值)→ `CloseQty` 默认 0 +2. `SaveSwapDeal`(cs:1597)的 `item.Quantity = unwindData.CloseQty` 被 `eventType == 平仓` 条件挡住 → 互换(3)不执行 + +但这不是核心问题——**核心是状态没流转**。互换的 Quantity 语义与平仓不同(互换是收益结清,不是按数量平仓),数量为 0 可能是设计如此。状态卡死才是阻止收盘的直接原因。 + +--- + +## 四、引入时间(git blame) + +| 路径 | 提交 | 时间 | 说明 | +|------|------|------|------| +| `SwapIncome` 到期判断(正确)| `f9d8a256` | 2024-05-09 | 山证基线,一直有 | +| `ApproveSwapTrade` 状态分支(漏判断)| `083848fb` | 2025-05-28 | 审核流程重构,漏同步 ExerciseDate 判断 | + +`083848fb`(2025-05-28)重构审核流程时把状态分支改成 `CloseMethod==全部平仓` 判断,但**忘了同步 `SwapIncome` 里的 `ExerciseDate` 到期判断**。 + +--- + +## 五、涉及文件 + +| 文件:行号 | 问题 | +|----------|------| +| `SwapDealService.cs:1525-1538`(ApproveSwapTrade)| **漏 ExerciseDate 判断**,到期互换退回"确认成交" | +| `SwapDealService.cs:1481-1485`(SwapIncome)| 正确对照(有到期判断)| +| `SwapDealService.cs:216`(InitIncome)| 不设 CloseMethod → 默认 0,必然走 else | +| `EodCheckMaturityTrade.cs:27-33` | 阻止收盘的检查(只认 TradeStatus)| +| `ConsTrade.cs:33` | TradeCompleteStatus 定义(确认成交不在内)| + +--- + +## 六、修复方向(最小改动) + +在 `ApproveSwapTrade`(cs:1534-1538)的 else 分支补到期判断,与 `SwapIncome` 对齐: + +```csharp +else +{ + // 补:到期互换应设为"已到期"(与 SwapIncome cs:1481 对齐) + if (td.ExerciseDate <= swapEvent.unwindData.ValueDate) + { + td.Notional = 0; + td.StockEqvNotional = 0; + td.TradeStatus = "已到期"; + } + else + { + td.TradeStatus = ConsTrade.确认成交; + td.HasPartialUnWind = 1; + } +} +``` + +改后到期互换审核通过 → 状态正确变成"已到期" → 收盘检查通过。 + +--- + +## 七、团队决策问题 + +1. **互换的 CloseMethod 是否应该设值**:当前 `InitIncome` 不设 CloseMethod(默认 0),是否应在互换初始化时根据是否全额结清设为"全部平仓"?这会影响 `ApproveSwapTrade` 走哪个分支。 +2. **互换 Quantity=0 是否符合预期**:`InitIncome` 不设 CloseQty,导致互换 flow_event 的 Quantity=0。需业务确认这是设计意图还是遗漏。 +3. **历史卡住的交易怎么处理**:GLMS-20260427-0001 等已卡住的交易,是否需要手动改 TradeStatus 为"已到期"才能继续收盘? diff --git a/项目文档/互换模块可测性改造Seam实践指南.md b/项目文档/互换模块可测性改造Seam实践指南.md new file mode 100644 index 00000000..4140d0e4 --- /dev/null +++ b/项目文档/互换模块可测性改造Seam实践指南.md @@ -0,0 +1,228 @@ +# Seams 模式实践指南 — 遗留代码可测试化方法论 + +## 背景 + +本指南基于互换合约交易系统(OTC衍生品后台)的实际改造经验提炼。目标是在不改变生产行为的前提下,让 LLM 能够验证核心业务逻辑的正确性。 + +## 核心模式:Protected Virtual Seams + +### 原理 + +遗留代码的典型问题:业务逻辑与基础设施(数据库、缓存、外部服务)紧耦合。直接单元测试需要真实数据库,成本极高。 + +**Seams 模式**的解法: +1. 识别方法中的外部依赖点 +2. 提取 `protected virtual` 方法作为"接缝" +3. 默认实现保持原有行为 +4. 测试时创建子类 override,注入内存数据 + +```csharp +// 生产代码:添加虚方法 +protected virtual trade FindTrade(int swapTradeId) + => DbContext.trade.Find(swapTradeId); + +// 测试代码:override 返回内存数据 +protected override trade FindTrade(int swapTradeId) + => _trades.TryGetValue(swapTradeId, out var t) ? t : null; +``` + +**关键原则:最小侵入** +- 不改变方法签名 +- 不改变访问修饰符(除了 private → protected) +- 不引入新接口或依赖注入框架 +- 默认实现完全等价于原有行为 + +### 依赖类型与提取策略 + +| 依赖类型 | 示例 | 提取策略 | +|----------|------|---------| +| DbContext 查询 | `DbContext.trade.Where(predicate)` | 提取为 `FindXxx()` 虚方法 | +| DbContext 写入 | `DbContext.eod_swap_position.Add(entity)` | 提取为 `PersistXxx()` 虚方法 | +| DbContext 事务 | `DbContext.Database.BeginTransaction()` | 提取为 `ExecuteInTransaction(Action)` | +| DbContext 保存 | `DbContext.SaveChanges()` | 提取为 `SaveAllChanges()` | +| new XxxService() | `new EodCurrencyRateService(UserInfo).GetRate()` | 提取为业务语义虚方法如 `GetCurrencyRate()` | +| 静态方法 | `EodPriceQueryService.TryGetPrice()` | 提取为虚方法包装 | +| DataCacheProvider | `DataCacheProvider.GetUnderlyingDataSource()` | 提取为 `GetUnderlyingData()` | +| 基类非虚方法 | `base.InitInterestDate()` | 提取为 Wrapper 虚方法 | + +## 可测试子类模式 + +### 构造函数注入 + +```csharp +public class TestableSwapEodPositionService : SwapEodPositionService +{ + private readonly Func _priceProvider; + private readonly Func _currencyRateProvider; + + public TestableSwapEodPositionService( + OptUserInfo optUser, + Func priceProvider = null, + Func currencyRateProvider = null + ) : base(optUser) + { + _priceProvider = priceProvider ?? ((code, date) => 100m); + _currencyRateProvider = currencyRateProvider ?? (() => 1m); + } + + protected override decimal GetUnderlyingPrice(string code, DateTime date) + => _priceProvider(code, date); +} +``` + +### 输出捕获 + +```csharp +public List CreatedEodPositions { get; } = new(); + +protected override void PersistEodSwapPosition(eod_swap_position position) + => CreatedEodPositions.Add(position); +``` + +### 事务 No-op + +```csharp +protected override void ExecuteInTransaction(Action action) + => action(); // 测试中跳过事务 +``` + +### 委托参数过多时用自定义委托 + +```csharp +// Func 最多16个类型参数,超出时用 delegate +public delegate bool InterestDateDelegate( + DateTime? preSettleDate, DateTime valueDate, trade td, + bool tdClose, bool calcLastNew, + out DateTime interestStart, out DateTime interestEnd); +``` + +## 测试体系设计 + +### 三层测试 + +| 层级 | 目的 | 特点 | +|------|------|------| +| 合成测试 | 验证业务逻辑正确性 | 程序化构造输入,断言输出字段 | +| 黄金文件 | 回归保护 | JSON 存储 input/output,回放对比 | +| 端到端测试 | 验证方法间协作 | 调用主入口方法,验证全流程 | + +### 合成测试模式 + +```csharp +[TestMethod] +public void Scenario3_HasPrevEod_HasCloseEvent_ShouldUpdatePosition() +{ + // Arrange + var prevEod = CreateEodPosition(positionId: 1, qty: 1000, grossPrice: 1.0020m); + var service = CreateService(); + var positions = new List { CreatePosition(id: 1, qty: 1000) }; + var flowEvents = new List + { + CreateCloseFlowEvent(positionId: 1, qty: 400, markClosePnl: 500m) + }; + + // Act + var result = service.ExecuteDealFloatPositions( + positions, new List(), + new List { prevEod }, new List(), + SettleDate, CreateTrade(), PreSettleDate, flowEvents); + + // Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual(600, result[0].PosiQuantity, "1000-400=600"); + Assert.AreEqual(400, result[0].TdCloseQty); +} +``` + +### 黄金文件格式 + +```json +{ + "Scenario": "有平仓事件→更新", + "ScenarioId": 3, + "TradeDate": "2025-04-24T00:00:00", + "Description": "...", + "SwapTradeId": 100, + "Positions": [...], + "PrevEodPositions": [...], + "FlowEvents": [...], + "ExpectedCreatedPositions": [...], + "ExpectError": false +} +``` + +### 回放测试框架 + +```csharp +[TestMethod] +public void ReplayAllGoldenFiles() +{ + var files = Directory.GetFiles(GoldenDir, "*.json"); + foreach (var file in files) + { + var golden = Deserialize(file); + var service = CreateService(golden); + var result = Execute(service, golden); + AssertResults(result, golden.ExpectedCreatedPositions); + } +} +``` + +## 渐进式改造节奏 + +### 推荐:由内而外,逐层深入 + +``` +Week 1: 子方法可测试化(ComposePage, DealFloatPositions, DealInterests) +Week 2: 主入口方法可测试化(SwapPositionCompose 端到端) +Week 3: 横向扩展到其他 Service(SwapDealService, SwapTradeService) +``` + +### 每个 Service 的改造步骤 + +1. **分析**:列出方法清单,统计硬依赖密度(deps/100行),选择 ROI 最高的方法 +2. **提取**:添加 `#region 可测试化`,逐一提取虚方法 +3. **重构**:将硬依赖调用替换为虚方法调用 +4. **子类**:创建 TestableXxxService,override 虚方法 +5. **测试**:编写合成测试 → 黄金文件 → 回放测试 +6. **验证**:全量测试通过,无回归 + +### 虚方法命名约定 + +| 类型 | 命名 | 示例 | +|------|------|------| +| 查询 | `FindXxx` | `FindTrade`, `FindEodSwapPositions` | +| 持久化 | `PersistXxx` | `PersistEodSwapPosition`, `PersistFlowEvent` | +| 计算 | `CalcXxx` / `GetXxx` | `CalcBondPayment`, `GetCurrencyRate` | +| 操作 | `ExecuteXxx` / `SaveXxx` | `ExecuteInTransaction`, `SaveAllChanges` | +| 包装 | `XxxWrapper` | `InitInterestDateWrapper` | + +## 实际成果 + +### SwapEodPositionService(Day 5a-5d) + +| 指标 | 数值 | +|------|------| +| 虚方法数 | 29 | +| 重构方法数 | 7 | +| 合成测试 | 24 | +| 黄金文件 | 18 | +| 端到端测试 | 6 | +| 总测试数 | 65(56 passed + 9 skipped recording) | + +### SwapDealService(Day 6a) + +| 指标 | 数值 | +|------|------| +| 虚方法数 | 2 | +| 合成测试 | 6 | +| 总测试数 | 71(62 passed + 9 skipped) | + +## 经验教训 + +1. **先识别路径再提取**:DealFloatPositions 有 3 条路径、DealInterests 有 5 条路径,理解路由逻辑后才知道哪些虚方法可以复用 +2. **黄金文件需要 rebuild 才生效**:JSON 资源文件通过 csproj 的 CopyToOutputDirectory 复制,修改后需要 rebuild +3. **枚举值别靠记忆**:平仓=2、互换=3、自动互换=4 — 写黄金文件时查枚举定义 +4. **Func 参数上限**:C# Func 最多 16 个类型参数,超出用自定义 delegate +5. **swap_position.SwapIntervalList 是只读**:从 InterestSwapInterval JSON 反序列化,不能直接赋值 +6. **匹配键是 id 不是 PositionId**:DealFloatPositions 用 `posi.id` 匹配 eodPosition 和 flowEvent diff --git a/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md b/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md new file mode 100644 index 00000000..030c8084 --- /dev/null +++ b/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md @@ -0,0 +1,295 @@ +# 互换部分平仓后:利息端/预付金"平仓盈亏默认值"偏大问题分析 + +> 本文分析收益互换"昨天收益结算(互换)→ 收盘 → 今天平仓"场景下,**预付金平仓盈亏和利息端平仓盈亏的默认值计算偏大**的问题:根因、涉及的文件、如何确认、修复方案与自测方法。 +> 成文于 2026-07-01。 + +--- + +## 一、问题概述 + +**复现场景**: +1. 第一天:对互换交易做**收益结算(互换)** → 收盘 +2. 第二天:再次进入**平仓**页 + +**现象**:平仓页的"预付金平仓盈亏"和"利息端平仓盈亏"**默认值偏大**——包含了之前已经通过互换/收益结算平掉的那部分利息,没有扣除。 + +**用户怀疑**:"这部分是不是没有减去之前已经平掉的部分?"——**怀疑成立**,下文逐层证明。 + +--- + +## 二、数据流:这两个默认值从哪来 + +### 关键结论:默认值是后端算的,前端只展示 + +前端 `unwindSwapTrade.js` **不计算**这两个默认值,只是把后端返回的 `InterestClosePnL` 原样填到输入框: + +| 界面字段 | 绑定字段 | 数据来源 | +|---------|---------|---------| +| 预付金平仓盈亏 | `marginList[].InterestClosePnL` | 后端接口返回 | +| 利息端平仓盈亏 | `interestList[].InterestClosePnL` | 后端接口返回 | +| 浮动端平仓盈亏 | `floatPosition.FloatPnlSum`(前端现算)| 前端 `calcFloatClosePnl` | + +前端调接口(`unwindSwapTrade.js:264-265`): +```js +{ valueDate, unwindDate, tradeId, closePercent, eventType: 2 } +``` + +### 后端接口链路 + +``` +POST /swaptrade2/GetUnwindInterestList (SwapTrade2Controller.cs:287) + └─ SwapDealService.GetUnwindInterests (SwapDealService.cs:302) + ├─ 取 closeList = 当天已完成的平仓/互换事件 (cs:336) + └─ GetInterests (cs:358) + ├─ CalcNotionalByMode (cs:453) ← 按利息模式算计息基数 + ├─ CalcUnwindInterest → InitSwapDealInterest (cs:595/626) + │ └─ CalcDailySimpleInterest / CalcDailyCompoundInterest (cs:771/705) + │ ↑ 这里全量重算利息,不扣历史已平部分 + └─ 同日去重:closeList 扣减 (cs:418-446) ← 只扣当天,跨天漏 +``` + +--- + +## 三、根因:互换结清后 eod 的 InterestProfitSum(待实现)没扣减已实现部分 + +> ✅ 6-29 收盘后用交易 1889(GLMS-20260616-0004)三天对比实测坐实。用户的判断"从开头算了"正确。 + +### 3.1 实测坐实(6-29 收盘后,6-29/6-30/7-1 三天对比) + +利息腿 PositionId=34695(标的期初全价),29号收益结算(互换) + 29号收盘后: + +``` +日期 平仓默认值 eod待实现(IPS) eod已实现(RI) 说明 +6-29(互换前) 0.0003 77.26(6-28eod) 0.00 当天看=0,正确 +6-30 83.6986 77.26(6-29eod) 77.26 偏大! +7-1 90.1370 77.26(6-29eod) 77.26 更偏大! +``` + +**核心铁证**:6-29 收盘后,`InterestProfitSum`(待实现)= **77.26**,`RealizedInterest`(已实现)= **77.26**。**两个相等——互换已经把全部利息实现了(77.26),但"待实现"没归零**,还是 77.26。 + +所以后续平仓默认值 = 待实现(77.26) + 每日增量(6.44/天)。6-30 = 77.26+6.44=83.70,7-1 = 77.26+12.88=90.14。**偏大的量正是那个本该归零却没归零的 77.26**。 + +### 3.2 根因机制:eod 扣减公式对"互换"场景失效 + +`SwapEodPositionService.cs:829`(收盘更新待实现利息): +```csharp +InterestIncomeSum = pre.InterestIncomeSum + TdInterestIncome - TdCloseInterest; +// = 77.26(6-28) + 77.26(当日新计) - 77.26(当日互换实现) +// = 77.26 ← 抵消了,没扣减! +``` + +**为什么抵消**:互换操作当日会重新计息(`TdInterestIncome` = 77.26,相当于把到期前的利息算了一遍),同时互换实现(`TdCloseInterest` = 77.26)。两个相等,相减为 0,所以 `InterestIncomeSum` 维持在 77.26 不变。 + +**正常平仓不会这样**:平仓时 `TdInterestIncome` 是"昨日到今日的增量"(小),`TdCloseInterest` 是"平仓实现的全部"(大),两者不等 → 能扣减。但互换(收益结算)的计息逻辑把全程利息都算了一遍(`TdInterestIncome ≈ TdCloseInterest`),导致扣减失效。 + +### 3.3 CalcDailySimpleInterest 读这个"没扣减的待实现"作为起点 + +`SwapDealService.cs:771`(CalcDailySimpleInterest): +```csharp +decimal interestProfitSum = preEodPosition.InterestProfitSum; // :774 读 77.26(本该归零) +decimal interest = interestProfitSum * closePercent; // :776 起点 = 77.26 +for (int i = 0; i <= calcDays; i++) // :784 补算昨日之后增量 +{ + if (accrueDate > preEodPosition.ValueDate) // :789 + interest += interest1; // :823 每天+6.44 +} +// 结果:6-30 = 77.26 + 6.44 = 83.70(偏大,含已实现的77.26) +``` + +`GetInterests` 的同日去重(cs:435-443)只覆盖"当天"的已完成事件,29号互换 EventDate=6-29 ≠ 平仓日 6-30 → **跨天去重不生效**。 + +### 3.4 根因总结 + +``` +互换结清后正确的状态应该是:InterestProfitSum(待实现) = 0(全部已实现) +但实际:InterestProfitSum = 77.26(= RealizedInterest,没归零) + ↓ 原因 +eod 公式 cs:829:IPS = pre.IPS + TdInterestIncome - TdCloseInterest + 互换时 TdInterestIncome ≈ TdCloseInterest → 抵消 → IPS 不变 + ↓ 后果 +后续平仓默认值 = IPS(77.26,本该0) + 增量 → 偏大 77.26 +``` + +--- + +## 四、涉及哪些文件 + +| 层 | 文件:行号 | 作用/问题 | +|----|----------|----------| +| 后端·默认值入口 | `SwapTrade2Controller.cs:287` | GetUnwindInterestList 接口 | +| 后端·默认值计算 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| 主计算链路 | +| 后端·单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| 起点=InterestProfitSum×closePercent,补算昨日之后增量 | +| 后端·复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| 从头算,注释"只能用要平仓的名义本金从头开始算" | +| 后端·方向系数 | `SwapDealService.cs:673`(interestRatio)| 收取=1/支付=-1,**实测符号相反疑似此处** | +| 后端·同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435`(算尾跳过)| 跨天不扣 | +| 后端·固定值本金(可疑B)| `SwapDealService.cs:461-463` | 固定值强制100% | +| 后端·eod扣减待实现 | `SwapEodPositionService.cs:829` | InterestIncomeSum 扣 TdCloseInterest(**有扣减**)| +| 前端·展示(非源)| `unwindSwapTrade.js:262-275` | 仅展示后端值 | + +> **前端不是 bug 源**:前端只把后端 `InterestClosePnL` 填进输入框,无额外计算。 + +--- + +## 五、引入时间(git blame) + +| 代码点 | 提交 | 时间 | 说明 | +|--------|------|------|------| +| `interestProfitSum = preEodPosition.InterestProfitSum`(cs:774)| `f9d8a256` | 2024-05-09 | 山证基线 | +| `interest = interestProfitSum * closePercent`(cs:776)| `6708878d` | 2026-04-13 hjhan | 改起点为×closePercent | +| 同日去重(cs:435-443)| `535497c68` | 2026-06-04 吴方海 | 加同日去重补救,仅覆盖当天 | +| eod 扣减 TdCloseInterest(cs:829)| 基线 | 2024-05 | eod 层有扣减已实现 | + +**结论**:利息计算链路是基线设计,经历过多次局部修补(`6708878d` 改起点、`535497c68` 加同日去重)。**具体哪个改动引入了用户反馈的偏差,需用干净样本定位**。 + +--- + +## 六、如何确认 bug(已坐实) + +### 6.1 实测坐实(交易 1889,6-29 收盘后三天对比,Step0d) + +用真实的 `GetUnwindInterests` 分别测 6-29/6-30/7-1 三天,结果(PositionId=34695 标的期初全价): + +``` +日期 平仓默认值 eod待实现(IPS) eod已实现(RI) 说明 +6-29(互换前) 0.0003 77.26(6-28eod) 0.00 当天看=0,正确 +6-30 83.6986 77.26(6-29eod) 77.26 偏大!含已实现的77.26 +7-1 90.1370 77.26(6-29eod) 77.26 更偏大! +``` + +**核心铁证**:6-29 收盘后 `InterestProfitSum`(待实现)= 77.26 = `RealizedInterest`(已实现)= 77.26。互换已全部实现,但待实现没归零 → 后续默认值 = 77.26(本该0) + 增量 → 偏大。 + +### 6.2 录制测试 + +`SwapPartialUnwindInterestDefaultTest.Step0d_DiagnoseTrade1889_FullTimeline`: +- 展示 swap_event 全历史(甄别回退) +- 展示 eod 按日序列(InterestProfitSum/RealizedInterest 演变) +- 调用真实 `GetUnwindInterests` 拿默认值 +- 对比"默认值 vs eod待实现"判定是否重复计入 + +--- + +## 七、修复方案 + +> 根因已精确定位:互换结清后 `InterestProfitSum` 没归零(= RealizedInterest),导致后续平仓默认值偏大。 + +### 方案 A(推荐·最小改动):平仓默认值起点扣除 RealizedInterest + +`CalcDailySimpleInterest`(cs:774/776)读 `InterestProfitSum` 作为起点。既然 `InterestProfitSum` 没扣已实现,就在这里扣: + +```csharp +// SwapDealService.cs:774-776 改前 +decimal interestProfitSum = preEodPosition.InterestProfitSum; +var TdInterestPrincipal = preEodPosition.TdInterestPrincipal; +decimal interest = interestProfitSum * closePercent; + +// 改后:扣除已通过互换/平仓实现的部分 +decimal interestProfitSum = preEodPosition.InterestProfitSum - preEodPosition.RealizedInterest; +var TdInterestPrincipal = preEodPosition.TdInterestPrincipal; +decimal interest = interestProfitSum * closePercent; +``` + +**原理**:`InterestProfitSum`(待实现累计)- `RealizedInterest`(已实现累计)= **真正未实现的利息**。互换结清后 IPS=77.26、RI=77.26,相减=0,起点正确归零。后续只加增量。 + +**验证**(用 1889 数据): +- 6-30 默认值 = (77.26 - 77.26) + 6.44(一天增量) = **6.44** ✅(而非偏大的 83.70) +- 7-1 默认值 = (77.26 - 77.26) + 12.88 = **12.88** ✅ + +**优点**:改动 1 行,利用现有字段,覆盖单利路径。 +**风险**:需确认所有场景下 `RealizedInterest` 都正确累加了已实现利息。对复利路径(`CalcDailyCompoundInterest` cs:705)需同步检查。 + +### 方案 B(治本):修正 eod 扣减公式,让互换结清后 InterestProfitSum 正确归零 + +根因在 `SwapEodPositionService.cs:829`——互换时 `TdInterestIncome ≈ TdCloseInterest` 导致扣减抵消。需在互换(EventType=互换/自动互换)的收盘路径里特殊处理: + +```csharp +// SwapEodPositionService.cs:829 附近 +// 互换结清时,待实现利息应直接归零(已全部实现),不能靠 TdInterestIncome - TdCloseInterest 抵消 +if (isSwap && closePercent == 1) // 全量互换结清 +{ + newEodPayPosition.InterestIncomeSum = 0; // 待实现归零 +} +else +{ + newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + + newEodPayPosition.TdInterestIncome + - newEodPayPosition.TdCloseInterest; +} +``` + +**优点**:从源头修正,所有读 `InterestProfitSum` 的地方都受益。 +**缺点**:改动 eod 核心逻辑,风险较大,需全面回归。 + +### 方案 C(补充):closeList 扩展到历史 + +`GetUnwindInterests` 的 closeList(cs:336)只查当天,跨天互换事件漏掉。可扩展到历史: +```csharp +// cs:336 改 UnwindDate == unwindDate 为 UnwindDate <= unwindDate +``` +但 cs:435 的 `!calcLast` 限制仍会导致算尾配置下不扣。作为方案 A/B 的补充。 + +### 方案对比 + +| 方案 | 改动量 | 治本程度 | 风险 | +|------|--------|---------|------| +| A(扣 RealizedInterest)| 极小(1行) | 修默认值 | 低,需验证字段口径 | +| B(eod 公式归零)| 中 | 修源头 | 较高,改 eod 核心 | +| C(closeList 扩历史)| 小 | 修去重 | 中,算尾仍漏 | + +**建议**:优先方案 A 止血(最小改动,直接验证),方案 B 作为后续治本。 + +--- + +## 八、录制 golden source + +### 已坐实(Step0d,交易 1889,6-29 收盘后三天对比) + +`Step0d_DiagnoseTrade1889_FullTimeline` 已调用真实服务坐实 bug: +- 6-29 收盘后 InterestProfitSum(待实现)=77.26 = RealizedInterest(已实现)=77.26,**没归零** +- 6-30 默认值 83.70 = 77.26(本该归零的待实现) + 6.44(一天增量) → 偏大 77.26 +- 7-1 默认值 90.14 = 77.26 + 12.88(两天增量) → 偏大 77.26 + +### 录制 golden JSON + +当前 Step0d 是诊断输出(Console),尚未序列化 golden 文件。建议后续: +1. 把 1889 的快照(swap_event + eod + 默认值结果)序列化为 `SwapPartialUnwindInterest/partial_unwind_trade_1889.json` +2. 红灯断言:`Assert(默认值 > eod待实现 × 合理倍数)`,修复后反转 + +### 注意:1889 有大量回退 + +1889 有 52 条 swap_event(48 条 Invalid=True 的回退历史),测试环境反复回退复用。录制 golden 时需注意甄别有效事件(Invalid=False)。Step0d 已处理:只展示 Invalid=False 的 4 条有效事件。 + +### 与前两个 golden 的关系 + +三份 golden 构成"互换损益计算"回归基线: +- `SwapDividend/`(分红重复计算)✅ +- `SwapReEodDeleteCash/`(重收盘误删资金)✅ +- `SwapPartialUnwindInterest/`(本次)⏳ Step0d 已坐实,golden 待序列化 + +--- + +## 九、团队决策问题 + +1. **业务口径确认**:部分平仓/互换后再次平仓,利息端盈亏默认值应该是"剩余持仓的应计利息(扣已实现)"?当前行为是"从头算(含已实现)",业务上预期哪个? +2. **方案选择**:方案 A(closeList 扩历史,推荐)/ 方案 B(扣 RealizedInterest)? +3. **回退复用的影响**:1889 被反复回退,eod 只到 6-22(29号收盘数据疑似被回退清掉)。需确认:回退是否应该清理 eod?还是回退后 eod 应保留? +4. **是否补单测**:建议把 Step0d 的坐实逻辑固化为 golden + 红灯断言,防止复发。 + +--- + +## 附录:关键代码位置索引 + +| 项 | 文件:行号 | +|----|----------| +| 默认值接口入口 | `SwapTrade2Controller.cs:287`(GetUnwindInterestList)| +| 默认值计算主方法 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| +| 单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| +| 复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| +| InterestProfitSum 读取(起点)| `SwapDealService.cs:774` / `:645`(InitSwapDealInterest)| +| 方向系数(实测符号相反疑似)| `SwapDealService.cs:673`(interestRatio)| +| 同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435-443`(扣减)| +| 固定值本金(可疑B)| `SwapDealService.cs:461-463`(CalcNotionalByMode)| +| eod 扣减待实现(有扣减)| `SwapEodPositionService.cs:829`(InterestIncomeSum 扣 TdCloseInterest)| +| 平仓后回写持仓 | `SwapDealService.cs:1664`(InterestAmount 累加)/ `:1668`(预付金扣减)| +| 前端展示(非源)| `unwindSwapTrade.js:262-275` / `SwapUnwind.cshtml:113,146`| +| 数据模型·待实现 | `EodSwapPosition.cs:282`(InterestProfitSum)/ `:270`(InterestIncomeSum)| +| 数据模型·已实现 | `EodSwapPosition.cs:355`(RealizedInterest)/ `:368`(RealizedPnl)| diff --git a/项目文档/互换重收盘误删手动互换资金记录问题分析.md b/项目文档/互换重收盘误删手动互换资金记录问题分析.md new file mode 100644 index 00000000..9e2ece12 --- /dev/null +++ b/项目文档/互换重收盘误删手动互换资金记录问题分析.md @@ -0,0 +1,420 @@ +# 互换重收盘误删"手动互换"资金记录:问题分析与修复方案 + +> 本文分析互换交易"重新收盘/重算 EOD"后,**用户手动互换产生的资金记录被错误删除**的问题:根因、引入时间、三种修复方案与自测方法。 +> 成文于 2026-07-01,排查范围覆盖 `5f97909f`(2026-04-29)至当前 HEAD。 + +--- + +## 一、问题概述 + +互换交易每次"重新收盘"(EOD 重算)时,会先清理旧数据再重算。当前实现会把**用户手动互换**产生的资金记录(`ClientCashInCashOut` 表中 `Action=系统操作-互换` 的记录)**一并删除**,导致历史资金流水丢失。 + +**复现条件**:一笔互换交易,用户已手动做过互换(EventType=3,生成 `Action=系统操作-互换` 资金记录),随后对该交易日重新收盘 → 该手动互换资金记录消失。 + +--- + +## 二、先澄清概念:哪些表是什么 + +| 表名 | 实体类 | 用途 | 与本问题关系 | +|------|--------|------|-------------| +| `ClientCashInCashOut` | `ClientCashInCashOut.cs:232` | **资金记录/出入金流水**(金额、方向、银行账户)| ⚠️ **被误删的就是这张表** | +| `swap_event` | `SwapEvent.cs` | 互换事件主记录(含 `ClientCashId` 外键)| 删除时按 EventType 过滤,安全 | +| `swap_flow_event` | `SwapFlowEvent.cs:14` | 开平仓事件明细(盈亏/计息),通过 `ClientCashId` 关联资金记录 | EOD 清理时按 EventType 过滤,**未误删** | +| ~~swap_capital_flow / swap_fund_flow~~ | 不存在 | —— | 代码库中无此表 | + +**关键**:`swap_flow_event` 不是"资金记录",它和 `ClientCashInCashOut` 通过 `ClientCashId` 字段 1:1 关联。本问题中 `swap_flow_event` 没被误删,被误删的是 `ClientCashInCashOut`。 + +--- + +## 三、根因:删除逻辑按 Action 字符串过滤,无法区分手动/自动 + +### 问题代码位置 + +`YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs` 的 `ClearSwapPositions` 方法,**第 398–409 行**: + +```csharp +// SwapTradeBaseService.cs:398-409(bug 所在) +var swapTradeIds = swapEvents.Select(s => s.SwapTradeId).Distinct().ToList(); +var actions = new List() { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 }; +var premiumCashRecords = DbContext.ClientCashInCashOut + .Where(x => swapTradeIds.Contains(x.TradeId ?? 0) + && x.HappenDate >= valueDate + && actions.Contains(x.Action)) + .ToList(); +if (premiumCashRecords.Any()) +{ + DbContext.ClientCashInCashOut.RemoveRange(premiumCashRecords); +} +``` + +### 为什么会误删 + +`系统操作_互换` 这个 Action 字符串**手动互换和自动互换共用**,而 `ClientCashInCashOut` 表**没有任何字段区分手动 vs 自动来源**: + +| 来源 | 写入位置 | Action 值 | +|------|---------|----------| +| 手动互换 `SwapIncome` | `SwapDealService.cs:1471` | `系统操作-互换` | +| 手动互换 `ApproveSwapTrade`(审批通过)| `SwapDealService.cs:1519` | `系统操作-互换` | +| 自动互换(利息腿)| `SwapEodPositionService.cs:516` | `系统操作-互换` | +| 自动互换(分红)| `SwapEodPositionService.cs:531` | `系统操作-互换` | + +`AddClientCashInCashOut`(`SwapTradeBaseService.cs:285-318`)生成资金记录时**不设任何来源标记**。所以按 `TradeId + HappenDate>=valueDate + Action` 删除,无法过滤掉手动互换的记录。 + +> 注:第 391–396 行按 `swap_event.ClientCashId` 精准删除是**安全**的(只删自动互换 event 自己链接的那条)。**第 398–409 行的"按 Action 兜底模糊删除"才是误删元凶。** + +--- + +## 四、引入时间线:是"修每次收盘多出一条"修出来的新 bug + +本 bug 是修复"重复收盘导致互换收入重复计入"时,删除范围逐步扩大,最终修过头引入的。三次提交演进: + +| 时间 | 提交 | 作者 | 改动 | 性质 | +|------|------|------|------|------| +| **2026-04-29** | `5f97909f` | hjhan | `fix(swap): 修复重复收盘导致互换收入(含预付金利息)重复计入的问题`——在删 `swap_event` 前按 `swap_event.ClientCashId` 精准删除关联资金记录 | ✅ **正确** | +| **2026-05-11** | `ebe711d3` | 吴方海 | `#EQD-5850 预付金返息的发生时间有问题`——自动互换把预付金返息单独拆条记录,并在删除时**新增按 `TradeId + 日期 + Action=预付金返息` 批删** | ⚠️ 首次把删除从"按主键ID"放宽到"按Action字符串" | +| **2026-05-14** | `44e89726` | 吴方海 | `收盘自动互换,需要将当前计息基数给回到持仓名义本金或固定值`——**把 `系统操作_互换` 加进删除 Action 列表** | ❌ **误删手动互换的直接引入点** | + +`44e89726` 的 diff(实测验证)只改一行: + +```diff +- && x.Action == ClientCashInCashOut.系统操作_预付金返息) ++ var actions = new List() { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 }; ++ && actions.Contains(x.Action)) +``` + +加进去的理由是"自动互换的互换主金额也要清掉重算",但没意识到 `系统操作_互换` 跟手动互换共用。 + +--- + +## 五、约束分析:为什么不能简单回退到 `5f97909f` + +`5f97909f` 的精准删除(按 `swap_event.ClientCashId`)在**当时**是对的,但现在的自动互换逻辑已演进——**一次自动互换会生成多条资金记录**: + +`SaveAutoSwapDeal`(`SwapEodPositionService.cs:506`)按条件最多生成 3 条 `ClientCashInCashOut`: +- `:516` 利息腿 `系统操作_互换`(`SwapCloseAmount != 0` 时) +- `:522` 预付金 `系统操作_预付金返息`(`SwapMarginRebatePnl != 0` 时) +- `:531` 分红 `系统操作_互换`(`SwapDividendPnl != 0` 时) + +但 `swap_event.ClientCashId`(`:535`)**只指向最后一条非 0 的 id**,前面的被覆盖。所以单纯按 `ClientCashId` 删,会**漏删**自动互换的前几条资金记录 → 复发"每次收盘多出一条"老 bug(`5f97909f` 当时修的那个)。 + +> 手动互换也有同样结构:`SwapIncome:1471` 生成主金额(id 存进 event),`:1474` 预付金返息的 id 被丢弃。手动互换若被精准删除只删主金额那条,预付金返息会成"孤儿"残留——但手动互换**本就不该被重收盘删除**,所以这反而是"正确残留"。 + +--- + +## 六、修复方案(三选一,供团队决策) + +### 方案 A(推荐,最小改动 + 治本):按 swap_event 链 + EventType 区分 + +**思路**:删除自动互换资金记录时,**只删与自动互换 swap_event 关联的记录**,不碰手动互换的 swap_event(EventType=3/互换)。 + +删除条件从"按 Action 字符串"改为"按 `swap_event.ClientCashId` 集合",并扩展 `swap_event` 与资金记录的关联(让被覆盖的前几条也能被找到): + +```csharp +// 修复后(SwapTradeBaseService.cs:398-409 替换) +// 注意:本方法入参 eventTypes 调用方只传了 {自动互换}(SwapEodPositionService.cs:92), +// 所以 swapEvents 只含自动互换事件,手动互换事件不在内 —— 这就是安全的关键。 +var swapEventIds = swapEvents.Select(s => s.id).ToList(); +if (swapEventIds.Any()) +{ + // 已有:按 swap_event.ClientCashId 精准删除主金额那条(保留第391-396行不变) + + // 新增:按 swap_flow_event.ClientCashId 反查自动互换生成的全部资金记录 + // 自动互换的每条 swap_flow_event 都带 ClientCashId(指向其资金记录) + var autoCashIds = DbContext.swap_flow_event + .Where(x => swapEventIds.Contains(x.EventId ?? 0) && x.ClientCashId > 0) + .Select(x => x.ClientCashId.Value) + .Distinct() + .ToList(); + // 合并 swap_event.ClientCashId(主金额) + var eventCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId.Value); + autoCashIds = autoCashIds.Union(eventCashIds).ToList(); + + if (autoCashIds.Any()) + { + var autoCashRecords = DbContext.ClientCashInCashOut + .Where(x => autoCashIds.Contains(x.id)) + .ToList(); + DbContext.ClientCashInCashOut.RemoveRange(autoCashRecords); + } +} +// ❌ 删除原来的"按 Action 字符串模糊删除"整段(第398-409行) +``` + +**⚠️ 关键前提缺陷(已确认)**:当前 `SaveAutoSwapDeal:538-542` 生成 flow_event 时**只设了 `EventId`,没有给 `ClientCashId` 赋值**: + +```csharp +// SwapEodPositionService.cs:538-542(现状,缺 ClientCashId 赋值) +flowEvents.ForEach(x => +{ + x.EventId = swapEvent.id; + DbContext.swap_flow_event.Add(x); +}); +``` + +对比平仓路径 `ComposePage:685-686` 是有赋值的(`x.ClientCashId = clientCashId`)。所以**方案 A 必须同时补这个赋值**才能落地,否则反查 `swap_flow_event.ClientCashId` 会漏(复发"每次收盘多出一条"老 bug)。这是方案 A 的硬性前置改动,见第七节"改动 2"。 + +**优点**: +- 删除范围严格限定在自动互换事件链内,手动互换零误伤。 +- 不新增字段,不改数据库结构。 +- 语义清晰:删哪个 event,就删它关联的资金记录。 + +**风险**: +- 依赖 `swap_flow_event.ClientCashId` 的赋值完整性——当前自动互换分支未设,需补(改动 2)。 +- **资金记录与 flow_event 不是 1:1**(已确认): + - `:420` 普通自动互换:生成 2 条资金记录(利息汇总 `:516` + 预付金汇总 `:522`),flow_event 是利息明细腿(多条)。 + - `:498` 分红型自动互换:生成 1 条资金记录(分红汇总 `:531`),flow_event 是分红明细腿(多条)。 + - 所以一条资金记录会被多条 flow_event 引用——改动 2 给 flow_event 设 ClientCashId 时,让同一笔互换的所有明细腿都指向该次生成的资金记录 id(利息腿指向 `:516` 的 id,预付金腿指向 `:522` 的 id)。删除时 `Distinct()` 去重即可。 +- 由于 `clientCashId` 变量在 `SaveAutoSwapDeal:516/522/531` 被多次覆盖,最终 `swap_event.ClientCashId`(`:535`)只存最后一条。**改动 2 必须在每次 `AddClientCashInCashOut` 后立即把 id 赋给对应的 flow_event 组**,不能依赖最后的 `clientCashId` 值。需配套单测兜底(见第八节)。 + +--- + +### 方案 B(保守):保留 Action 过滤 + 加来源标记字段 + +**思路**:给 `ClientCashInCashOut` 加一个来源字段区分手动/自动,删除时排除手动。 + +```csharp +// 1. 数据库迁移:ClientCashInCashOut 加列 +ALTER TABLE ClientCashInCashOut ADD COLUMN Source VARCHAR(20) DEFAULT '手动'; + +// 2. AddClientCashInCashOut 增加来源参数(SwapTradeBaseService.cs:285) +public int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate, string source = "手动") +{ + ... + ee.Source = source; // 新增 + ... +} + +// 3. 自动互换调用处传 source="自动"(SwapEodPositionService.cs:516/522/531) + +// 4. 删除过滤加排除条件(SwapTradeBaseService.cs:401-404) +.Where(x => swapTradeIds.Contains(x.TradeId ?? 0) + && x.HappenDate >= valueDate + && actions.Contains(x.Action) + && x.Source == "自动") // 新增:只删自动生成的 +``` + +**优点**:删除逻辑最直白,改一处过滤条件即可。 + +**缺点**: +- 需要数据库迁移(生产加列),风险高。 +- 历史数据 `Source` 为默认值"手动",但历史自动互换记录也是 NULL/默认 → 需数据回填脚本,否则老 bug 数据残留。 +- `Source` 字段语义需长期维护。 + +--- + +### 方案 C(最长远,工程量大):用 swap_event 作为唯一删除入口 + +**思路**:所有资金记录都必须挂到某个 `swap_event` 上(通过 `swap_flow_event.ClientCashId` 或新增 `ClientCashInCashOut.SwapEventId`),删除时一律按 event 链,彻底废弃按 Action 字符串删除。 + +**优点**:语义最清晰,根除"按 Action 字符串删除"这类隐患。 + +**缺点**:改动面大,需梳理所有生成资金记录的路径,确保都有 event 挂载。适合作为长期重构目标,不适合当下快速止血。 + +--- + +### 方案对比 + +| 维度 | 方案A(推荐)| 方案B | 方案C | +|------|-------------|-------|-------| +| 改动量 | 小(1个方法 + flow_event赋值补全)| 中(加列+迁移+多处调用)| 大(全链路重构)| +| 数据库迁移 | 否 | **是** | 视实现 | +| 治本程度 | 高 | 高 | 最高 | +| 风险 | 需确认 flow_event.ClientCashId 完整性 | 迁移风险+历史数据回填 | 改动面大 | +| 止血速度 | 快 | 慢 | 慢 | + +--- + +## 七、具体小修复代码(方案 A) + +### 改动 1:`SwapTradeBaseService.cs:398-409` 替换删除逻辑 + +```csharp +// ❌ 删除原第398-409行(按 Action 字符串模糊删除) +// ✅ 替换为:按自动互换 swap_event 链精准删除 + +// 自动互换一次可能生成多条资金记录(利息腿/预付金/分红), +// 但 swap_event.ClientCashId 只指向最后一条。通过 swap_flow_event.ClientCashId +// 反查自动互换事件关联的全部资金记录,避免误伤手动互换(EventType=3)。 +var autoFlowCashIds = DbContext.swap_flow_event + .Where(x => eventIds.Contains(x.EventId ?? 0) && x.ClientCashId > 0) + .Select(x => x.ClientCashId.Value) + .Distinct() + .ToList(); +var autoEventCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId.Value).ToList(); +autoFlowCashIds.AddRange(autoEventCashIds); +autoFlowCashIds = autoFlowCashIds.Distinct().ToList(); + +if (autoFlowCashIds.Any()) +{ + var autoCashRecords = DbContext.ClientCashInCashOut + .Where(x => autoFlowCashIds.Contains(x.id)) + .ToList(); + DbContext.ClientCashInCashOut.RemoveRange(autoCashRecords); +} +``` + +> 说明:`eventIds` 已在第 377 行算好(`swapEvents.Select(s => s.id)`)。本方法入参 `eventTypes` 在重收盘入口(`SwapEodPositionService.cs:92`)只传 `{自动互换}`,所以 `swapEvents` 只含自动互换事件,手动互换事件天然不在删除范围内 —— 这是安全的根本保证。 + +### 改动 2(前提):补全 `swap_flow_event.ClientCashId` 赋值 + +自动互换 `SaveAutoSwapDeal`(`SwapEodPositionService.cs:506`)当前生成 flow_event 时只设了 `EventId`,没设 `ClientCashId`(`:538-542`、`:549-553`)。需补上,让方案 A 反查能找到全部资金记录。 + +由于资金记录是"汇总"的(利息汇总/预付金汇总/分红汇总各 1 条),而 flow_event 是"明细腿"(多条),赋值规则是:**同一笔互换的明细腿都指向该次生成的资金记录 id**。需要在 `SaveAutoSwapDeal` 内用独立变量保存每条资金记录 id(不能依赖被覆盖的 `clientCashId`): + +```csharp +// SwapEodPositionService.cs:512-532 改造(用独立变量分别记录每个资金记录 id) +int interestCashId = 0; // 利息腿资金记录 id +int premiumCashId = 0; // 预付金腿资金记录 id +int dividendCashId = 0; // 分红资金记录 id + +if (unwindData.SwapCloseAmount != 0) +{ + interestCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, cashHappenDate); +} +if (unwindData.SwapMarginRebatePnl != 0) +{ + premiumCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); +} +unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL; +if (unwindData.SwapDividendPnl != 0) +{ + var dividendPayDate = (dividendEvents != null && dividendEvents.Count > 0) + ? dividendEvents.First().PayDate.Value : unwindData.ValueDate; + dividendCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapDividendPnl), ClientCashInCashOut.系统操作_互换, dividendPayDate); +} + +// 保留 clientCashId 给 swap_event(取最后一条非0,与原行为一致) +int clientCashId = dividendCashId != 0 ? dividendCashId : (premiumCashId != 0 ? premiumCashId : interestCashId); +``` + +然后在 flow_event 赋值处按腿类型设 `ClientCashId`: + +```csharp +// SwapEodPositionService.cs:536-555(flowEvents 是利息明细腿,指向利息资金记录) +if (flowEvents != null) +{ + flowEvents.ForEach(x => + { + x.EventId = swapEvent.id; + x.ClientCashId = interestCashId; // 补:利息明细腿指向利息汇总资金记录 + DbContext.swap_flow_event.Add(x); + }); + UpdateInitalPostion(flowEvents, td.id); +} +// dividendEvents 是分红明细腿,指向分红资金记录 +if (dividendEvents != null) +{ + dividendEvents.ForEach(x => + { + x.EventId = swapEvent.id; + x.ClientCashId = dividendCashId; // 补:分红明细腿指向分红资金记录 + DbContext.swap_flow_event.Add(x); + }); + UpdateInitalPostion(dividendEvents, td.id); +} +``` + +> ⚠️ 预付金资金记录 `premiumCashId` 没有对应的 flow_event 可挂载(预付金是单独汇总的,无明细腿)。它只能靠 `swap_event.ClientCashId`(当它是最后一条时)或方案 A 删除逻辑里单独处理。**实施时需验证:当只有预付金非0时,`swap_event.ClientCashId` 是否指向预付金那条**(按上面 `clientCashId` 三元表达式,`premiumCashId != 0` 时会取它,✓ 覆盖)。 + +--- + +## 八、如何自测 + +### 自测 1:SQL 验证(最快,纯查询) + +找一笔**当天有手动互换操作**的互换交易,重收盘前后对比资金记录: + +```sql +-- 步骤1:盘前记录手动互换资金记录的 id 快照 +SELECT id, TradeId, HappenDate, Action, Money, State, OptName, OptTime +FROM ClientCashInCashOut +WHERE TradeId = <手动互换过的交易ID> + AND Action = '系统操作-互换' +ORDER BY id; +-- 记下这些 id(比如 id 列表 = 1001, 1002) + +-- 步骤2:对该交易日执行重收盘(前端操作或调 SwapPositionCompose) + +-- 步骤3:盘后再查同样的 id +SELECT id, TradeId, Action, Money +FROM ClientCashInCashOut +WHERE id IN (1001, 1002); -- 替换为步骤1记下的 id +``` + +**判断标准**: +- **修复前(bug)**:步骤3 查不到记录(被 `RemoveRange` 删除)。 +- **修复后(正确)**:步骤3 记录原样存在(id/Money/OptName 不变)。 + +### 自测 2:守恒校验(验证没修过头 → 自动互换仍能清理) + +确保修复后**自动互换的重复资金记录仍被清理**,否则"每次收盘多出一条"老 bug 复发: + +```sql +-- 对一笔纯自动互换交易,连续重收盘 3 次,每次收盘后查记录数 +SELECT COUNT(*) AS cnt +FROM ClientCashInCashOut +WHERE TradeId = <自动互换交易ID> + AND Action IN ('系统操作-互换', '系统操作-预付金返息') + AND HappenDate >= '<收盘日>'; +``` + +**判断**:连跑 3 次后 `cnt` 应**稳定不变**(每次清掉重算),**不应**线性递增(若递增说明自动互换资金漏删,老 bug 复发)。 + +### 自测 3:录制测试(复用已有 golden 模式,可进回归) + +参考本仓库 `UnitTestProject/Modules/SwapModule/SwapDividendGoldenRecordTest.cs` 的录制模式(连真实测试库 `192.168.2.96/glms_yltrs_ylcms`),骨架: + +```csharp +[Ignore] // 有写副作用,标 Ignore 不进 CI;手动跑 +[TestCategory("DBRecording")] +public void ReEod_ShouldNotDeleteManualSwapCashRecord() +{ + int tradeId = <有手动互换的交易ID>; + DateTime settleDate = <收盘日>; + + using var ctx = DbContextFactory.GetYLDbContext(); + + // 盘前快照:手动互换资金记录 id + var manualCashIds = ctx.ClientCashInCashOut + .Where(x => x.TradeId == tradeId + && x.Action == ClientCashInCashOut.系统操作_互换 + && x.HappenDate >= settleDate) + .Select(x => x.id).ToList(); + Console.WriteLine($"盘前手动互换资金记录数: {manualCashIds.Count}"); + + // 触发重收盘 + var service = new SwapEodPositionService(userInfo); + service.SwapPositionCompose(settleDate, preSettleDate, new List{ clientId }); + + // 盘后验证:手动互换记录必须原样保留 + var survivors = ctx.ClientCashInCashOut + .Where(x => manualCashIds.Contains(x.id)).ToList(); + Assert.AreEqual(manualCashIds.Count, survivors.Count, + "手动互换资金记录不应被重收盘删除"); +} +``` + +--- + +## 九、团队决策问题 + +1. **选哪个方案?** 推荐方案 A(最小改动+治本),但需先确认 `swap_flow_event.ClientCashId` 在自动互换路径的赋值完整性。方案 B 需数据库迁移,方案 C 适合长期。 + +2. **历史被误删的数据怎么办?** 若生产已发生"手动互换资金记录被重收盘删除",是否需要从 `clientcashincashout_history`(`YLContext.cs:127`)或备份恢复?需业务确认影响范围。 + +3. **是否补单测兜底?** 当前 Swap 模块缺少"重收盘不误删手动数据"的单测,建议补自测 3 的录制测试进回归,防止 `44e89726` 类问题复发。 + +--- + +## 附录:关键代码位置索引 + +| 项 | 文件:行号 | +|----|----------| +| 删除逻辑(bug 所在)| `SwapTradeBaseService.cs:374`(ClearSwapPositions),第 398-409 行 | +| 重收盘调用入口 | `SwapEodPositionService.cs:94`(SwapPositionCompose,`removeEventTyps={自动互换}`, `delAfter=true`)| +| 资金记录生成(手动互换)| `SwapDealService.cs:1471`(SwapIncome)/ `:1519`(ApproveSwapTrade)| +| 资金记录生成(自动互换)| `SwapEodPositionService.cs:516`(利息腿)/ `:522`(预付金)/ `:531`(分红)| +| `AddClientCashInCashOut`(不设来源标记)| `SwapTradeBaseService.cs:285-318` | +| `ClientCashInCashOut` 实体(无来源字段)| `ClientCashInCashOut.cs:232` | +| 引入 bug 的提交 | `44e89726`(2026-05-14,把 `系统操作_互换` 加进删除 Action 列表)| +| 安全的对照基线 | `5f97909f`(2026-04-29,按 ClientCashId 精准删除)| diff --git a/项目文档/数据库/排查SQL/互换分红损益_数据导出与验证.sql b/项目文档/数据库/排查SQL/互换分红损益_数据导出与验证.sql new file mode 100644 index 00000000..358c67a0 --- /dev/null +++ b/项目文档/数据库/排查SQL/互换分红损益_数据导出与验证.sql @@ -0,0 +1,435 @@ + +SELECT sfe.SwapTradeId, + MAX(sfe.SwapTradeNo) AS SwapTradeNo, + SUM(sfe.DividendIn) AS 分红合计, + SUM(sfe.MarkClosePnl) AS 盯市合计, + -- 关键:必须有 eod 归档才算"走完EOD的有效样本" + (SELECT COUNT(*) FROM eod_swap_position esp + WHERE esp.SwapTradeId = sfe.SwapTradeId) AS eod持仓快照数, + (SELECT COUNT(*) FROM eod_swap es + WHERE es.SwapTradeId = sfe.SwapTradeId) AS eod汇总数 +FROM swap_flow_event sfe +WHERE sfe.EventType IN (3,4) AND sfe.DividendIn <> 0 AND sfe.DataState = 100 +GROUP BY sfe.SwapTradeId +HAVING eod持仓快照数 > 0 AND eod汇总数 > 0 -- ← 这道过滤保证种子完整 +ORDER BY ABS(SUM(sfe.DividendIn)) DESC, MAX(sfe.SwapTradeNo) DESC; + + + +-- ============================================================================ +-- 互换分红损益:数据导出与重复计算验证 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; + + +-- ============================================================================ +-- 第 2.5 步:分红被计算两次的完整证明(含事件/价格/费用/付息明细) +-- ============================================================================ +-- 本节用 4 个查询把"分红重复计算"的证据链完整还原: +-- 段1事件明细:每条平仓/互换事件,把 MarkClosePnl 拆成 价差+费+分红 三成分。 +-- 若"价差"残差~0 且 DividendIn<>0,证明 MarkClosePnl 里装的就是分红。 +-- 段2 EOD逐日:盯市列(TdCloseMtmPnl) 与 分红列(TdCloseDividend) 同日出现等额分红。 +-- 段3 累计对照:盯市列累计 RealizedMtmPnL 含了分红成分,分红列累计 RealizedDividend 又含一份。 +-- 段4 一句话结论:重复金额 = 盯市列里的分红成分(按 PositionId 汇总避免双向腿抵消)。 +-- +-- 已用 golden 数据(1875纯分红型重复302400、1891混合型重复-18.66)校验本 SQL 结果正确。 + +SET @TargetTradeId := 1875; -- 替换为实际样本(1875=纯分红型 / 1891=混合型) + +-- 段1 事件明细拆解:MarkClosePnl = 价差 + 费(CloseFee) + 分红(DividendIn) +-- 价差残差 = MarkClosePnl - DividendIn - CloseFee +-- 纯分红型:价差残差~0(MarkClosePnl 全是分红) +-- 混合型 :价差残差<>0(MarkClosePnl 含真实价差 + 少量分红) +SELECT sfe.id, + sfe.EventDate, + CASE sfe.EventType WHEN 1 THEN '开仓' WHEN 2 THEN '平仓' + WHEN 3 THEN '互换' WHEN 4 THEN '自动互换' END AS 事件类型, + sfe.PositionId, + sfe.UnderlyingCode, + sfe.Quantity, + sfe.TradingAmountAvg AS 成交均价不含费, + sfe.TradingAmountFeeAvg AS 含费均价, + sfe.TradingAmountNetAvg AS 成交净价不含费, + sfe.TradingAmountNetFeeAvg AS 成交净价含费, + sfe.TradingAmount AS 成交金额, + sfe.TradingFee AS 交易佣金, + sfe.TradingFeePending AS 待结算后付费用, + sfe.CloseFee AS 平仓费用_费成分, + sfe.DividendPending AS 待结算分红, + sfe.DividendIn AS 分红流入_分红成分, + sfe.MarkClosePnl AS 盯市盈亏_总额, + (sfe.MarkClosePnl - sfe.DividendIn - sfe.CloseFee) AS 价差残差, + CASE WHEN sfe.EventType IN (2,3,4) + AND ABS(sfe.MarkClosePnl - sfe.DividendIn - sfe.CloseFee) < 0.01 + AND sfe.DividendIn <> 0 + THEN '根因:价差~0->MarkClosePnl全是分红' + WHEN sfe.EventType IN (2,3,4) AND sfe.DividendIn <> 0 + THEN '混合:价差+分红(分红成分进盯市列)' + ELSE '' END AS 事件诊断 +FROM swap_flow_event sfe +WHERE sfe.SwapTradeId = @TargetTradeId + AND sfe.DataState = 100 + AND sfe.EventType IN (2,3,4) +ORDER BY sfe.PositionId, sfe.EventDate, sfe.EventType; + +-- 段2 EOD逐日:盯市列 vs 分红列(同日同腿,分红同时出现在两列) +SELECT esp.ValueDate, + esp.PositionId, + esp.TdCloseQty, + esp.UnderlyingPrice AS 当日盯市价, + esp.PosiGrossPrice AS 期初成本价, + esp.TdCloseMtmPnl AS 当日盯市列, + esp.RealizedMtmPnL AS 累计盯市列, + esp.TdCloseDividend AS 当日分红列, + esp.RealizedDividend AS 累计分红列, + esp.TdCloseFee AS 当日费用列, + esp.TdCloseInterest AS 当日利息列, + esp.RealizedPnl AS 持仓层累计已实现, + CASE WHEN esp.TdCloseMtmPnl <> 0 AND esp.TdCloseDividend <> 0 + THEN CONCAT('分红', esp.TdCloseDividend, '同时进盯市列和分红列') + ELSE '' END AS 当日诊断 +FROM eod_swap_position esp +WHERE esp.SwapTradeId = @TargetTradeId + AND (esp.TdCloseMtmPnl <> 0 OR esp.TdCloseDividend <> 0) +ORDER BY esp.PositionId, esp.ValueDate; + +-- 段3 累计对照:按 PositionId 取最终累计,算出盯市列里的分红成分 +-- 盯市列里的分红成分 = 该腿所有平仓/互换事件的 DividendIn 之和 +SELECT fin.PositionId, + fin.RealizedMtmPnL AS 最终累计盯市列, + fin.RealizedDividend AS 最终累计分红列, + fin.RealizedFee AS 最终累计费用列, + fin.RealizedInterest AS 最终累计利息列, + fin.RealizedPnl AS 最终持仓层已实现, + dv.盯市列分红成分, + (fin.RealizedMtmPnL - dv.盯市列分红成分 - fin.RealizedFee) AS 盯市列纯价差成分 +FROM (SELECT PositionId, RealizedMtmPnL, RealizedDividend, RealizedFee, RealizedInterest, RealizedPnl + FROM eod_swap_position esp + WHERE esp.SwapTradeId = @TargetTradeId + AND esp.id IN (SELECT MAX(id) FROM eod_swap_position + WHERE SwapTradeId = @TargetTradeId GROUP BY PositionId) + ) fin +LEFT JOIN (SELECT PositionId, SUM(DividendIn) AS 盯市列分红成分 + FROM swap_flow_event + WHERE SwapTradeId = @TargetTradeId AND DataState = 100 AND EventType IN (2,3,4) + GROUP BY PositionId) dv ON dv.PositionId = fin.PositionId +ORDER BY fin.PositionId; + +-- 段4 一句话结论:重复金额合计 = 盯市列里的分红成分之和 +SELECT @TargetTradeId AS SwapTradeId, + SUM(盯市列分红成分) AS 重复计入分红金额, + (SELECT SUM(RealizedPnl) FROM eod_swap_position + WHERE SwapTradeId = @TargetTradeId + AND id IN (SELECT MAX(id) FROM eod_swap_position + WHERE SwapTradeId = @TargetTradeId GROUP BY PositionId)) AS 当前RealizedPnl合计, + CASE WHEN ABS(SUM(盯市列分红成分)) > 0.01 + THEN CONCAT('坐实重复计算: 分红 ', SUM(盯市列分红成分), + ' 既在 RealizedMtmPnL(盯市列) 又在 RealizedDividend(分红列)', + ', 修复后 RealizedPnl 应减少 ', SUM(盯市列分红成分)) + ELSE '未检测到重复(可能已修复或无分红平仓/互换事件)' END AS 结论 +FROM (SELECT PositionId, SUM(DividendIn) AS 盯市列分红成分 + FROM swap_flow_event + WHERE SwapTradeId = @TargetTradeId AND DataState = 100 AND EventType IN (2,3,4) + GROUP BY PositionId) t; +-- 预期结果: +-- 1875(纯分红型) -> 重复计入 302400.00(MarkClosePnl 全是分红,价差=0) +-- 1891(混合型) -> 重复计入 -18.66 (MarkClosePnl 含价差395604 + 分红-18.66) + + +-- ============================================================================ +-- 第 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 -u -p 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 主记录,作为"一笔带分红互换交易"的完整快照纳入版本库。