Files
zszq-trs/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs
T
hjhan 4a3fee9292 refactor(swap)+test: 删 GetInterests/CalcSwapInterests 死参数 needPrice/grossPrice;补工厂→接缝映射钉子
死参数收口(另一半):
- SwapDealService.GetInterests 删 needPrice/grossPrice(体内零消费,2026-08 验证);
  InitSwapDealInterest.needPrice 同为死参数一并删
- SwapEodPositionService.CalcSwapInterests 签名+转发同步;两个 EOD 生产调用点
  (SaveAutoEodInterestPosition/SaveEodInterestPositionCopy) 重排实参;
  CalcEodPostCloseSettleInterests/GetIntradayUnwindInterests 委托同步
- 14 个测试文件 ~44 处直调点机械更新(8 处 override 签名 + 36 处调用实参)
- 注意:EOD 编排链(DealInterests→Save*家族)的 grossPrice(期初不含费价)有真实用途,保留未动

新增钉子:CalcEodPostCloseSettleInterests 工厂→接缝参数映射测试——
CalcSwapInterestsCapture 捕获 stub 断言 EodPostCloseSettle 的完整转发契约
(posi=平仓后剩余/closePosi=平掉额/恒1/settment:false/orginPv 等 11 项)。
该段位置转发含三个相邻同型 decimal,编译器不查错位,此测试兜底。

验证:定向 241 测试通过(含 T0/T1 Excel 验证期望值、EntrySemantics 精确值钉子——
任何 decimal 错位即红);全量 903=145失败/746通过/12跳过,与基线逐位一致。
2026-08-14 16:57:56 +08:00

165 lines
8.8 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
using YLErp.Modules.SwapModule.Margin;
namespace UnitTestProject.Modules.SwapModule.Margin
{
/// <summary>
/// 黄金回放验证(连真实测试库 192.168.2.96):对真实保证金交易逐日 EOD 比对
/// GetInterests(settment=true,保证金分支现走 CalcMarginInterest) vs 直接调 CalcMarginInterest
/// 验证 GetInterests→CalcMarginInterest 接线的参数对齐(rate/posiPrincipal/preEod 等)正确。
///
/// 数据来自 96 库的真实保证金交易,覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、
/// 跨 EOD 续接等单元测试够不到的边界。作为保证金计息迁移后的真实库回归守护。
/// </summary>
[TestClass]
public class MarginInterestGoldenReplayTest
{
// 96 库里已确认含真实保证金腿的交易
private static readonly string[] TradeNumbers =
{
"GLMS-20260701-0008",
"GLMS-20260701-0013",
"GLMS-20260701-0006",
};
private sealed class StubSvc : SwapDealService
{
public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestGoldenReplayTest), OptUserFrom.UnitTest)) { }
}
/// <summary>
/// 逐交易、逐 EOD 日,比对保证金腿新旧计息 InterestAmount/TdInterestAmount。
/// 入参对齐口径(与 GetInterests 内部一致):
/// posiPrincipal = InterestPrincipalFixclosePrincipal = Fix×closePercent(EOD=1)
/// rate = oldEvt.InterestRate(严格取旧管线算出的 rate,消除 GetFixedRate 差异);
/// 方向 = FlipDirection(position.InterestDirection)GetInterests:742 对保证金翻转);
/// preEod = 该 PositionId 上一日终 eod_swap_positionannualDays/calcFirst/calcLast 来自 trade_extend。
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void 保证金腿_真实库_EOD逐日新旧比对()
{
YLContext db;
try { db = DbContextFactory.GetYLDbContext(); }
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(192.168.2.96){ex.Message}"); return; }
var svc = new StubSvc();
int totalCompared = 0, mismatches = 0, skipped = 0;
var diffLog = new StringBuilder();
foreach (var tradeNumber in TradeNumbers)
{
var td = db.trade.FirstOrDefault(t => t.TradeNumber == tradeNumber);
if (td == null) { Console.WriteLine($"跳过:库无 {tradeNumber}"); skipped++; continue; }
var extend = db.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
int annualDays = extend?.ExtendObj.AnnualDays ?? 365;
bool calcFirst = extend?.ExtendObj.InterestCalcMode?.StartsWith("1") ?? true;
bool calcLast = extend?.ExtendObj.InterestCalcMode?.EndsWith("1") ?? true;
var marginPositions = db.swap_position
.Where(p => p.SwapTradeId == td.id && !p.Invalid
&& (p.InterestMode == (int)InterestModeEnum.初始预付金
|| p.InterestMode == (int)InterestModeEnum.追加预付金))
.ToList();
if (marginPositions.Count == 0) { Console.WriteLine($"跳过:{tradeNumber} 无保证金腿"); skipped++; continue; }
// 该交易保证金腿的 EOD 日期序列
var eodDates = db.eod_swap_position
.Where(e => e.SwapTradeId == td.id && (e.InterestMode == 5 || e.InterestMode == 6))
.Select(e => e.ValueDate).Distinct().OrderBy(d => d).ToList();
Console.WriteLine($"===== {tradeNumber} (id={td.id}){marginPositions.Count} 条保证金腿,{eodDates.Count} 个 EOD 日 =====");
foreach (var valueDate in eodDates)
{
// 上一日终 preEod(取 eod_swap 最近 < valueDate 的日期)
var preDate = db.eod_swap
.Where(e => e.SwapTradeId == td.id && e.ValueDate < valueDate)
.OrderByDescending(e => e.ValueDate)
.Select(e => (DateTime?)e.ValueDate).FirstOrDefault();
var preEods = preDate == null
? new List<eod_swap_position>()
: db.eod_swap_position
.Where(e => e.SwapTradeId == td.id && e.ValueDate == preDate.Value
&& (e.InterestMode == 5 || e.InterestMode == 6))
.ToList();
// 旧管线:GetInterests(settment=true)。保证金分支不用 posiNotionalValue/closePosiNotionalValue/grossPrice/orginPv,传 0。
List<swap_flow_event> oldList;
try
{
oldList = svc.GetInterests(td, extend, valueDate, valueDate,
preEods, marginPositions,
0m, 0m, 0m, 0m, 1.0m,
(int)SwapEventTypeEnum.自动互换, tdClose: false,
orginPv: 0m,
add: false, settment: true, newCalcLast: false, closeList: null);
}
catch (Exception ex)
{
Console.WriteLine($" {tradeNumber} @ {valueDate:yyyy-MM-dd} 旧管线异常:{ex.GetType().Name} {ex.Message}");
continue;
}
// 新方法:逐保证金腿
foreach (var pos in marginPositions)
{
var oldEvt = oldList.FirstOrDefault(i => i.PositionId == pos.id);
if (oldEvt == null) continue;
var preEod = preEods.FirstOrDefault(e => e.PositionId == pos.id) ?? new eod_swap_position { id = 0 };
var posClone = pos.Clone();
posClone.InterestDirection = MarginCalc.FlipDirection(pos.InterestDirection);
decimal rate = oldEvt.InterestRate; // 严格对齐旧管线 rate(含 GetFixedRate + Round(12)
swap_flow_event newEvt;
try
{
newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, posClone, rate,
pos.InterestPrincipalFix, pos.InterestPrincipalFix, 1.0m,
annualDays, calcFirst, calcLast, preEod,
(int)SwapEventTypeEnum.自动互换, add: false, settment: true, swap: false);
}
catch (Exception ex)
{
mismatches++;
diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} @ {valueDate:yyyy-MM-dd} 新方法异常:{ex.GetType().Name} {ex.Message}");
continue;
}
totalCompared++;
decimal diffI = Math.Abs(newEvt.InterestAmount - oldEvt.InterestAmount);
decimal diffTd = Math.Abs(newEvt.TdInterestAmount - oldEvt.TdInterestAmount);
const decimal tol = 0.000001m;
if (diffI > tol || diffTd > tol)
{
mismatches++;
diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} Mode={pos.InterestMode} @ {valueDate:yyyy-MM-dd}: " +
$"旧 I={oldEvt.InterestAmount} Td={oldEvt.TdInterestAmount} | " +
$"新 I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} | " +
$"diffI={diffI} diffTd={diffTd} | " +
$"preEod.id={preEod.id} TdIntPrin={preEod.TdInterestPrincipal} ProfitSum={preEod.InterestProfitSum} | " +
$"Fix={pos.InterestPrincipalFix} rate={rate} IntType={pos.InterestType} IsAnnualized={pos.IsAnnualized}");
}
}
}
}
Console.WriteLine($"\n===== 比对汇总:共 {totalCompared} 条,不一致 {mismatches} 条,跳过 {skipped} 个交易 =====");
if (diffLog.Length > 0) Console.WriteLine(diffLog.ToString());
Assert.IsTrue(mismatches == 0,
$"保证金新旧管线 EOD 真实库比对有 {mismatches}/{totalCompared} 条不一致——提交2 前必须解决(详见输出)");
}
}
}