diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs
new file mode 100644
index 00000000..32912c84
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs
@@ -0,0 +1,223 @@
+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)
+ {
+ return new swap_position
+ {
+ id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
+ InterestDirection = (int)SwapDirectionEnum.收取,
+ InterestMode = (int)InterestModeEnum.初始预付金,
+ InterestRateDefault = 0.01m, 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, UnderlyingNotional, UnderlyingNotional, closePercent,
+ (int)SwapEventTypeEnum.平仓,
+ false, false, 0, 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)
+ {
+ eodPositions ??= new List();
+ var td = MakeTrade(notional);
+ var position = MakePrepayPosition(fix);
+ var interests = _svc.GetInterests(td, td.trade_extend, UnwindDate, UnwindDate,
+ eodPositions, new List { position },
+ notional, notional, notional, notional, closePercent,
+ (int)SwapEventTypeEnum.平仓,
+ false, false, 0, 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 客户截图级_全平_应返还本金应等于保证金本金()
+ {
+ // 客户截图症状:支付预付金 9,185,755.81;平仓"应返还本金"=-287,820,348.6
+ // 反推标的名义本金 = 2*9,185,755.81 + 287,820,348.6 = 306,191,860.22(保证金比例 3%,正常)
+ const decimal notional = 306_191_860.22m;
+ const decimal fix = 9_185_755.81m;
+ var fe = CalcUnwindWith(1m, null, notional, fix);
+ Console.WriteLine($"[TDD][客户] 实测 InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount} (期望Principal={fix})");
+ Assert.AreEqual(fix, fe.InterestPrincipal,
+ "客户级: 应返还本金应=保证金本金 9,185,755.81,不应被算成 -287,820,348.6");
+ Assert.IsTrue(fe.InterestAmount > 0 && fe.InterestAmount < fix,
+ "客户级: 利息基数应基于保证金本金(小额正),证明 orginPv 已对齐 Fix 而非交易名义本金");
+ }
+
+ [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 误差)
+ const decimal notional = 12_100_000m;
+ const decimal fix = 35_140m;
+ var fe = CalcUnwindWith(1m, null, notional, fix);
+ 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.IsTrue(fe.InterestAmount > 0 && fe.InterestAmount < fix,
+ "Trade1813: 利息基数应基于保证金本金(小额正),证明 orginPv 已对齐 Fix 而非交易名义本金");
+ }
+
+ // ---- 多次部分平仓(验证最小修复是否覆盖"多次部分成交")----
+
+ [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.22m, 100_000m);
+ total += r1.InterestPrincipal;
+ var r2 = CalcUnwindWith(0.5m, null, 306_191_860.22m, 70_000m); // 剩余 7万
+ total += r2.InterestPrincipal;
+ var r3 = CalcUnwindWith(1.0m, null, 306_191_860.22m, 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万");
+ }
+
+ [TestMethod]
+ public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金()
+ {
+ // 根因修复后:预付金腿的 orginPv 已对齐为其自身保证金(Fix),
+ // 不仅"应返还本金"(InterestPrincipal) 正确,逐日利息计息基数也正确
+ // (基于保证金本金,而非交易名义本金),故 InterestAmount 应为小额正。
+ const decimal notional = 306_191_860.22m;
+ const decimal fix = 9_185_755.81m;
+ var fe = CalcUnwindWith(1m, null, notional, fix);
+
+ 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");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index 16730262..12b6442c 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -867,6 +867,19 @@ namespace YLErp.Modules.SwapModule
interest.DataState = (int)SwapFlowDateStateEnum.完成;
interest.ClientId = td.ClientId;
interest.UnwindDate = endDate;
+
+ // 根因修复:预付金(保证金)腿的计息基数维度应为"保证金本金"自身,而非整笔交易的名义本金(orginPv)。
+ // 否则公式 dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv
+ // 会把交易名义本金(千万~亿级)当减项扣掉,使"应返还本金"(InterestPrincipal)与计息基数变成巨负值。
+ // 此处将预付金腿的 orginPv 对齐为其自身保证金(InterestPrincipalFix),
+ // 与日终路径(SwapEodPositionService 对预付金腿 orginPv=InterestPrincipalFix)保持一致。
+ // 仅作用于初始预付金(5)/追加预付金(6);其它计息模式(含债券本金腿 标的期初全价=9)仍用交易名义本金,不受影响。
+ if (position.InterestMode == (int)InterestModeEnum.初始预付金
+ || position.InterestMode == (int)InterestModeEnum.追加预付金)
+ {
+ orginPv = position.InterestPrincipalFix;
+ }
+
if (swap)
{
interest.InterestAmount = 0;