test(swap): 铺 §7-1 守恒不变量护栏,守 EOD平仓后收盘×部分平仓 裸格(§6 空洞1)
新增 4 个免库纯内存测试(GetInterestsEntrySemanticsTest):①部分平仓 剩余持仓前递且 期初=剩余+平掉;②全平 剩余前递=0;③两次部分平仓 逐日守恒 期初-剩余前递=平掉;④ClosePercentMath 多次平仓累计比例=1-∏(1-各次剩余口径)纯数学。 守恒断言落在 preEod.PosiNotionalValue(剩余前递)而非 InterestPrincipal——后者在 InitSwapDealInterest:1164 后被利息算法重赋值(1197 复利特判/1266 单利路径),语义随入口/模式变,非稳健观测点;剩余前递由 CalcUnwindInterest:1084 在 preEod.id==0 时写入,与利息算法无关,最稳健。 用途:作为 GetInterests 早路由(Tier A)合入的前置护栏——funding-leg(mode2)不触发早路由 continue,故本类任何回归都直接暴露早路由对 EOD平仓后收盘路径的破坏。验证:YLErp_UNIT_TEST_SKIP_INITIALIZATION=1 dotnet test --filter Name~守恒 全过;整类仅 2 个旧复利测试因无 96 库失败,与本次无关。
This commit is contained in:
@@ -303,5 +303,138 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 守恒不变量(§7-1, 免 oracle/免库, 守 EOD平仓后收盘×部分平仓 裸格)
|
||||
|
||||
// 守恒不变量统一断言在"剩余持仓前递"(preEod.PosiNotionalValue)上:该字段由 CalcUnwindInterest/
|
||||
// InitSwapDealInterest 在 preEod.id==0 时写入(posiPrincipal),与利息算法(单/复、FR007)无关,
|
||||
// 是最稳健、码算、免库的守恒观测点。期初(orginPv) = 前递剩余 + 平掉额(closePosiNotionalValue) 必须成立。
|
||||
// 全部内存构造(StubSwapDealService 避库);funding-leg(mode2)不触发早路由 continue,故亦是早路由改动护栏。
|
||||
|
||||
/// <summary>
|
||||
/// 建一个"无历史 eod"快照(id==0),使引擎把本次剩余持仓写入 preEod.PosiNotionalValue。
|
||||
/// </summary>
|
||||
private static eod_swap_position NewPreEod(decimal carryPrincipal)
|
||||
=> new()
|
||||
{
|
||||
id = 0, SwapTradeId = 1, PositionId = 1001,
|
||||
ValueDate = new DateTime(2026, 4, 29), ClientId = 999998,
|
||||
FloatRate = 0m, TdInterestPrincipal = carryPrincipal,
|
||||
PosiNotionalValue = carryPrincipal, InterestIncomeSum = 0.05m, InterestProfitSum = 0.05m
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// §7-1 守恒①:EOD平仓后收盘×部分平仓,引擎把剩余持仓(700)前递进 preEod.PosiNotionalValue,
|
||||
/// 且 期初 = 前递剩余(码算) + 平掉额(输入) = 1000。
|
||||
/// 守 2035e1df 裸格(§6 空洞1):若 EOD 入口把前递值误写成平掉额/期初,守恒等式即破。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void EOD平仓后收盘_部分平仓_守恒_剩余前递且期初等于剩余加平掉额()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利);
|
||||
var preEod = NewPreEod(Remaining); // 无历史 eod → 引擎写回剩余
|
||||
var eodPositions = new List<eod_swap_position> { preEod };
|
||||
var positions = new List<swap_position> { position };
|
||||
|
||||
var result = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||||
eodPositions, positions, Remaining, Closed, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||||
add: false, settment: false, newCalcLast: false, closeList: null);
|
||||
|
||||
Assert.AreEqual(1, result.Count, "EOD平仓后收盘部分平仓应产生 1 条利息事件");
|
||||
// 码算:引擎把剩余持仓前递(return 700)
|
||||
Assert.AreEqual(Remaining, preEod.PosiNotionalValue,
|
||||
"EOD平仓后收盘必须把剩余持仓(700)前递进 preEod.PosiNotionalValue;若误写平掉额/期初则守恒破坏");
|
||||
// 守恒:期初 = 前递剩余(码算) + 平掉额(输入)
|
||||
Assert.AreEqual(PreClose, preEod.PosiNotionalValue + Closed,
|
||||
"期初(orginPv=1000) 必须 = 剩余(700) + 平掉额(300);本金口径不守恒则利息算错");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// §7-1 守恒②:EOD平仓后收盘×全平,剩余持仓前递=0(清仓)。守全平非零边界的互补面。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void EOD平仓后收盘_全平_守恒_剩余前递归零()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利);
|
||||
var preEod = NewPreEod(0m);
|
||||
var eodPositions = new List<eod_swap_position> { preEod };
|
||||
var positions = new List<swap_position> { position };
|
||||
|
||||
var result = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||||
eodPositions, positions, 0m, PreClose, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||||
add: false, settment: false, newCalcLast: false, closeList: null);
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual(0m, preEod.PosiNotionalValue,
|
||||
"全平后剩余持仓前递必须为 0;非 0 表示平仓未清仓,守恒破坏");
|
||||
Assert.AreEqual(PreClose, preEod.PosiNotionalValue + PreClose,
|
||||
"全平守恒:期初(1000) = 剩余(0) + 平掉额(1000)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// §7-1 守恒③(逐日):两次部分平仓,Day2 剩余前递 = 当日剩余(码算),且 期初 - 前递剩余 = 平掉额,
|
||||
/// 构成跨日携带链守恒。Day1 期初1000→平300剩700;Day2 期初700→平210剩490;累计平掉510+剩余490=1000。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void EOD平仓后收盘_两次部分平仓_逐日守恒_期初减剩余前递等于平掉额()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利);
|
||||
|
||||
// Day1:期初1000,平300,剩700
|
||||
var preEod1 = NewPreEod(PreClose);
|
||||
var result1 = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||||
new List<eod_swap_position> { preEod1 }, new List<swap_position> { position },
|
||||
Remaining, Closed, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||||
add: false, settment: false, newCalcLast: false, closeList: null);
|
||||
Assert.AreEqual(1, result1.Count);
|
||||
Assert.AreEqual(Remaining, preEod1.PosiNotionalValue, "Day1 剩余前递应为 700");
|
||||
|
||||
// Day2:期初=Day1剩余700,平210,剩490
|
||||
const decimal day2OrginPv = 700m;
|
||||
const decimal day2Closed = 210m;
|
||||
const decimal day2Remaining = 490m;
|
||||
var preEod2 = NewPreEod(day2OrginPv); // 承载=Day1剩余700
|
||||
var result2 = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||||
new List<eod_swap_position> { preEod2 }, new List<swap_position> { position },
|
||||
day2Remaining, day2Closed, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: day2OrginPv,
|
||||
add: false, settment: false, newCalcLast: false, closeList: null);
|
||||
|
||||
Assert.AreEqual(1, result2.Count);
|
||||
// 码算:Day2 剩余前递=当日剩余(490)
|
||||
Assert.AreEqual(day2Remaining, preEod2.PosiNotionalValue, "Day2 剩余前递=剩余(490,码算值)");
|
||||
// 逐日守恒:期初 - 剩余前递 = 平掉额(210)
|
||||
Assert.AreEqual(day2Closed, day2OrginPv - preEod2.PosiNotionalValue,
|
||||
"Day2 守恒:期初(700) - 剩余前递(490) 必须 = 平掉额(210);跨日携带链本金不守恒则利息算错");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// §7-1 守恒④(纯数学,ClosePercentMath):多次平仓累计占期初比例 = 1 - ∏(1 - 各次剩余口径)。
|
||||
/// 初次占期初30%(平300/名义1000)→剩余口径0.3;二次占期初50%(平350/剩余700)→剩余口径0.5;
|
||||
/// 累计平掉 = 1 - 0.7×0.5 = 0.65。验证 ClosePercentMath 双口径换算在多次平仓下不漂移。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 多次平仓_占期初累计比例等于各次剩余口径连乘补数()
|
||||
{
|
||||
var b1 = ClosePercentMath.ToRemainingClosePercent(0.3m, 1000m, 1000m);
|
||||
Assert.AreEqual(0.3m, b1, "初次平仓占期初30% → 剩余口径应为 0.3");
|
||||
var b2 = ClosePercentMath.ToRemainingClosePercent(0.5m, 700m, 700m);
|
||||
Assert.AreEqual(0.5m, b2, "二次平仓占期初50%(占剩余700) → 剩余口径应为 0.5");
|
||||
|
||||
var cumulativeClosed = 1m - (1m - b1) * (1m - b2);
|
||||
Assert.AreEqual(0.65m, cumulativeClosed, 0.0000001m,
|
||||
"多次平仓累计平掉比例必须=各次剩余口径连乘的补数;否则本金口径在多次平仓下分裂");
|
||||
|
||||
var back = ClosePercentMath.ToOriginalClosePercent(cumulativeClosed, 1000m, 1000m);
|
||||
Assert.AreEqual(0.65m, back, 0.0000001m, "累计占期初比例反向还原必须一致");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user