diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260819Fr007TradeDiscoveryTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260819Fr007TradeDiscoveryTest.cs new file mode 100644 index 00000000..21fa137f --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GLMS20260819Fr007TradeDiscoveryTest.cs @@ -0,0 +1,120 @@ +using System; +using System.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using YLErp.DBModels; + +namespace YLErp.Modules.SwapModule +{ + /// + /// EQD-6968 UAT 辅助:从 96 真实库抽取"在途 FR007 互换"具体历史交易, + /// 打印完整交易要素,供 UAT 直接选用(替代手动猜要素)。 + /// 标 [Ignore],手动跑一次即可;依赖 app.config 中 xray 连接(你的环境已指向 96)。 + /// 复用 GLMS20260105GoldenTest 的连库写法:DbContextFactory.GetYLDbContext()。 + /// 用原生 ADO.NET 读结果,规避 EF 实体映射类型踩坑。 + /// + [TestClass] + public class GLMS20260819Fr007TradeDiscoveryTest + { + private const string TradeSql = @" +SELECT + t.id AS TradeId, + p.id AS PositionId, + t.TradeNumber AS TradeNumber, + t.StartDate AS StartDate, + t.ExerciseDate AS ExerciseDate, + t.ValidState AS ValidState, + p.interest_rest_days AS interest_rest_days, + p.interest_rule AS interest_rule, + p.FloatRateUnderlyingCode AS FloatRateUnderlyingCode, + p.IsInitial AS IsInitial, + p.InterestType AS InterestType, + p.InterestMode AS InterestMode, + CASE WHEN te.ExtendJson LIKE '%""InterestCalcMode""%' + THEN SUBSTRING_INDEX(SUBSTRING_INDEX(te.ExtendJson, '""InterestCalcMode"":""', -1), '""', 1) + ELSE '11' END AS InterestCalcMode +FROM trade t +JOIN swap_position p ON p.SwapTradeId = t.id +LEFT JOIN trade_extend te ON te.TradeId = t.id +WHERE t.ValidState = 'Valid' + AND p.Invalid = 0 + AND p.IsInitial = 1 + AND p.FloatRateUnderlyingCode = 'FR007' + AND t.ExerciseDate >= CURDATE() +ORDER BY t.StartDate;"; + + private const string FixingSql = @" +SELECT ValueDate, ReferencePrice +FROM eod_commodity_future_price +WHERE FutureContractId = 'FR007' + AND ValueDate >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) +ORDER BY ValueDate DESC;"; + + private TestContext _testContext; + public TestContext TestContext + { + get => _testContext; + set => _testContext = value; + } + + private static string Fmt(object v) => + v == null || v == DBNull.Value ? "NULL" + : (v is DateTime dt ? dt.ToString("yyyy-MM-dd") : v.ToString()); + + [TestMethod] + [Ignore] + [TestCategory("Discovery")] + public void Discover_InTransitFr007Trades() + { + using (var db = DbContextFactory.GetYLDbContext()) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != ConnectionState.Open) conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = TradeSql; + using (var reader = cmd.ExecuteReader()) + { + int n = 0; + while (reader.Read()) + { + n++; + TestContext.WriteLine( + $"TradeId={reader["TradeId"]} PosId={reader["PositionId"]} No={reader["TradeNumber"]} " + + $"Start={Fmt(reader["StartDate"])} Expr={Fmt(reader["ExerciseDate"])} " + + $"CalcMode={Fmt(reader["InterestCalcMode"])} rule={Fmt(reader["interest_rule"])} rest={Fmt(reader["interest_rest_days"])} " + + $"IntType={Fmt(reader["InterestType"])} Mode={Fmt(reader["InterestMode"])}"); + } + TestContext.WriteLine($"=== 在途 FR007 互换共 {n} 笔 ==="); + } + } + } + } + + [TestMethod] + [Ignore] + [TestCategory("Discovery")] + public void Discover_Fr007FixingStatus() + { + using (var db = DbContextFactory.GetYLDbContext()) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != ConnectionState.Open) conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = FixingSql; + using (var reader = cmd.ExecuteReader()) + { + int n = 0; + while (reader.Read()) + { + n++; + TestContext.WriteLine($"FR007 ValueDate={Fmt(reader["ValueDate"])} ReferencePrice={Fmt(reader["ReferencePrice"])}"); + } + TestContext.WriteLine($"=== FR007 定盘近 30 天共 {n} 条 ==="); + } + } + } + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/SwapFlowFr007EntryPrecisionTest.cs b/UnitTestProject/Modules/SwapModule/SwapFlowFr007EntryPrecisionTest.cs new file mode 100644 index 00000000..db9779f4 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapFlowFr007EntryPrecisionTest.cs @@ -0,0 +1,30 @@ +namespace YLErp.Modules.SwapModule +{ + /// + /// FR007 界面手工录入落库精度契约(EQD-6968 测试期间发现): + /// FR007 官方发布为百分数下 4 位(1.4150%),前端 ÷100 转 6 位小数(0.014150)传后端; + /// 原 Math.Round(,4) 截成 0.0142(丢 0.5bp,1 亿本金 7 天约 96 元)。修复后保留 6 位。 + /// bond-sync 自动同步链(BigDecimal 全精度)不经此路径,无影响。 + /// + [TestClass] + public class SwapFlowFr007EntryPrecisionTest + { + [TestMethod] + public void 官方四位百分数定盘_六位小数全精度保留() + { + // 2026-08-19 官方发布 1.4150% —— 前端 1.4150/100 后的入参 + Assert.AreEqual(0.01415, SwapFlowService.RoundFr007Price(1.4150 / 100.0), 1e-9, + "1.4150% 落库应保留 0.014150,不得截成 0.0142(丢 0.5bp)"); + Assert.AreEqual(0.021137, SwapFlowService.RoundFr007Price(2.1137 / 100.0), 1e-9, + "百分数下第3、4位(小数第5、6位)必须保留"); + } + + [TestMethod] + public void 常规两位百分数定盘_行为不变() + { + // 历史常见形态(2.11% 等):修复前后结果一致 + Assert.AreEqual(0.0211, SwapFlowService.RoundFr007Price(0.0211), 0d); + Assert.AreEqual(0.0142, SwapFlowService.RoundFr007Price(0.0142), 0d); + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs index 0ab56b30..224a5495 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs @@ -110,9 +110,9 @@ namespace YLErp.Modules.SwapModule DbContext.Add(frdata); } frdata.UnderlyingId = EodPriceService.ResolveUnderlyingIdForCode(frdata.UnderlyingCode, frdata.UnderlyingId ?? 0, frUnderlying.id); - frdata.ClosePrice = Math.Round(price, 4); - frdata.SettlePrice = Math.Round(price, 4); - frdata.ReferencePrice = Math.Round(price, 4); + frdata.ClosePrice = RoundFr007Price(price); + frdata.SettlePrice = RoundFr007Price(price); + frdata.ReferencePrice = RoundFr007Price(price); frdata.OptId = UserInfo.UserId; frdata.OptName = UserInfo.UserName; frdata.OptDate = DateTime.Now; @@ -121,6 +121,14 @@ namespace YLErp.Modules.SwapModule return true; } + /// + /// FR007 界面手工录入定盘的落库精度。FR007 官方发布为百分数下 4 位(如 1.4150%), + /// 小数口径需 6 位(0.014150)——原 Math.Round(,4) 只保留百分数下 2 位,1.4150% 被截成 + /// 1.4200%(丢 0.5bp)。取 6 位与前端 toNumber(value/100, 6) 对齐;DB 列 double(18,10) 容纳无虞; + /// bond-sync 自动同步链(BigDecimal 全精度透传)不经此函数。 + /// + internal static double RoundFr007Price(double price) => Math.Round(price, 6); + /// /// 查询互换流水导入 ///