fix(swap): 修复平仓预付金应返还本金被 closePercent^N 指数级缩小

根因:CalcDailySimpleInterest 单利非重置日 else 分支曾把已×closePercent 的
InterestPrincipal 回填给计息基数 tdDynomicPrincipal,使下一个非重置日再乘一次,
累积成 InterestPrincipal = Fix × closePercent^N(N=计息天数),部分平仓时指数级缩小。
100% 因 1^N=1 不显现,故此前漏测。

修复:非重置日与重置日、日终 CalcDailySimpleInterestByEod 三者对齐——
显示本金 InterestPrincipal = 基数×closePercent(只缩放一次),计息基数不缩放。

TDD 铁证(GLMS-20260701-0006, interest_rest_days=7, Fix=9,180,000):
- 修复前 50%→71,718.75(=Fix×0.5^7)、10%→0.918(=Fix×0.1^7)
- 修复后 50%→4,590,000、10%→918,000、100%→9,180,000
新增 CalcUnwindMultiDay helper + 3 用例,SwapUnwindPrepayPrincipalBugTdd 共 9 绿,
SwapModule 全量 186 通过无回归。
This commit is contained in:
hjhan
2026-07-14 16:33:59 +08:00
parent b9462510de
commit 83a55dab33
2 changed files with 112 additions and 1 deletions
@@ -212,6 +212,111 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(100_000m, total, "多次部分平仓合计应=保证金本金 10万");
}
// ---- 盘中路径 CalcDailySimpleInterest 的 closePercent^N 指数级缩小 bug ----
// 生产铁证 GLMS-20260701-0006:预付金腿 Fix=9,180,000、interest_rest_days=7、单利、不计息。
// 平仓弹窗(swaptrade2/GetUnwindInterestList → 盘中路径 CalcDailySimpleInterest)返回:
// 100% → 9,180,000 (对) 50% → 71,718.75 (错) 10% → 0.918 (错)
// 数学关系精确成立:9,180,000×0.5^7 = 71,718.75、9,180,000×0.1^7 = 0.918。
// 根因:CalcDailySimpleInterest 非重置日 else 分支
// flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
// tdDynomicPrincipal = flowEvent.InterestPrincipal; // ★把"已×closePercent"的值回填
// 使下一个非重置日再乘一次 closePercent → InterestPrincipal = Fix × closePercent^NN=计息天数),
// 而正确应为 Fix × closePercent(线性,与日终 CalcDailySimpleInterestByEod:1164-1165 只乘一次一致)。
// 现有 6 个用例 interest_rest_days=1 且 UnwindDate=StartDate+1(calcDays=1),循环首尾都被 continue 跳过、
// 从不进 else,故漏掉此 bug;本组用例用 restDays=7、跨多日、带 eod 归档触发 else 累积复现之。
private const decimal ProdPrepayFix = 9_180_000m;
private static readonly DateTime ProdPosiStart = new(2026, 7, 2);
private static readonly DateTime ProdEodValueDate = new(2026, 7, 4);
private static readonly DateTime ProdUnwindDate = new(2026, 7, 13);
/// <summary>
/// 盘中路径复现:restDays=7、PosiStart→Unwind 跨 11 天、eod 归档到 07-04。
/// 与生产 GLMS-20260701-0006 完全对齐,buggy 代码产出 Fix × closePercent^7。
/// </summary>
private swap_flow_event CalcUnwindMultiDay(decimal closePercent, decimal fix = ProdPrepayFix, int restDays = 7,
decimal rate = 0m)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾(与生产一致)
SettlementRules = 0
})
};
var td = new trade
{
id = 1, TradeNumber = "UT-PREPAY-EXP", ClientId = 999998,
TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart,
ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)fix, Notional = (double)fix,
trade_extend = extend
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestRateDefault = rate, InterestPrincipalFix = fix,
PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = restDays,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
var eod = new List<eod_swap_position>
{
new eod_swap_position
{
id = 7, SwapTradeId = 1, PositionId = 1001,
ValueDate = ProdEodValueDate,
TdInterestPrincipal = fix, // 生产 eod_swap_position(35774) TdInterestPrincipal=9,180,000
PosiNotionalValue = fix,
InterestProfitSum = 0m, FloatRate = 0m
}
};
var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate,
eod, new List<swap_position> { position },
fix, fix, fix, fix, closePercent,
(int)SwapEventTypeEnum.,
false, false, 0, fix, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "预付金腿应生成 1 条 flow_event");
return interests[0];
}
[TestMethod]
public void 50_7_应返还本金应线性缩放而非指数级()
{
var fe = CalcUnwindMultiDay(0.5m);
Console.WriteLine($"[TDD][盘中50%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=71,718.75, 期望=4,590,000)");
// 正确:Fix × closePercent = 9,180,000 × 0.5 = 4,590,000100%返 9,180,000 的一半)。
// buggyFix × 0.5^7 = 71,718.75(生产实测),被指数级缩小 ~64 倍。
Assert.AreEqual(4_590_000m, fe.InterestPrincipal,
"50% 平仓: 应返还本金应=Fix×0.5=4,590,000,不应被 closePercent^7 缩成 71,718.75");
}
[TestMethod]
public void 10_7_应返还本金应线性缩放而非指数级()
{
var fe = CalcUnwindMultiDay(0.1m);
Console.WriteLine($"[TDD][盘中10%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=0.918, 期望=918,000)");
// 正确:Fix × 0.1 = 918,000。buggyFix × 0.1^7 = 0.918(生产实测),缩小 100 万倍。
Assert.AreEqual(918_000m, fe.InterestPrincipal,
"10% 平仓: 应返还本金应=Fix×0.1=918,000,不应被 closePercent^7 缩成 0.918");
}
[TestMethod]
public void _盘中重置周期7天_应返还本金应等于保证金本金()
{
// closePercent=1 → 1^N=1,指数 bug 对 100% 无影响(故用户看 100% 正常),此用例锚定不回归。
var fe = CalcUnwindMultiDay(1m);
Console.WriteLine($"[TDD][盘中100%] 实测 InterestPrincipal={fe.InterestPrincipal} (期望=9,180,000)");
Assert.AreEqual(ProdPrepayFix, fe.InterestPrincipal,
"100% 平仓: 应返还本金应=Fix=9,180,000closePercent=1 时指数 bug 不显现,须保持正确)");
}
[TestMethod]
public void _计息基数也被根因修复_利息基于保证金本金()
{