死参数收口(另一半): - SwapDealService.GetInterests 删 needPrice/grossPrice(体内零消费,2026-08 验证); InitSwapDealInterest.needPrice 同为死参数一并删 - SwapEodPositionService.CalcSwapInterests 签名+转发同步;两个 EOD 生产调用点 (SaveAutoEodInterestPosition/SaveEodInterestPositionCopy) 重排实参; CalcEodPostCloseSettleInterests/GetIntradayUnwindInterests 委托同步 - 14 个测试文件 ~44 处直调点机械更新(8 处 override 签名 + 36 处调用实参) - 注意:EOD 编排链(DealInterests→Save*家族)的 grossPrice(期初不含费价)有真实用途,保留未动 新增钉子:CalcEodPostCloseSettleInterests 工厂→接缝参数映射测试—— CalcSwapInterestsCapture 捕获 stub 断言 EodPostCloseSettle 的完整转发契约 (posi=平仓后剩余/closePosi=平掉额/恒1/settment:false/orginPv 等 11 项)。 该段位置转发含三个相邻同型 decimal,编译器不查错位,此测试兜底。 验证:定向 241 测试通过(含 T0/T1 Excel 验证期望值、EntrySemantics 精确值钉子—— 任何 decimal 错位即红);全量 903=145失败/746通过/12跳过,与基线逐位一致。
153 lines
8.2 KiB
C#
153 lines
8.2 KiB
C#
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=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 来源)。
|
||
/// ============================================================================
|
||
/// </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=N(lastEod 尚未缩减)。</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, 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(线性拆分,无重复/遗漏)");
|
||
}
|
||
}
|
||
}
|