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:
@@ -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^N(N=计息天数),
|
||||
// 而正确应为 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,000(100%返 9,180,000 的一半)。
|
||||
// buggy:Fix × 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。buggy:Fix × 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,000(closePercent=1 时指数 bug 不显现,须保持正确)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user