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);
+
///
/// 查询互换流水导入
///