using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
///
/// 【同日多次部分平仓 · 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=N(lastEod)。
/// 第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 来源)。
/// ============================================================================
///
[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
};
}
/// 标的期初全价腿(mode=9),单利、重置周期1天(无重置日分支,隔离基数滚动行为)。
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
};
}
/// 上一日终归档:basis 锚点。TdInterestPrincipal=N、PosiNotionalValue=N(lastEod 尚未缩减)。
private static List MakeLastEod()
{
return new List
{
new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = LastEodDate,
TdInterestPrincipal = N,
PosiNotionalValue = N,
InterestProfitSum = 0m, FloatRate = 0m
}
};
}
///
/// 驱动一次盘中平仓(与前端平仓页相同路径,仅用内存对象、不查库)。
/// = 本次平仓时实时持仓名义本金;
/// = 占剩余比例(前端 ToRemainingClosePercent 转换后的值)。
/// orginPv 取 lastEod 名义本金 N(与 GetUnwindInterests 真实传参 ResolveUnwindPreviousNotional(lastEod) 一致)。
///
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 { 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(线性拆分,无重复/遗漏)");
}
}
}