Files
zszq-trs/UnitTestProject/Modules/SwapModule/GoldenReplayFramework.cs
hjhan d11e332ff3 test(swap): golden录制/回放基础设施+DealInterests场景
引入golden回放框架,补充合成测试的精确值验证缺口。

新增GoldenReplayFramework.cs:
- GoldenScenarioModel: 通用golden数据模型(输入+期望输出)
- GoldenAssert: 精确字段对比(容许指定位数误差),逐字段验证

新增DealInterestsGoldenReplayTest.cs:
- Record_AllGoldenScenarios: 生成golden JSON(标Ignore,手动跑)
- Replay_AllGoldenFiles: 读golden重跑+精确对比(进CI)

2个golden场景:
- 互换结清后待实现归零: InterestIncomeSum=0.82191780822(精确到11位)
- 普通日归档递增(回放暂不支持自动重放,留后续)

golden文件持久化到Resources/GoldenFiles/DealInterestsGolden/

价值:重构时如果任何字段变了(哪怕第8位小数),回放立刻失败。
守恒测试验证大方向对,golden验证精确值对。

验证: 119+1(回放)=120全通过。
2026-07-02 09:37:18 +08:00

161 lines
7.0 KiB
C#
Raw Permalink 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
{
#region Golden 数据模型
/// <summary>
/// Golden 文件的通用数据模型。
/// 每个场景序列化为一个 JSON 文件,包含:输入数据 + 期望输出。
///
/// JSON 结构:
/// {
/// "Scenario": "互换结清后待实现归零",
/// "Description": "攒10天后互换,验证InterestIncomeSum≈当天新计",
/// "Input": {
/// "Trade": { ... },
/// "Positions": [ ... ],
/// "PreEodPositions": [ ... ],
/// "FlowEvents": [ ... ]
/// },
/// "Expected": {
/// "EodPositions": [
/// { "PositionId": 1001, "InterestIncomeSum": 0.0274, "TdCloseInterest": 2.74, ... }
/// ]
/// }
/// }
/// </summary>
public class GoldenScenarioModel
{
/// <summary>场景名称</summary>
public string Scenario { get; set; }
/// <summary>场景描述</summary>
public string Description { get; set; }
/// <summary>输入数据</summary>
public GoldenInput Input { get; set; }
/// <summary>期望输出(精确到小数点后N位的字段值)</summary>
public GoldenExpected Expected { get; set; }
/// <summary>数据来源:synthetic(合成) / recorded(真实库录制)</summary>
public string Source { get; set; } = "synthetic";
/// <summary>录制时间(如果是 recorded</summary>
public DateTime? RecordedAt { get; set; }
}
public class GoldenInput
{
public JObject Trade { get; set; }
public JArray Positions { get; set; }
public JArray PreEodPositions { get; set; }
public JArray FlowEvents { get; set; }
// 可选的配置参数
public decimal? PosiLongNotional { get; set; }
public decimal? PosiShortNotional { get; set; }
public decimal? CloseNational { get; set; }
public decimal? GrossPrice { get; set; }
public decimal? OrginPv { get; set; }
public DateTime? SettleDate { get; set; }
}
public class GoldenExpected
{
/// <summary>期望生成的 eod 持仓数量</summary>
public int? PositionCount { get; set; }
/// <summary>期望的 eod 持仓精确字段(每个 PositionId 一条)</summary>
public JArray EodPositions { get; set; }
}
#endregion
#region Golden 回放辅助
/// <summary>
/// Golden 回放的通用辅助方法。
/// 提供精确字段对比(容许指定位数的误差)。
/// </summary>
public static class GoldenAssert
{
/// <summary>默认精度容差(小数点后9-2=7位)</summary>
public static decimal DefaultTolerance => 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2);
/// <summary>对比 decimal 字段,容许指定位数误差</summary>
public static void AssertField(decimal? expected, decimal actual, string fieldName, long positionId, decimal? tolerance = null)
{
if (expected == null) return; // golden 里没存这个字段就跳过
var tol = tolerance ?? DefaultTolerance;
Assert.IsTrue(Math.Abs(expected.Value - actual) <= tol,
$"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}, diff={expected.Value - actual}");
}
/// <summary>对比 int 字段</summary>
public static void AssertField(int? expected, int actual, string fieldName, long positionId)
{
if (expected == null) return;
Assert.AreEqual(expected.Value, actual,
$"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}");
}
/// <summary>对比 long 字段</summary>
public static void AssertField(long? expected, long actual, string fieldName, long positionId)
{
if (expected == null) return;
Assert.AreEqual(expected.Value, actual,
$"PositionId={positionId} {fieldName} 不匹配: expected={expected.Value}, actual={actual}");
}
/// <summary>
/// 对比一个 eod_swap_position 的所有 golden 字段。
/// golden JSON 里只存了需要验证的字段,未存的跳过。
/// </summary>
public static void AssertEodPosition(JObject expected, eod_swap_position actual)
{
var positionId = expected["PositionId"]?.Value<long>() ?? actual.PositionId;
AssertField(expected["InterestIncomeSum"]?.Value<decimal>(), actual.InterestIncomeSum, "InterestIncomeSum", positionId);
AssertField(expected["InterestProfitSum"]?.Value<decimal>(), actual.InterestProfitSum, "InterestProfitSum", positionId);
AssertField(expected["TdInterestIncome"]?.Value<decimal>(), actual.TdInterestIncome, "TdInterestIncome", positionId);
AssertField(expected["TdCloseInterest"]?.Value<decimal>(), actual.TdCloseInterest, "TdCloseInterest", positionId);
AssertField(expected["TdInterestPrincipal"]?.Value<decimal>(), actual.TdInterestPrincipal, "TdInterestPrincipal", positionId);
AssertField(expected["RealizedInterest"]?.Value<decimal>(), actual.RealizedInterest, "RealizedInterest", positionId);
AssertField(expected["RealizedInterestFee"]?.Value<decimal>(), actual.RealizedInterestFee, "RealizedInterestFee", positionId);
AssertField(expected["RealizedPnl"]?.Value<decimal>(), actual.RealizedPnl, "RealizedPnl", positionId);
AssertField(expected["SwapPositionValue"]?.Value<decimal>(), actual.SwapPositionValue, "SwapPositionValue", positionId);
AssertField(expected["InterestFeeSum"]?.Value<decimal>(), actual.InterestFeeSum, "InterestFeeSum", positionId);
AssertField(expected["TdInterestFee"]?.Value<decimal>(), actual.TdInterestFee, "TdInterestFee", positionId);
AssertField(expected["TdCloseInterestFee"]?.Value<decimal>(), actual.TdCloseInterestFee, "TdCloseInterestFee", positionId);
}
/// <summary>
/// 序列化一个 eod_swap_position 到 JObject(用于生成 golden 文件)。
/// 只存关键字段,避免 JSON 过大。
/// </summary>
public static JObject EodPositionToJson(eod_swap_position eod)
{
return new JObject
{
["PositionId"] = eod.PositionId,
["InterestIncomeSum"] = eod.InterestIncomeSum,
["InterestProfitSum"] = eod.InterestProfitSum,
["TdInterestIncome"] = eod.TdInterestIncome,
["TdCloseInterest"] = eod.TdCloseInterest,
["TdInterestPrincipal"] = eod.TdInterestPrincipal,
["RealizedInterest"] = eod.RealizedInterest,
["RealizedPnl"] = eod.RealizedPnl,
["SwapPositionValue"] = eod.SwapPositionValue,
["InterestFeeSum"] = eod.InterestFeeSum
};
}
}
#endregion
}