删除 项目文档/双入口口径裁决-复利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 与基线逐位一致——纯注释/文档/日志文案改动,零逻辑变更。
197 lines
11 KiB
C#
197 lines
11 KiB
C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||
using Newtonsoft.Json;
|
||
using YLErp;
|
||
using YLErp.DBModels.Enums;
|
||
using YLErp.Modules.SwapModule;
|
||
|
||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||
{
|
||
/// <summary>
|
||
/// 契约参考实现 oracle 落地(TEST-MATRIX §7 第 5 步)——两段式:
|
||
///
|
||
/// ① oracle 自验证:手算锚点直接钉 ContractReferenceCalc(独立于生产引擎,公式正确性
|
||
/// 由手算锚点保证——真实规模 5000 万/2.05%/90 天 与玩具 4 天。Excel 金标准期望值亦符合本公式(2026-08-18 复核)。
|
||
/// ② 引擎对照:主力族(mode9 标的期初全价 / mode2 合约名义本金规模 × FR007 × 复利 × "10")
|
||
/// 盘中 T+0 部分平仓 30%,GetInterests 重放结果 必须 == 契约 oracle(容差 0.01 元,§7.4)。
|
||
/// 这是本矩阵第一个"契约公式独立参考实现"级 oracle 的引擎对照用例(此前仅有 Excel 手算/工单值)。
|
||
///
|
||
/// 引擎对照用恒定 FR007 利率表——刻意免疫"利率确定日=重置日上一营业日 vs 当日"的取价日
|
||
/// 约定差异(任何确定日取到的都是同一利率),单独验证 ∏ 公式/重置期切分/算头不算尾/末段收口;
|
||
/// 取价日维度(E 维,66a97e03)由变利率用例在 oracle 侧钉住(§①第 4 例),引擎侧后续补。
|
||
///
|
||
/// 坐标登记:mode9/mode2 × 复利 × "10" × T+0 × 部分平仓30% × B=跨12个完整重置期+末段 × E=恒定利率。
|
||
/// </summary>
|
||
[TestClass]
|
||
public class ContractReferenceOracleTest
|
||
{
|
||
// ── 生产参数(TEST-MATRIX §8:7 天重置 / A365 / 真实点差 +0.25% / 千万级名义)──
|
||
private const decimal Spread = 0.0025m; // 点差 +0.25%(确认书真实点差)
|
||
private const decimal Fr007 = 0.018m; // FR007 示意水平 1.8% → all-in 2.05%
|
||
private const int ResetDays = 7;
|
||
private const int AnnualDaysConst = 365;
|
||
private const decimal Notional = 50_000_000m; // 名义 5000 万
|
||
private const decimal ClosedNotional = 15_000_000m; // 平掉 30% = 1500 万
|
||
private const decimal ClosePercent = 0.3m;
|
||
|
||
private static readonly DateTime StartDate = new(2026, 4, 27); // 周一,起息日
|
||
private static readonly DateTime Unwind90 = new(2026, 7, 26); // 90 天 = 12×7 + 6 末段
|
||
private static readonly DateTime Unwind89 = new(2026, 7, 25); // 89 天 = 12×7 + 5 末段
|
||
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
|
||
|
||
#region ① oracle 自验证(手算锚点)
|
||
|
||
[TestMethod]
|
||
public void 契约公式_恒定利率_90天12整期加6天末段_等于手算()
|
||
{
|
||
var rate = ContractReferenceCalc.ReferenceRateAbsolute(
|
||
StartDate, Unwind90, ResetDays, Spread, _ => Fr007,
|
||
calcFirst: true, calcLast: false, annualDays: AnnualDaysConst);
|
||
// 手算:(1+0.0205×7/365)^12 × (1+0.0205×6/365) − 1(python 高精度复核)
|
||
Assert.AreEqual(0.0050666026m, rate, 0.0000000009m, "90 天参考利率(绝对)必须等于 ∏ 公式手算值");
|
||
|
||
var interest = ContractReferenceCalc.ClosedInterest(ClosedNotional, rate);
|
||
Assert.AreEqual(75999.04m, interest, 0.01m, "平掉 1500 万 × 参考利率 = 确认书公式应结值");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 契约公式_恒定利率_89天末段5天_等于手算()
|
||
{
|
||
var rate = ContractReferenceCalc.ReferenceRateAbsolute(
|
||
StartDate, Unwind89, ResetDays, Spread, _ => Fr007,
|
||
calcFirst: true, calcLast: false, annualDays: AnnualDaysConst);
|
||
Assert.AreEqual(0.0050101727m, rate, 0.0000000009m, "89 天参考利率(绝对)手算值");
|
||
|
||
var interest = ContractReferenceCalc.ClosedInterest(ClosedNotional, rate);
|
||
Assert.AreEqual(75152.59m, interest, 0.01m);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 契约公式_玩具参数_算头算尾4天_等于手算锚点()
|
||
{
|
||
// 手算:300×[(1+0.011×3/365)×(1+0.011×1/365)−1] = 0.0361652(重置 3 天,利差 1%,FR 0.1%)
|
||
var rate = ContractReferenceCalc.ReferenceRateAbsolute(
|
||
new DateTime(2026, 4, 27), new DateTime(2026, 4, 30), resetDays: 3,
|
||
spread: 0.01m, fixing: _ => 0.001m,
|
||
calcFirst: true, calcLast: true, annualDays: 365);
|
||
var interest = ContractReferenceCalc.ClosedInterest(300m, rate);
|
||
Assert.AreEqual(0.0361652m, interest, 0.000001m);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 契约公式_分段变利率_利率确定日为重置日上一营业日()
|
||
{
|
||
// 计息期 [5/4(一), 5/15(五)) "10" → 11 天 = 7 + 4 末段;重置日 5/4、5/11(均为周一)
|
||
// 契约:利率确定日 = 重置日上一营业日 → 5/1(五)、5/8(五)
|
||
Assert.AreEqual(new DateTime(2026, 5, 1), ContractReferenceCalc.PreviousBusinessDay(new DateTime(2026, 5, 4)), "5/4(一)的上一营业日是 5/1(五)");
|
||
Assert.AreEqual(new DateTime(2026, 5, 8), ContractReferenceCalc.PreviousBusinessDay(new DateTime(2026, 5, 11)), "5/11(一)的上一营业日是 5/8(五)");
|
||
|
||
var fixings = new Dictionary<DateTime, decimal>
|
||
{
|
||
[new DateTime(2026, 5, 1)] = 0.02m, // 第一段 FR007 2.0% → all-in 2.25%
|
||
[new DateTime(2026, 5, 8)] = 0.03m, // 第二段 FR007 3.0% → all-in 3.25%
|
||
};
|
||
var rate = ContractReferenceCalc.ReferenceRateAbsolute(
|
||
new DateTime(2026, 5, 4), new DateTime(2026, 5, 15), ResetDays, Spread,
|
||
d => fixings[d], calcFirst: true, calcLast: false, annualDays: AnnualDaysConst);
|
||
// 手算:(1+0.0225×7/365)×(1+0.0325×4/365)−1 = 0.0007878249
|
||
Assert.AreEqual(0.0007878249m, rate, 0.0000000009m,
|
||
"分段变利率下每段必须用各自确定日的 FR007(E 维:取价日=重置日上一营业日)");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region ② 引擎对照(恒定 FR007,免疫取价日约定)
|
||
|
||
private sealed class StubSwapDealService : SwapDealService
|
||
{
|
||
public StubSwapDealService() : base(
|
||
new OptUserInfo(0, nameof(ContractReferenceOracleTest), OptUserFrom.UnitTest)) { }
|
||
|
||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||
{
|
||
if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) { rate = 0; return false; }
|
||
rate = (double)Fr007;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>fresh 重放无历史已结利息,覆写掉 DB 查询(本场景语义即 0)。</summary>
|
||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
|
||
}
|
||
|
||
private static trade CreateTrade()
|
||
{
|
||
var extend = new trade_extend
|
||
{
|
||
TradeId = 1,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDaysConst,
|
||
InterestCalcMode = "10", // 算头不算尾(生产主力条款)
|
||
SettlementRules = 0
|
||
})
|
||
};
|
||
return new trade
|
||
{
|
||
id = 1, TradeNumber = "UT-CONTRACT-REF-ORACLE", ClientId = 999998,
|
||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
|
||
trade_extend = extend
|
||
};
|
||
}
|
||
|
||
private static swap_position CreatePosition(InterestModeEnum mode) =>
|
||
new()
|
||
{
|
||
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)mode,
|
||
InterestRateDefault = Spread, InterestPrincipalFix = Notional,
|
||
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
|
||
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true, interest_rest_days = ResetDays, interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(
|
||
new List<IntervalModel> { new() { Date = ExerciseDate, Rate = Spread, Settlement = 0 } })
|
||
};
|
||
|
||
/// <summary>引擎盘中重放(T+0 fresh 持仓,T0 形状)vs 契约 oracle,容差 0.01 元。</summary>
|
||
private static void AssertEngineMatchesOracle(
|
||
InterestModeEnum mode, DateTime unwindDate, decimal posi, decimal closePosi,
|
||
decimal expectedOracleInterest)
|
||
{
|
||
var td = CreateTrade();
|
||
var position = CreatePosition(mode);
|
||
var interests = new StubSwapDealService().GetInterests(
|
||
td, td.trade_extend, unwindDate, unwindDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
posi, closePosi, ClosePercent,
|
||
(int)SwapEventTypeEnum.平仓,
|
||
tdClose: false, orginPv: posi, add: false, settment: false, newCalcLast: false, closeList: null);
|
||
|
||
Assert.AreEqual(1, interests.Count);
|
||
Assert.IsTrue(Math.Abs(interests[0].InterestAmount - expectedOracleInterest) <= 0.01m,
|
||
$"mode={mode} 引擎重放 {interests[0].InterestAmount} vs 契约 oracle {expectedOracleInterest}," +
|
||
$"diff={interests[0].InterestAmount - expectedOracleInterest}——引擎偏离确认书公式(TEST-MATRIX §8a)");
|
||
}
|
||
|
||
private static decimal OracleInterest(DateTime unwindDate) =>
|
||
ContractReferenceCalc.ClosedInterest(ClosedNotional,
|
||
ContractReferenceCalc.ReferenceRateAbsolute(
|
||
StartDate, unwindDate, ResetDays, Spread, _ => Fr007,
|
||
calcFirst: true, calcLast: false, annualDays: AnnualDaysConst));
|
||
|
||
[TestMethod]
|
||
public void 引擎_mode9_复利FR007_10_部分平仓30_90天_等于契约oracle()
|
||
=> AssertEngineMatchesOracle(InterestModeEnum.标的期初全价, Unwind90, Notional, Notional, OracleInterest(Unwind90));
|
||
|
||
[TestMethod]
|
||
public void 引擎_mode9_复利FR007_10_部分平仓30_89天_等于契约oracle()
|
||
=> AssertEngineMatchesOracle(InterestModeEnum.标的期初全价, Unwind89, Notional, Notional, OracleInterest(Unwind89));
|
||
|
||
[TestMethod]
|
||
public void 引擎_mode2_复利FR007_10_部分平仓30_显式平掉额_等于契约oracle()
|
||
=> AssertEngineMatchesOracle(InterestModeEnum.合约名义本金规模, Unwind90, Notional, ClosedNotional, OracleInterest(Unwind90));
|
||
|
||
#endregion
|
||
}
|
||
}
|