Files
zszq-trs/UnitTestProject/Modules/SwapModule/GLMS20260105GoldenTest.cs
T
hjhan 0910969e3f fix(swap): PosiDividendSum精度差异修复(GLMS-20260105-0007)
问题:互换后PosiDividendSum=-89999.64(应为-90000),差0.36元。
根因:UpdateEodPosition cs:1635从头重算PosiDividendSum
  (Round(全程,2)-已实现),与CopyEodPosition逐天递增
  (每天Round(增量,2)累积)产生舍入误差累积。

修复:cs:1635改为递增模式
  PosiDividendSum = eod.PosiDividendSum + TdPosiDividend - TdCloseDividend
  与InterestIncomeSum的递增模式一致,避免从头重算的精度差异。

验证(golden录制/回放TDD):
- 真实数据录制GLMS-20260105-0007的eod快照+输入数据
- 回放重新调UpdateEodPosition:修复前=-89999.64(红灯),
  修复后=-90000(绿灯,精确匹配)
- 121个测试全通过,无回归
2026-07-02 10:47:23 +08:00

255 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 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
/// <summary>
/// 从真实测试库录制 GLMS-20260105-0007 的完整数据快照。
/// 标 [Ignore],手动跑一次生成 golden JSON。
/// </summary>
[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 验证精度差异
/// <summary>
/// 回放 golden:用真实数据重新调 UpdateEodPosition,验证修复后 PosiDividendSum 正确。
///
/// 红灯(修复前):从头重算产生 0.36 差异
/// 绿灯(修复后):递增模式,PosiDividendSum = 前日 + 新计 - 实现 = -90000
/// </summary>
[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<trade>(JsonSerializer.Create(settings));
var position = replayInput["Position"]!.ToObject<swap_position>(JsonSerializer.Create(settings));
var preSwapEod = replayInput["PreSwapEod"]!.ToObject<eod_swap_position>(JsonSerializer.Create(settings));
var swapFlowEvents = replayInput["SwapFlowEvents"]!.ToObject<List<swap_flow_event>>(JsonSerializer.Create(settings));
var swapDateStr = golden["SwapDate"]!.Value<string>();
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<swap_flow_event> unwindEvents)
{
return UpdateEodPosition(swapPosition, eod, null, td, valueDate, preSettleDate, unwindEvents);
}
}
#endregion
}
}