问题:互换后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个测试全通过,无回归
255 lines
12 KiB
C#
255 lines
12 KiB
C#
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
|
||
}
|
||
}
|