- 新增 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 通过。
204 lines
11 KiB
C#
204 lines
11 KiB
C#
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 归零");
|
||
}
|
||
}
|
||
}
|