Files
zszq-trs/UnitTestProject/Modules/SwapModule/DealInterestsGoldenReplayTest.cs
hjhan ef37c8e71d refactor(swap): 合并多空名义本金参数为单一 posiTotalNotional 并加固测试
- DealInterests 的 posiLongNotionalValue + posiShortNotionalValue 合并为 posiTotalNotional(调用点以 posiLongNotional+posiShortNotional 求和传入),净减一个参数

- SwapDealService / SwapEodPositionService / InterestCalcRequest 同步收敛多空死管道参数

- 19 个测试调用点适配新签名

- SwapEodPositionServiceIntegrationTest 参数计数断言由裸数字改为参数名集合断言(CollectionAssert.AreEquivalent,对增删/重排/改名敏感)
2026-08-14 17:30:46 +08:00

339 lines
16 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
{
/// <summary>
/// DealInterests Golden 回放测试
/// ============================================================================
/// 用 golden JSON 存"输入数据 + 期望输出的精确字段值",
/// 回放时从 JSON 重跑,逐字段精确对比。
///
/// 两类方法:
/// - Record*: 连库录制/生成 golden(标 Ignore,手动跑)
/// - Replay*: 读 golden 重跑对比(进 CI
///
/// 价值:重构时如果任何一步的输出变了(哪怕第8位小数),立刻失败。
/// 守恒测试验证"大方向对"golden 验证"精确值对"。
/// ============================================================================
[TestClass]
public class DealInterestsGoldenReplayTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "DealInterestsGolden");
#region Stub(复用 DealInterestsScenarioTest 的模式)
private sealed class StubEodService : TestableSwapEodPositionService
{
public StubEodService() : base(nameof(DealInterestsGoldenReplayTest))
{
}
public void ExecuteSaveEodInterestPosition(
eod_swap_position eodPayPosition, swap_position position, trade td,
DateTime valueDate, List<swap_flow_event> flowEvents)
{
SaveEodInterestPosition(eodPayPosition, null, position, td, valueDate, flowEvents);
}
// public 包装:直接调用 protected virtual DealInterests(录制场景2用)
public void ExecuteDealInterestsForRecord(
List<swap_position> interestList, List<eod_swap_position> eodPositions,
DateTime settleDate, trade td,
decimal posiLongNational, decimal grossPrice, decimal orginPv)
{
DealInterests(interestList, eodPositions, new List<eod_swap_position>(),
settleDate, td, new List<swap_flow_event>(), new List<swap_flow_event>(), null,
posiLongNational + 0m, 0m, grossPrice, orginPv);
}
}
#endregion
#region 录制:生成 golden JSON(标 Ignore,手动跑)
/// <summary>
/// 生成所有 golden JSON 文件。
/// 手动取消 [Ignore] 运行,会覆盖 bin 目录下的 golden 文件。
/// 生成后复制到 UnitTestProject/Resources/GoldenFiles/ 持久化。
/// </summary>
[TestMethod]
[Ignore]
[TestCategory("GoldenRecord")]
public void Record_AllGoldenScenarios()
{
Directory.CreateDirectory(GoldenDir);
Record_SwapSettleZeroInterestIncomeSum();
Record_NormalDayIncrement();
Console.WriteLine($"\n录制完成,输出目录: {GoldenDir}");
}
/// <summary>场景1:互换结清后 InterestIncomeSum≈当天新计</summary>
private void Record_SwapSettleZeroInterestIncomeSum()
{
const decimal Principal = 10000m;
const decimal Rate = 0.03m;
const int AnnualDays = 365;
var startDate = new DateTime(2026, 4, 27);
var td = new trade
{
id = 1, TradeNumber = "GOLDEN-001", ClientId = 999998,
TradeType = "收益互换", TradeDate = startDate, StartDate = startDate,
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
ValidState = "Valid", StructureType = "单标的",
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) }
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
InterestPrincipalFix = Principal, PosiStartDate = startDate,
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null
};
var settleDate = startDate.AddDays(10);
int days = (settleDate - startDate).Days;
decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var preEod = new eod_swap_position
{
id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1),
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestIncomeSum = accumulated, InterestProfitSum = accumulated,
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1
};
var swapEvent = new swap_flow_event
{
EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001,
InterestAmount = accumulated, InterestClosePnL = accumulated,
InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestPrincipal = Principal, FloatRate = 0m,
DataState = (int)SwapFlowDateStateEnum.完成
};
var service = new StubEodService();
service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List<swap_flow_event> { swapEvent });
var result = service.PersistedPositions[0];
var golden = new GoldenScenarioModel
{
Scenario = "互换结清后待实现归零",
Description = $"攒{days}天后互换,InterestIncomeSum应≈当天新计",
Input = new GoldenInput
{
SettleDate = settleDate,
PosiLongNotional = Principal,
OrginPv = Principal
},
Expected = new GoldenExpected
{
PositionCount = 1,
EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) }
}
};
string json = JsonConvert.SerializeObject(golden, Formatting.Indented);
string path = Path.Combine(GoldenDir, "golden_互换结清后待实现归零.json");
File.WriteAllText(path, json);
Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}");
Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}");
Console.WriteLine($" TdCloseInterest={result.TdCloseInterest:F11}");
Console.WriteLine($" RealizedInterest={result.RealizedInterest:F11}");
}
/// <summary>场景2:普通日 InterestIncomeSum 递增</summary>
private void Record_NormalDayIncrement()
{
const decimal Principal = 10000m;
const decimal Rate = 0.03m;
const int AnnualDays = 365;
var startDate = new DateTime(2026, 4, 27);
decimal dailyInc = Math.Round(Principal * Rate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var td = new trade
{
id = 1, TradeNumber = "GOLDEN-002", ClientId = 999998,
TradeType = "收益互换", TradeDate = startDate, StartDate = startDate,
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
ValidState = "Valid", StructureType = "单标的",
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) }
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
InterestPrincipalFix = Principal, PosiStartDate = startDate,
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = null
};
// 用 DealInterests 走 copy 分支
var settleDate = startDate.AddDays(2); // 第3天
var preEod = new eod_swap_position
{
id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1),
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestIncomeSum = dailyInc, InterestProfitSum = dailyInc,
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1
};
var service = new StubEodService();
// 直接调用 protected virtual DealInterestscopy 分支需要 CalcSwapInterests
service.ExecuteDealInterestsForRecord(
new List<swap_position> { position },
new List<eod_swap_position> { preEod },
settleDate, td, Principal, 1m, Principal);
if (service.PersistedPositions.Count == 0)
{
Console.WriteLine("⚠ 场景2未生成eodCalcSwapInterests可能需要接缝),跳过");
return;
}
var result = service.PersistedPositions[0];
var golden = new GoldenScenarioModel
{
Scenario = "普通日归档递增",
Description = "第3天收盘,InterestIncomeSum应=2天+1天=3天利息",
Expected = new GoldenExpected
{
PositionCount = 1,
EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) }
}
};
string json = JsonConvert.SerializeObject(golden, Formatting.Indented);
string path = Path.Combine(GoldenDir, "golden_普通日归档递增.json");
File.WriteAllText(path, json);
Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}");
Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}");
}
#endregion
#region 回放:读 golden 重跑+精确对比(进 CI
/// <summary>
/// 回放所有 golden 文件,逐字段精确对比。
/// 如果任何字段变了(哪怕是第8位小数),测试失败。
/// </summary>
[TestMethod]
public void Replay_AllGoldenFiles()
{
if (!Directory.Exists(GoldenDir))
{
Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Record_AllGoldenScenarios");
return;
}
var files = Directory.GetFiles(GoldenDir, "*.json").OrderBy(f => f).ToArray();
Assert.IsTrue(files.Length > 0, "应至少有1个golden文件");
int passed = 0, failed = 0;
foreach (var file in files)
{
try
{
var golden = JsonConvert.DeserializeObject<GoldenScenarioModel>(File.ReadAllText(file));
Console.WriteLine($"\n回放: {Path.GetFileName(file)} - {golden.Scenario}");
// 回放互换场景(场景1的模式)
if (golden.Scenario?.Contains("互换结清") == true)
{
ReplaySwapSettle(golden);
}
else
{
Console.WriteLine($" (场景类型'{golden.Scenario}'暂不支持自动回放,跳过)");
continue;
}
passed++;
Console.WriteLine($" ✅ 通过");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($" ❌ 失败: {ex.Message}");
}
}
Console.WriteLine($"\n回放结果: {passed}通过 {failed}失败 / {files.Length}总");
Assert.AreEqual(0, failed, $"{failed}个golden文件回放失败");
}
private void ReplaySwapSettle(GoldenScenarioModel golden)
{
const decimal Principal = 10000m;
const decimal Rate = 0.03m;
const int AnnualDays = 365;
var startDate = new DateTime(2026, 4, 27);
var settleDate = golden.Input.SettleDate ?? startDate.AddDays(10);
int days = (settleDate - startDate).Days;
decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var td = new trade
{
id = 1, TradeNumber = "GOLDEN-REPLAY", ClientId = 999998,
TradeType = "收益互换", TradeDate = startDate, StartDate = startDate,
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
ValidState = "Valid", StructureType = "单标的",
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) }
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
InterestPrincipalFix = Principal, PosiStartDate = startDate,
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
interest_rest_days = 1, interest_rule = 0
};
var preEod = new eod_swap_position
{
id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1),
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestIncomeSum = accumulated, InterestProfitSum = accumulated,
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1
};
var swapEvent = new swap_flow_event
{
EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001,
InterestAmount = accumulated, InterestClosePnL = accumulated,
InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestPrincipal = Principal, DataState = (int)SwapFlowDateStateEnum.完成
};
var service = new StubEodService();
service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List<swap_flow_event> { swapEvent });
// 对比 golden 期望
Assert.AreEqual(golden.Expected.PositionCount ?? 1, service.PersistedPositions.Count, "持仓数量");
var expectedEods = golden.Expected.EodPositions?.ToObject<List<JObject>>() ?? new List<JObject>();
foreach (var expected in expectedEods)
{
var pid = expected["PositionId"]?.Value<long>() ?? 1001;
var actual = service.PersistedPositions.FirstOrDefault(x => x.PositionId == pid);
Assert.IsNotNull(actual, $"未找到PositionId={pid}");
GoldenAssert.AssertEodPosition(expected, actual);
}
}
#endregion
}
}