删除 项目文档/双入口口径裁决-复利mode2部分平仓-20260816.md: 四轮状态改写的过程文档,终态有价值的技术事实已转移到代码注释与测试锚点。 保留的技术事实(新家): - SwapEodPositionService:1325:恒1重算中间值不进结算现金流;系统端到端结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核);改动口径前必读该测试家族——破坏 ∏ 恒等式的调整会被其拦截 - ContractReferenceOracleTest:13:Excel 金标准期望值亦符合确认书公式(复核记录) - EntrySemanticsTest 手算锚点 0.036165 保留(公式自含,去文档引用) 清理的 saga 痕迹:契约修复/裁决文档/暂缓/证伪 等过程性话语全部移除, 测试方法名与注释改为中性事实描述;[EOD平仓后收盘结息] 日志文案 '恒1全额结息(历史行为,契约修复暂缓)'→'全额结息(恒1惯例)'。 验证:编译 0 错误;EntrySemantics+Oracle 16/16 绿;全量 928=145/771/12 与基线逐位一致——纯注释/文档/日志文案改动,零逻辑变更。
471 lines
27 KiB
C#
471 lines
27 KiB
C#
using Newtonsoft.Json;
|
||
using YLErp.DBModels.Enums;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// GetInterests 双显式入口语义字符化测试(Step3"特判降级"的前置钉子)。
|
||
///
|
||
/// 背景:GetIntradayUnwindInterests(盘中:平仓前剩余×实际比例)与
|
||
/// CalcEodPostCloseSettleInterests(EOD平仓后收盘:平仓后剩余×恒1)是同一经济事件
|
||
/// (部分平仓)的两套传参语义,靠 GetInterests 内 mode2 无条件覆盖 / mode9 全平兜底粘合。
|
||
/// 本测试钉死当前行为,使后续特判降级/语义重构有回归网:
|
||
/// ① 复利×mode2:closePrincipal(特判产物)是 CalcDailyCompoundInterest 的重放本金——
|
||
/// 两入口 closePosiNotionalValue 均为实际平掉额 → InterestAmount 必须相等;
|
||
/// ② 单利×mode2:CalcDailySimpleInterest 消费的是 posiPrincipal×closePercent——
|
||
/// 盘中(平仓前×比例) vs EOD(剩余×1) 数值口径可能不同,本测试【记录现状】(见各断言注释);
|
||
/// ③ mode9 全平(posi=0):兜底覆盖生效,结息额非零。
|
||
///
|
||
/// 数据基建复用 GetInterestsUnitTest_T0 的构建器口径(T+0,4/27起息,"11"算头算尾)。
|
||
/// </summary>
|
||
[TestClass]
|
||
public class GetInterestsEntrySemanticsTest
|
||
{
|
||
private const decimal Principal = 1000m;
|
||
private const decimal FixedRate = 0.01m;
|
||
private const decimal FloatRate = 0.001m;
|
||
private const int AnnualDays = 365;
|
||
private const int ResetPeriod = 3;
|
||
|
||
private static readonly DateTime TradeDate = new(2026, 4, 27);
|
||
private static readonly DateTime StartDate = new(2026, 4, 27);
|
||
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
|
||
private static readonly DateTime UnwindDate = new(2026, 4, 30);
|
||
|
||
// 平仓前剩余 1000,平掉 30%(300),收盘后剩余 700
|
||
private const decimal PreClose = 1000m;
|
||
private const decimal Closed = 300m;
|
||
private const decimal Remaining = 700m;
|
||
private const decimal ClosePercent = 0.3m;
|
||
|
||
#region Stub(浮动利率内存取价,与 T0 同款)
|
||
|
||
private sealed class StubSwapDealService : SwapDealService
|
||
{
|
||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||
public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary<DateTime, double> floatRates) : base(optUser)
|
||
{
|
||
_floatRates = floatRates;
|
||
}
|
||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||
{
|
||
if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) { rate = 0; return false; }
|
||
if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true;
|
||
rate = 0;
|
||
return false;
|
||
}
|
||
// 离线自洽:本测试场景无历史已结利息,等价于此前"空库查询返回 0"的行为,
|
||
// 使复利路径(GetConsumedInterest)不再依赖数据库连通(YLErp_UNIT_TEST_SKIP_INITIALIZATION=1 可跑)。
|
||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||
=> 0m;
|
||
}
|
||
|
||
private static SwapDealService CreateService() => new StubSwapDealService(
|
||
new OptUserInfo(0, nameof(GetInterestsEntrySemanticsTest), OptUserFrom.UnitTest),
|
||
new Dictionary<DateTime, double>
|
||
{
|
||
[new DateTime(2026, 4, 27)] = (double)FloatRate,
|
||
[new DateTime(2026, 4, 28)] = (double)FloatRate,
|
||
[new DateTime(2026, 4, 29)] = (double)FloatRate,
|
||
[new DateTime(2026, 4, 30)] = (double)FloatRate,
|
||
});
|
||
|
||
#endregion
|
||
|
||
#region 数据构建(T0 口径)
|
||
|
||
private static trade CreateTrade()
|
||
{
|
||
var extend = new trade_extend
|
||
{
|
||
TradeId = 1,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "11", // 算头算尾
|
||
SettlementRules = 0
|
||
})
|
||
};
|
||
return new trade
|
||
{
|
||
id = 1, TradeNumber = "UT-INT-ENTRY-SEMANTICS", ClientId = 999998,
|
||
TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate,
|
||
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
|
||
trade_extend = extend
|
||
};
|
||
}
|
||
|
||
private static swap_position CreatePosition(InterestModeEnum mode, InterestTypeEnum interestType, bool floating = false)
|
||
{
|
||
var intervalModels = new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
|
||
};
|
||
return new swap_position
|
||
{
|
||
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)mode,
|
||
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
|
||
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
|
||
IsInitial = true, Invalid = false, InterestType = (int)interestType,
|
||
IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = 0,
|
||
FloatRateUnderlyingCode = floating ? "FR007" : null,
|
||
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
|
||
};
|
||
}
|
||
|
||
private static eod_swap_position CreatePreEod(decimal interestSum, decimal principal)
|
||
=> new()
|
||
{
|
||
id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = new DateTime(2026, 4, 29),
|
||
ClientId = 999998, FloatRate = FloatRate, TdInterestPrincipal = principal,
|
||
PosiNotionalValue = principal, InterestIncomeSum = interestSum, InterestProfitSum = interestSum
|
||
};
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 复利×mode2×部分平仓30%:【同请求形状⇒同额】oracle(契约目标语义,修复落地时的现成回归网)。
|
||
///
|
||
/// 修复前(b01b485e 钉住的分歧):盘中 0.036165(平掉额全程重放=确认书公式)vs
|
||
/// EOD 0.059042(恒1 掉进全平专属分支,全腿待实现+末段增量,无契约依据,重算结果被丢弃)。
|
||
/// 说明:EOD 平仓后收盘走恒1惯例形状;端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障
|
||
/// (平仓前剩余+实际平掉额+真实比例),部分平仓不再进 closePrecent==1 分支。
|
||
/// (最终全平=剩余额×∏利率,2026-08-18 手算复核 Excel BL/BN 均符合)。
|
||
/// 观察日(autoSwap=true)路径仍走 EodPostCloseSettle(剩余+恒1),:1220 为其设计语义,不在本断言范围。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 复利_mode2_部分平仓_双入口契约口径一致()
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.复利, floating: true);
|
||
var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose);
|
||
var eodPositions = new List<eod_swap_position> { preEod };
|
||
var positions = new List<swap_position> { position };
|
||
|
||
var intraday = CreateService().GetIntradayUnwindInterests(InterestCalcRequest.IntradayUnwind(
|
||
td, td.trade_extend, UnwindDate, UnwindDate, eodPositions, positions,
|
||
PreClose, Closed, ClosePercent,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: true, orginPv: PreClose, add: true, newCalcLast: false, closeList: null));
|
||
|
||
// 契约口径形状(与盘中同形状):平仓前剩余 1000 + 平掉额 300 + 真实比例 0.3
|
||
var eodPostClose = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||
eodPositions, positions, PreClose, Closed, ClosePercent,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||
add: true, settment: false, newCalcLast: false, closeList: null);
|
||
|
||
Assert.AreEqual(1, intraday.Count);
|
||
Assert.AreEqual(1, eodPostClose.Count);
|
||
Console.WriteLine($"[复利mode2] 盘中 InterestAmount={intraday[0].InterestAmount} / EOD={eodPostClose[0].InterestAmount}");
|
||
|
||
// 契约 oracle:两入口同请求形状必须同额(=确认书公式"平掉额×全程参考利率")
|
||
Assert.AreEqual(intraday[0].InterestAmount, eodPostClose[0].InterestAmount, 0.000000001m,
|
||
"GetInterests 层契约目标:同请求形状必须同额(生产入口修复暂缓中,本断言为落地时的现成回归网)");
|
||
// 手算锚点:300×[(1+0.011×3/365)×(1+0.011×1/365)−1]=0.036165(确认书公式)
|
||
Assert.AreEqual(0.036165m, Math.Round(intraday[0].InterestAmount, 6, MidpointRounding.AwayFromZero),
|
||
"盘中重放=契约公式手算锚点 0.036165");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【回归钉子】复利×mode2×部分平仓:观察日路径(EodPostCloseSettle 剩余+恒1)保持设计语义不回退。
|
||
/// 修复只改 autoSwap=false 分支;观察日恒1 全量结息是 :1220 分支的设计意图(结现),锁死其当前值。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 复利_mode2_部分平仓_观察日恒1语义保持()
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.复利, floating: true);
|
||
var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose);
|
||
var eodPositions = new List<eod_swap_position> { preEod };
|
||
var positions = new List<swap_position> { position };
|
||
|
||
var observationDay = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||
eodPositions, positions, Remaining, Closed, 1m,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||
add: true, settment: false, newCalcLast: false, closeList: null);
|
||
|
||
Assert.AreEqual(1, observationDay.Count);
|
||
Assert.AreEqual(0.059041913305m, observationDay[0].InterestAmount, 0.000000001m,
|
||
"观察日(autoSwap=true)路径:剩余+恒1 的全平分支为其设计语义(结现),修复不得改变此值");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单利×mode2×部分平仓30%:记录两入口当前口径(快照×比例 vs 重放基数差异面)。
|
||
/// 单利消费 posiPrincipal×closePercent:盘中 1000×0.3 vs EOD 700×1 —— 若两值不等,
|
||
/// 这是当前系统的已知口径差异面(非断言失败项),数值以 Console 留档,供特判降级时对照。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 单利_mode2_部分平仓_双入口口径留档()
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利);
|
||
var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose);
|
||
var eodPositions = new List<eod_swap_position> { preEod };
|
||
var positions = new List<swap_position> { position };
|
||
|
||
var intraday = CreateService().GetIntradayUnwindInterests(InterestCalcRequest.IntradayUnwind(
|
||
td, td.trade_extend, UnwindDate, UnwindDate, eodPositions, positions,
|
||
PreClose, Closed, ClosePercent,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: true, orginPv: PreClose, add: true, newCalcLast: false, closeList: null));
|
||
|
||
// 契约口径形状(与盘中同形状):平仓前剩余 1000 + 平掉额 300 + 真实比例 0.3
|
||
var eodPostClose = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||
eodPositions, positions, PreClose, Closed, ClosePercent,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||
add: true, settment: false, newCalcLast: false, closeList: null);
|
||
|
||
Assert.AreEqual(1, intraday.Count);
|
||
Assert.AreEqual(1, eodPostClose.Count);
|
||
Console.WriteLine($"[单利mode2] 盘中 InterestAmount={intraday[0].InterestAmount} / EOD={eodPostClose[0].InterestAmount}");
|
||
Console.WriteLine($"[单利mode2] TdInterestAmount: 盘中={intraday[0].TdInterestAmount} / EOD={eodPostClose[0].TdInterestAmount}");
|
||
// 契约目标:两入口同请求形状必须同额(单利:平掉额基数 + 快照×比例链路一致)
|
||
Assert.AreEqual(intraday[0].InterestAmount, eodPostClose[0].InterestAmount, 0.000000001m,
|
||
"GetInterests 层契约目标:单利×mode2 同请求形状必须同额(生产入口修复暂缓中)");
|
||
Assert.IsTrue(intraday[0].InterestAmount != 0m, "盘中单利结息额不应为0");
|
||
}
|
||
|
||
/// <summary>
|
||
/// mode9 全平(契约目标形状:平仓前剩余=平掉额=1000、比例恒1):
|
||
/// 结息额非零且=全平语义(:1220 全平分支:待实现+末段增量,尾差一次带走——设计意图维持)。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 复利_mode9_全平_兜底覆盖生效结息额非零()
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(InterestModeEnum.标的期初全价, InterestTypeEnum.复利, floating: true);
|
||
var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose);
|
||
var eodPositions = new List<eod_swap_position> { preEod };
|
||
var positions = new List<swap_position> { position };
|
||
|
||
// 全平:平仓前剩余=平掉=1000,比例恒1(全平专属分支)
|
||
var result = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
|
||
eodPositions, positions, PreClose, PreClose, 1m,
|
||
(int)SwapEventTypeEnum.平仓, tdClose: false, orginPv: PreClose,
|
||
add: true, settment: false, newCalcLast: false, closeList: null);
|
||
|
||
Assert.AreEqual(1, result.Count);
|
||
Console.WriteLine($"[复利mode9全平] InterestAmount={result[0].InterestAmount}");
|
||
Assert.IsTrue(result[0].InterestAmount != 0m,
|
||
"mode9 全平:结息本金=平掉额(1000),结息额非零(全平语义钉子)");
|
||
}
|
||
|
||
#region CalcEodPostCloseSettleInterests 接缝映射钉子
|
||
|
||
/// <summary>
|
||
/// 参数捕获 stub:拦下 CalcSwapInterests 的全部实参,不触库、不真算。
|
||
/// </summary>
|
||
private sealed class CalcSwapInterestsCapture : TestableSwapEodPositionService
|
||
{
|
||
public CalcSwapInterestsCapture() : base(nameof(GetInterestsEntrySemanticsTest)) { }
|
||
|
||
public List<swap_flow_event> CapturedCloseList = null;
|
||
public bool CapturedTdClose;
|
||
public int CapturedEventType;
|
||
public decimal CapturedPosiNotional;
|
||
public decimal CapturedClosePosiNotional;
|
||
public decimal CapturedClosePercent;
|
||
public decimal CapturedOrginPv;
|
||
public bool CapturedAdd;
|
||
public bool CapturedSettment;
|
||
public bool CapturedNewCalcLast;
|
||
public int CallCount;
|
||
|
||
protected override List<swap_flow_event> CalcSwapInterests(
|
||
trade td, trade_extend tradeExtend,
|
||
DateTime valueDate, DateTime unwindDate,
|
||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||
decimal posiNotionalValue,
|
||
decimal closePosiNotionalValue, decimal closePrecent,
|
||
int eventType, bool tdClose,
|
||
decimal orginPv,
|
||
bool add = false, bool settment = true, bool newCalcLast = false,
|
||
List<swap_flow_event> closeList = null)
|
||
{
|
||
CallCount++;
|
||
CapturedTdClose = tdClose; CapturedEventType = eventType;
|
||
CapturedPosiNotional = posiNotionalValue; CapturedClosePosiNotional = closePosiNotionalValue;
|
||
CapturedClosePercent = closePrecent; CapturedOrginPv = orginPv;
|
||
CapturedAdd = add; CapturedSettment = settment; CapturedNewCalcLast = newCalcLast;
|
||
CapturedCloseList = closeList;
|
||
return new List<swap_flow_event>();
|
||
}
|
||
|
||
public List<swap_flow_event> ExposedEodPostCloseSettle(InterestCalcRequest req)
|
||
=> CalcEodPostCloseSettleInterests(req);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 钉死 InterestCalcRequest.EodPostCloseSettle 工厂 → CalcEodPostCloseSettleInterests →
|
||
/// CalcSwapInterests 的位置参数转发契约。这段转发是位置传参最易错位的环节
|
||
/// (posiNotionalValue/closePosiNotionalValue/orginPv 三个相邻同型 decimal,编译器不查错位),
|
||
/// 任何映射改动(含将来删 needPrice/grossPrice 死参数)都必须保持本断言绿。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void EOD平仓后收盘_工厂到接缝_参数映射钉死()
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利);
|
||
var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose);
|
||
var positions = new List<swap_position> { position };
|
||
|
||
var stub = new CalcSwapInterestsCapture();
|
||
var req = InterestCalcRequest.EodPostCloseSettle(
|
||
td, td.trade_extend, UnwindDate, UnwindDate,
|
||
new List<eod_swap_position> { preEod }, positions,
|
||
remainingNotionalAfterClose: Remaining,
|
||
closedNotional: Closed,
|
||
eventType: (int)SwapEventTypeEnum.平仓, tdClose: false,
|
||
orginPv: PreClose, add: true, newCalcLast: false);
|
||
|
||
stub.ExposedEodPostCloseSettle(req);
|
||
|
||
Assert.AreEqual(1, stub.CallCount, "默认实现应恰好调用一次 CalcSwapInterests(虚接缝兼容既有测试替身)");
|
||
Assert.AreEqual(Remaining, stub.CapturedPosiNotional, "posiNotionalValue 位 = 平仓后剩余(700)——语义核心,错位即红");
|
||
Assert.AreEqual(Closed, stub.CapturedClosePosiNotional, "closePosiNotionalValue 位 = 实际平掉额(300)");
|
||
Assert.AreEqual(1m, stub.CapturedClosePercent, "closePrecent 恒 1(全额结息)");
|
||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, stub.CapturedEventType);
|
||
Assert.IsFalse(stub.CapturedTdClose);
|
||
Assert.AreEqual(PreClose, stub.CapturedOrginPv, "orginPv 位 = 上一日终本金——与相邻 decimal 最易错位处");
|
||
Assert.IsTrue(stub.CapturedAdd);
|
||
Assert.IsFalse(stub.CapturedSettment, "settment=false:走盘中重放算法(EOD平仓后收盘复用重放)");
|
||
Assert.IsFalse(stub.CapturedNewCalcLast);
|
||
Assert.IsNull(stub.CapturedCloseList, "该场景不传 closeList");
|
||
}
|
||
|
||
#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
|
||
}
|
||
}
|