test(margin): 新增 CalcMarginInterest 影子对账——保证金计息消除 orginPv/差分,与旧管线数值一致

- 新增 CalcMarginInterest(SwapDealService):保证金专属计息,复用 SimpleInterestAccrual 纯函数,notional 直接取保证金余额(EOD=昨日终本金/盘中=今日本金),消除融资腿差分公式 accrualBasis=TdInterestPrincipal+posiPrincipal-orginPv 与 orginPv 维度 hack(对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余)
- 保留累计语义(priorAccrued+增量),满足下游 SwapEodPositionService 字段契约
- 新增 MarginInterestShadowTest:5 场景对账(EOD 续接/首日、盘中全平/部分平仓/互换),新旧 InterestAmount/TdInterestAmount 严格一致
- 本提交仅影子对账,生产路径未改(GetInterests 仍走 CalcEodInterest/CalcUnwindInterest);下一步提交2 切换生产 + 删 orginPv hack

零生产风险;编译 0 错误;影子对账 5/5 + 现有保证金/利息回归 60/60 通过。
This commit is contained in:
hjhan
2026-08-13 09:37:42 +08:00
parent 905bd407e2
commit a86b496f87
2 changed files with 315 additions and 128 deletions
+112 -128
View File
@@ -345,56 +345,6 @@ namespace YLErp.Modules.SwapModule
/// <param name="tradeId"></param>
/// <returns></returns>
/// <exception cref="ServiceException"></exception>
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<int> eventTyps = new List<int>() { (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;
}
/// <summary>
/// 平仓初始化
/// </summary>
@@ -966,6 +916,118 @@ namespace YLErp.Modules.SwapModule
return interest;
}
/// <summary>
/// 保证金腿(InterestMode 5/6)专属计息——替代 CalcEodInterest/CalcUnwindInterest 对保证金的处理。
///
/// 保证金是纯固定利率单利:浮动利率(FR007)/分段利率/复利对其均为死分支(前端无入口、
/// 确认书不含、FundingLegRate.Build 对空 FloatRateUnderlyingCode 恒返回 Fixed)。故本方法直接用
/// SimpleInterestAccrual 纯函数计息,notional 取保证金余额本身:
/// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源)
/// 盘中 = 今日本金 posiPrincipalInterestPrincipalFix
/// 消除融资腿差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 与 orginPv 维度
/// hack——对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余。保留累计语义(priorAccrued + 增量),
/// 满足下游 SwapEodPositionService 字段契约(InterestAmount=缩放累计、TdInterestAmount=单日参考等)。
/// </summary>
/// <param name="settment">true=收盘归档(EOD)false=盘中平仓/互换。</param>
/// <param name="swap">互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。</param>
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;
}
/// <summary>
/// 计算盘中利息(平仓/互换)
/// </summary>
@@ -1797,84 +1859,6 @@ namespace YLErp.Modules.SwapModule
unwindData.SwapRealizedPnL = Math.Round(unwindData.SwapRealizedPnL, 2, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 多空组合平仓
/// </summary>
/// <param name="unwindData"></param>
/// <exception cref="ServiceException"></exception>
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();
}
}
/// <summary>
/// 多空组合互换
/// </summary>
/// <param name="swap_Deal"></param>
/// <exception cref="ServiceException"></exception>
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();
}
}
/// <summary>
/// 互换
/// </summary>
/// <param name="swap_Deal"></param>