Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapUnwindFloatingLegDiagnosticTdd.cs
T
hjhan 3c9d22f60d test(swap): 浮动腿 fpositions 用 orig 不影响预付金结果 — 诊断测试
验证 GLMS-20260701-0008 的预付金 bug 修复无需触碰浮动腿(fpositions / posiLong/posiShort):

- 本 deal 利息腿仅 mode 9(标的期初全价) + mode 5(初始预付金),无「多头/空头存续名义本金」腿。
- CalcNotionalByMode 中 posiLong/posiShort 仅在「多头/空头存续名义本金」分支被消费(L709-716),
  mode 9/5 均不依赖之;故 fpositions 用 orig 100M 不影响预付金「返回预付金」计算。
- 诊断用例用反射调 private CalcNotionalByMode 实证:mode9 closePrincipal=posiNotional×比例、
  mode5 closePrincipal=实时腿 Fix×比例,均与 posiLong(orig 100M) 无关;并证明若存在
  多头存续名义本金腿则 orig 会算错(10M vs 正确 6.89M)——普通收益互换的潜在同类 bug,本 deal 不触发,属修复范围外。

不改动任何生产代码。3 个诊断用例全过。
2026-07-16 13:00:21 +08:00

113 lines
8.4 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 Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 诊断测试:验证「浮动腿 fpositions 仍用 origPositions(orig 100M)」对本 deal 的
/// 预付金/返回预付金结果是否产生影响。结论预期:本 deal 利息腿只有 mode 9(标的期初全价)
/// 与 mode 5(初始预付金)CalcNotionalByMode 中 posiLong/posiShort 仅在「多头/空头存续名义本金」
/// 分支被消费(L709-716),故本 deal 即便 fpositions 用 orig 100M,预付金腿结果也不受其影响。
/// 本测试仅做诊断/验证,不改动任何生产代码;用反射调用 private CalcNotionalByMode 以直接证明
/// “mode 9 / mode 5 的 closePrincipal 不依赖 posiLong/posiShort”。
/// </summary>
[TestClass]
public class SwapUnwindFloatingLegDiagnosticTdd
{
private sealed class StubSwapDealService : SwapDealService
{
public StubSwapDealService(OptUserInfo optUser) : base(optUser) { }
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{ rate = 0; return false; }
}
private const decimal OrigFix = 99_000m; // 期初预付金腿初始本金
private const decimal RealFix = 66_813.12m; // 实时预付金腿剩余本金(4 次平仓后)
private const decimal OrigLong = 100_000_000m; // 期初标的(多头)名义本金
private const decimal RealLong = 68_947_200m; // 实时标的(多头)剩余名义本金
private const decimal ClosePct = 0.1m; // 本次平仓比例 10%
private static readonly DateTime D0 = new(2026, 7, 1);
private static readonly DateTime D1 = new(2026, 7, 16);
private SwapDealService _svc;
[TestInitialize] public void Init() => _svc = new StubSwapDealService(new OptUserInfo(0, nameof(SwapUnwindFloatingLegDiagnosticTdd), OptUserFrom.UnitTest));
// ---- GLMS 双轨持仓构造 ----
private static swap_position OrigPrepay(decimal fix = OrigFix) => new swap_position
{ id = 35798, SwapTradeId = 1993, PosiDirection = 0, InterestMode = (int)InterestModeEnum.初始预付金,
InterestPrincipalFix = fix, IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
interest_rest_days = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestSwapInterval = "[]" };
private static swap_position RealPrepay(decimal fix = RealFix) => new swap_position
{ id = 35871, SwapTradeId = 1993, PositionId = 35798, PosiDirection = 0, InterestMode = (int)InterestModeEnum.初始预付金,
InterestPrincipalFix = fix, IsInitial = false, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
interest_rest_days = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestSwapInterval = "[]" };
private static swap_position OrigBasePrice() => new swap_position
{ id = 35797, SwapTradeId = 1993, PosiDirection = 0, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestPrincipalFix = 0, IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
interest_rest_days = 1, InterestSwapInterval = "[]" };
private static swap_position RealBasePrice() => new swap_position
{ id = 35870, SwapTradeId = 1993, PositionId = 35797, PosiDirection = 0, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestPrincipalFix = 0, IsInitial = false, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
interest_rest_days = 1, InterestSwapInterval = "[]" };
private static swap_position OrigLongLeg() => new swap_position
{ id = 35799, SwapTradeId = 1993, PosiDirection = 2, PositionType = (int)PositionTypeFlag.Long, InterestMode = 0,
PosiNotionalValue = OrigLong, IsInitial = true, Invalid = false };
private static swap_position RealLongLeg() => new swap_position
{ id = 35872, SwapTradeId = 1993, PositionId = 35799, PosiDirection = 2, PositionType = (int)PositionTypeFlag.Long, InterestMode = 0,
PosiNotionalValue = RealLong, IsInitial = false, Invalid = false };
private static trade MakeTrade()
{
var extend = new trade_extend { TradeId = 1993, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{ AnnualDays = 365, InterestCalcMode = "10", SettlementRules = 0 }) };
return new trade { id = 1993, TradeNumber = "GLMS-20260701-0008", ClientId = 999998, TradeType = "收益互换",
TradeDate = D0, StartDate = D0, ExerciseDate = D1, TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)RealLong, Notional = (double)RealLong, trade_extend = extend };
}
/// <summary>用反射调用 private CalcNotionalByMode,直接证明各 mode 的 closePrincipal 是否依赖 posiLong/posiShort。</summary>
private (decimal close, decimal posi, decimal pct) CallCalcNotionalByMode(swap_position position, decimal closePct, decimal posiNotional, decimal posiLong, decimal posiShort)
{
var m = typeof(SwapDealService).GetMethod("CalcNotionalByMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return ((decimal, decimal, decimal))m.Invoke(_svc, new object[] { position, closePct, posiNotional, posiLong, posiShort });
}
[TestMethod]
public void 诊断_mode9_标的期初全价_closePrincipal_不依赖posiLong_而用posiNotional()
{
// mode 9 分支:closePrincipal = posiNotional * closePercent
var baseP = OrigBasePrice();
var (close, posi, _) = CallCalcNotionalByMode(baseP, ClosePct, RealLong * ClosePct, OrigLong, 0m);
Console.WriteLine($"[mode9] posiNotional={RealLong * ClosePct} posiLong(orig)={OrigLong} → closePrincipal={close}");
Assert.AreEqual(RealLong * ClosePct * ClosePct, close, "mode9 应 = posiNotional(=real剩余*closePct) * closePct,与 posiLong(orig 100M) 无关");
}
[TestMethod]
public void 诊断_mode5_预付金_closePrincipal_用自身Fix_不依赖posiLong()
{
// mode 5 分支:closePrincipal = position.InterestPrincipalFix * closePercent(用 Clone 后的 real Fix
var prepay = RealPrepay(); // Fix = RealFix(66,813.12)
var (close, posi, _) = CallCalcNotionalByMode(prepay, ClosePct, RealLong * ClosePct, OrigLong, 0m);
Console.WriteLine($"[mode5] Fix(cloned real)={RealFix} posiLong(orig)={OrigLong} → closePrincipal={close}");
Assert.AreEqual(RealFix * ClosePct, close, "mode5 应 = 实时腿剩余本金(real Fix) * closePct,与 posiLong(orig 100M) 无关");
Assert.AreNotEqual(OrigFix * ClosePct, close, "务必不是期初 99,000 * closePct(证明后端修复生效)");
}
[TestMethod]
public void 诊断_若将来有_多头存续名义本金_腿_posiLong用orig才出错_本deal无此腿_故不影响()
{
// 构造一个「多头存续名义本金」腿,证明此时 posiLong 取值(orig vs real)会直接决定结果——
// 说明本 deal 没有这种腿,所以 fpositions 用 orig 100M 不影响;但普通收益互换若有此腿则会踩坑。
var longLeg = new swap_position { id = 35799, InterestMode = (int)InterestModeEnum.多头存续名义本金 };
var byOrig = CallCalcNotionalByMode(longLeg, ClosePct, RealLong * ClosePct, OrigLong, 0m); // 当前代码:posiLong=orig 100M
var byReal = CallCalcNotionalByMode(longLeg, ClosePct, RealLong * ClosePct, RealLong, 0m); // 若修正为 real 75.6M
Console.WriteLine($"[多头存续名义本金] orig100M→close={byOrig.close} ; real75.6M→close={byReal.close}");
Assert.AreEqual(OrigLong * ClosePct, byOrig.close, "现状:多头存续名义本金用 orig 100M → 多次部分平仓后会偏大");
Assert.AreEqual(RealLong * ClosePct, byReal.close, "正确应:用 real 剩余本金 75.6M");
Assert.AreNotEqual(byOrig.close, byReal.close, "★ 潜在同类 bug:普通收益互换(含多头/空头存续名义本金腿)在多次部分平仓后,posiLong/posiShort 用 orig 会算错——本 deal 无此腿故不触发,属本轮修复范围外");
}
}
}