diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs
new file mode 100644
index 00000000..603c49c0
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using YLErp;
+using YLErp.DBModels;
+using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule;
+using YLErp.Modules.SwapModule.Accrual;
+using YLErp.Modules.SwapModule.Margin;
+using YLErp.Derivatives.Interest;
+
+namespace UnitTestProject.Modules.SwapModule.Margin
+{
+ ///
+ /// 影子对账:保证金腿新方法 CalcMarginInterest(无 orginPv/差分)vs
+ /// 旧通用管线 CalcDailySimpleInterestByEod/CalcDailySimpleInterest(带差分 + orginPv hack)。
+ ///
+ /// 保证金是纯固定利率单利(FloatRateUnderlyingCode 恒空、InterestType 恒单利、SwapIntervalList 单段),
+ /// 旧管线差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 对保证金恒等于 posiPrincipal
+ /// (因 orginPv 经 PreviousBalance 对齐到昨日终保证金余额),故新方法直接用 posiPrincipal/昨日终本金作
+ /// notional 应与旧管线严格数值一致。本测试即在多种场景下证明这一等价,为提交2 切换生产路径提供安全网。
+ ///
+ [TestClass]
+ public class MarginInterestShadowTest
+ {
+ private const decimal Principal = 2_000_000m; // 保证金本金(InterestPrincipalFix)
+ private const decimal Rate = 0.03m; // 3% 年化固定利率
+ private const int AnnualDays = 365;
+ private static readonly DateTime StartDate = new(2026, 7, 1);
+ private static readonly DateTime ExerciseDate = new(2027, 6, 30);
+
+ private sealed class StubSvc : SwapDealService
+ {
+ public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestShadowTest), OptUserFrom.UnitTest)) { }
+ }
+
+ private static trade CreateTrade() => new trade
+ {
+ id = 1, TradeNumber = "UT-MARGIN-SHADOW", ClientId = 999998,
+ TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
+ ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
+ trade_extend = new trade_extend
+ {
+ TradeId = 1,
+ ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
+ { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 })
+ }
+ };
+
+ /// 保证金腿(初始预付金 mode 5):固定利率、单利、年化、无浮动标的。
+ private static swap_position CreateMarginPosition() => new swap_position
+ {
+ id = 2001, SwapTradeId = 1, PosiDirection = 0,
+ InterestDirection = (int)SwapDirectionEnum.收取,
+ InterestMode = (int)InterestModeEnum.初始预付金,
+ InterestRateDefault = Rate, InterestPrincipalFix = Principal,
+ PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
+ IsInitial = true, Invalid = false,
+ InterestType = (int)InterestTypeEnum.单利,
+ IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
+ FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
+ };
+
+ /// 构造昨日终 eod_swap_position(已含累计利息 InterestProfitSum 与昨日终本金)。
+ private static eod_swap_position CreatePreEod(DateTime valueDate, decimal profitSum) => new eod_swap_position
+ {
+ id = 1, SwapTradeId = 1, PositionId = 2001,
+ ValueDate = valueDate,
+ TdInterestPrincipal = Principal, InterestPrincipalFix = Principal,
+ InterestProfitSum = profitSum, PosiNotionalValue = Principal, FloatRate = 0m
+ };
+
+ // ──────────────────────────── EOD 路径 ────────────────────────────
+
+ /// EOD 续接单日:有历史归档,notional=昨日终本金。
+ [TestMethod]
+ public void 影子_EOD续接单日_新旧一致()
+ {
+ var td = CreateTrade();
+ var position = CreateMarginPosition();
+ var valueDate = StartDate.AddDays(5);
+ const decimal profitSum = 820m;
+
+ // 旧方法
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ svc.CalcDailySimpleInterestByEod(CreatePreEod(StartDate.AddDays(4), profitSum),
+ valueDate, td.StartDate.Value, position, Principal, Principal,
+ new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
+
+ // 新方法(独立 preEod,相同初始值)
+ var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
+ AnnualDays, calcFirst: true, calcLast: true,
+ CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: true, swap: false);
+
+ Console.WriteLine($"旧: I={oldI} Td={oldTd}");
+ Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} ClosePnL={newEvt.InterestClosePnL}");
+ Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
+ Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
+ }
+
+ /// EOD 首日(preEod.id==0):首日初始化 notional=posiPrincipal。
+ [TestMethod]
+ public void 影子_EOD首日_新旧一致()
+ {
+ var td = CreateTrade();
+ var position = CreateMarginPosition();
+ var valueDate = StartDate;
+
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ svc.CalcDailySimpleInterestByEod(new eod_swap_position { id = 0 },
+ valueDate, td.StartDate.Value, position, Principal, Principal,
+ new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
+
+ var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
+ AnnualDays, calcFirst: true, calcLast: true,
+ new eod_swap_position { id = 0 }, 0, add: false, settment: true, swap: false);
+
+ Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
+ Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
+ }
+
+ // ──────────────────────────── 盘中路径 ────────────────────────────
+
+ /// 盘中全平(closePercent=1):新方法 notional=posiPrincipal,旧方法差分 accrualBasis 恒=posiPrincipal。
+ [TestMethod]
+ public void 影子_盘中全平_新旧一致()
+ {
+ var td = CreateTrade();
+ var position = CreateMarginPosition();
+ var valueDate = StartDate.AddDays(5);
+ const decimal profitSum = 820m;
+
+ // 旧方法:orginPv 经 PreviousBalance 对齐到昨日终保证金余额 → accrualBasis 恒= Principal
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
+ decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
+ svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
+ new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, orginPv,
+ calcFirst: true, calcLast: false, ref oldI, ref oldTd);
+
+ // 新方法:notional = posiPrincipal(无差分、无 orginPv)
+ var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
+ AnnualDays, calcFirst: true, calcLast: false,
+ CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
+
+ Console.WriteLine($"旧: I={oldI} Td={oldTd}");
+ Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
+ Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
+ Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
+ }
+
+ /// 盘中部分平仓(closePercent=0.5):缩放累计,新旧线性等价。
+ [TestMethod]
+ public void 影子_盘中部分平仓_新旧一致()
+ {
+ var td = CreateTrade();
+ var position = CreateMarginPosition();
+ var valueDate = StartDate.AddDays(5);
+ const decimal profitSum = 820m;
+ const decimal closePct = 0.5m;
+
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ var preEodOld = CreatePreEod(StartDate.AddDays(4), profitSum);
+ decimal orginPv = MarginCalc.PreviousBalance(preEodOld, Principal);
+ svc.CalcDailySimpleInterest(preEodOld, valueDate, position, Principal,
+ new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, closePct, orginPv,
+ calcFirst: true, calcLast: false, ref oldI, ref oldTd);
+
+ var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate,
+ Principal * closePct, Principal, closePct,
+ AnnualDays, calcFirst: true, calcLast: false,
+ CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
+
+ Console.WriteLine($"旧: I={oldI} Td={oldTd}");
+ Console.WriteLine($"新: I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount}");
+ Assert.AreEqual(oldI, newEvt.InterestAmount, "InterestAmount 一致");
+ Assert.AreEqual(oldTd, newEvt.TdInterestAmount, "TdInterestAmount 一致");
+ }
+
+ /// 互换事件(swap=true,盘中):利息应归零。
+ [TestMethod]
+ public void 影子_盘中互换_利息归零()
+ {
+ var td = CreateTrade();
+ var position = CreateMarginPosition();
+ var valueDate = StartDate.AddDays(5);
+
+ var svc = new StubSvc();
+ var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
+ AnnualDays, calcFirst: true, calcLast: false,
+ CreatePreEod(StartDate.AddDays(4), 820m), 0, add: false, settment: false, swap: true);
+
+ Assert.AreEqual(0m, newEvt.InterestAmount, "互换利息归零");
+ Assert.AreEqual(0m, newEvt.TdInterestAmount, "互换 TdInterestAmount 归零");
+ Assert.AreEqual(0m, newEvt.InterestClosePnL, "互换 InterestClosePnL 归零");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index beeeef4e..fec4dfaf 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -345,56 +345,6 @@ namespace YLErp.Modules.SwapModule
///
///
///
- public UnwindData InitLongShortUnwind(int tradeId, SwapEventTypeEnum eventTypeEnum)
- {
- var td = DbContext.trade.Find(tradeId);
- if (td == null)
- {
- throw new ServiceException("未找到交易信息");
- }
- var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && x.IsInitial && !x.Invalid);
- List eventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
- var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
- //CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制
- var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
- td.trade_extend = tradeExtend;
- var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps);
- double stockEqvNotional = td.StockEqvNotional;//剩余名义本金
- var hasProcess = HasTradeProcess();
- swap_flow_event floatEvent = new swap_flow_event();
- UnwindData unwindData = new UnwindData();
- if (((valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess)) && (td.TradeStatus == ConsTrade.平仓待复核 || td.TradeStatus == ConsTrade.互换待复核))
- {
- var swapEvent = GetSwapEvent(tradeId, (int)eventTypeEnum);
- if (swapEvent == null)
- {
- throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
- }
- unwindData = swapEvent.unwindData;
- }
- else
- {
- unwindData.StartDate = td.TradeDate.Value;
- if (preDealDate.HasValue)
- {
- unwindData.StartDate = preDealDate.Value;
- }
- unwindData.ValueDate = dealDate;
- unwindData.UnwindDate = dealDate;
- unwindData.PayDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(td.trade_extend.ExtendObj.SettlementRules));
- unwindData.SwapTradeId = tradeId;
- unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
- unwindData.NotionalQty = positions.Sum(s => s.PosiQuantity);
- unwindData.PosiNotionalValue = Convert.ToDecimal(stockEqvNotional);
- unwindData.PositionQty = 0;//平仓只做了结为0,互换用不上
- unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
- if (eventTypeEnum == SwapEventTypeEnum.平仓)
- {
- unwindData.FlowEvents = GetUnwindInterests(dealDate, unwindData.UnwindDate.Value, tradeId, 1, (int)SwapEventTypeEnum.平仓);
- }
- }
- return unwindData;
- }
///
/// 平仓初始化
///
@@ -966,6 +916,118 @@ namespace YLErp.Modules.SwapModule
return interest;
}
+ ///
+ /// 保证金腿(InterestMode 5/6)专属计息——替代 CalcEodInterest/CalcUnwindInterest 对保证金的处理。
+ ///
+ /// 保证金是纯固定利率单利:浮动利率(FR007)/分段利率/复利对其均为死分支(前端无入口、
+ /// 确认书不含、FundingLegRate.Build 对空 FloatRateUnderlyingCode 恒返回 Fixed)。故本方法直接用
+ /// SimpleInterestAccrual 纯函数计息,notional 取保证金余额本身:
+ /// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源)
+ /// 盘中 = 今日本金 posiPrincipal(InterestPrincipalFix)
+ /// 消除融资腿差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 与 orginPv 维度
+ /// hack——对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余。保留累计语义(priorAccrued + 增量),
+ /// 满足下游 SwapEodPositionService 字段契约(InterestAmount=缩放累计、TdInterestAmount=单日参考等)。
+ ///
+ /// true=收盘归档(EOD),false=盘中平仓/互换。
+ /// 互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。
+ public swap_flow_event CalcMarginInterest(
+ trade td, DateTime valueDate, swap_position position, decimal rate,
+ decimal closePrincipal, decimal posiPrincipal, decimal closePercent,
+ int annualDays, bool calcFirst, bool calcLast,
+ eod_swap_position preEod, int eventType, bool add, bool settment, bool swap)
+ {
+ // 当日是否计息(算头算尾)——同 CalcEodInterest
+ bool calcToday = true;
+ if (!calcFirst && valueDate == td.StartDate.Value) calcToday = false;
+ if (!calcLast && valueDate == td.ExerciseDate.Value) calcToday = false;
+ if (valueDate < position.PosiStartDate) calcToday = false;
+
+ // 首日初始化 preEod——同 CalcEodInterest
+ if (preEod.id == 0)
+ {
+ preEod.FloatRate = 0m;
+ preEod.TdInterestPrincipal = posiPrincipal;
+ preEod.PosiNotionalValue = posiPrincipal;
+ }
+
+ // 字段映射(保证金 FloatRate 恒 0;方向 position.InterestDirection 已由 GetInterests 翻转)
+ var interest = new swap_flow_event
+ {
+ SwapTradeId = td.id,
+ SwapTradeNo = td.TradeNumber,
+ EventType = eventType,
+ EventReason = "交易",
+ EventDate = valueDate,
+ PositionId = position.id,
+ InterestDirection = position.InterestDirection,
+ InterestRate = rate,
+ InterestPrincipal = closePrincipal,
+ InterestSwapInterval = position.InterestSwapInterval,
+ InterestMode = position.InterestMode,
+ FloatRate = 0m,
+ DataState = (int)SwapFlowDateStateEnum.完成,
+ ClientId = td.ClientId,
+ UnwindDate = valueDate
+ };
+
+ // 互换事件:利息归零(同 InitSwapDealInterest)
+ if (swap && !settment)
+ {
+ interest.InterestAmount = 0m;
+ interest.TdInterestAmount = 0m;
+ interest.InterestClosePnL = 0m;
+ if (add) UpdateDbOption(interest);
+ return interest;
+ }
+
+ decimal interestAmount = 0m;
+ decimal tdInterestAmount = 0m;
+ var legRate = FundingLegRate.Fixed(rate); // 保证金纯固定(无浮动)
+
+ if (calcToday)
+ {
+ if (settment)
+ {
+ // EOD:单日增量,累计 = 昨日累计 + 今日增量;notional = 昨日终本金(无差分)
+ var policy = AccrualPolicy.BuildEod(position, annualDays, isCompound: false);
+ var r = SimpleInterestAccrual.AccrueEod(
+ priorAccrued: preEod.InterestProfitSum,
+ priorNotional: preEod.TdInterestPrincipal,
+ unwindFraction: 1m,
+ rate: legRate, policy: policy, eodDate: valueDate);
+ interestAmount = r.Accrued;
+ tdInterestAmount = r.AccruedToday;
+ }
+ else
+ {
+ // 盘中:notional = 今日本金 posiPrincipal(无差分、无 orginPv)
+ var segmentRates = new List<(DateTime, decimal)> { (position.PosiStartDate, rate) };
+ var r = SimpleInterestAccrual.AccruePeriod(
+ priorAccrued: preEod.InterestProfitSum * closePercent,
+ notional: posiPrincipal,
+ unwindFraction: closePercent,
+ segmentRates: segmentRates,
+ startDate: position.PosiStartDate,
+ endDate: valueDate,
+ priorValueDate: preEod.ValueDate,
+ boundary: AccrualBoundary.Of(calcFirst, calcLast),
+ annualDays: annualDays,
+ isAnnualized: position.IsAnnualized);
+ interestAmount = r.Accrued;
+ tdInterestAmount = r.AccruedToday;
+ interest.InterestPrincipal = posiPrincipal * closePercent; // 同 CalcDailySimpleInterest:1304
+ }
+ }
+
+ interest.InterestAmount = Math.Round(interestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
+ interest.TdInterestAmount = Math.Round(tdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
+ var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
+ interest.InterestClosePnL = interest.InterestAmount * interestRatio;
+
+ if (add) UpdateDbOption(interest);
+ return interest;
+ }
+
///
/// 计算盘中利息(平仓/互换)
///
@@ -1797,84 +1859,6 @@ namespace YLErp.Modules.SwapModule
unwindData.SwapRealizedPnL = Math.Round(unwindData.SwapRealizedPnL, 2, MidpointRounding.AwayFromZero);
}
///
- /// 多空组合平仓
- ///
- ///
- ///
- public void SwapLongShortUnwind(UnwindData unwindData)
- {
- var td = DbContext.trade.Find(unwindData.SwapTradeId);
- if (td == null)
- {
- throw new ServiceException("未找到交易信息");
- }
- UnwindNormalizer.NormalizeEventUnwindDate(unwindData);
- unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
- NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
- var trans = DbContext.Database.BeginTransaction();
- try
- {
- int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
- RecordMarginCashFlow(td, unwindData);
- SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
- td.UnWindDate = unwindData.UnwindDate;
- td.StockEqvNotional = 0;
- td.TradeStatus = "已平仓";
- DbContext.SaveChanges();
- trans.Commit();
- }
- catch (Exception ex)
- {
- trans.Rollback();
- throw;
- }
- finally
- {
- trans.Dispose();
- }
-
- }
- ///
- /// 多空组合互换
- ///
- ///
- ///
- public void SwapLongShort(UnwindData unwindData)
- {
- var td = DbContext.trade.Find(unwindData.SwapTradeId);
- if (td == null)
- {
- throw new ServiceException("未找到交易信息");
- }
- UnwindNormalizer.NormalizeEventUnwindDate(unwindData);
- unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
- NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.互换, "系统操作_互换");
- var trans = DbContext.Database.BeginTransaction();
- try
- {
- int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
- SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.互换, clientCashId, "系统操作_互换");
- td.UnWindDate = unwindData.UnwindDate;
- if (td.ExerciseDate <= unwindData.ValueDate)
- {
- td.Notional = 0;
- td.StockEqvNotional = 0;
- td.TradeStatus = "已到期";
- }
- DbContext.SaveChanges();
- trans.Commit();
- }
- catch (Exception ex)
- {
- trans.Rollback();
- throw;
- }
- finally
- {
- trans.Dispose();
- }
- }
- ///
/// 互换
///
///