using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
///
/// 预付金(保证金)腿 平仓"应返还本金" bug 的回归测试(根因修复后应为全绿)。
/// ---------------------------------------------------------------
/// 业务预期:平仓"应返还本金"(swap_flow_event.InterestPrincipal) 应等于该预付金腿的
/// 保证金本金(InterestPrincipalFix * closePercent),且与逐日利息计算无关;
/// 同时预付金腿的逐日利息计息基数也应基于"保证金本金"自身,而非整笔交易的名义本金。
///
/// 根因:GetUnwindInterests 对全部腿统一用 orginPv = lastEod.NotionalValue ?? stockEqvNotional(整笔交易名义本金),
/// 缺了"预付金腿用自身保证金"的分支;公式 dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv
/// 把交易名义本金(千万~亿级)当减项扣掉,使 InterestPrincipal 与计息基数变成巨负值。
///
/// 根因修复(SwapDealService.InitSwapDealInterest):对预付金腿(初始/追加)在利息计算前把
/// orginPv 对齐为 position.InterestPrincipalFix,与日终路径(SwapEodPositionService)一致。
/// 仅作用于 InterestMode 5/6;债券本金腿(标的期初全价=9)等仍用交易名义本金,不受影响。
///
/// 设计:标的名义本金 100万、预付金(保证金)本金 10万(维度不同,放大错配);
/// 另含客户截图级 / 真实库 Trade1813 的精确复现用例。
///
[TestClass]
public class SwapUnwindPrepayPrincipalBugTdd
{
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 UnderlyingNotional = 1_000_000m; // 标的名义本金(股票维度)
private const decimal PrepayPrincipal = 100_000m; // 预付金/保证金本金(预付金维度)
private const int AnnualDays = 365;
private static readonly DateTime StartDate = new(2026, 4, 27);
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
private static readonly DateTime UnwindDate = new(2026, 4, 28);
private SwapDealService _svc;
[TestInitialize]
public void Init() => _svc = new StubSwapDealService(new OptUserInfo(0, nameof(SwapUnwindPrepayPrincipalBugTdd), OptUserFrom.UnitTest));
private static trade MakeTrade(decimal notional = UnderlyingNotional)
{
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-PREPAY-TDD", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)notional, Notional = (double)notional,
trade_extend = extend
};
}
private static swap_position MakePrepayPosition(decimal fix = PrepayPrincipal, decimal rate = 0.01m)
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestRateDefault = rate, InterestPrincipalFix = fix,
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
}
private swap_flow_event CalcUnwind(decimal closePercent, List eodPositions)
{
eodPositions ??= new List();
var td = MakeTrade();
var position = MakePrepayPosition();
var interests = _svc.GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
eodPositions, new List { position },
UnderlyingNotional, UnderlyingNotional, closePercent,
(int)SwapEventTypeEnum.平仓,
false, UnderlyingNotional, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "预付金腿应生成 1 条 flow_event");
return interests[0];
}
///
/// 客户/真实库场景:自定义 标的名义本金(notional) 与 保证金本金(fix)。
/// orginPv 用 notional(与 GetUnwindInterests 行为一致:lastEod.NotionalValue ?? stockEqvNotional)。
///
private swap_flow_event CalcUnwindWith(decimal closePercent, List eodPositions, decimal notional, decimal fix, decimal rate = 0.01m)
{
eodPositions ??= new List();
var td = MakeTrade(notional);
var position = MakePrepayPosition(fix, rate);
var interests = _svc.GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
eodPositions, new List { position },
notional, notional, closePercent,
(int)SwapEventTypeEnum.平仓,
false, notional, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "预付金腿应生成 1 条 flow_event");
return interests[0];
}
[TestMethod]
public void 无历史归档_全平_应返还本金应等于保证金本金()
{
var fe = CalcUnwind(1m, null); // 无 eod 归档 → preEod.id==0
Console.WriteLine($"[TDD] 无归档 实测 InterestPrincipal={fe.InterestPrincipal} (期望={PrepayPrincipal})");
Assert.AreEqual(PrepayPrincipal, fe.InterestPrincipal,
"无归档全平: InterestPrincipal(应返还本金) 应=保证金本金(预付金本金),不应被利息公式改写为含 -orginPv 与 double 的怪值");
}
[TestMethod]
public void 有历史归档_全平_应返还本金应等于保证金本金()
{
var eod = new List
{
new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = new DateTime(2026, 4, 27),
TdInterestPrincipal = PrepayPrincipal,
PosiNotionalValue = PrepayPrincipal,
InterestProfitSum = 0m
}
};
var fe = CalcUnwind(1m, eod);
Console.WriteLine($"[TDD] 有归档 实测 InterestPrincipal={fe.InterestPrincipal} (期望={PrepayPrincipal})");
Assert.AreEqual(PrepayPrincipal, fe.InterestPrincipal,
"有归档全平: 计息区间被跳过,InterestPrincipal 应保持初始正确值=保证金本金");
}
// ---- 客户截图级 / 真实库场景(验证"前后是否真 Fix")----
[TestMethod]
public void 客户截图级_全平_应返还本金应等于保证金本金()
{
// 生产铁证(用户提供真实交易):TradeAmount=3亿,StockEqvNotional=306,191,860.26,
// StructureType=普通债券类收益互换;预付金腿 swap_position id=34009 InterestMode=5
// InterestPrincipalFix=9,185,755.81。
// swap_flow_event(该腿, mode5) 三条:
// 9202 EventId=null dir2 IP=9,185,755.81 (建仓支付预付金 ✓)
// 9489 EventId=15997 dir1 IP=-287,820,348.64 (平仓, 盘中路径 BUG ✗)
// 9492 EventId=15998 dir1 IP=9,185,755.81 (平仓, EOD正确路径 ✓)
// 同一腿出现"盘中错 / EOD对"两条平仓记录,恰好佐证修复方向(盘中 orginPv 对齐 EOD=Fix)正确。
// 根因复现:2*Fix - Notional = 2*9,185,755.81 - 306,191,860.26 = -287,820,348.64(与生产 15997 精确 0 误差)。
// 该预付金腿三条 event 的 InterestAmount 全=0(债券类预付金腿不计息),
// 故本笔生产仅 InterestPrincipal 中招、计息基数未受影响 → rate=0 贴合生产。
const decimal notional = 306_191_860.26m;
const decimal fix = 9_185_755.81m;
var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0m);
Console.WriteLine($"[TDD][客户] 实测 InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount} (期望Principal={fix})");
Assert.AreEqual(fix, fe.InterestPrincipal,
"客户级: 应返还本金应=保证金本金 9,185,755.81,不应被算成 -287,820,348.64");
Assert.AreEqual(0m, fe.InterestAmount,
"客户级: 该预付金腿不计息,InterestAmount 应=0(与生产三条 event 全为 0 一致);仅 InterestPrincipal 中招");
}
[TestMethod]
public void 真实库Trade1813_全平_应返还本金应等于保证金本金()
{
// 测试库 Trade=1813 / Pos=34204:Fix=35,140,Notional=12,100,000,
// 实际存储 InterestPrincipal=-12,029,720.00(=2*35,140-12,100,000,公式精确 0 误差)。
// 同属债券类预付金腿(与生产同模式,不计息),rate=0 贴合生产,仅验证 InterestPrincipal 修复。
const decimal notional = 12_100_000m;
const decimal fix = 35_140m;
var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0m);
Console.WriteLine($"[TDD][Trade1813] 实测 InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount} (期望Principal={fix})");
Assert.AreEqual(fix, fe.InterestPrincipal,
"Trade1813: 应返还本金应=保证金本金 35,140,不应被算成 -12,029,720.00");
Assert.AreEqual(0m, fe.InterestAmount,
"Trade1813: 同属债券类预付金腿不计息,InterestAmount 应=0;仅 InterestPrincipal 中招");
}
// ---- 多次部分平仓(验证最小修复是否覆盖"多次部分成交")----
[TestMethod]
public void 多次部分平仓_显示值每次返回比例份额且总计等于保证金()
{
// 模拟分 3 次平仓:0.3 / 0.5 / 1.0(剩余)。每次传入的 fix = 该次剩余保证金本金
// (真实系统中每次部分平仓后 position.InterestPrincipalFix 会被扣减,下一笔用剩余值)。
// 根因修复后:InterestPrincipal 由利息公式基于 Fix 正确得出 = fix * closePercent。
decimal total = 0;
var r1 = CalcUnwindWith(0.3m, null, 306_191_860.26m, 100_000m);
total += r1.InterestPrincipal;
var r2 = CalcUnwindWith(0.5m, null, 306_191_860.26m, 70_000m); // 剩余 7万
total += r2.InterestPrincipal;
var r3 = CalcUnwindWith(1.0m, null, 306_191_860.26m, 35_000m); // 剩余 3.5万
total += r3.InterestPrincipal;
Console.WriteLine($"[TDD][多次部分] r1={r1.InterestPrincipal} r2={r2.InterestPrincipal} r3={r3.InterestPrincipal} 合计={total}");
Assert.AreEqual(30_000m, r1.InterestPrincipal, "第1次(30%)应返还 3万");
Assert.AreEqual(35_000m, r2.InterestPrincipal, "第2次(50% of 剩余7万)应返还 3.5万");
Assert.AreEqual(35_000m, r3.InterestPrincipal, "第3次(剩余全平)应返还 3.5万");
Assert.AreEqual(100_000m, total, "多次部分平仓合计应=保证金本金 10万");
}
// ---- 盘中路径 CalcDailySimpleInterest 的 closePercent^N 指数级缩小 bug ----
// 生产铁证 GLMS-20260701-0006:预付金腿 Fix=9,180,000、interest_rest_days=7、单利、不计息。
// 平仓弹窗(swaptrade2/GetUnwindInterestList → 盘中路径 CalcDailySimpleInterest)返回:
// 100% → 9,180,000 (对) 50% → 71,718.75 (错) 10% → 0.918 (错)
// 数学关系精确成立:9,180,000×0.5^7 = 71,718.75、9,180,000×0.1^7 = 0.918。
// 根因:CalcDailySimpleInterest 非重置日 else 分支
// flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
// tdDynomicPrincipal = flowEvent.InterestPrincipal; // ★把"已×closePercent"的值回填
// 使下一个非重置日再乘一次 closePercent → InterestPrincipal = Fix × closePercent^N(N=计息天数),
// 而正确应为 Fix × closePercent(线性,与日终 CalcDailySimpleInterestByEod:1164-1165 只乘一次一致)。
// 现有 6 个用例 interest_rest_days=1 且 UnwindDate=StartDate+1(calcDays=1),循环首尾都被 continue 跳过、
// 从不进 else,故漏掉此 bug;本组用例用 restDays=7、跨多日、带 eod 归档触发 else 累积复现之。
private const decimal ProdPrepayFix = 9_180_000m;
private static readonly DateTime ProdPosiStart = new(2026, 7, 2);
private static readonly DateTime ProdEodValueDate = new(2026, 7, 4);
private static readonly DateTime ProdUnwindDate = new(2026, 7, 13);
///
/// 盘中路径复现:restDays=7、PosiStart→Unwind 跨 11 天、eod 归档到 07-04。
/// 与生产 GLMS-20260701-0006 完全对齐,buggy 代码产出 Fix × closePercent^7。
///
private swap_flow_event CalcUnwindMultiDay(decimal closePercent, decimal fix = ProdPrepayFix, int restDays = 7,
decimal rate = 0m)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾(与生产一致)
SettlementRules = 0
})
};
var td = new trade
{
id = 1, TradeNumber = "UT-PREPAY-EXP", ClientId = 999998,
TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart,
ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)fix, Notional = (double)fix,
trade_extend = extend
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestRateDefault = rate, InterestPrincipalFix = fix,
PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = restDays,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
var eod = new List
{
new eod_swap_position
{
id = 7, SwapTradeId = 1, PositionId = 1001,
ValueDate = ProdEodValueDate,
TdInterestPrincipal = fix, // 生产 eod_swap_position(35774) TdInterestPrincipal=9,180,000
PosiNotionalValue = fix,
InterestProfitSum = 0m, FloatRate = 0m
}
};
var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate,
eod, new List { position },
fix, fix, closePercent,
(int)SwapEventTypeEnum.平仓,
false, fix, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "预付金腿应生成 1 条 flow_event");
return interests[0];
}
[TestMethod]
public void 部分平仓50_盘中重置周期7天_应返还本金应线性缩放而非指数级()
{
var fe = CalcUnwindMultiDay(0.5m);
Console.WriteLine($"[TDD][盘中50%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=71,718.75, 期望=4,590,000)");
// 正确:Fix × closePercent = 9,180,000 × 0.5 = 4,590,000(100%返 9,180,000 的一半)。
// buggy:Fix × 0.5^7 = 71,718.75(生产实测),被指数级缩小 ~64 倍。
Assert.AreEqual(4_590_000m, fe.InterestPrincipal,
"50% 平仓: 应返还本金应=Fix×0.5=4,590,000,不应被 closePercent^7 缩成 71,718.75");
}
[TestMethod]
public void 部分平仓10_盘中重置周期7天_应返还本金应线性缩放而非指数级()
{
var fe = CalcUnwindMultiDay(0.1m);
Console.WriteLine($"[TDD][盘中10%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=0.918, 期望=918,000)");
// 正确:Fix × 0.1 = 918,000。buggy:Fix × 0.1^7 = 0.918(生产实测),缩小 100 万倍。
Assert.AreEqual(918_000m, fe.InterestPrincipal,
"10% 平仓: 应返还本金应=Fix×0.1=918,000,不应被 closePercent^7 缩成 0.918");
}
[TestMethod]
public void 全平_盘中重置周期7天_应返还本金应等于保证金本金()
{
// closePercent=1 → 1^N=1,指数 bug 对 100% 无影响(故用户看 100% 正常),此用例锚定不回归。
var fe = CalcUnwindMultiDay(1m);
Console.WriteLine($"[TDD][盘中100%] 实测 InterestPrincipal={fe.InterestPrincipal} (期望=9,180,000)");
Assert.AreEqual(ProdPrepayFix, fe.InterestPrincipal,
"100% 平仓: 应返还本金应=Fix=9,180,000(closePercent=1 时指数 bug 不显现,须保持正确)");
}
// ---- 非预付金腿(标的期初全价=9)同样验证:证明修复对所有"单利盘中"腿通用且正确 ----
// CalcDailySimpleInterest 是所有单利腿(InterestType=0)的盘中计息通用函数,非预付金专用。
// 用户关切:修复会否波及非预付金腿?结论——
// · closePercent=1(日常计息/全平)时 1^N=1=1^1,修复前后逐位恒等,零影响;
// · closePercent<1(部分平仓)时,所有单利腿此前都被同一 bug 指数级缩小,修复后统一为
// 正确的线性缩放(平仓 X% => 本金×X),这是修正而非破坏。
// 本组用非预付金腿(标的期初全价=9,orginPv 不被对齐为 Fix、走交易名义本金)独立复现并锁定。
private const decimal NonPrepayNotional = 1_000_000m;
private swap_flow_event CalcUnwindMultiDayNonPrepay(decimal closePercent, decimal notional = NonPrepayNotional,
int restDays = 7, decimal rate = 0m)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾(与生产一致)
SettlementRules = 0
})
};
var td = new trade
{
id = 1, TradeNumber = "UT-NONPREPAY-EXP", ClientId = 999998,
TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart,
ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)notional, Notional = (double)notional,
trade_extend = extend
};
var position = new swap_position
{
id = 2002, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, // 非预付金腿(=9):orginPv 不会被对齐为 Fix
InterestRateDefault = rate, InterestPrincipalFix = 0m,
PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = restDays,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
var eod = new List
{
new eod_swap_position
{
id = 8, SwapTradeId = 1, PositionId = 2002,
ValueDate = ProdEodValueDate,
TdInterestPrincipal = notional, // 计息基数=名义本金 → dynomicPrincipal = eodTd + notional - orginPv = notional
PosiNotionalValue = notional,
InterestProfitSum = 0m, FloatRate = 0m
}
};
// orginPv 传 notional:非预付金腿不走 877-881 的 Fix 对齐,dynomicPrincipal = notional + notional - notional = notional
var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate,
eod, new List { position },
notional, notional * closePercent, closePercent,
(int)SwapEventTypeEnum.平仓,
false, notional, false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "非预付金腿应生成 1 条 flow_event");
return interests[0];
}
[TestMethod]
public void 非预付金腿_部分平仓50_盘中重置周期7天_应线性缩放不受指数bug影响()
{
var fe = CalcUnwindMultiDayNonPrepay(0.5m);
Console.WriteLine($"[TDD][非预付金50%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=7,812.5, 期望=500,000)");
// 正确:N×0.5=500,000。buggy:N×0.5^7=7,812.5(同一指数 bug,证明非预付金腿此前也中招)。
Assert.AreEqual(500_000m, fe.InterestPrincipal,
"非预付金腿(标的期初全价) 50% 平仓应=名义本金×0.5=500,000,不应被 closePercent^7 缩小");
}
[TestMethod]
public void 非预付金腿_部分平仓10_盘中重置周期7天_应线性缩放不受指数bug影响()
{
var fe = CalcUnwindMultiDayNonPrepay(0.1m);
Console.WriteLine($"[TDD][非预付金10%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=0.1, 期望=100,000)");
Assert.AreEqual(100_000m, fe.InterestPrincipal,
"非预付金腿(标的期初全价) 10% 平仓应=名义本金×0.1=100,000,不应被 closePercent^7 缩小");
}
[TestMethod]
public void 非预付金腿_全平_修复前后恒等_零影响()
{
// closePercent=1 时 1^N=1=1^1:这是"修复不波及非平仓/全平计息"的数学不变量证明。
var fe = CalcUnwindMultiDayNonPrepay(1m);
Console.WriteLine($"[TDD][非预付金100%] 实测 InterestPrincipal={fe.InterestPrincipal} (期望=1,000,000)");
Assert.AreEqual(NonPrepayNotional, fe.InterestPrincipal,
"非预付金腿 全平应=名义本金(closePercent=1 时修复前后恒等,日常计息/全平零影响)");
}
[TestMethod]
public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金()
{
// 显式带息加固用例(合成,非用户那笔生产的真实症状):
// 用户那笔生产(3亿债券类TRS)预付金腿不计息(InterestAmount 全=0),仅 InterestPrincipal 中招;
// 本例用 rate=0.01 构造"若该腿计息"的场景,验证根因修复后计息基数也基于保证金本金自身
// (而非交易名义本金):InterestAmount 为小额正、且 < fix。
const decimal notional = 306_191_860.26m;
const decimal fix = 9_185_755.81m;
var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0.01m);
Assert.AreEqual(fix, fe.InterestPrincipal, "显示值(应返还本金)已=保证金本金");
Console.WriteLine($"[TDD][计息基数] InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount}");
Assert.IsTrue(fe.InterestAmount > 0,
"根因修复后(显式带息): 预付金腿 InterestAmount 应基于保证金本金算出小额正值(约 fix*rate),不再是巨负");
Assert.IsTrue(fe.InterestAmount < fix,
"利息基数必须为保证金维度(远小于 fix),证明 orginPv 已用预付金自身 Fix,而非交易名义本金 notional");
}
// ===== 覆盖完整性补强:所有单利腿模式 + 日终路径 =====
// 调用链事实(已用代码确认):
// CalcDailySimpleInterest 的唯一真实调用链 = GetInterests(settment=false) → CalcUnwindInterest → 本函数。
// 日终(settment=true)走 CalcEodInterest → CalcDailySimpleInterestByEod(closePercent 硬编码 1m、
// 且该函数从不改写 InterestPrincipal),根本不调用本函数。故"含日终"的正确命题是:
// 日终不受本 bug 影响,且应有用例锁定这一不变量。
// 本组用同一入口驱动各 InterestMode 在 closePercent<1 + rest_days=7 多天场景,断言
// InterestPrincipal = closePrincipal(线性),捕捉任何指数级回归;并显式加日终(settment=true)用例,
// 断言日终结果恒为线性 closePrincipal(证明日终不受盘中 bug 影响,与正确的 ByEod 变体对齐)。
private swap_flow_event CalcByMode(int mode, decimal baseP, decimal closePercent, int restDays = 7, bool eodPath = false)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾(与生产一致)
SettlementRules = 0
})
};
var td = new trade
{
id = 1, TradeNumber = "UT-MODE-COV", ClientId = 999998,
TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart,
ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
StockEqvNotional = (double)baseP, Notional = (double)baseP,
trade_extend = extend
};
bool isPrepayOrFixed = mode == (int)InterestModeEnum.初始预付金
|| mode == (int)InterestModeEnum.追加预付金
|| mode == (int)InterestModeEnum.固定值;
var position = new swap_position
{
id = 3003, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = mode,
InterestRateDefault = 0m,
InterestPrincipalFix = isPrepayOrFixed ? baseP : 0m,
PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = restDays,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
// 使 dynomicPrincipal = posiPrincipal:eod.TdInterestPrincipal = orginPv(=baseP),
// 非预付金腿 orginPv 传 baseP;预付金/固定值腿 orginPv 被内部对齐为 Fix=baseP(同样成立)。
var eodPos = new List
{
new eod_swap_position
{
id = 30, SwapTradeId = 1, PositionId = 3003,
ValueDate = ProdEodValueDate,
TdInterestPrincipal = baseP,
PosiNotionalValue = baseP,
InterestProfitSum = 0m, FloatRate = 0m
}
};
var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate,
eodPos, new List { position },
baseP, baseP * closePercent, closePercent,
(int)SwapEventTypeEnum.平仓,
false, baseP, false, settment: eodPath, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, $"mode={mode} 应生成 1 条 flow_event");
return interests[0];
}
// ---- 追加预付金(6):与初始预付金(5)同源修复,显式覆盖避免遗漏 ----
[TestMethod]
public void 追加预付金腿_盘中_部分平仓重置周期7天_应线性缩放()
{
var fe = CalcByMode((int)InterestModeEnum.追加预付金, ProdPrepayFix, 0.5m);
Assert.AreEqual(4_590_000m, fe.InterestPrincipal, "追加预付金 50% 应=Fix×0.5(与初始预付金同源修复)");
var fe1 = CalcByMode((int)InterestModeEnum.追加预付金, ProdPrepayFix, 0.1m);
Assert.AreEqual(918_000m, fe1.InterestPrincipal, "追加预付金 10% 应=Fix×0.1");
}
// ---- 合约名义本金规模(2):CalcNotionalByMode 默认分支(posiNotional×cp) ----
[TestMethod]
public void 合约名义本金规模腿_盘中_部分平仓重置周期7天_应线性缩放()
{
const decimal baseP = 2_000_000m;
var fe = CalcByMode((int)InterestModeEnum.合约名义本金规模, baseP, 0.5m);
Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "合约名义本金规模 50% 应=posiNotional×0.5");
}
// ---- 固定值(1):合同写死的固定值,不随平仓比例变化 ----
// 固定值腿的计息基数由合约约定(InterestPrincipalFix),无论平仓比例多少都恒等于该值。
// closePrincipal = posiPrincipal = Fix; newClosePercent = 1m 是正确的业务行为,不是缺陷。
[TestMethod]
public void 固定值腿_盘中_部分平仓_计息基数恒等于Fix不随比例变化()
{
const decimal baseP = 2_000_000m;
var fe = CalcByMode((int)InterestModeEnum.固定值, baseP, 0.5m);
Assert.AreEqual(baseP, fe.InterestPrincipal, "固定值腿计息基数恒=Fix,不随平仓比例变化(合同约定)");
}
// ---- 日终路径(settment=true):证明走 CalcDailySimpleInterestByEod,结果恒为线性 closePrincipal,不受盘中 bug 影响 ----
[TestMethod]
public void 日终_预付金腿_部分平仓_结果应线性且不受盘中bug影响()
{
var fe = CalcByMode((int)InterestModeEnum.初始预付金, ProdPrepayFix, 0.5m, eodPath: true);
Console.WriteLine($"[TDD][EOD 预付金50%] InterestPrincipal={fe.InterestPrincipal} (期望={4_590_000m})");
Assert.AreEqual(4_590_000m, fe.InterestPrincipal, "日终预付金 50% 应=Fix×0.5(ByEod 正确变体,closePercent 走 closePrincipal 线性)");
var fe1 = CalcByMode((int)InterestModeEnum.初始预付金, ProdPrepayFix, 0.1m, eodPath: true);
Assert.AreEqual(918_000m, fe1.InterestPrincipal, "日终预付金 10% 应=Fix×0.1");
}
[TestMethod]
public void 日终_非预付金腿_部分平仓_结果应线性且不受盘中bug影响()
{
const decimal baseP = 2_000_000m;
var fe = CalcByMode((int)InterestModeEnum.标的期初全价, baseP, 0.5m, eodPath: true);
Console.WriteLine($"[TDD][EOD 标的期初全价50%] InterestPrincipal={fe.InterestPrincipal} (期望={1_000_000m})");
Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "日终非预付金腿 50% 应=名义本金×0.5(ByEod 正确,不受影响)");
}
}
}