Files
zszq-trs/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs
T
hjhan a86b496f87 test(margin): 新增 CalcMarginInterest 影子对账——保证金计息消除 orginPv/差分,与旧管线数值一致
- 新增 CalcMarginInterest(SwapDealService):保证金专属计息,复用 SimpleInterestAccrual 纯函数,notional 直接取保证金余额(EOD=昨日终本金/盘中=今日本金),消除融资腿差分公式 accrualBasis=TdInterestPrincipal+posiPrincipal-orginPv 与 orginPv 维度 hack(对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余)
- 保留累计语义(priorAccrued+增量),满足下游 SwapEodPositionService 字段契约
- 新增 MarginInterestShadowTest:5 场景对账(EOD 续接/首日、盘中全平/部分平仓/互换),新旧 InterestAmount/TdInterestAmount 严格一致
- 本提交仅影子对账,生产路径未改(GetInterests 仍走 CalcEodInterest/CalcUnwindInterest);下一步提交2 切换生产 + 删 orginPv hack

零生产风险;编译 0 错误;影子对账 5/5 + 现有保证金/利息回归 60/60 通过。
2026-08-13 09:37:42 +08:00

204 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 System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Modules.SwapModule.Margin;
using YLErp.Derivatives.Interest;
namespace UnitTestProject.Modules.SwapModule.Margin
{
/// <summary>
/// 影子对账:保证金腿新方法 CalcMarginInterest(无 orginPv/差分)vs
/// 旧通用管线 CalcDailySimpleInterestByEod/CalcDailySimpleInterest(带差分 + orginPv hack)。
///
/// 保证金是纯固定利率单利(FloatRateUnderlyingCode 恒空、InterestType 恒单利、SwapIntervalList 单段),
/// 旧管线差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 对保证金恒等于 posiPrincipal
/// (因 orginPv 经 PreviousBalance 对齐到昨日终保证金余额),故新方法直接用 posiPrincipal/昨日终本金作
/// notional 应与旧管线严格数值一致。本测试即在多种场景下证明这一等价,为提交2 切换生产路径提供安全网。
/// </summary>
[TestClass]
public class MarginInterestShadowTest
{
private const decimal Principal = 2_000_000m; // 保证金本金(InterestPrincipalFix
private const decimal Rate = 0.03m; // 3% 年化固定利率
private const int AnnualDays = 365;
private static readonly DateTime StartDate = new(2026, 7, 1);
private static readonly DateTime ExerciseDate = new(2027, 6, 30);
private sealed class StubSvc : SwapDealService
{
public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestShadowTest), OptUserFrom.UnitTest)) { }
}
private static trade CreateTrade() => new trade
{
id = 1, TradeNumber = "UT-MARGIN-SHADOW", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{ AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 })
}
};
/// <summary>保证金腿(初始预付金 mode 5):固定利率、单利、年化、无浮动标的。</summary>
private static swap_position CreateMarginPosition() => new swap_position
{
id = 2001, SwapTradeId = 1, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestRateDefault = Rate, InterestPrincipalFix = Principal,
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false,
InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
};
/// <summary>构造昨日终 eod_swap_position(已含累计利息 InterestProfitSum 与昨日终本金)。</summary>
private static eod_swap_position CreatePreEod(DateTime valueDate, decimal profitSum) => new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 2001,
ValueDate = valueDate,
TdInterestPrincipal = Principal, InterestPrincipalFix = Principal,
InterestProfitSum = profitSum, PosiNotionalValue = Principal, FloatRate = 0m
};
// ──────────────────────────── EOD 路径 ────────────────────────────
/// <summary>EOD 续接单日:有历史归档,notional=昨日终本金。</summary>
[TestMethod]
public void 影子_EOD续接单日_新旧一致()
{
var td = CreateTrade();
var position = CreateMarginPosition();
var valueDate = StartDate.AddDays(5);
const decimal profitSum = 820m;
// 旧方法
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterestByEod(CreatePreEod(StartDate.AddDays(4), profitSum),
valueDate, td.StartDate.Value, position, Principal, Principal,
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
// 新方法(独立 preEod,相同初始值)
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: true,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: true, swap: false);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} ClosePnL={newEvt.InterestClosePnL}");
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
}
/// <summary>EOD 首日(preEod.id==0):首日初始化 notional=posiPrincipal。</summary>
[TestMethod]
public void 影子_EOD首日_新旧一致()
{
var td = CreateTrade();
var position = CreateMarginPosition();
var valueDate = StartDate;
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterestByEod(new eod_swap_position { id = 0 },
valueDate, td.StartDate.Value, position, Principal, Principal,
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: true,
new eod_swap_position { id = 0 }, 0, add: false, settment: true, swap: false);
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
}
// ──────────────────────────── 盘中路径 ────────────────────────────
/// <summary>盘中全平(closePercent=1):新方法 notional=posiPrincipal,旧方法差分 accrualBasis 恒=posiPrincipal。</summary>
[TestMethod]
public void 影子_盘中全平_新旧一致()
{
var td = CreateTrade();
var position = CreateMarginPosition();
var valueDate = StartDate.AddDays(5);
const decimal profitSum = 820m;
// 旧方法:orginPv 经 PreviousBalance 对齐到昨日终保证金余额 → accrualBasis 恒= Principal
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, orginPv,
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
// 新方法:notional = posiPrincipal(无差分、无 orginPv
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
}
/// <summary>盘中部分平仓(closePercent=0.5):缩放累计,新旧线性等价。</summary>
[TestMethod]
public void 影子_盘中部分平仓_新旧一致()
{
var td = CreateTrade();
var position = CreateMarginPosition();
var valueDate = StartDate.AddDays(5);
const decimal profitSum = 820m;
const decimal closePct = 0.5m;
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, closePct, orginPv,
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate,
Principal * closePct, Principal, closePct,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
}
/// <summary>互换事件(swap=true,盘中):利息应归零。</summary>
[TestMethod]
public void 影子_盘中互换_利息归零()
{
var td = CreateTrade();
var position = CreateMarginPosition();
var valueDate = StartDate.AddDays(5);
var svc = new StubSvc();
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), 820m), 0, add: false, settment: false, swap: true);
Assert.AreEqual(0m, newEvt.InterestAmount, "互换利息归零");
Assert.AreEqual(0m, newEvt.TdInterestAmount, "互换 TdInterestAmount 归零");
Assert.AreEqual(0m, newEvt.InterestClosePnL, "互换 InterestClosePnL 归零");
}
}
}