Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
T
hjhan 9d8cfb5ad0 refactor(accrual): 拆分FundingLegAccrual→SimpleInterestAccrual/CompoundInterestAccrual
单利与复利语义完全不同(单利本金恒定/复利重置日并本金),
拆成两个独立静态类,各自只含自己的方法:

SimpleInterestAccrual:
- AccrueEod (原AccrueSimpleEod)
- AccruePeriod (原AccrueSimplePeriod)

CompoundInterestAccrual:
- EodBasis (原CompoundEodBasis)
- AccrueEod (原AccrueCompoundEod)
- AccruePeriod (原AccrueCompoundPeriod)

方法名去掉Simple/Compound前缀(类名已携带类型),消除冗余
SwapModule零回归(7基线/510通过)
2026-08-12 13:40:47 +08:00

153 lines
8.2 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 Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
/// ============================================================================
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
/// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
/// 既有测试(AS_* / SwapUnwindPrepay*Tdd)全是「单事件」场景,没有覆盖
/// 「同一天第 2 次部分平仓」:第 1 次平仓后持仓已缩减,第 2 次平仓传入的
/// notional 应是缩减后的实时值。本文件用内存对象驱动真实 GetInterests 两次,
/// 定性验证「同日多次部分平仓」的应返还本金/计息基数是否按线性拆分。
///
/// 建模:标的期初全价腿(mode=9),初始名义本金 N=1,000,000;上一日终归档
/// eod.TdInterestPrincipal=N、PosiNotionalValue=NlastEod)。
/// 第1次平仓 30%closePercent=0.3,传入 notional=N
/// 第2次平仓剩余 50%closePercent=0.5,传入 notional=0.7N=实时缩减后)
/// 预期(领域线性):IP1=0.3N、IP2=0.5×0.7N=0.35N,合计 0.65N。
/// 若公式在 notional 正确传入时仍非线性 → 暴露 unwind 基数滚动缺陷。
/// 注:本测试同时是「前置条件护栏」——它证明"只要调用方传入实时缩减后的
/// notional,公式即线性正确";若生产在第2次平仓时传入的是未缩减的陈旧 notional,
/// 则结果会偏离,需另查调用方(GetUnwindInterests 的 notional 来源)。
/// ============================================================================
/// </summary>
[TestClass]
public class SwapUnwindSameDayDoublePartialTest
{
private sealed class StubSwapDealService : SwapDealService
{
public StubSwapDealService(OptUserInfo optUser) : base(optUser) { }
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = 0;
return false; // 标的期初全价腿无浮动标的,不查库
}
}
private const decimal N = 1_000_000m; // 初始名义本金(标的期初全价维度)
private const int AnnualDays = 365;
private static readonly DateTime StartDate = new(2026, 8, 1);
private static readonly DateTime LastEodDate = new(2026, 8, 4);
private static readonly DateTime UnwindDate = new(2026, 8, 5);
private SwapDealService _svc;
[TestInitialize]
public void Init() => _svc = new StubSwapDealService(new OptUserInfo(0, nameof(SwapUnwindSameDayDoublePartialTest), OptUserFrom.UnitTest));
private static trade MakeTrade()
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾
SettlementRules = 0
})
};
return new trade
{
id = 1, TradeNumber = "UT-SAMEDAY-2UNWIND", ClientId = 999997,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = new DateTime(2027, 8, 1), TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)N, Notional = (double)N,
trade_extend = extend
};
}
/// <summary>标的期初全价腿(mode=9),单利、重置周期1天(无重置日分支,隔离基数滚动行为)。</summary>
private static swap_position MakePosition(decimal posiNotionalValue)
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价,
InterestRateDefault = 0.01m, InterestPrincipalFix = 0m,
PosiStartDate = StartDate, PosiMatuirityDate = new DateTime(2027, 8, 1),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]",
PosiNotionalValue = posiNotionalValue
};
}
/// <summary>上一日终归档:basis 锚点。TdInterestPrincipal=N、PosiNotionalValue=NlastEod 尚未缩减)。</summary>
private static List<eod_swap_position> MakeLastEod()
{
return new List<eod_swap_position>
{
new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = LastEodDate,
TdInterestPrincipal = N,
PosiNotionalValue = N,
InterestProfitSum = 0m, FloatRate = 0m
}
};
}
/// <summary>
/// 驱动一次盘中平仓(与前端平仓页相同路径,仅用内存对象、不查库)。
/// <paramref name="currentNotional"/> = 本次平仓时实时持仓名义本金;
/// <paramref name="closePercent"/> = 占剩余比例(前端 ToRemainingClosePercent 转换后的值)。
/// orginPv 取 lastEod 名义本金 N(与 GetUnwindInterests 真实传参 ResolveUnwindPreviousNotional(lastEod) 一致)。
/// </summary>
private swap_flow_event CalcUnwind(decimal currentNotional, decimal closePercent)
{
var td = MakeTrade();
var position = MakePosition(currentNotional);
var interests = _svc.GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
MakeLastEod(), new List<swap_position> { position },
currentNotional, currentNotional, currentNotional, currentNotional * closePercent, closePercent,
(int)SwapEventTypeEnum.平仓,
false, false, 0, N, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "标的期初全价腿应生成 1 条 flow_event");
return interests[0];
}
[TestMethod]
public void 同日两次部分平仓_应返还本金应线性拆分且合计等于65pct()
{
// 第1次:平仓 30%(持仓仍满 N)
var fe1 = CalcUnwind(N, 0.3m);
// 第2次:同日再平剩余 50%(持仓已缩减为 0.7N,传入实时 notional
var fe2 = CalcUnwind(0.7m * N, 0.5m);
Console.WriteLine($"[表征] 第1次(30%) InterestPrincipal={fe1.InterestPrincipal} InterestAmount={fe1.InterestAmount}");
Console.WriteLine($"[表征] 第2次(剩余50%) InterestPrincipal={fe2.InterestPrincipal} InterestAmount={fe2.InterestAmount}");
Console.WriteLine($"[表征] 合计 InterestPrincipal={fe1.InterestPrincipal + fe2.InterestPrincipal} (期望=0.65N={(0.65m * N)})");
// 领域预期(线性):第1次返 0.3N,第2次返 0.5×0.7N=0.35N,合计 0.65N
Assert.AreEqual(0.3m * N, fe1.InterestPrincipal,
"第1次平仓30%: 应返还本金应=0.3N(线性)");
Assert.AreEqual(0.35m * N, fe2.InterestPrincipal,
"第2次平仓剩余50%: 应返还本金应=0.5×0.7N=0.35N(基于实时缩减后的 notional,线性)");
Assert.AreEqual(0.65m * N, fe1.InterestPrincipal + fe2.InterestPrincipal,
"同日两次部分平仓合计应返还本金应=0.65N(线性拆分,无重复/遗漏)");
}
}
}