Files
zszq-trs/UnitTestProject/Modules/SwapModule/Accrual/ContractReferenceOracleTest.cs
T
hjhanandClaude Opus 4.7 07764e7e3f test(swap): 契约参考实现落地,引擎主力族盘中重放首次对齐确认书公式 oracle
ContractReferenceCalc 独立实现 §8a 确认书公式(∏/重置期切分/末段收口/
利率确定日=重置日上一营业日),禁止引用生产计息类防同源;4 例手算锚点
自验证(真实规模 5000万/2.05%/90天=75999.04 与玩具锚点)。

引擎对照首批 3 例全绿:mode9/mode2 × 复利 × "10" × T+0 部分平仓30%,
盘中重放与契约 oracle 容差 0.01 元内逐分吻合——盘中口径=确认书公式
自此有机器强制,后续补格直接复用 oracle 供给(TEST-MATRIX §7.5 已登记)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-16 12:19:28 +08:00

197 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(独立于生产引擎,公式正确性
/// 由裁决文档 §1.1/§1.2 已核过的手算值保证——真实规模 5000 万/2.05%/90 天 与玩具 4 天)。
/// ② 引擎对照:主力族(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 §87 天重置 / 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) 1python 高精度复核)
Assert.AreEqual(0.0050666026m, rate, 0.0000000009m, "90 天参考利率(绝对)必须等于 ∏ 公式手算值");
var interest = ContractReferenceCalc.ClosedInterest(ClosedNotional, rate);
Assert.AreEqual(75999.04m, interest, 0.01m, "平掉 1500 万 × 参考利率 = 裁决文档 §1.1 应结值");
}
[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天_等于裁决文档手算锚点()
{
// 裁决文档 §1.2300×[(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
}
}