引入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全通过。
161 lines
7.0 KiB
C#
161 lines
7.0 KiB
C#
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
|
||
}
|