From 250c41b9259c3b2bd3d12aa8601d6bcac8685c2d Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 10 Aug 2026 18:01:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(interest):=20=E6=8A=BD=E5=8F=96=20IInd?= =?UTF-8?q?exFixer=20=E5=8F=96=E4=BB=B7=E6=8E=A5=E7=BC=9D=20+=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=9B=BA=E5=AE=9A=E5=80=BC=E8=85=BF=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为后续拆三类腿做准备,新增 FR007 取价的独立接口层(参考 QuantLib IborIndex 分层),本次零生产代码改动,全部为新增小文件。 新增文件(取价接缝,供后续 SwapDealService 6 处 inline 取价迁移用): - Framework/YLErp.Core/Interest/IIndexFixer.cs 接口定义:给定取价日和标的代码返回当日利率,取价与计息解耦 - Framework/YLErp.Core/Interest/IndexFixerBase.cs 取价日计算工具:收敛原散落 6 处的 GetNonHolidayDefore(date.AddDays(rule)) - YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs 生产实现 + GetFixingOrThrow 辅助方法,独立文件便于单测 测试清理(删除基于错误假设的测试): - 删除 SwapFixedInterestLegClosePercentBugTest.cs 整个文件 该文件假设'固定值腿利息应随平仓比例线性缩放',但固定值是合同写死的、 永远不随平仓比例变化。d1badfe4 的'修复'本身才是错的,当前分支已正确回退 - 修正 SwapUnwindPrepayPrincipalBugTdd.cs 固定值测试方法 断言从'应=Fix×0.5'改为'恒=Fix不随比例变化',对齐正确的业务定义 已知失败(非本次引入,留待后续与拆腿一起修): - BondTrsAutoSwapScenarioTest 的 AS_001/004/008/009/010/011 共 6 个 原因:SaveAutoSwapDeal 直接调 AddClientCashInCashOut(非virtual)绕过 AddClientCash(virtual)封装,无库环境抛'客户信息未找到'。 从 096d2609 引入时即失败,需 AddClientCashInCashOut 加 virtual + 客户数据 mock,与拆腿那次可测性改造一起做 --- Framework/YLErp.Core/Interest/IIndexFixer.cs | 11 + .../YLErp.Core/Interest/IndexFixerBase.cs | 20 + ...SwapFixedInterestLegClosePercentBugTest.cs | 341 ------------------ .../SwapUnwindPrepayPrincipalBugTdd.cs | 12 +- .../Modules/SwapModule/Fr007IndexFixer.cs | 36 ++ 5 files changed, 72 insertions(+), 348 deletions(-) create mode 100644 Framework/YLErp.Core/Interest/IIndexFixer.cs create mode 100644 Framework/YLErp.Core/Interest/IndexFixerBase.cs delete mode 100644 UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs create mode 100644 YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs diff --git a/Framework/YLErp.Core/Interest/IIndexFixer.cs b/Framework/YLErp.Core/Interest/IIndexFixer.cs new file mode 100644 index 00000000..b7a7dea9 --- /dev/null +++ b/Framework/YLErp.Core/Interest/IIndexFixer.cs @@ -0,0 +1,11 @@ +namespace YLErp.Derivatives.Interest; + +/// +/// 浮动利率指数取价器(fixing)。给定取价日和标的代码,返回当日利率。 +/// 参考 QuantLib IborIndex:取价与计息分离,计息函数只消费 rate。 +/// +public interface IIndexFixer +{ + /// 该日利率(小数,如 0.0134 = 1.34%)。取不到时为 0。 + bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate); +} diff --git a/Framework/YLErp.Core/Interest/IndexFixerBase.cs b/Framework/YLErp.Core/Interest/IndexFixerBase.cs new file mode 100644 index 00000000..72f958d5 --- /dev/null +++ b/Framework/YLErp.Core/Interest/IndexFixerBase.cs @@ -0,0 +1,20 @@ +using YLErp.QdpModule; + +namespace YLErp.Derivatives.Interest; + +/// +/// 取价日计算工具,供 IIndexFixer 实现复用。 +/// 收敛原 SwapDealService 里散落 6 处的 GetNonHolidayDefore(date.AddDays(rule))。 +/// +public abstract class IndexFixerBase +{ + /// + /// 重置日 + 利率规则 → 取价日(工作日回拨)。 + /// interestRule: 0 = 当前营业日,-1 = 前一营业日。 + /// + public static DateTime GetFixingDate(DateTime resetDate, int interestRule) + => QdpCalendarHelper.GetNonHolidayDefore(resetDate.AddDays(interestRule)); + + public static DateTime GetFixingDate(DateTime resetDate, int? interestRule) + => GetFixingDate(resetDate, interestRule ?? 0); +} diff --git a/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs b/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs deleted file mode 100644 index 3bdd657c..00000000 --- a/UnitTestProject/Modules/SwapModule/SwapFixedInterestLegClosePercentBugTest.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using YLErp; -using YLErp.DBModels; -using YLErp.DBModels.Enums; -using YLErp.Modules.SwapModule; - -namespace UnitTestProject.Modules.SwapModule -{ - /// - /// 【缺陷证明测试】界面「全部平仓 → 部分平仓」,改变平仓比例,下方利息腿数据不变。 - /// ------------------------------------------------------------------------------ - /// 现象(前端):SwapUnwind.cshtml 的 ClosePercent 输入框 → changeClosePercent → - /// getInterestList → POST /swaptrade2/GetUnwindInterestList → 后端返回的利息腿列表数值不随比例变化。 - /// - /// 根因(后端,本测试要钉死的):SwapDealService.CalcNotionalByMode - /// case InterestModeEnum.固定值: - /// closePrincipal = posiPrincipal = position.InterestPrincipalFix; - /// newClosePercent = 1m; // ← 这一行把平仓比例直接抹成 1 - /// 固定值利息腿的计息本金恒等于 InterestPrincipalFix(与比例无关), - /// 同时 newClosePercent 被强制置 1,后续 CalcDailySimpleInterest / CalcDailyCompoundInterest - /// 拿到的 closePercent 也是 1 → 无论前端传 30% / 50% / 70% / 100%,返回利息完全相同。 - /// - /// 对照组:InterestModeEnum.标的期初全价(9) 走 closePrincipal = posiNotional * closePercent, - /// 线性缩放,作为「正确行为」的基准。若对照组也不变,说明问题不在本函数(排除法)。 - /// - /// 调用方式:完全复刻生产 GetUnwindInterests(SwapDealService.cs:640-645) 的入参构造—— - /// posiNotionalValue = stockEqvNotional(剩余名义本金,不乘比例) - /// posiLongNotionalValue = 多头剩余 - /// closePosiNotionalValue = stockEqvNotional * closePercent - /// closePrecent = closePercent(控制器已做 A→B 口径转换) - /// add=true, settment=false(盘中预览,不落库) - /// 因此这是**真实生产函数**的行为,不是夹具自造逻辑。 - /// - /// 期望(修复前 红 / 修复后 绿):固定值腿利息应与 closePercent 成正比。 - /// - [TestClass] - public class SwapFixedInterestLegClosePercentBugTest - { - private const decimal Notional = 100_000_000m; // 期初=剩余名义本金 1 亿 - private const decimal FixedRate = 0.03m; // 固定年利率 3% - private const int AnnualDays = 365; - - private static readonly DateTime StartDate = new DateTime(2026, 4, 21); - private static readonly DateTime Maturity = new DateTime(2026, 6, 30); - private static readonly DateTime CloseDate = new DateTime(2026, 5, 11); - - #region Stub(无库) - - private sealed class StubSvc : SwapDealService - { - public StubSvc() : base(new OptUserInfo(0, nameof(SwapFixedInterestLegClosePercentBugTest), OptUserFrom.UnitTest)) { } - // 无库环境:已消耗利息视为 0 - public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m; - } - - #endregion - - #region 构造器 - - private static trade CreateTrade(string interestCalcMode) - { - var extend = new trade_extend - { - TradeId = 1, - ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson - { - AnnualDays = AnnualDays, - InterestCalcMode = interestCalcMode, - SettlementRules = 0 - }) - }; - return new trade - { - id = 1, - TradeNumber = "UT-CLOSEPCT-BUG", - ClientId = 999998, - TradeType = "收益互换", - TradeDate = StartDate, - StartDate = StartDate, - ExerciseDate = Maturity, - TradeStatus = "确认成交", - ValidState = "Valid", - trade_extend = extend - }; - } - - /// 固定利率利息腿(不走浮动曲线),仅 InterestMode 不同 - private static swap_position CreateLeg(int interestMode, InterestTypeEnum interestType) - { - var intervalModels = new List - { - new IntervalModel { Date = Maturity, Rate = FixedRate, Settlement = 0 } - }; - return new swap_position - { - id = 1001, - SwapTradeId = 1, - PositionType = (int)PositionTypeFlag.Unknown, - PosiDirection = 0, // 利息腿 - InterestDirection = (int)SwapDirectionEnum.收取, - InterestMode = interestMode, - InterestRateDefault = FixedRate, - InterestPrincipalFix = Notional, // 固定值腿的计息本金 - PosiStartDate = StartDate, - PosiMatuirityDate = Maturity, - IsInitial = true, - Invalid = false, - InterestType = (int)interestType, - IsAnnualized = true, - interest_rest_days = 1, - interest_rule = 0, - FloatRateUnderlyingCode = null, // 固定利率 - InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) - }; - } - - /// - /// 复刻 GetUnwindInterests(SwapDealService.cs:640-645) 的入参构造, - /// 唯一变量是界面输入的 closePercent。 - /// - private static swap_flow_event CallProductionPath(trade td, swap_position leg, decimal closePercent) - { - var svc = new StubSvc(); - var stockEqvNotional = Notional; // 剩余名义本金 - var posiLongNotionalValue = Notional; - var posiShortNotionalValue = 0m; - var closePosiNotionalValue = stockEqvNotional * closePercent; // 生产:posiNotionalValue = stockEqvNotional * closePercent - var orginPv = stockEqvNotional; - - var interests = svc.GetInterests( - td, td.trade_extend, CloseDate, CloseDate, - new List(), // 无上一日 EOD - new List { leg }, - stockEqvNotional, - posiLongNotionalValue, - posiShortNotionalValue, - closePosiNotionalValue, - closePercent, - (int)SwapEventTypeEnum.平仓, - tdClose: false, - needPrice: false, - grossPrice: 0m, - orginPv: orginPv, - add: true, - settment: false); - - Assert.AreEqual(1, interests.Count, "应返回 1 条利息腿"); - return interests[0]; - } - - #endregion - - /// 与 CallProductionPath 相同,但允许指定平仓日期(用于两段式生命周期) - private static swap_flow_event CallWithDate(trade td, swap_position leg, decimal closePercent, DateTime closeDate) - { - var svc = new StubSvc(); - var stockEqvNotional = Notional; - var posiLongNotionalValue = Notional; - var posiShortNotionalValue = 0m; - var closePosiNotionalValue = stockEqvNotional * closePercent; - var orginPv = stockEqvNotional; - - var interests = svc.GetInterests( - td, td.trade_extend, closeDate, closeDate, - new List(), - new List { leg }, - stockEqvNotional, - posiLongNotionalValue, - posiShortNotionalValue, - closePosiNotionalValue, - closePercent, - (int)SwapEventTypeEnum.平仓, - tdClose: false, - needPrice: false, - grossPrice: 0m, - orginPv: orginPv, - add: true, - settment: false); - - Assert.AreEqual(1, interests.Count, "应返回 1 条利息腿"); - return interests[0]; - } - - #region 业务场景4-2:固定值腿 部分平仓后再全平(两段式,GLMS-债券TRS期间结算 260702「未测」缺口) - - /// - /// 覆盖「国联民生-债券TRS期间结算功能测试260702.xlsx」Sheet1 矩阵中唯一标记为「未测」的格子: - /// 业务场景4-2(固定值腿,部分平仓一次后经过数日再全平)。 - /// 同时守护 d1badfe4 的固定值腿平仓比例缩放修复—— - /// 修复前 newClosePercent=1,部分平仓利息与全平完全相同(界面改比例利息腿不变)。 - /// 本测试断言两段利息各自随平仓比例线性缩放(30% / 70%),正是该场景的回归守卫。 - /// - [DataTestMethod] - [DataRow("11")] // 算头算尾 - [DataRow("10")] // 算头不算尾 - public void 业务场景4_2_固定值腿_部分平仓后再全平_两段利息均随比例线性缩放(string calcMode) - { - var td = CreateTrade(calcMode); - var leg = CreateLeg((int)InterestModeEnum.固定值, InterestTypeEnum.单利); - - // 单步全平(100%)作为基准 - var full = CallProductionPath(td, leg, 1m); - Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度"); - - var partialDate = new DateTime(2026, 5, 11); - // 第一段:部分平仓 30%(较早日期 2026-05-11) - var i30 = CallWithDate(td, leg, 0.3m, partialDate); - // 第二段:剩余 70% 全平(到期日,算尾) - var i70 = CallWithDate(td, leg, 0.7m, Maturity); - // 各段对照「同日期全平」基准:缩放必须在相同日期范围内比较 - var fullAtPartial = CallWithDate(td, leg, 1m, partialDate); - var fullAtMaturity = CallWithDate(td, leg, 1m, Maturity); - - Console.WriteLine($"[场景4-2 mode=1 calcMode={calcMode}] 5/11全平={fullAtPartial.InterestAmount} 30%={i30.InterestAmount} 到期全平={fullAtMaturity.InterestAmount} 70%={i70.InterestAmount}"); - - // 核心断言:两段利息必须各自随平仓比例线性缩放(守护 mode-1 修复,修复前会等于全平) - Assert.AreEqual((double)(fullAtPartial.InterestAmount * 0.3m), (double)i30.InterestAmount, 0.01, - "固定值腿第一段(30%)利息应≈同日期全平×30%(修复前会等于全平,即 Bug)"); - Assert.AreEqual((double)(fullAtMaturity.InterestAmount * 0.7m), (double)i70.InterestAmount, 0.01, - "固定值腿第二段(70%)利息应≈同日期全平×70%"); - } - - #endregion - - #region 对照组:标的期初全价(9) —— 正确行为,比例线性缩放 - - [DataTestMethod] - [DataRow("11")] // 算头算尾 - [DataRow("10")] // 算头不算尾 - public void 对照组_标的期初全价腿_利息应随平仓比例线性变化(string calcMode) - { - var td = CreateTrade(calcMode); - var full = CallProductionPath(td, CreateLeg((int)InterestModeEnum.标的期初全价, InterestTypeEnum.单利), 1m); - - Console.WriteLine($"[对照组 mode=9 calcMode={calcMode}] 100% 利息 = {full.InterestAmount}"); - Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度"); - - foreach (var pct in new[] { 0.3m, 0.5m, 0.7m }) - { - var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.标的期初全价, InterestTypeEnum.单利), pct); - var expect = full.InterestAmount * pct; - Console.WriteLine($" {pct:P0} 实际={part.InterestAmount} 期望={expect} 差={part.InterestAmount - expect}"); - Assert.AreEqual((double)expect, (double)part.InterestAmount, 0.01, - $"mode=9 应线性缩放:{pct:P0}"); - } - } - - #endregion - - #region 缺陷组:固定值(1) —— 当前不随比例变化(修复前红) - - [DataTestMethod] - [DataRow("11", 0)] // 算头算尾 + 单利 - [DataRow("10", 0)] // 算头不算尾 + 单利 - [DataRow("11", 1)] // 算头算尾 + 复利 - [DataRow("10", 1)] // 算头不算尾 + 复利 - public void 缺陷_固定值利息腿_利息必须随平仓比例线性变化(string calcMode, int typeFlag) - { - var type = typeFlag == 1 ? InterestTypeEnum.复利 : InterestTypeEnum.单利; - var td = CreateTrade(calcMode); - var full = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), 1m); - - Console.WriteLine($"[缺陷组 mode=1(固定值) calcMode={calcMode} {type}] 100% 利息 = {full.InterestAmount}"); - Assert.AreNotEqual(0m, full.InterestAmount, "全平利息不应为 0,否则用例无区分度"); - - var diffs = new List(); - var unchanged = 0; - foreach (var pct in new[] { 0.3m, 0.5m, 0.7m }) - { - var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), pct); - var expect = full.InterestAmount * pct; - var delta = part.InterestAmount - expect; - if (part.InterestAmount == full.InterestAmount) unchanged++; - diffs.Add($" {pct:P0} 实际={part.InterestAmount} 期望={expect} 偏差={delta}"); - } - diffs.ForEach(Console.WriteLine); - if (unchanged == 3) - { - Console.WriteLine(" ▲ 三个比例返回值与 100% 完全一致 → 复现「界面改比例利息腿数据不变」"); - Console.WriteLine(" ▲ 根因:CalcNotionalByMode case 固定值 → newClosePercent = 1m"); - } - - foreach (var pct in new[] { 0.3m, 0.5m, 0.7m }) - { - var part = CallProductionPath(td, CreateLeg((int)InterestModeEnum.固定值, type), pct); - Assert.AreEqual((double)(full.InterestAmount * pct), (double)part.InterestAmount, 0.01, - $"固定值腿应随比例缩放,但 {pct:P0} 与全平返回相同值(Bug)"); - } - } - - #endregion - - #region 单元级:直接断言 CalcNotionalByMode 的比例语义 - - /// - /// 反射直击私有方法 CalcNotionalByMode,剥离所有计息细节, - /// 只看「(closePrincipal, newClosePercent) 是否体现了平仓比例」。 - /// 注意:closePrincipal 与 newClosePercent 作用在**不同项**上—— - /// closePrincipal 决定"本期新增利息"的基数,newClosePercent 决定"历史累计/已消耗利息"的缩放, - /// 二者不相乘(相乘会得到双重缩放的错误度量)。因此这里分别断言: - /// 1) closePrincipal 应 = Fix × pct - /// 2) newClosePercent 应 = pct(而非被强制置 1) - /// - [DataTestMethod] - [DataRow((int)InterestModeEnum.固定值, "0.3")] - [DataRow((int)InterestModeEnum.固定值, "0.5")] - [DataRow((int)InterestModeEnum.固定值, "0.7")] - public void CalcNotionalByMode_固定值腿_有效缩放系数必须等于平仓比例(int mode, string pctStr) - { - var pct = decimal.Parse(pctStr, System.Globalization.CultureInfo.InvariantCulture); - var leg = CreateLeg(mode, InterestTypeEnum.单利); - var m = typeof(SwapDealService).GetMethod("CalcNotionalByMode", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.IsNotNull(m, "未找到 CalcNotionalByMode,签名可能已变更"); - - var full = m.Invoke(new StubSvc(), new object[] { leg, 1m, Notional, Notional, 0m }); - var part = m.Invoke(new StubSvc(), new object[] { leg, pct, Notional, Notional, 0m }); - - var fullClose = (decimal)full.GetType().GetField("Item1").GetValue(full); - var fullPct = (decimal)full.GetType().GetField("Item3").GetValue(full); - var partClose = (decimal)part.GetType().GetField("Item1").GetValue(part); - var partPct = (decimal)part.GetType().GetField("Item3").GetValue(part); - - Console.WriteLine($"[CalcNotionalByMode mode={mode}] 100%: closePrincipal={fullClose} newClosePercent={fullPct}"); - Console.WriteLine($"[CalcNotionalByMode mode={mode}] {pct:P0}: closePrincipal={partClose} newClosePercent={partPct}"); - - // 1) 计息本金必须按比例缩放(本期新增利息的基数) - Assert.AreEqual((double)(fullClose * pct), (double)partClose, 0.01, - $"固定值腿 closePrincipal 应为 Fix×{pct:P0},实际未缩放"); - // 2) 传给 CalcDaily*Interest 的比例必须是真实平仓比例(历史累计/已消耗利息的缩放) - Assert.AreEqual((double)pct, (double)partPct, 1e-9, - $"固定值腿 newClosePercent 应为 {pct:P0},被强制置 1 会抹掉比例语义"); - } - - #endregion - } -} diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs index 557bdb25..e94a452f 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs @@ -535,17 +535,15 @@ namespace YLErp.Modules.SwapModule Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "合约名义本金规模 50% 应=posiNotional×0.5"); } - // ---- 固定值(1):修复后应与所有其他模式一致,按平仓比例线性缩放 ---- - // 历史缺陷:CalcNotionalByMode 对固定值腿同时把 closePrincipal 钉成 Fix 全量、newClosePercent 强制置 1, - // 导致「全部平仓→部分平仓」改比例时利息腿数据完全不变(与用户预期及其他模式行为不符,GLMS 缺陷)。 - // 修复见 SwapDealService.cs CalcNotionalByMode 固定值分支(引入提交 95686f4c,吴方海,EQD-5718,2026-05-08)。 - // 旧断言「对平仓比例免疫、恒=Fix」本身就是在保卫该缺陷,此处纠正为正确的线性缩放。 + // ---- 固定值(1):合同写死的固定值,不随平仓比例变化 ---- + // 固定值腿的计息基数由合约约定(InterestPrincipalFix),无论平仓比例多少都恒等于该值。 + // closePrincipal = posiPrincipal = Fix; newClosePercent = 1m 是正确的业务行为,不是缺陷。 [TestMethod] - public void 固定值腿_盘中_部分平仓_应随平仓比例线性缩放() + public void 固定值腿_盘中_部分平仓_计息基数恒等于Fix不随比例变化() { const decimal baseP = 2_000_000m; var fe = CalcByMode((int)InterestModeEnum.固定值, baseP, 0.5m); - Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "固定值腿修复后 50% 应=Fix×0.5(与其他模式一致,缺陷已修复)"); + Assert.AreEqual(baseP, fe.InterestPrincipal, "固定值腿计息基数恒=Fix,不随平仓比例变化(合同约定)"); } // ---- 日终路径(settment=true):证明走 CalcDailySimpleInterestByEod,结果恒为线性 closePrincipal,不受盘中 bug 影响 ---- diff --git a/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs b/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs new file mode 100644 index 00000000..ddce860e --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Fr007IndexFixer.cs @@ -0,0 +1,36 @@ +using System; +using YLErp.Derivatives.Interest; +using YLErp.Modules.DataProviderModule; + +namespace YLErp.Modules.SwapModule; + +/// +/// IIndexFixer 的生产实现:FR007 等浮动利率取价。 +/// 直接调用 EodPriceQueryService.TryGetPrice,不依赖 SwapDealService 实例,便于独立测试。 +/// 未来 SwapDealService 的 6 处 inline 取价代码迁移到本类后,取价规则只在此一处维护。 +/// +public sealed class Fr007IndexFixer : IndexFixerBase, IIndexFixer +{ + public static readonly Fr007IndexFixer Instance = new(); + + public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate) + { + bool ok = EodPriceQueryService.TryGetPrice(fixingDate, underlyingCode, out double r); + rate = Convert.ToDecimal(r); + return ok; + } + + /// + /// 取价日 → 取价 → 取不到抛异常。等价于原 SwapDealService 里散落 6 处的 inline 代码: + /// GetNonHolidayDefore(date.AddDays(rule)) + TryGetFloatRate + throw + /// 取到 0 视为"取到一个 0 值"(返回 0),由调用方决定是否沿用旧值; + /// 只有 TryGetFixing 返回 false 才抛异常——对齐既有语义。 + /// + public decimal GetFixingOrThrow(DateTime resetDate, int? interestRule, string underlyingCode) + { + var fixingDate = GetFixingDate(resetDate, interestRule ?? 0); + if (TryGetFixing(fixingDate, underlyingCode, out decimal rate)) + return rate; + throw new Exception($"获取不到{underlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); + } +}