From 3670dde9bfc9ba55fae8b703426c60fd9df5af49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 30 Jul 2026 23:29:07 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E5=88=A9?= =?UTF-8?q?=E6=81=AF=E4=BA=92=E6=8D=A2=E5=B9=B3=E4=BB=93=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=B0=BE=E5=B7=AE=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E6=9C=AC=E9=87=91=E8=AE=A1=E7=AE=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复ExecuteSaveAutoEodInterestPosition方法参数传递,添加lastEodSwap、posiLongNotional和orginPv参数 - 修正平仓利息计算逻辑,区分手动结算和平仓后自动结算的利息处理 - 新增ResolveUnwindPreviousNotional静态方法,优化平仓前本金计算逻辑 - 修复部分平仓后利息累积边界问题,确保仅从上次EOD快照后开始计算 - 完善自动结算利息的四舍五入处理,避免精度丢失 - 修正支付端方向符号应用,确保方向只应用一次 - 更新TdInterestPrincipal计算逻辑,处理不同利息模式下的本金赋值 - 修复TdCloseInterest计算,合并手动和自动结算利息金额 - 优化InterestIncomeSum计算,正确处理结算后剩余未实现利息 --- .../SwapModule/DealInterestsScenarioTest.cs | 243 +++++++++++++++++- .../Modules/SwapModule/SwapDealService.cs | 25 +- .../SwapModule/SwapEodPositionService.cs | 34 ++- 3 files changed, 284 insertions(+), 18 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs index f1e87b49..ba6f8790 100644 --- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -92,10 +92,12 @@ namespace YLErp.Modules.SwapModule // public 包装:验证自动互换时的“高精度应结 -> 两位实际结算 -> 待实现尾差”链路。 public eod_swap_position ExecuteSaveAutoEodInterestPosition( eod_swap_position eodPayPosition, swap_position position, trade td, - DateTime valueDate, IntervalModel interval) + DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap = null, + decimal posiLongNotional = DealInterestsScenarioTest.Principal, + decimal orginPv = DealInterestsScenarioTest.Principal) { SaveAutoEodInterestPosition(eodPayPosition, null, position, td, valueDate, interval, - null, DealInterestsScenarioTest.Principal, 0m, 1m, DealInterestsScenarioTest.Principal); + lastEodSwap, posiLongNotional, 0m, 1m, orginPv); return PersistedPositions.LastOrDefault(); } @@ -697,10 +699,10 @@ namespace YLErp.Modules.SwapModule var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, 0.01m); firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓; firstCloseFlow.InterestPrincipal = 50m; - // 模拟 CalcUnwindInterest: 上日尾差 + 本次平仓后的高精度待实现。 + // 模拟 CalcUnwindInterest:上日尾差加当日新增,尚未扣除本次 0.01 平仓结算。 service.AutoInterests = new List { - CreateAutoSwapFlowEvent(firstCloseDate, 0.006383561644m) + CreateAutoSwapFlowEvent(firstCloseDate, 0.016383561644m) }; service.AutoInterests[0].InterestPrincipal = 50m; var firstCloseResult = service.ExecuteSaveAutoEodWithCloseInterestPosition( @@ -733,6 +735,239 @@ namespace YLErp.Modules.SwapModule "全平后累计已实现应包含自动互换和两次平仓"); } + [TestMethod] + public void DI_AUTO_SETTLEMENT_004_PartialCloseAccruesOnlyAfterPreviousEod() + { + const decimal originalNotional = 10012.35m; + const decimal remainingNotional = 5006.17m; + const decimal closeNotional = 5006.172835m; + const decimal rate = 0.0299m; + const decimal pendingInterest = 0.820379534246m; + const decimal settledInterest = 0.82m; + const decimal expectedPendingInterest = 0.820569301369m; + var service = new StubEodPositionService(); + var td = CreateTrade(); + td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "01", + SettlementRules = 0 + }); + var position = CreateInterestPosition(); + position.InterestRateDefault = rate; + position.InterestSwapInterval = null; + var previousEodDate = StartDate.AddDays(2); + var closeDate = previousEodDate.AddDays(1); + var previousEod = CreatePreEod(previousEodDate, pendingInterest, settledInterest); + previousEod.TdInterestPrincipal = originalNotional; + var closeFlow = CreateSwapFlowEvent(closeDate, settledInterest); + closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓; + closeFlow.InterestPrincipal = 5006.18m; + closeFlow.InterestRate = rate; + + var result = service.ExecuteSaveAutoEodWithCloseInterestPosition( + previousEod, position, td, closeDate, null, + remainingNotional, 0m, new List { closeFlow }, + closeNotional, false); + + AssertDecimal(expectedPendingInterest, result.InterestIncomeSum, + "Partial close must accrue only the day after the previous EOD snapshot"); + AssertDecimal(remainingNotional, result.TdInterestPrincipal, + "The close-day snapshot must carry the remaining principal into the next EOD"); + AssertDecimal(1.64m, result.RealizedInterest, + "Realized interest must include the previous and current settlements"); + Assert.AreEqual(previousEodDate, service.LastInterestCalculationEodPosition.ValueDate, + "The previous EOD ValueDate must be preserved for accrual boundaries"); + Assert.AreEqual(previousEod.id, service.LastInterestCalculationEodPosition.id, + "The previous EOD identity must not be reset to a new position"); + } + + [TestMethod] + public void DI_AUTO_SETTLEMENT_005_AutoSettlementKeepsRemainingPrincipal() + { + const decimal originalNotional = 10012.35m; + const decimal remainingNotional = 5006.17m; + const decimal rate = 0.0299m; + var settlementDate = new DateTime(2026, 7, 14); + var service = new StubEodPositionService(); + var td = CreateTrade(); + td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "01", + SettlementRules = 0 + }); + var position = CreateInterestPosition(); + position.InterestRateDefault = rate; + position.InterestSwapInterval = null; + var previousEod = CreatePreEod(settlementDate.AddDays(-1), 2.460947197259m, 1.64m); + previousEod.TdInterestPrincipal = remainingNotional; + var staleAggregate = new eod_swap { NotionalValue = originalNotional }; + + var result = service.ExecuteSaveAutoEodInterestPosition( + previousEod, position, td, settlementDate, + new IntervalModel { Date = settlementDate, Rate = rate, Settlement = 1 }, + staleAggregate, remainingNotional, originalNotional); + + AssertDecimal(2.87m, result.TdCloseInterest, + "Automatic settlement must round the half-position interest to 2.87"); + AssertDecimal(remainingNotional, result.TdInterestPrincipal, + "Automatic settlement must not restore the original principal from eod_swap"); + } + + [TestMethod] + public void DI_AUTO_SETTLEMENT_006_CloseAndAutoSettlementOnlySettlesRemainder() + { + var settleDate = new DateTime(2026, 7, 16); + var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m); + var service = new StubEodPositionService + { + AutoInterests = new List { autoFlow } + }; + var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m); + closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓; + + var result = service.ExecuteSaveAutoEodWithCloseInterestPosition( + CreatePreEod(settleDate.AddDays(-1), 0m), CreateInterestPosition(), CreateTrade(), + settleDate, new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 }, + 50m, 0m, new List { closeFlow }, 50m, true); + + AssertDecimal(0.73m, autoFlow.InterestAmount, + "Automatic settlement must deduct the 0.50 already settled by the close"); + AssertDecimal(0.73m, autoFlow.InterestClosePnL, + "The automatic flow PnL must use the actual 2-decimal remainder"); + AssertDecimal(1.23m, result.TdCloseInterest, + "EOD realized interest must include both manual and automatic settlements"); + AssertDecimal(0.0045m, result.InterestIncomeSum, + "The high-precision total less actual settlements must remain unrealized"); + AssertDecimal(1.23m, result.RealizedInterest, + "Cumulative realized interest must add the combined actual settlement once"); + } + + [TestMethod] + public void DI_AUTO_SETTLEMENT_007_PayLegKeepsUnsignedSettlementAndAppliesDirectionOnce() + { + var settleDate = new DateTime(2026, 7, 16); + var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m); + autoFlow.InterestDirection = (int)SwapDirectionEnum.支付; + autoFlow.InterestClosePnL = -1.2345m; + var service = new StubEodPositionService + { + AutoInterests = new List { autoFlow } + }; + var position = CreateInterestPosition(); + position.InterestDirection = (int)SwapDirectionEnum.支付; + var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m); + closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓; + closeFlow.InterestDirection = (int)SwapDirectionEnum.支付; + closeFlow.InterestClosePnL = -0.50m; + + var result = service.ExecuteSaveAutoEodWithCloseInterestPosition( + CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate, + new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 }, + 50m, 0m, new List { closeFlow }, 50m, true); + + AssertDecimal(0.73m, autoFlow.InterestAmount); + AssertDecimal(-0.73m, autoFlow.InterestClosePnL); + AssertDecimal(1.23m, result.TdCloseInterest, + "TdCloseInterest follows the unsigned settlement convention used by other interest branches"); + AssertDecimal(0.0045m, result.InterestIncomeSum); + AssertDecimal(-1.23m, result.RealizedInterest, + "The pay direction must be applied exactly once when cumulative realized interest is stored"); + } + + [TestMethod] + public void DI_AUTO_SETTLEMENT_008_MarginLegAppliesReversedDirectionOnce() + { + var settleDate = new DateTime(2026, 7, 16); + var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m); + autoFlow.InterestMode = (int)InterestModeEnum.初始预付金; + var service = new StubEodPositionService + { + AutoInterests = new List { autoFlow } + }; + var position = CreateInterestPosition(); + position.InterestMode = (int)InterestModeEnum.初始预付金; + position.InterestPrincipalFix = 50m; + var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m); + closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓; + closeFlow.InterestMode = (int)InterestModeEnum.初始预付金; + + var result = service.ExecuteSaveAutoEodWithCloseInterestPosition( + CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate, + new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 }, + 50m, 0m, new List { closeFlow }, 50m, true); + + AssertDecimal(0.73m, autoFlow.InterestAmount); + AssertDecimal(0.73m, autoFlow.InterestClosePnL); + AssertDecimal(1.23m, result.TdCloseInterest); + AssertDecimal(0.0045m, result.InterestIncomeSum); + AssertDecimal(-1.23m, result.RealizedInterest, + "A received margin principal produces payable interest, so the margin ratio reverses once"); + } + + [TestMethod] + public void DI_MANUAL_CLOSE_006_FinalCloseIncludesCloseDateInterest() + { + const decimal originalNotional = 10012.35m; + const decimal remainingNotional = 5006.17m; + const decimal rate = 0.0299m; + const decimal pendingInterest = 0.411136145205m; + const decimal expectedInterest = 0.821230619178m; + var closeDate = new DateTime(2026, 7, 16); + var service = new StubEodPositionService(); + var td = CreateTrade(); + td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "01", + SettlementRules = 0 + }); + var position = CreateInterestPosition(); + position.InterestRateDefault = rate; + position.InterestSwapInterval = null; + var previousEod = CreatePreEod(closeDate.AddDays(-1), pendingInterest, 4.51m); + previousEod.TdInterestPrincipal = remainingNotional; + var previousFloatingPosition = new eod_swap_position + { + PosiDirection = (int)SwapDirectionEnum.支付, + PosiNotionalValue = remainingNotional + }; + var previousAggregate = new eod_swap + { + NotionalValue = originalNotional, + NotionalValueLong = remainingNotional + }; + var orginPv = SwapDealService.ResolveUnwindPreviousNotional( + previousAggregate, new List { previousEod, previousFloatingPosition }, + remainingNotional); + + AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional( + previousAggregate, Array.Empty(), originalNotional), + "Missing details must fall back to the aggregate directional notionals"); + AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional( + null, null, remainingNotional), + "Missing EOD data must fall back to the current notional"); + AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional( + new eod_swap { NotionalValue = originalNotional }, Array.Empty(), + remainingNotional), + "Zero directional notionals must not override a non-zero current remaining notional"); + + var result = new SwapDealService(service).GetInterests( + td, td.trade_extend, closeDate, closeDate, + new List { previousEod }, new List { position }, + remainingNotional, remainingNotional, 0m, remainingNotional, 1m, + (int)SwapEventTypeEnum.平仓, false, false, 1m, orginPv, + false, settment: false, newCalcLast: false, closeList: null).Single(); + + AssertDecimal(remainingNotional, result.InterestPrincipal, + "Final close must accrue on the remaining principal"); + AssertDecimal(expectedInterest, result.InterestAmount, + "InterestCalcMode 01 must include the final close date"); + AssertDecimal(0.82m, Math.Round(result.InterestAmount, ConsGlobal.MoneyRound, + MidpointRounding.AwayFromZero), "Final close cash interest must be 0.82"); + } + /// /// [DI_MATURITY_SETTLEMENT_001] 到期日存在手动互换但未带齐待实现时不能清零; /// 当前事件按两位覆盖全部可结金额后,才可视为最终结算并清零。 diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 20237217..70a5e50e 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -565,7 +565,7 @@ namespace YLErp.Modules.SwapModule var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金 var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue); var posiNotionalValue = stockEqvNotional * closePercent;//剩余名义本金 - var orginPv = lastEod != null ? lastEod.NotionalValue : stockEqvNotional; + var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional); var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice; var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成).ToList(); bool tdClose = closeList.Count > 0; @@ -607,6 +607,29 @@ namespace YLErp.Modules.SwapModule }).ToList(); } + public static decimal ResolveUnwindPreviousNotional( + eod_swap lastEod, + IEnumerable lastEodPositions, + decimal currentNotional) + { + var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList(); + decimal previousNotional; + if (floatingPositions?.Count > 0) + { + previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue); + } + else + { + previousNotional = lastEod == null + ? currentNotional + : Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort); + } + + return previousNotional == 0m && currentNotional != 0m + ? currentNotional + : previousNotional; + } + /// /// 获取利息腿"已通过历史互换结出的累计利息"(用于复利重算时扣除,类比分红的 CalcConsumedDividend)。 /// 数据源为事件级 swap_flow_event.InterestAmount(互换/自动互换 完成态事件,互换当时即落库,不依赖日终归档)。 diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 8ca05ad6..cb0458b7 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1161,13 +1161,7 @@ namespace YLErp.Modules.SwapModule } var tradeExtend = td.trade_extend.ExtendObj; - decimal oriPosiNotionalValue = posiLongNotional + posiShortNational; - decimal posiNotionalValue = oriPosiNotionalValue; - bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString(); - if (lastEodSwap != null) - { - posiNotionalValue = lastEodSwap.NotionalValue; - } + decimal posiNotionalValue = posiLongNotional + posiShortNational; decimal closePercent = 1; decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 if (marginTypes.Contains(position.InterestMode)) @@ -1293,10 +1287,11 @@ namespace YLErp.Modules.SwapModule var lastInterestFeeSum = eodPayPosition?.InterestFeeSum ?? 0m; var lastRealizedInterest = eodPayPosition?.RealizedInterest ?? 0m; var lastRealizedInterestFee = eodPayPosition?.RealizedInterestFee ?? 0m; - eodPayPosition = new eod_swap_position(); + // 保留上一日日终标识和计息上下文,部分平仓只从 ValueDate 之后续算,不能重置到交易起始日。 + eodPayPosition = eodPayPosition?.Clone() ?? new eod_swap_position(); eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; - // CalcSwapInterests 按 PositionId 查找上一日日终;id 仍保持 0,沿用盘中平仓的原有计息日期语义。 + // CalcSwapInterests 按 PositionId 匹配上一日日终。 eodPayPosition.PositionId = position.id; eodPayPosition.PosiStartDate = td.StartDate.Value; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; @@ -1338,7 +1333,17 @@ namespace YLErp.Modules.SwapModule preEodPositions.Add(eodPayPosition); 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); + decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount); + decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount); + decimal autoSettledInterestAmount = 0m; + if (autoSwap && interests.Count > 0) + { + autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount); + var autoInterest = interests[0]; + autoInterest.InterestAmount = autoSettledInterestAmount; + autoInterest.InterestClosePnL = autoSettledInterestAmount + * (autoInterest.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m); + } newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); @@ -1359,7 +1364,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.interest_rest_days = position.interest_rest_days; newEodPayPosition.interest_rule = position.interest_rule; //利息端估值用信息 - newEodPayPosition.TdInterestPrincipal = interests.Count > 0 ? interests.First().InterestPrincipal : 0; + newEodPayPosition.TdInterestPrincipal = position.InterestMode == (int)InterestModeEnum.标的期初全价 + ? posiNotionalValue + : interests.Count > 0 ? interests.First().InterestPrincipal : 0; if (interval != null) { newEodPayPosition.TdInterestRate = interval.Rate; @@ -1371,7 +1378,7 @@ namespace YLErp.Modules.SwapModule //当日已实现,平仓时已处理 newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; - newEodPayPosition.TdCloseInterest = flowEvents.Sum(s => s.InterestClosePnL); + newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount; var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); if (position.IsAnnualized) { @@ -1390,7 +1397,8 @@ namespace YLErp.Modules.SwapModule } else { - newEodPayPosition.InterestIncomeSum = InterestAmount; + newEodPayPosition.InterestIncomeSum = RoundEodInterest( + interestAmountBeforeSettlement - newEodPayPosition.TdCloseInterest); newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; } //持仓内容-利息腿-损益统计(本方视角) From 8ed4bc0f2f310552ebc8e846b3802d6ccc296dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 31 Jul 2026 09:18:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(swaptrade):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=80=BA=E5=88=B8=E4=BB=B7=E6=A0=BC=E8=BE=93=E5=85=A5=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E5=92=8C=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 绑定三个债券价格字段到当前Vue方法,确保正确的v-model和事件监听器 - 修复精度输入组件的事件契约,保留keydown、input、enter事件处理逻辑 - 解决格式刷新时不覆盖未提交输入的问题,维护用户输入状态一致性 - 将模板中的@change事件替换为@blur事件,改进输入焦点处理逻辑 --- .../fe-tests/bondCalc.integration.test.js | 62 +++++++++++++++++++ .../app/swaptrade/swapPricePrecisionHelper.js | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/YLErpWeb/fe-tests/bondCalc.integration.test.js b/YLErpWeb/fe-tests/bondCalc.integration.test.js index b1a88e9f..1a39b62a 100644 --- a/YLErpWeb/fe-tests/bondCalc.integration.test.js +++ b/YLErpWeb/fe-tests/bondCalc.integration.test.js @@ -47,6 +47,68 @@ global.main = { const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js'); +describe('TradeEdit bond price input bindings', () => { + const view = fs.readFileSync(path.join(__dirname, '..', 'Views', 'SwapTrade2', 'TradeEdit.cshtml'), 'utf8'); + const methods = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapTradeEdit.js'), 'utf8'); + const precisionHelper = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapPricePrecisionHelper.js'), 'utf8'); + + test('binds the three bond price fields to current Vue methods', () => { + expect(view).toMatch(/v-model="item\.PosiGrossPrice"[^>]*v-on:input="onDpPriceInput\(item\)"/); + expect(view).toMatch(/v-model="item\.PosiNetNoFeePrice"[^>]*v-on:input="onBondPriceEdit\(item,'CP'\)"/); + expect(view).toMatch(/v-model="item\.InitYtm"[^>]*v-on:input="onBondPriceEdit\(item,'YD'\)"/); + expect(view).not.toContain('v-on:input="onBondPriceInput('); + expect(methods).toMatch(/^\s*onBondPriceEdit\s*\(/m); + }); + + test('precision input preserves the keydown, input, enter event contract', () => { + const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({}); + const component = helper.createVueInputComponent(); + expect(component.template).toContain('@blur="onChange"'); + expect(component.template).not.toContain('@change="onChange"'); + const emitted = []; + const vm = { + text: '99.5', + enterPressed: false, + format: { precision: 4, percent: true }, + $emit: (event, value) => emitted.push({ event, value }) + }; + Object.keys(component.methods).forEach(name => { + vm[name] = component.methods[name].bind(vm); + }); + const target = { + value: vm.text, + blur: () => vm.onChange({ target }) + }; + + vm.onKeydown({ keyCode: 13, target }); + + expect(emitted.map(x => x.event)).toEqual(['keydown', 'input', 'enter']); + expect(emitted[1].value).toBe('0.995'); + expect(emitted[2].value).toBe('0.995'); + }); + + test('equal format refresh does not overwrite uncommitted input', () => { + const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({}); + const component = helper.createVueInputComponent(); + const format = { precision: 4, percent: true }; + const vm = { + value: '0.995', + text: '99.5', + format, + formatSnapshot: JSON.stringify(format), + $emit: () => {} + }; + Object.keys(component.methods).forEach(name => { + vm[name] = component.methods[name].bind(vm); + }); + + vm.onInput({ target: { value: '99.51' } }); + component.watch.format.handler.call(vm); + + expect(vm.text).toBe('99.51'); + }); +}); + // ============================================================================ // 根因 1:vue-number-input keydown 事件 emit 链路 // ============================================================================ diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js index be9e3ed6..2150a5de 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js @@ -242,7 +242,7 @@ var swapPricePrecision = (function (global) { } } }, - template: '' + template: '' }; } From a507d0ce66817452168d1b02a42d69f141c82221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 31 Jul 2026 16:26:06 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=88=A9?= =?UTF-8?q?=E7=8E=87=E6=94=B6=E7=9B=8A=E7=8E=87=20=E5=80=BA=E5=88=B8?= =?UTF-8?q?=E6=8C=87=E6=95=B0=E5=B0=8F=E6=95=B0=E7=82=B9=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpWeb/App_Data/Config/swappriceprecision.js | 2 ++ .../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/YLErpWeb/App_Data/Config/swappriceprecision.js b/YLErpWeb/App_Data/Config/swappriceprecision.js index b76e12d5..ca3cca8a 100644 --- a/YLErpWeb/App_Data/Config/swappriceprecision.js +++ b/YLErpWeb/App_Data/Config/swappriceprecision.js @@ -41,6 +41,8 @@ window.main.swapPricePrecision = { ExRate: { integerDigits: 2, precision: 8 }, Shibor: { integerDigits: 2, precision: 4 }, FixingRepoRate: { integerDigits: 2, precision: 4 }, + RateYield: {integerDigits: 6, precision: 8}, + BondIndex: {integerDigits: 6, precision: 4}, // TODO: 利率收益率(6+8)、债券指数(6+4)、黄金期货(6+4)待对应的 UnderlyingInstrumentType 枚举确认后启用。 }; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js index 43350695..6ff80ccc 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js @@ -39,7 +39,9 @@ var swapPricePrecision = (function (global) { AbroadStockIndex: { integerDigits: 8, precision: 4 }, ExRate: { integerDigits: 2, precision: 8 }, Shibor: { integerDigits: 2, precision: 4 }, - FixingRepoRate: { integerDigits: 2, precision: 4 } + FixingRepoRate: { integerDigits: 2, precision: 4 }, + RateYield: {integerDigits: 6, precision: 8}, + BondIndex: {integerDigits: 6, precision: 4}, // TODO: Add InterestYield, BondIndex and GoldFutures after their enum values are confirmed. }); From 2eba4ce2a9c90c72e8a592752c46e1449d2b6563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 31 Jul 2026 16:57:49 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(settlement):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A0=87=E7=9A=84=E7=B1=BB=E5=9E=8B=E5=88=A4=E6=96=AD=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E6=94=AF=E6=8C=81=E4=BB=B7=E6=A0=BC=E8=A1=A8=E5=88=86?= =?UTF-8?q?=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ConsGlobal中为所有标的类型常量添加中文注释说明 - 新增UseStockPriceTable方法用于判断标的类型是否使用股票价格表 - 修改SettlementPriceImportService中的价格表分流逻辑 - 添加单元测试验证股指期货、基金等进入股票价格表的逻辑 - 确保Shibor、利率收益率和债券指数进入正确的商品期货价格表 --- Framework/YLErp.Core/ConsGlobal.cs | 54 +++++++++---------- .../Modules/EodModule/EodPriceDtoTest.cs | 15 ++++++ .../EodModule/SettlementPriceImportService.cs | 8 ++- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/Framework/YLErp.Core/ConsGlobal.cs b/Framework/YLErp.Core/ConsGlobal.cs index 8e078bdf..5cb686b3 100644 --- a/Framework/YLErp.Core/ConsGlobal.cs +++ b/Framework/YLErp.Core/ConsGlobal.cs @@ -109,33 +109,33 @@ namespace YLErp /// public static class InstrumentType { - public const string Stock = "Stock"; - public const string StockIndex = "StockIndex"; - public const string StockIF = "StockIF"; - public const string CommoditySpot = "CommoditySpot"; - public const string CommodityFutures = "CommodityFutures"; - public const string NewOtcStock = "NewOtcStock"; - public const string HKStock = "HKStock"; - public const string HKStockIndex = "HKStockIndex"; - public const string Fund = "Fund"; - public const string TBonds = "TBonds"; - public const string CreditBonds = "CreditBonds"; //信用债 - public const string OtherBonds = "OtherBonds"; //其它债券 - public const string Bonds = "Bond"; //债券 - public const string GoldFutures = "GoldFutures"; - public const string TBFutures = "TBFutures"; - public const string OtherFutures = "OtherFutures"; - public const string GoldSpot = "GoldSpot"; - public const string OtherSpot = "OtherSpot"; - public const string AbroadFutures = "AbroadFutures"; - public const string AbroadSpot = "AbroadSpot"; - public const string AbroadStock = "AbroadStock"; - public const string AbroadStockIndex = "AbroadStockIndex"; - public const string ExRate = "ExRate"; - public const string Shibor = "Shibor"; - public const string FixingRepoRate = "FixingRepoRate"; - public const string OtherRate = "OtherRate"; - public const string RateYield = "RateYield"; //利率收益率 + public const string Stock = "Stock"; // 股票 + public const string StockIndex = "StockIndex"; // 股指 + public const string StockIF = "StockIF"; // 股指期货 + public const string CommoditySpot = "CommoditySpot"; // 商品现货 + public const string CommodityFutures = "CommodityFutures"; // 商品期货 + public const string NewOtcStock = "NewOtcStock"; // 新三板挂牌股票 + public const string HKStock = "HKStock"; // 香港股票 + public const string HKStockIndex = "HKStockIndex"; // 香港股指 + public const string Fund = "Fund"; // 基金及基金专户 + public const string TBonds = "TBonds"; // 利率债 + public const string CreditBonds = "CreditBonds"; // 信用债 + public const string OtherBonds = "OtherBonds"; // 其它债券 + public const string Bonds = "Bond"; // 债券 + public const string GoldFutures = "GoldFutures"; // 黄金期货 + public const string TBFutures = "TBFutures"; // 国债期货 + public const string OtherFutures = "OtherFutures"; // 其他期货 + public const string GoldSpot = "GoldSpot"; // 黄金现货 + public const string OtherSpot = "OtherSpot"; // 其他现货 + public const string AbroadFutures = "AbroadFutures"; // 境外期货 + public const string AbroadSpot = "AbroadSpot"; // 境外现货 + public const string AbroadStock = "AbroadStock"; // 境外股票 + public const string AbroadStockIndex = "AbroadStockIndex"; // 境外股指 + public const string ExRate = "ExRate"; // 汇率 + public const string Shibor = "Shibor"; // Shibor + public const string FixingRepoRate = "FixingRepoRate"; // 银行间回购定盘 + public const string OtherRate = "OtherRate"; // 其他利率 + public const string RateYield = "RateYield"; // 利率收益率 public const string BondIndex = "BondIndex"; // 债券指数 //public const string OtherUnderlying = "OtherUnderlying"; diff --git a/UnitTestProject/Modules/EodModule/EodPriceDtoTest.cs b/UnitTestProject/Modules/EodModule/EodPriceDtoTest.cs index 459ba7af..f1053933 100644 --- a/UnitTestProject/Modules/EodModule/EodPriceDtoTest.cs +++ b/UnitTestProject/Modules/EodModule/EodPriceDtoTest.cs @@ -85,6 +85,21 @@ namespace YLErp.Modules.EodModule #endregion + #region 手工上传日终价格落表分流 + + [TestMethod] + [Description("股指期货和基金进入股票价格表,Shibor、利率收益率和债券指数进入商品期货价格表")] + public void 手工上传_按标的类型选择股票价格表() + { + Assert.IsTrue(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.StockIF)); + Assert.IsTrue(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.Fund)); + Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.Shibor)); + Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.RateYield)); + Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.BondIndex)); + } + + #endregion + #region 问题3:债券数据来源按是否手工改过区分"人工"/"系统" [TestMethod] diff --git a/YLErpDAL/Modules/EodModule/SettlementPriceImportService.cs b/YLErpDAL/Modules/EodModule/SettlementPriceImportService.cs index 594edcb4..db79110a 100644 --- a/YLErpDAL/Modules/EodModule/SettlementPriceImportService.cs +++ b/YLErpDAL/Modules/EodModule/SettlementPriceImportService.cs @@ -14,6 +14,12 @@ namespace YLErp.Modules.EodModule } + public static bool UseStockPriceTable(string instrumentType) + { + return ConsGlobal.InstrumentType.EquityTypes().Contains(instrumentType) + || instrumentType == ConsGlobal.InstrumentType.Fund; + } + /// /// 导入xlsx数据 /// @@ -134,7 +140,7 @@ namespace YLErp.Modules.EodModule EodPriceService.StampBondOperator(eodPrice, UserId, eodPrice.id == 0); result.SuccessCount++; } - else if (!underlying.CalcTypeIsStock()) + else if (!UseStockPriceTable(underlying.UnderlyingInstrumentType)) { var eodPrice = DbContext.eod_commodity_future_price .FirstOrDefault(p => p.ValueDate == item.date && p.UnderlyingCode == underlying.UnderlyingCode);