test(accrual): 新增同日双次部分平仓字符化测试 + EOD基数快照replay诊断
- SwapUnwindSameDayDoublePartialTest: 内存Stub驱动同日两次部分平仓(30%/50%), 验证基数线性累加(0.3N+0.35N=0.65N),推翻"基数不滚动"假设;缺口仅在 调用方是否传陈旧notional,已文档化 - GLMS20260805ClosePercentDiffDiagnoseTest: 从Resources快照反序列化离线重跑 EOD基数诊断,去DB化;无快照Inconclusive,保留Assert.Fail作8/5 bug护栏
This commit is contained in:
@@ -96,6 +96,97 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
#endregion
|
||||
|
||||
#region 1.5) 离线回放:从已录快照重跑 EOD 基数诊断(不连库)
|
||||
|
||||
/// <summary>
|
||||
/// 去 DB 化回放:从 <see cref="Record_RealSnapshot"/> 落盘的 snapshot_*.json 反序列化
|
||||
/// eod_swap_position / swap_position,离线重跑「EOD 预付金基数是否=初始本金」诊断。
|
||||
///
|
||||
/// 目的:原 Diagnose_100vs40_InterestDiff 直接连 96 库跑 GetUnwindInterests,
|
||||
/// 依赖数据库可用性、且每次重跑都重新查库。本方法把「一次录制、内存多次回放」
|
||||
/// 落地——录制一次(连库)后,后续诊断完全在内存完成,确定性、可重复、不依赖库。
|
||||
///
|
||||
/// 语义保持为 bug 护栏:若快照录制时 EOD 基数用了初始本金而非实时剩余,本测试
|
||||
/// 仍会 Assert.Fail(不掩盖生产 bug)。录制一份「修复后」的快照即可转绿。
|
||||
/// 无快照时 Inconclusive(须先连库跑一次 Record_RealSnapshot)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestCategory("DbDiagnose")]
|
||||
public void Replay_100vs40_FromSnapshot()
|
||||
{
|
||||
var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "DbDiagnose", "GLMS20260805");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Assert.Inconclusive($"未找到快照目录 {dir},请先连库跑一次 Record_RealSnapshot 录制真实数据快照");
|
||||
return;
|
||||
}
|
||||
var files = Directory.GetFiles(dir, "snapshot_*.json").OrderByDescending(f => f).ToArray();
|
||||
if (files.Length == 0)
|
||||
{
|
||||
Assert.Inconclusive($"目录 {dir} 下无 snapshot_*.json,请先连库跑一次 Record_RealSnapshot");
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshotPath = files[0];
|
||||
Console.WriteLine($"✅ 载入快照(离线回放): {snapshotPath}");
|
||||
|
||||
var snapshot = JObject.Parse(File.ReadAllText(snapshotPath));
|
||||
var tradeNumber = snapshot.Value<string>("TradeNumber");
|
||||
Console.WriteLine($"===== 离线回放 交易 {tradeNumber} =====");
|
||||
|
||||
var eodPrepay = JsonConvert.DeserializeObject<List<eod_swap_position>>(snapshot["EodPositions"].ToString())
|
||||
.Where(e => !e.Invalid
|
||||
&& (e.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| e.InterestMode == (int)InterestModeEnum.追加预付金))
|
||||
.OrderBy(e => e.ValueDate).ThenBy(e => e.PositionId).ToList();
|
||||
|
||||
var origPrepay = JsonConvert.DeserializeObject<List<swap_position>>(snapshot["Positions"].ToString())
|
||||
.Where(p => !p.Invalid && p.IsInitial
|
||||
&& (p.InterestMode == (int)InterestModeEnum.初始预付金 || p.InterestMode == (int)InterestModeEnum.追加预付金)).ToList();
|
||||
var realPrepay = JsonConvert.DeserializeObject<List<swap_position>>(snapshot["Positions"].ToString())
|
||||
.Where(p => !p.Invalid && !p.IsInitial
|
||||
&& (p.InterestMode == (int)InterestModeEnum.初始预付金 || p.InterestMode == (int)InterestModeEnum.追加预付金)).ToList();
|
||||
|
||||
bool bugDetected = false;
|
||||
if (eodPrepay.Count == 0)
|
||||
{
|
||||
Console.WriteLine(" (快照无预付金腿 EOD 记录 → 无可诊断的基数 bug)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" {"ValueDate",-12}{"PosId",-8}{"Mode",-6}{"TdInterestPrincipal",-20}{"InterestProfitSum",-20}");
|
||||
foreach (var e in eodPrepay)
|
||||
{
|
||||
Console.WriteLine($" {e.ValueDate:yyyy-MM-dd} {e.PositionId,-8}{e.InterestMode,-6}{e.TdInterestPrincipal,-20}{e.InterestProfitSum,-20}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\n ---- 预付金本金基数三方对比(离线)----");
|
||||
foreach (var orig in origPrepay)
|
||||
{
|
||||
var real = realPrepay.FirstOrDefault(r => r.PositionId == orig.id);
|
||||
var latestEod = eodPrepay.Where(e => e.PositionId == orig.id).OrderByDescending(e => e.ValueDate).FirstOrDefault();
|
||||
var realFix = real?.InterestPrincipalFix ?? 0;
|
||||
var eodTd = latestEod?.TdInterestPrincipal ?? 0;
|
||||
Console.WriteLine($" PosId={orig.id} origFix(初始)={orig.InterestPrincipalFix} realFix(剩余)={realFix} EOD.TdInterestPrincipal(最新)={eodTd}");
|
||||
|
||||
bool eodMatchesOrig = Math.Abs((double)(eodTd - orig.InterestPrincipalFix)) < 0.01;
|
||||
bool eodMatchesReal = Math.Abs((double)(eodTd - realFix)) < 0.01;
|
||||
if (eodMatchesOrig && !eodMatchesReal && orig.InterestPrincipalFix != realFix)
|
||||
{
|
||||
bugDetected = true;
|
||||
Console.WriteLine($" ⚠⚠ EOD 基数=初始本金(≠剩余)→ 坐实:日终用了初始预付金本金而非实时剩余,后续利息计算基数错误!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bug 护栏:快照若录制到基数 bug,离线回放仍须红,不掩盖生产事故。
|
||||
// 修复生产并重新录制快照后,此断言自然转绿。
|
||||
Assert.IsFalse(bugDetected,
|
||||
"离线回放复现 8/5 基数 bug:EOD 预付金基数用了初始本金而非实时剩余。需先修复生产、再录制新快照让本测试转绿。");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 2) 诊断:100% vs 40% 利息差异根因定位(连库跑)
|
||||
|
||||
[TestMethod]
|
||||
|
||||
Reference in New Issue
Block a user