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>
This commit is contained in:
hjhan
2026-08-16 12:19:28 +08:00
co-authored by Claude Opus 4.7
parent f596bd80bc
commit 07764e7e3f
3 changed files with 273 additions and 1 deletions
@@ -0,0 +1,72 @@
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 契约参考实现(确认书公式,TEST-MATRIX §8a)——全矩阵统一 oracle 供给。
///
/// 【独立性约束·勿破坏】本类只实现确认书公式原文,禁止引用任何生产计息引擎类
/// YLErp.Modules.SwapModule.Accrual.* / SwapDealService),否则 oracle 与被测对象同源,
/// 失去"独立参考"资格(oracle 分级第一级,见 TEST-MATRIX §7.4)。
///
/// 确认书公式(国联民生收益互换确认书-现券/ETF 四份一致):
/// 参考利率(绝对) = ∏[i=1..k] ( 1 + (FR007i + 利差) × di / 365 ) 1
/// 结息额(平仓部分) = 实际平掉额 × 参考利率(绝对)
/// - k = 计息期包含的重置期个数;完整重置期 di = 重置频率(生产 7 天),末段不足按实际日历日
/// - 重置期自计息期首日按重置频率依次推算;首个重置期始于计息期首日;末段收口到计息期最后一日
/// - 利率确定日 = 每个重置期首日(重置日)的上一个营业日,取该日 FR007
/// - 计息期 = 自起始日(含)至到期日(不含)——即算头不算尾 "10"(生产主力条款)
/// - 计息基准 A/365
///
/// 营业日准则:本参考实现按周末近似(周六/周日非营业日);法定节假日历由调用方通过
/// 取价委托自行吸收(如按确定日提供同一利率)。测试与生产参数对齐(§8):重置 7 天 / 365。
/// </summary>
public static class ContractReferenceCalc
{
/// <summary>
/// 参考利率(绝对) = ∏(1 + (FR007i+利差)×di/annualDays) 1。
/// </summary>
/// <param name="startDate">计息期首日(含)</param>
/// <param name="endDate">计息期末日("10"不含/"11"含,由 calcLast 决定)</param>
/// <param name="resetDays">重置频率天数(生产 7</param>
/// <param name="spread">利差(InterestRateDefault,如 +0.25% = 0.0025</param>
/// <param name="fixing">取价委托:入参=利率确定日(重置日上一营业日),返回该日 FR007</param>
/// <param name="calcFirst">算头(生产 "10"/"11" 为 true</param>
/// <param name="calcLast">算尾(生产 "10" 为 false</param>
/// <param name="annualDays">计息基准(生产 365</param>
public static decimal ReferenceRateAbsolute(
DateTime startDate, DateTime endDate,
int resetDays, decimal spread,
Func<DateTime, decimal> fixing,
bool calcFirst = true, bool calcLast = false,
int annualDays = 365)
{
var totalDays = (endDate - startDate).Days + (calcFirst ? 0 : -1) + (calcLast ? 1 : 0);
if (totalDays <= 0) return 0m;
decimal factor = 1m;
var resetDate = startDate; // 首个重置期始于计息期首日
var remaining = totalDays;
while (remaining > 0)
{
var di = Math.Min(resetDays, remaining); // 完整期 di=resetDays,末段按实际日历日
var fixingDate = PreviousBusinessDay(resetDate);
var allIn = fixing(fixingDate) + spread;
factor *= 1m + allIn * di / annualDays;
remaining -= di;
resetDate = resetDate.AddDays(di);
}
return factor - 1m;
}
/// <summary>结息额(平仓部分)= 实际平掉额 × 参考利率(绝对)。</summary>
public static decimal ClosedInterest(decimal closedNotional, decimal referenceRate)
=> closedNotional * referenceRate;
/// <summary>利率确定日 = 重置日的上一营业日(周末近似)。</summary>
public static DateTime PreviousBusinessDay(DateTime date)
{
do { date = date.AddDays(-1); }
while (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday);
return date;
}
}
}
@@ -0,0 +1,196 @@
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
}
}