using BaseOUDAL; using Newtonsoft.Json; using NPOI.POIFS.Properties; using System; using System.Linq.Expressions; using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; using YLErp.Helpers; using YLErp.Model; using YLErp.Model.Enum; using YLErp.Models; using YLErp.Modules.DataProviderModule; using YLErp.Modules.EodModule; using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.SwapModule.ReturnLegs; using YLErp.Modules.TradeModule.DealModule; using YLErp.QdpModule; namespace YLErp.Modules.SwapModule { /// /// 互换流水日终归档服务 /// public class SwapEodPositionService : SwapTradeBaseService { private static readonly IYcLogger Log = LogFactory.GetLogger(typeof(SwapEodPositionService).FullName); public SwapEodPositionService(OptUserInfo optUser) : base(optUser) { } public SwapEodPositionService(YLBaseService baseService) : base(baseService) { } /// /// 在一组日终快照中选择严格早于指定日期的最近日。 /// 回退到除权日 D 时必须得到 D 之前的基线;若使用 D 自身,除权后的 2000/50 /// 会被当成除权前状态,重收盘时就可能再次套用 10 送 10。严格使用 < valueDate /// 也覆盖周末、节假日:周一没有周日 EOD 时,直接选择上一个实际有快照的交易日。 /// public static DateTime? SelectLatestEodDateBefore( IEnumerable eodPositions, DateTime valueDate) { if (eodPositions == null) { return null; } var date = valueDate.Date; return eodPositions .Where(x => x != null && !x.Invalid && x.ValueDate.Date < date) .Select(x => (DateTime?)x.ValueDate.Date) .OrderByDescending(x => x.Value) .FirstOrDefault(); } /// /// 返回指定交易在 valueDate 之前最近实际 EOD 日的全部有效明细。 /// 这是回退和纯单元测试共用的选择规则;调用方不得退化为 AddDays(-1),因为自然日 /// 不等于交易日。若没有快照返回空集合,表示只能保留当前实时持仓,不能伪造基线。 /// public static List SelectLatestEodPositionsBefore( IEnumerable eodPositions, DateTime valueDate) { var latestDate = SelectLatestEodDateBefore(eodPositions, valueDate); if (!latestDate.HasValue) { return new List(); } return eodPositions .Where(x => x != null && !x.Invalid && x.ValueDate.Date == latestDate.Value.Date) .ToList(); } /// /// 从已经确认的 Stock/Fund EOD 快照恢复实时浮动腿的有效基线。 /// 该方法只复制 EOD 已落库的数量、价格、名义本金及累计分红/待结费用,不再次计算 /// 公司行动系数,因此是幂等的。例:原 1000 份、期初价 100,10 送 10 后 EOD 为 /// 2000 份、50;下一日盘中直接恢复 2000/50,不能再变成 4000/25。 /// 非 Fund、空快照或标的腿不满足收取方向时返回 false,保持原有逻辑。 /// public static bool RestoreFundPositionFromEod( swap_position realtimePosition, eod_swap_position eodPosition) { if (realtimePosition == null || eodPosition == null || realtimePosition.PosiDirection <= 0 || !IsTrsCorporateActionInstrument(realtimePosition.UnderlyingInstrumentType) || !IsTrsCorporateActionInstrument(eodPosition.UnderlyingInstrumentType)) { return false; } realtimePosition.PosiQuantity = eodPosition.PosiQuantity; realtimePosition.PosiGrossPrice = eodPosition.PosiGrossPrice; realtimePosition.PosiNetPrice = eodPosition.PosiNetPrice; realtimePosition.PosiNetFeePrice = eodPosition.PosiNetFeePrice; realtimePosition.PosiNetNoFeePrice = eodPosition.PosiNetNoFeePrice; realtimePosition.PosiNotionalValue = Math.Round( eodPosition.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); realtimePosition.PosiTradingFeePending = eodPosition.PosiFeePending; realtimePosition.PosiDividendIncome = eodPosition.PosiDividendSum; return true; } /// /// 查询 valueDate 之前最近一份有效 Stock/Fund EOD 快照,作为盘中操作的日初基线。 /// 必须严格使用 < valueDate:试算日当天的 EOD 可能尚未完成,或是重收盘留下的待重建数据, /// 不能反向覆盖盘中实时持仓。例:D 日 10 送 10 后 EOD 为 2000 份/50,D+1 盘中读取 D; /// D 日盘中只读取 D-1,不会误把 D 日半成品当成已生效基线。Invalid 明细始终排除。 /// 调用方还需检查该 EOD 之后是否已有完成流水,避免覆盖当日部分平仓结果。 /// public virtual eod_swap_position GetLatestValidEodPosition( int swapTradeId, long positionId, DateTime valueDate) { return DbContext.eod_swap_position .Where(x => x.SwapTradeId == swapTradeId && x.PositionId == positionId && !x.Invalid && x.ValueDate < valueDate.Date) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) .FirstOrDefault(); } /// /// 回退/收益互换等路径需要的最近实际 EOD 快照集合;严格早于 valueDate,且过滤作废行。 /// public virtual List GetLatestEodPositionsBefore( int swapTradeId, DateTime valueDate) { var candidates = DbContext.eod_swap_position .Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate < valueDate.Date) .ToList(); return SelectLatestEodPositionsBefore(candidates, valueDate); } private int GetStorageDeliveryPriceRound(string underlyingInstrumentType, string underlyingCode) { if (ConsGlobal.InstrumentType.IsBond(underlyingInstrumentType)) { return ConsGlobal.PriceRound; } if (string.IsNullOrEmpty(underlyingCode)) { return ConsGlobal.SwapDeliveryPriceRound; } return GetUnderlyingData(underlyingCode)?.IsBond() == true ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound; } #region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变 /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) protected virtual void PersistEodSwapPosition(eod_swap_position position) { // 所有新增或更新的日终持仓都经过此入口,避免不同日终分支出现精度差异。 EodPnlCalculator.NormalizeEodPositionForStorage(position); var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode); position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.PosiNotionalValue = Math.Round(position.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (position.id == 0) { DbContext.eod_swap_position.Add(position); } else { UpdateDbOption(position); } } /// 保存所有变更(生产: DbContext.SaveChanges;测试: 计数) protected virtual void SaveAllChanges() { DbContext.SaveChanges(); } /// 获取汇率(生产: EodCurrencyRateService;测试: 返回固定值) protected virtual double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) { return new EodCurrencyRateService(UserInfo).GetCurrencyRate(quoteCurrency, settlementCurrency, valueDate, seekPreday, currencyRateType); } /// /// 计算利息腿利息明细(生产: new SwapDealService(this).GetInterests;测试: 用StubSwapDealService内存算)。 /// 参数与 SwapDealService.GetInterests 完全一致,保证行为不变。 /// (needPrice/grossPrice 死参数已随 2026-08 收口删除,两侧同步。) /// protected virtual List CalcSwapInterests( trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate, List eodPositions, List positions, decimal posiNotionalValue, decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false, List closeList = null) { return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate, eodPositions, positions, posiNotionalValue, closePosiNotionalValue, closePrecent, eventType, tdClose, orginPv, add, settment, newCalcLast, closeList); } /// /// 【EOD 当日有平仓后的收盘结息】显式入口——原 SaveAutoEodWithCloseInterestPosition 直调 /// CalcSwapInterests(settment:false) 的具名封装(2026-08 显式化重构)。 /// 语义契约见 InterestCalcRequest.EodPostCloseSettle 工厂注释(平仓后剩余 + 实际平掉额 + 恒1全额结息, /// 触发 GetInterests 内 mode2/mode9 本金修正)。计息走 CalcUnwindInterest 全区间重放。 /// 默认实现仍经 CalcSwapInterests 转发,保持既有测试替身对该虚接缝的拦截不变。 /// protected virtual List CalcEodPostCloseSettleInterests(InterestCalcRequest req) => CalcSwapInterests(req.Td, req.TradeExtend, req.ValueDate, req.UnwindDate, req.EodPositions, req.Positions, req.PosiNotionalValue, req.ClosePosiNotionalValue, req.ClosePercent, req.EventType, req.TdClose, req.OrginPv, req.Add, settment: false, req.NewCalcLast, req.CloseList); /// /// 持仓延续腿重置日再定盘(EQD-6968 口径自洽化接缝)。 /// 生产:Fr007IndexFixer.GetFixingOrThrow——缺价抛异常,与 ByEod 重置日再定盘/EodCheckSettlePrice /// 同口径(EOD 时点当日 FR007 已由收盘前检查把关);测试:override 注入受控定盘。 /// protected virtual decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate) => Fr007IndexFixer.Instance.GetFixingOrThrow(valueDate, position.interest_rule, position.FloatRateUnderlyingCode); // FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复) /// 查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典) protected virtual trade_extend FindTradeExtend(int tradeId) { return DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId); } /// 查找指定日期范围的 eod 持仓(生产: DbContext.eod_swap_position.Where;测试: 内存列表) protected virtual List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) { return DbContext.eod_swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList(); } /// 查找交易持仓(生产: DbContext.swap_position.Where;测试: 内存列表) protected virtual List FindSwapPositions(int swapTradeId) { return DbContext.swap_position.ActiveByTrade(swapTradeId).ToList(); } /// 查找框架合约日终汇总(生产: DbContext.eod_swap.FirstOrDefault;测试: 内存字典) protected virtual eod_swap FindEodSwap(int swapTradeId, DateTime valueDate) { return DbContext.eod_swap.FirstOrDefault(x => x.SwapTradeId == swapTradeId && x.ValueDate == valueDate); } /// 添加互换事件(生产: new SwapEventService(this).AddSwapEventDate;测试: 收集到列表) protected virtual swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) { return new SwapEventService(this).AddSwapEventDate(tradeDate, swapTradeId, eventType, data, clientCashId, save, reason); } /// 持久化互换流水事件(生产: DbContext.swap_flow_event.Add;测试: 收集到列表) protected virtual void PersistFlowEvent(swap_flow_event flowEvent) { DbContext.swap_flow_event.Add(flowEvent); } /// 在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务) protected virtual void ExecuteInTransaction(Action action) { var trans = DbContext.Database.BeginTransaction(); try { action(); trans.Commit(); } catch { trans.Rollback(); throw; } finally { trans.Dispose(); } } /// 添加资金记录(生产: AddClientCashInCashOut;测试: 收集到计数器) protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate) { return AddClientCashInCashOut(td, amount, action, valueDate); } /// 保存框架合约日终汇总(生产: SaveEodSwap私有方法;测试: 收集到列表) protected virtual void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { SaveEodSwap(td, settleDate, preSettleDate); } /// 清理旧持仓事件(生产: ClearSwapPositions;测试: 空操作) protected virtual void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) { ClearSwapPositions(td, tradeDate, eventTypes, false); } /// 获取标的市场价格(生产: UnderlyingCodePrice查缓存+中债估值;测试: 返回固定值) protected virtual decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) { return UnderlyingCodePrice(code, settleDate, out vobp); } /// 获取用于互换浮动腿盯市的标的价格。 private decimal GetSwapValuationPrice(string code, DateTime settleDate, out decimal vobp) { var price = GetUnderlyingPrice(code, settleDate, out vobp); var underlying = GetUnderlyingData(code); if (underlying?.IsBond() == true) { return Math.Round(price, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } return Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } /// 获取标的缓存数据(生产: DataCacheProvider;测试: 返回内存对象) protected virtual underlying_manager GetUnderlyingData(string underlyingCode) { return DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); } /// /// 计算期间现金流(生产: BondPaymentService;测试: 返回固定值)。 /// BondPaymentService 会按每条付款记录的数据来源换算:bond_payment_info 原生期间付息 /// 按每 100 份,公司行为表补充的现金分红按每 10 份。不能只按 Fund/Stock 标的类型 /// 选择一个统一除数,否则两类记录同时命中时会错算。 /// protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) { return new BondPaymentService(UserInfo).CalcPayment( underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); } protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty) { if (!corporateActionQty.HasValue) { return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); } var service = new BondPaymentService(UserInfo); var payments = service.GetBondPayments(underlyingCode, fromDate, toDate); return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty); } // ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)---- /// 查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表) protected virtual List FindActiveSwapTrades(DateTime settleDate, IEnumerable clientIds) { var tradePredicate = PredicateBuilder.Create(n => n.ValidState != ConsGlobal.InValid && n.TradeType == "收益互换" && n.TradeDate <= settleDate && n.ExerciseDate >= settleDate && (n.TradeStatus == ConsTrade.确认成交 || n.UnWindDate >= settleDate) ); if (clientIds != null && clientIds.Any()) { tradePredicate = tradePredicate.And(x => clientIds.Contains(x.ClientId)); } return DbContext.trade.Where(tradePredicate).ToList(); } /// 查找交易的所有持仓(含初始+实际,生产: DbContext.swap_position;测试: 内存列表) protected virtual List FindAllSwapPositions(List tradeIds) { return DbContext.swap_position.Where(t => tradeIds.Contains(t.SwapTradeId) && !t.Invalid).ToList(); } /// 批量查找交易扩展(生产: DbContext.trade_extend;测试: 内存列表) protected virtual List FindTradeExtends(List tradeIds) { return DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList(); } /// 查找指定日期的日终汇总(生产: DbContext.eod_swap;测试: 内存列表) protected virtual List FindEodSwapsByDate(DateTime valueDate) { return DbContext.eod_swap.Where(x => x.ValueDate == valueDate).ToList(); } /// 查找交易在指定日期的完成流水事件(生产: DbContext.swap_flow_event;测试: 内存列表) protected virtual List FindFlowEvents(int swapTradeId, DateTime settleDate) { Expression> eventExpression = x => x.SwapTradeId == swapTradeId && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.EventDate == settleDate; return DbContext.swap_flow_event.Where(eventExpression).ToList(); } protected virtual List FindCompletedFlowEvents(List tradeIds) { return DbContext.swap_flow_event .Where(x => tradeIds.Contains(x.SwapTradeId) && x.DataState == (int)SwapFlowDateStateEnum.完成) .ToList(); } /// 查询登记日或真实生效日命中的有效公司行为。 protected virtual List FindCorporateActionInfos(DateTime settleDate) { return DbContext.ex_dividend_info .Where(x => x.ValidStatus && ((x.ExDividendDate.HasValue && x.ExDividendDate.Value == settleDate.Date) || (x.EffectiveDate.HasValue && x.EffectiveDate.Value == settleDate.Date))) .ToList(); } /// 查询交易已有公司行为事件,用于登记日/生效日幂等匹配。 protected virtual List FindCorporateActionEvents(int swapTradeId) { return DbContext.swap_event .Where(x => x.SwapTradeId == swapTradeId && x.EventType == (int)SwapEventTypeEnum.公司行为 && !x.Invalid) .ToList(); } /// 更新已存在的公司行为事件;默认只标记实体,统一由收盘事务保存。 protected virtual void UpdateCorporateActionEventRecord(swap_event swapEvent) { UpdateDbOption(swapEvent); } /// /// 获取公司行为公式使用的收盘价。 /// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日 /// ExDividendDate;不能在 除权日 EOD 误取 除权日收盘价重算 登记日 /// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。 /// protected virtual decimal GetFundCorporateActionClosePrice( ex_dividend_info dividendInfo, decimal fallbackPrice) { if (!dividendInfo.ExDividendDate.HasValue) { return fallbackPrice; } var closePrice = new EodPriceProvider(dividendInfo.ExDividendDate.Value) .GetPrice(dividendInfo.UnderlyingCode, SettlementTypeEnum.ClosePrice); return Convert.ToDecimal(closePrice); } public static bool IsCorporateActionInstrument(string instrumentType) { // TRS 公司行为本期只覆盖 Stock/Fund。TBonds 等类型继续走原债券付息链路, // 这里不能用“非空标的类型”放宽,否则会把期权、期货等未验证品种一并启用。 return string.Equals(instrumentType, ConsGlobal.InstrumentType.Fund, StringComparison.OrdinalIgnoreCase) || string.Equals(instrumentType, ConsGlobal.InstrumentType.Stock, StringComparison.OrdinalIgnoreCase); } private static bool IsTrsCorporateActionInstrument(string instrumentType) => IsCorporateActionInstrument(instrumentType); #endregion /// /// 多空组合 互换流水合成持仓 /// /// 清算日期 public void SwapFlowEventCompose(DateTime tradeDate) { new SysJobService(UserInfo).UpdateJob("互换流水合成持仓", 41, "互换流水合成持仓进行中"); var eventQueryGroup = DbContext.swap_flow_event.Where(n => n.DataState == (int)SwapFlowDateStateEnum.等待完成 && n.EventDate == tradeDate).AsEnumerable().GroupBy(g => g.SwapTradeId); foreach (var eventQueryGroupItem in eventQueryGroup) { ComposePage(eventQueryGroupItem.Key, eventQueryGroupItem.ToList(), tradeDate); } new SysJobService(UserInfo).UpdateJob("互换流水合成持仓", 42, "互换流水合成持仓完成"); } /// /// 多空组合 互换流水合成持仓 /// /// 清算日期 public void SwapFlowEventCompose(List flowEventIds, DateTime tradeDate) { new SysJobService(UserInfo).UpdateJob("互换流水合成持仓", 41, "互换流水合成持仓进行中"); var eventQueryGroup = DbContext.swap_flow_event.Where(n => flowEventIds.Contains(n.id)).ToList().GroupBy(g => g.SwapTradeId); foreach (var eventQueryGroupItem in eventQueryGroup) { ComposePage(eventQueryGroupItem.Key, eventQueryGroupItem.ToList(), tradeDate, false); } new SysJobService(UserInfo).UpdateJob("互换流水合成持仓", 42, "互换流水合成持仓完成"); } /// /// 收盘生成归档信息 /// /// 结算日期 public void SwapPositionCompose(DateTime settleDate, DateTime preSettleDate, IEnumerable ClientIds) { var dateStr = settleDate.ToString("yyyy-MM-dd"); Log.Info("SwapPositionCompose:" + "settleDate:" + settleDate + " preSettleDate:" + preSettleDate + " ClientIds:" + JsonHelper.Serialize(ClientIds)); var tradeQueryList = FindActiveSwapTrades(settleDate, ClientIds); var tradeIds = tradeQueryList.Select(s => s.id).ToList(); var allTradePositionList = FindAllSwapPositions(tradeIds); var tradePositionList = allTradePositionList.Where(t => t.IsInitial).ToList(); var tradeRealPositionList = allTradePositionList.Where(t => !t.IsInitial).ToList(); var tradeExtendList = FindTradeExtends(tradeIds); var eodSwapList = FindEodSwapsByDate(preSettleDate); var completedFlowEvents = FindCompletedFlowEvents(tradeIds); // 公司行为只取 settleDate 当天的有效单行;同一标的出现多条记录必须中止本次收盘, // 否则 ToDictionary 会抛重复键,无法证明哪一条系数应生效。 var corporateActionInfos = FindCorporateActionInfos(settleDate) ?? new List(); // 除权日信息 var exDividendInfos = corporateActionInfos .Where(x => x != null && x.ValidStatus && x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date == settleDate.Date) .ToList(); // 登记日信息 var registrationInfos = corporateActionInfos .Where(x => x != null && x.ValidStatus && x.ExDividendDate.HasValue && x.ExDividendDate.Value.Date == settleDate.Date) .ToList(); // 公司行为去重 - 除权日 var duplicateDividend = exDividendInfos .GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase) .FirstOrDefault(x => x.Count() > 1); if (duplicateDividend != null) { throw new InvalidOperationException($"标的【{duplicateDividend.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效除权记录"); } // 公司行为去重 - 登记日 var duplicateRegistration = registrationInfos .GroupBy(x => x.UnderlyingCode, StringComparer.OrdinalIgnoreCase) .FirstOrDefault(x => x.Count() > 1); if (duplicateRegistration != null) { // 登记日现金权益不能依赖数据库返回顺序取 First;同一标的同一登记日 // 有多条有效记录时,系统无法证明应采用哪一条派现金额,必须中止收盘。 throw new InvalidOperationException($"标的【{duplicateRegistration.Key}】在【{settleDate:yyyy-MM-dd}】存在多条有效登记日记录"); } // 根据标的代码 创建map var exDividendByCode = exDividendInfos.ToDictionary( x => x.UnderlyingCode, x => x, StringComparer.OrdinalIgnoreCase); List eventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; foreach (var td in tradeQueryList) { ExecuteInTransaction(() => { List removeEventTyps = new List() { (int)SwapEventTypeEnum.自动互换 }; bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString(); ClearSwapPositions(td, settleDate, removeEventTyps, true); var positions = tradePositionList.Where(x => x.SwapTradeId == td.id); var realPositions = tradeRealPositionList.Where(s => s.SwapTradeId == td.id); var posiList = positions.Where(x => x.PosiQuantity > 0).ToList(); var realPosiList = realPositions.ToList(); var tradeCompletedFlowEvents = completedFlowEvents.Where(x => x.SwapTradeId == td.id).ToList(); var interestList = SwapDealService.ResolveInterestLegPositionsAsOf( positions.ToList(), realPosiList, tradeCompletedFlowEvents, settleDate) .Where(x => x.InterestDirection > 0).ToList(); DateTime posiDate = td.TradeDate.Value;//交易日期 var lastEodSwap = eodSwapList.FirstOrDefault(x => x.SwapTradeId == td.id); //上一交易日无日终归档,且不是交易日期,且当前收盘日期不是交易日期,报错 if (lastEodSwap == null && settleDate > posiDate) { throw new Exception($"交易{td.TradeNumber}在上一交易日【{preSettleDate:yyyy-MM-dd}】未收盘"); } var allEodPositions = FindEodSwapPositions(td.id, preSettleDate); var eodPositions = allEodPositions.Where(x => x.ValueDate == preSettleDate).ToList();//上一日终持仓信息 var tradeExtend = tradeExtendList.FirstOrDefault(x => x.TradeId == td.id); td.trade_extend = tradeExtend; var todyEodPositions = allEodPositions.Where(x => x.ValueDate == settleDate).ToList(); var allPositionQty = realPositions.Sum(x => x.PosiQuantity);//总剩余持仓数量 var orginPv = eodPositions.Sum(s => s.PosiNotionalValue); if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty != 0)//多空组合判断是否已到到期日且无持仓信息 { throw new Exception($"交易【{td.TradeNumber}】到期扔有持仓信息"); } var flowEvents = FindFlowEvents(td.id, settleDate); var preDealDate = GetPreDealDate(td.id, settleDate, eventTyps);//上一次平仓/互换/自动互换处理日期 List autoInterests = new List();//自动互换利息腿信息 // 处理浮动腿前先准备当日开盘基线:登记日 EOD 仍保存 // 1000 份/100 元,除权日收盘时先把上一 EOD 的基线转换为 // 2000 份/50 元,再处理当日平仓 300 份,最终才会得到 1700 份/50 元。 // 不能等 DealFloatPositions 处理完平仓后再把 700 份乘 2, // 否则会错误得到 1400 份;也不能直接修改数据库里的上一 EOD,否则登记日报表会被污染。 // 重置基线 - 除权日 var openingEodPositions = PrepareFundOpeningEodPositions( eodPositions, // 上一日终持仓 exDividendByCode, settleDate); // 构建公司行为前eod持仓 var corporateActionBeforePositions = BuildCorporateActionBeforePositions( eodPositions, // 上一日终持仓 posiList); var corporateActionCashDividendBeforePositions = corporateActionBeforePositions .Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode) && exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend) && dividend.GiveCashAmount != 0m) .ToList(); // 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。 // 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。 var floatPositionsForCompose = eodPositions.Count == 0 ? PrepareInitialCorporateActionPositions(posiList, exDividendByCode, settleDate) : posiList; // 处理浮动腿归档 var curEodPosis = DealFloatPositions( floatPositionsForCompose, // 初始腿 realPosiList, // 实时腿 openingEodPositions, // 开盘基线 todyEodPositions, // 当日终持仓 settleDate, // 收盘日期 td, // 交易 preSettleDate, // 上一交易日 flowEvents, // 流水事件 corporateActionCashDividendBeforePositions); // 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment // 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。 // 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。 // 公司行为事件 RecordCorporateActionEvents( td, curEodPosis, corporateActionBeforePositions, registrationInfos, exDividendInfos, settleDate); // 登记日 EOD 仍保存除权前快照, // 但下一交易日开盘读取的实时浮动腿需要先切换到生效后的 Q/P。 // 该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。 UpdateRealtimeCorporateActionPositions(td, curEodPosis, registrationInfos, exDividendInfos, settleDate); var posiLongNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue); var posiShortNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue); var closePosiNotional = curEodPosis.Where(s => s.TdCloseQty > 0).Sum(s => s.TdCloseQty * s.ContractSize * s.PosiGrossPrice); var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0; //处理利息腿 DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv); //获取自动互换的观察日信息,用于确定结算日期 IntervalModel observationInterval = null; foreach (var interest in interestList) { observationInterval = InterestEodScenarioDispatch.FindObservationInterval(interest, settleDate); if (observationInterval != null) break; } // 自动互换(仅利息/预付金,不含分红) DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, observationInterval); // 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关 DealDividends(curEodPosis, td, settleDate, tradeExtend); //多空组合判断是否已到到期日且无持仓信息 if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty == 0) { td.TradeStatus = "已到期"; td.UnWindDate = settleDate; } SaveAllChanges(); }); } } /// /// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。 /// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。 /// 例如 1000 份/100 元、10 送 10 的记录在 登记日 EOD 仍是 1000/100; /// 除权日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。 /// protected List PrepareFundOpeningEodPositions( IReadOnlyCollection previousEodPositions, IReadOnlyDictionary exDividendByCode, DateTime settleDate) { // 首日收盘或者当前非生效日 跳过 if (previousEodPositions == null || previousEodPositions.Count == 0 || exDividendByCode == null || exDividendByCode.Count == 0) { return previousEodPositions?.ToList() ?? new List(); } // Clone 后只调整本次收盘的内存输入,不改 DbContext 跟踪的上一日实体; // 否则重收盘或报表读取会把登记日的 Q/P 永久变成除权后 Q/P。 var openingPositions = previousEodPositions .Where(x => x != null) .Select(x => x.Clone()) .ToList(); // 应用公司行为 ApplyCorporateActions( openingPositions, exDividendByCode, settleDate); return openingPositions; } /// /// 对 TRS Stock/Fund 浮动腿应用一条已按 EffectiveDate 筛选的份额/价格公司行为。 /// 此方法用于直接测试/兼容已有调用方;正式收盘链路通过 /// PrepareFundOpeningEodPositions 在处理当日流水前执行同一动作。 /// 该步骤只改 EOD 持仓的份额/价格基线,不生成现金分红流水;现金模式下现金分红 /// 不下调期初价格,而是由同步任务写入 bond_payment_info,后续付息链路单独计入。 /// /// 幂等例子:原持仓 1000 份、期初价 100,每 10 份送 10 份。首次收盘得到 2000 份/50; /// 同日重跑时,若该腿没有新流水,先从前一日 EOD 恢复 1000/100,再计算为 2000/50, /// 不能直接在当日结果上再次计算成 4000/25。 /// /// /// 有流水时不在这里强行恢复前一日数量,因为 DealFloatPositions 已把当日开平仓滚动到当前结果; /// 盘中平仓不会再次套公式,而是读取严格早于 valueDate 的最近有效 EOD,必要时按当日 /// EffectiveDate 再生成开盘基线。 /// /// protected void ApplyCorporateActions( IEnumerable positions, IReadOnlyDictionary exDividendByCode, DateTime settleDate) { if (exDividendByCode.Count == 0) { return; } // TODO: 现金分红税率接入后,仅价格调整模式需要读取税率;TRS 现金模式下不参与除权系数。 // var dividendTaxRate = GetDividendTaxRate(); var dividendTaxRate = 0m; foreach (var position in positions) { // 不是浮动腿 或者 不是 Fund Stock类型的标的 或者 没有除权信息 或者 除权日不是结算日 - 跳过 if (position.PosiDirection <= 0 || !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType) || string.IsNullOrWhiteSpace(position.UnderlyingCode) || !exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividendInfo) || !dividendInfo.EffectiveDate.HasValue || dividendInfo.EffectiveDate.Value.Date != settleDate.Date) { continue; } // 获取除权参考价 - 登记日收盘价 var corporateActionClosePrice = GetFundCorporateActionClosePrice( dividendInfo, position.UnderlyingPrice); if (corporateActionClosePrice <= 0) { throw new InvalidOperationException( $"Stock/Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权"); } // Excel 公式 口径:PriceRatio 是“登记日收盘价 / 除权参考价”, // 因此期初价格和持仓数量都使用同一个系数:P' = P / M,Q' = Q * M。 // 配股已经进入 价格参考价,所以即使没有送股,配股也会调整 TRS 数量; // 现金分红不影响 TRS Stock/Fund 期初价格,现金权益由独立分红字段处理。 var originalQuantity = position.PosiQuantity; // 计算公司行为发生后的 Q/P var adjusted = CalculateCorporateActionValues( position.PosiQuantity, position.PosiGrossPrice, position.PosiNetPrice, position.PosiNetFeePrice, position.PosiNetNoFeePrice, dividendInfo, corporateActionClosePrice, dividendTaxRate, GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode)); position.PosiQuantity = adjusted.Quantity; position.TdChangedQty = position.PosiQuantity - originalQuantity; position.PosiGrossPrice = adjusted.GrossPrice; position.PosiNetPrice = adjusted.NetPrice; position.PosiNetFeePrice = adjusted.NetFeePrice; position.PosiNetNoFeePrice = adjusted.NetNoFeePrice; // 多空方向 var shortRatio = DirectionRatio.LongShort(position.PositionType); // 收付方向 var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection); // 处理价格的正负号(收支方向) position.PosiNotionalValue = Math.Round( position.PosiGrossPrice * position.PosiQuantity * position.ContractSize, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); position.UnderlyingMarketValue = MtmCalc.MarketValue( position.UnderlyingPrice, position.PosiQuantity, position.ContractSize, shortRatio); position.PosiMtmPnL = EodPnlCalculator.RoundMoney(MtmCalc.UnrealizedPnl( position.UnderlyingPrice, position.PosiGrossPrice, position.PosiQuantity, position.ContractSize, shortRatio, directionRatio)); position.PosiProfitSum = EodPnlCalculator.RoundMoney(MtmCalc.ReturnLegProfitSum( position.PosiMtmPnL, position.PosiDividendSum, position.PosiFeePending)); position.SwapPositionValue = EodPnlCalculator.RoundMoney(PositionValueCalc.Calc( position.InterestProfitSum, position.PosiProfitSum)); position.PosiStatus = position.PosiQuantity == 0 ? 1 : 0; } } /// /// 构造审计事件的调整前快照。优先克隆上一 EOD,保证后续调整不会污染历史实体; /// 交易首日没有 EOD 时才从初始持仓复制,并把累计分红/已实现字段初始化为 0。 /// private static List BuildCorporateActionBeforePositions( IReadOnlyCollection previousPositions, IReadOnlyCollection initialPositions) { if (previousPositions != null && previousPositions.Count > 0) { return previousPositions .Where(x => x != null) .Select(x => x.Clone()) .ToList(); } return (initialPositions ?? Array.Empty()) .Where(x => x != null) .Select(x => new eod_swap_position { PositionId = x.PositionId, UnderlyingCode = x.UnderlyingCode, UnderlyingInstrumentType = x.UnderlyingInstrumentType, PosiDirection = x.PosiDirection, PositionType = x.PositionType, ContractSize = x.ContractSize, CountRatio = x.CountRatio, PosiQuantity = x.PosiQuantity, PosiGrossPrice = x.PosiGrossPrice, PosiNetPrice = x.PosiNetPrice, PosiNetFeePrice = x.PosiNetFeePrice, PosiNetNoFeePrice = x.PosiNetNoFeePrice, PosiNotionalValue = x.PosiNotionalValue, PosiTradingFee = x.PosiTradingFee, PosiFeePending = x.PosiTradingFeePending, PosiDividendSum = 0m, RealizedDividend = 0m, PosiStatus = x.PosiQuantity == 0m ? 1 : 0 }) .ToList(); } /// /// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线。 /// 不直接修改初始持仓实体,避免重收盘或后续流程再次读取时重复套用系数。 /// private List PrepareInitialCorporateActionPositions( IReadOnlyCollection initialPositions, IReadOnlyDictionary exDividendByCode, DateTime settleDate) { var positions = (initialPositions ?? Array.Empty()) .Where(x => x != null) .Select(x => x.Clone()) .ToList(); if (positions.Count == 0 || exDividendByCode == null || exDividendByCode.Count == 0) { return positions; } foreach (var position in positions) { if (position.PosiDirection <= 0 || !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType) || string.IsNullOrWhiteSpace(position.UnderlyingCode) || !exDividendByCode.TryGetValue(position.UnderlyingCode, out var info)) { continue; } var closePrice = GetFundCorporateActionClosePrice(info, position.PosiGrossPrice); ApplyCorporateActionToPosition( position, info, closePrice, 0m); } return positions; } /// /// 同步公司行为后的实时浮动腿。 /// 登记日只更新下一交易日 BOD 使用的实时 Q/P,不改当日已落库的 EOD; /// 生效日则把已调整的 EOD 复制到实时腿。每次都先从当日 EOD 恢复,保证重跑幂等。 /// private void UpdateRealtimeCorporateActionPositions( trade td, IReadOnlyCollection currentEodPositions, IReadOnlyCollection registrationInfos, IReadOnlyCollection effectiveInfos, DateTime settleDate) { if (td == null || currentEodPositions == null || currentEodPositions.Count == 0) { return; } // 登记日收盘后即切换实时 BOD。 // EffectiveDate 只用于确认这条记录仍是未来生效的公司行为; // 无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。 var pendingInfos = (registrationInfos ?? Array.Empty()) .Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date) .ToList(); var appliedInfos = effectiveInfos ?? Array.Empty(); foreach (var eod in currentEodPositions.Where(x => x != null && x.PosiDirection > 0 && IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType) && !string.IsNullOrWhiteSpace(x.UnderlyingCode))) { // 实时腿 var realtime = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.PositionId == eod.PositionId); if (realtime == null) { continue; } // 对每条当日 EOD 浮动腿,按标的代码在 pendingInfos 中找匹配的公司行为。 var pending = pendingInfos.FirstOrDefault(x => string.Equals( x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase)); if (pending != null) { // 必须从登记日 EOD 基线生成下一交易日 BOD,而不是在旧实时腿上继续套系数; // 这样 100000/100 只会变成一次 200000/50,并且不会把初始腿改掉。 // 先将实时腿恢复为登记日 EOD 的旧基线,再只对实时腿应用一次公司行为。 // EOD 仍保持除权前快照;因此 7/10 EOD=100000/100,而 7/13 BOD=200000/50。 var baseline = eod.Clone(); UpdateSwapPositionWithRealTime(baseline); var closePrice = GetFundCorporateActionClosePrice(pending, baseline.PosiGrossPrice); ApplyCorporateActionToPosition(realtime, pending, closePrice, 0m); continue; } var applied = appliedInfos.FirstOrDefault(x => string.Equals( x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase)); if (applied != null) { // 生效日 EOD 已经完成 Q/P 调整,实时腿直接同步最终快照,不再二次套系数。 UpdateSwapPositionWithRealTime(eod.Clone()); } } } /// /// 写入公司行为生命周期审计事件。 /// 登记日:保存调整前快照并标记 Applied=false; /// 真实除权日:使用上一 EOD 与当前 EOD 补齐调整后快照并标记 Applied=true。 /// 事件数据只追加/补齐,不删除已生效记录, /// 便于交易回退后通过 BackId 关联新的回退记录。 /// protected virtual void RecordCorporateActionEvents( trade td, IReadOnlyCollection currentPositions, IReadOnlyCollection previousPositions, IReadOnlyCollection registrationInfos, IReadOnlyCollection effectiveInfos, DateTime settleDate) { if (td == null || currentPositions == null) { return; } // 登记日信息合并除权日信息 var infos = (registrationInfos ?? Array.Empty()) .Concat(effectiveInfos ?? Array.Empty()) .Where(x => x != null && x.ValidStatus && !string.IsNullOrWhiteSpace(x.UnderlyingCode)) .GroupBy(x => new { x.id, x.UnderlyingCode, ExDividendDate = x.ExDividendDate?.Date, EffectiveDate = x.EffectiveDate?.Date }) .Select(x => x.First()) .ToList(); if (infos.Count == 0) { return; } // 跟据交易id查当前交易关联事件 var existingEvents = FindCorporateActionEvents(td.id); foreach (var current in currentPositions.Where(x => x != null && x.PosiDirection > 0 && IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType))) { var info = infos.FirstOrDefault(x => string.Equals( x.UnderlyingCode, current.UnderlyingCode, StringComparison.OrdinalIgnoreCase)); if (info == null) { continue; } // 公司行为事件只使用“公司行为记录主键 + PositionId”作为幂等键。 var matchingEvents = existingEvents .Select(x => new { Event = x, Data = DeserializeCorporateActionEventData(x.EventData) }) .Where(x => x.Data != null && info.id > 0 && x.Data.ExDividendInfoId == info.id && x.Data.PositionId == current.PositionId) .ToList(); // 寻找applied = false的(登记日记录的) var eventData = matchingEvents.FirstOrDefault(x => !x.Data.Applied) ?? matchingEvents.FirstOrDefault(); var previous = previousPositions?.FirstOrDefault(x => x != null && x.PositionId == current.PositionId); // 登记日 false 除权日 true var isEffective = info.EffectiveDate.HasValue && info.EffectiveDate.Value.Date <= settleDate.Date && effectiveInfos != null && effectiveInfos.Any(x => x.id == info.id); // 如果没有匹配到事件或今天不是除权日 但找到的事件的applied=true(异常事件/重收盘),则创建新事件。 if (eventData == null || (!isEffective && eventData.Data.Applied)) { // 创建新事件 var pending = BuildCorporateActionEventData( info, previous ?? current, isEffective ? current : null, applied: isEffective); // 生命周期事件的发生日固定为登记日,EffectiveDate 只表示 Q/P 基线切换日。 // 这样回退后重收盘仍能按原登记日排序和追溯,不会把同一事件拆成两条历史。 var eventDate = info.ExDividendDate?.Date ?? info.EffectiveDate?.Date ?? settleDate.Date; var created = AddSwapEvent( eventDate, td.id, (int)SwapEventTypeEnum.公司行为, JsonConvert.SerializeObject(pending), 0, false, BuildCorporateActionReason(pending)); if (created == null) { created = new swap_event(); } // 测试接缝和历史实现可能返回只带 id 的实体;统一补齐字段, // 确保同一收盘事务内的生效步骤能找到刚创建的事件。 created.EventType = (int)SwapEventTypeEnum.公司行为; created.SwapTradeId = td.id; created.ValueDate = eventDate; created.EventData = JsonConvert.SerializeObject(pending); created.EventReason = BuildCorporateActionReason(pending); existingEvents.Add(created); continue; } // 如果不是生效日或事件已生效,则跳过。 if (!isEffective || eventData.Data.Applied) { continue; } // 生效日只补齐同一事件的 Before/After 快照,不重新套系数:Before* 来自 // 调整前 EOD,After* 来自生效日当前 EOD,current 已由开盘基线处理完成。 eventData.Data.BeforeNotional = previous?.PosiNotionalValue ?? eventData.Data.BeforeNotional; eventData.Data.BeforePrice = previous?.PosiGrossPrice ?? eventData.Data.BeforePrice; eventData.Data.BeforeQuantity = previous?.PosiQuantity ?? eventData.Data.BeforeQuantity; eventData.Data.BeforePendingDividend = previous?.PosiDividendSum ?? eventData.Data.BeforePendingDividend; eventData.Data.AfterNotional = current.PosiNotionalValue; eventData.Data.AfterPrice = current.PosiGrossPrice; eventData.Data.AfterQuantity = current.PosiQuantity; eventData.Data.AfterPendingDividend = current.PosiDividendSum; eventData.Data.CashFlowChange = current.RealizedDividend - (previous?.RealizedDividend ?? current.RealizedDividend); eventData.Data.Applied = true; eventData.Event.EventData = JsonConvert.SerializeObject(eventData.Data); eventData.Event.EventReason = BuildCorporateActionReason(eventData.Data); UpdateCorporateActionEventRecord(eventData.Event); } } public static CorporateActionEventData BuildCorporateActionEventData( ex_dividend_info info, eod_swap_position previous, eod_swap_position current, bool applied) { return new CorporateActionEventData { ExDividendInfoId = info.id, PositionId = (current ?? previous).PositionId, UnderlyingCode = (current ?? previous).UnderlyingCode, ExDividendDate = info.ExDividendDate, EffectiveDate = info.EffectiveDate, GiveCashAmount = info.GiveCashAmount, GiveShareAmount = info.GiveShareAmount, Split = info.Split, RationedSharesAmount = info.RationedSharesAmount, RationedSharesPrice = info.RationedSharesPrice, BeforeNotional = previous?.PosiNotionalValue ?? 0m, BeforePrice = previous?.PosiGrossPrice ?? 0m, BeforeQuantity = previous?.PosiQuantity ?? 0m, AfterNotional = applied ? current?.PosiNotionalValue ?? 0m : 0m, AfterPrice = applied ? current?.PosiGrossPrice ?? 0m : 0m, AfterQuantity = applied ? current?.PosiQuantity ?? 0m : 0m, BeforePendingDividend = previous?.PosiDividendSum ?? 0m, AfterPendingDividend = applied ? current?.PosiDividendSum ?? 0m : 0m, CashFlowChange = applied ? (current?.RealizedDividend ?? 0m) - (previous?.RealizedDividend ?? 0m) : 0m, Applied = applied, }; } public static bool ShouldCreateCorporateActionEvent( IEnumerable events, ex_dividend_info info, long positionId) { if (info == null) { return false; } // 幂等键与收盘事件匹配保持一致,只认 ExDividendInfoId + PositionId。 // 无法反序列化或缺少 ExDividendInfoId 的存量事件均不参与匹配。 return !(events ?? Enumerable.Empty()).Any(x => { if (!SwapEventService.TryDeserializeCorporateActionEventData(x, out var data)) { return false; } return info.id > 0 && data.ExDividendInfoId == info.id && data.PositionId == positionId; }); } private static CorporateActionEventData DeserializeCorporateActionEventData(string eventData) { if (string.IsNullOrWhiteSpace(eventData)) { return null; } try { return JsonConvert.DeserializeObject(eventData); } catch (JsonException) { return null; } } private static string BuildCorporateActionReason(CorporateActionEventData data) { return SwapEventService.BuildCorporateActionEventReason(data); } /// 公司行为调整后的持仓 Q/P 结果,供 EOD、实时腿和盘中平仓共用。 private readonly struct CorporateActionValues { public CorporateActionValues(decimal quantity, decimal grossPrice, decimal netPrice, decimal? netFeePrice, decimal? netNoFeePrice) { Quantity = quantity; GrossPrice = grossPrice; NetPrice = netPrice; NetFeePrice = netFeePrice; NetNoFeePrice = netNoFeePrice; } public decimal Quantity { get; } public decimal GrossPrice { get; } public decimal NetPrice { get; } public decimal? NetFeePrice { get; } public decimal? NetNoFeePrice { get; } } /// /// 统一计算公司行为后的 Q/P。EOD、实时腿和盘中平仓只负责提供基线, /// 不再各自复制数量、毛价和净价的调整公式。 /// private static CorporateActionValues CalculateCorporateActionValues( decimal quantity, decimal grossPrice, decimal netPrice, decimal? netFeePrice, decimal? netNoFeePrice, ex_dividend_info dividendInfo, decimal closePrice, decimal dividendTaxRate, int grossPriceRound) { // 计算除权系数 - adjustCashDividendPrice = false (现金分红模式) var factors = DividendService.CalculateCorporateActionFactors( dividendInfo, closePrice, dividendTaxRate, adjustCashDividendPrice: false); if (factors.PriceRatio <= 0) { throw new InvalidOperationException( $"标的【{dividendInfo?.UnderlyingCode}】计算得到无效除权系数"); } var adjustedQuantity = Math.Round(quantity * factors.PriceRatio, 12, MidpointRounding.AwayFromZero); var adjustedGrossPrice = Math.Round(grossPrice / factors.PriceRatio, grossPriceRound, MidpointRounding.AwayFromZero); var adjustedNetPrice = Math.Round(netPrice / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); var adjustedNetFeePrice = netFeePrice.HasValue ? Math.Round(netFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero) : (decimal?)null; var adjustedNetNoFeePrice = netNoFeePrice.HasValue ? Math.Round(netNoFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero) : (decimal?)null; return new CorporateActionValues( adjustedQuantity, adjustedGrossPrice, adjustedNetPrice, adjustedNetFeePrice, adjustedNetNoFeePrice); } /// /// 将一条真实生效日公司行为应用到盘中实时 TRS Stock/Fund 浮动腿。 /// 盘中先复制严格早于 valueDate 的 EOD,再调用此方法;因此重复调用时每次都会 /// 从同一份除权前 EOD 重新恢复,不会把 1000/100 重复变成 4000/25。 /// 例:8 月 14 日 EOD 为 1000/100,8 月 17 日生效的 10 送 10 会得到 2000/50。 /// 现金模式调用公式时使用 adjustCashDividendPrice=false,现金权益只进入分红字段, /// 不改变 Stock/Fund 的期初价格。 /// public static bool ApplyCorporateActionToPosition( swap_position position, ex_dividend_info dividendInfo, decimal corporateActionClosePrice, decimal dividendTaxRate) { if (position == null || dividendInfo == null || position.PosiDirection <= 0 || !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType) || corporateActionClosePrice <= 0) { return false; } var adjusted = CalculateCorporateActionValues( position.PosiQuantity, position.PosiGrossPrice, position.PosiNetPrice, position.PosiNetFeePrice, position.PosiNetNoFeePrice, dividendInfo, corporateActionClosePrice, dividendTaxRate, ConsGlobal.SwapDeliveryPriceRound); position.PosiQuantity = adjusted.Quantity; position.PosiGrossPrice = adjusted.GrossPrice; position.PosiNetPrice = adjusted.NetPrice; position.PosiNetFeePrice = adjusted.NetFeePrice; position.PosiNetNoFeePrice = adjusted.NetNoFeePrice; position.PosiNotionalValue = Math.Round( position.PosiGrossPrice * position.PosiQuantity * position.ContractSize, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); return true; } /// /// 框架合约汇总 /// /// /// /// public void SwapEodCompose(DateTime settleDate, DateTime preSettleDate, IEnumerable ClientIds) { Log.Info("SwapEodCompose:" + "settleDate:" + settleDate + " preSettleDate:" + preSettleDate + " ClientIds:" + JsonHelper.Serialize(ClientIds)); var tradePredicate = PredicateBuilder.Create(n => n.ValidState != ConsGlobal.InValid && n.TradeType == "收益互换" && n.TradeDate <= settleDate && n.ExerciseDate >= settleDate && (n.TradeStatus == ConsTrade.确认成交 || n.UnWindDate >= settleDate) ); if (ClientIds != null && ClientIds.Any()) { tradePredicate = tradePredicate.And(x => ClientIds.Contains(x.ClientId)); } var tradeQueryList = DbContext.trade.Where(tradePredicate).ToList(); foreach (var td in tradeQueryList) { Log.Info("【框架合约汇总】处理合约ID为:[" + td.id + "]的数据,收盘时间为:[" + settleDate + "]。开始"); SaveEodSwap(td, settleDate, preSettleDate); Log.Info("【框架合约汇总】处理合约ID为:[" + td.id + "]的数据,收盘时间为:[" + settleDate + "]。结束"); } DbContext.SaveChanges(); } /// /// 处理利息腿归档 /// /// 利息腿持仓信息 /// 上一日终归档持仓信息 /// 当日归档持仓信息 /// 当前结算日期 /// 互换交易主信息 /// 当日平仓/互换信息 /// 自动互换集合 /// 多空组合平仓利息腿信息 /// 上一日终框架合约 protected virtual void DealInterests(List interestList, List eodPositions, List todyEodPositions, DateTime settleDate, trade td, List flowEvents, List autoInterests, eod_swap lastEodSwap, decimal posiTotalNotional, decimal closeNational, decimal grossPrice, decimal orginPv) { Log.Info("===================处理利息腿归档===================="); // 添加详细的参数验证日志 Log.Info($"[DealInterests] 参数验证 - settleDate: {settleDate:yyyy-MM-dd}, td.id: {td?.id}, td.TradeNumber: {td?.TradeNumber}"); Log.Info($"[DealInterests] 参数验证 - interestList.Count: {interestList?.Count ?? 0}, eodPositions.Count: {eodPositions?.Count ?? 0}, todyEodPositions.Count: {todyEodPositions?.Count ?? 0}"); Log.Info($"[DealInterests] 参数验证 - flowEvents.Count: {flowEvents?.Count ?? 0}, autoInterests.Count: {autoInterests?.Count ?? 0}"); Log.Info($"[DealInterests] 参数验证 - posiTotalNotional: {posiTotalNotional}, closeNational: {closeNational}, grossPrice: {grossPrice}, orginPv: {orginPv}"); // 验证关键参数 if (td == null) { Log.Info("[DealInterests] 参数验证: td (trade) 为 null"); throw new ArgumentNullException(nameof(td), "交易对象不能为null"); } if (interestList == null) { Log.Info($"[DealInterests] 参数验证: interestList 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(interestList), "利息腿列表不能为null"); } if (flowEvents == null) { Log.Info($"[DealInterests] 参数验证: flowEvents 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(flowEvents), "流水事件列表不能为null"); } if (autoInterests == null) { Log.Info($"[DealInterests] 参数验证: autoInterests 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(autoInterests), "自动互换列表不能为null"); } var hasClose = flowEvents.Any(x => x.EventType == (int)SwapEventTypeEnum.平仓); var hasSwap = flowEvents.Any(x => x.EventType == (int)SwapEventTypeEnum.互换); foreach (var interest in interestList) { Log.Info($"InterestMode is {interest.InterestMode},HappenDate is {interest.HappenDate},settleDate is {settleDate}"); if (interest.InterestMode == (int)InterestModeEnum.追加预付金 && interest.HappenDate > settleDate) { continue; } var eodPosition = eodPositions.FirstOrDefault(x => x.PositionId == interest.id);//上一日日终利息信息 可能不存在 var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == interest.id);//当前结算日日终利息信息 var observationInterval = InterestEodScenarioDispatch.FindObservationInterval(interest, settleDate);//自动互换观察日信息 List dealInterests = new List(); dealInterests.AddRange(flowEvents); var dealInterest = dealInterests.FirstOrDefault(n => n.PositionId == interest.id);//当日是否做过互换或平仓 var swapEvents = flowEvents.Where(x => (x.EventType == (int)SwapEventTypeEnum.互换 || x.EventType == (int)SwapEventTypeEnum.平仓) && x.PositionId == interest.id).ToList(); //如果当日有互换/当日有平仓 不再重新生成或更新 Log.Info($"observationInterval is {observationInterval},hasSwap is {hasSwap},hasClose is {hasClose}"); // 分派优先级与粒度说明见 ResolveInterestScenario;8 组合表驱动覆盖见 InterestEodScenarioDispatchTest。 // 仅观察日两个分支把返回值收进 autoInterests(→资金记录)——分派错序=静默少结。 if (observationInterval != null && !hasSwap) { if (!hasClose)//当日无平仓 { var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv); if (_autoInterests.Count > 0) { autoInterests.AddRange(_autoInterests); } } else { var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: true, grossPrice, orginPv); if (_autoInterests.Count > 0) { autoInterests.AddRange(_autoInterests); } } } else if (hasSwap)//当日有互换,根据互换事件重新生成 { SaveEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, swapEvents); } else if (hasClose) { SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: false, grossPrice, orginPv); } else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息 { SaveEodInterestPositionCopy(eodPosition, tdEodPosition, settleDate, td, interest, lastEodSwap, true, posiTotalNotional, grossPrice, orginPv); } } } /// /// 处理浮动腿归档 /// /// /// /// /// /// /// /// /// protected List DealFloatPositions(List posiList, List realPosiList, List eodPositions, List todyEodPositions, DateTime settleDate, trade td, DateTime preSettleDate, List flowEvents, IReadOnlyCollection corporateActionBeforePositions = null) { string settleDateStr = settleDate.ToString("yyyy-MM-dd"); string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd"); Log.Info($"================开始处理{settleDateStr}浮动腿归档=================="); List list = new List(); Log.Info($"浮动腿归档各项参数如下:\n " + $"settleDate为:{settleDateStr} \n" + $"preSettleDate为:{preSettleDateStr} \n " + $"td为:{td.id} \n " + $"posiList为:{JsonHelper.Serialize(posiList)} \n " + $"realPosiList为:{JsonHelper.Serialize(realPosiList)} \n " + $"eodPositions为:{JsonHelper.Serialize(eodPositions)} \n " + $"todyEodPositions为:{JsonHelper.Serialize(todyEodPositions)} \n " + $"flowEvents为:{JsonHelper.Serialize(flowEvents)} \n "); foreach (var posi in posiList) { Log.Info($"posi为:{JsonHelper.Serialize(posi, false)}"); var eodPosition = eodPositions.Where(x => x.PositionId == posi.id).FirstOrDefault();//上一日日终持仓信息 var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息 var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息 var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id); var corporateActionBeforeQuantity = corporateActionBeforePositions? .FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity; eod_swap_position eodPosi = new eod_swap_position(); if (eodPosition == null) { eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity); } else if (unwindEvents.Count() == 0) { eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity); } else { eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity); } Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}"); list.Add(eodPosi); } Log.Info($"================{settleDateStr}浮动腿归档结束=================="); return list; } /// /// 处理自动互换数据 /// /// /// /// /// /// 自动互换观察日信息,用于获取结算日期 /// 当日浮动端EOD持仓 /// 交易扩展信息 /// /// 自动互换(仅利息/预付金,不含分红) /// private void DealAutoInterests(List autoInterests, trade td, DateTime settleDate, DateTime? preDealDate, decimal StockEqvNotional, IntervalModel interval) { if (autoInterests.Count == 0) return; UnwindData unwindData = new UnwindData(); unwindData.SwapTradeId = td.id; unwindData.ValueDate = settleDate; unwindData.StartDate = preDealDate ?? td.StartDate.Value; unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); unwindData.PosiNotionalValue = StockEqvNotional; unwindData.PayDate = settleDate; autoInterests.ForEach(x => x.PayDate = settleDate); var premiumModes = MarginModes.ForLinq; var premiumInterests = autoInterests.Where(x => premiumModes.Contains(x.InterestMode)).ToList(); var interestLegs = autoInterests.Where(x => !premiumModes.Contains(x.InterestMode)).ToList(); decimal premiumTotal = 0; premiumInterests.ForEach(x => { var ratio = -DirectionRatio.ReceivePay(x.InterestDirection); premiumTotal += x.InterestClosePnL * ratio; }); unwindData.SwapMarginRebatePnl = premiumTotal; decimal interestTotal = 0; interestLegs.ForEach(x => { var ratio = DirectionRatio.ReceivePay(x.InterestDirection); interestTotal += x.InterestClosePnL * ratio; }); unwindData.SwapCloseAmount = interestTotal ; unwindData.SwapDividendPnl = 0; unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount + unwindData.SwapMarginRebatePnl; SaveAutoSwapDeal(td, autoInterests, unwindData, interval); } /// /// 分红独立处理:当天有债券需要分红时,生成独立的分红自动互换事件 /// private void DealDividends(List curEodPositions, trade td, DateTime settleDate, trade_extend tradeExtend) { if (curEodPositions == null) return; var hasDividend = curEodPositions.Any(x => x.PosiDividendSum != 0); if (!hasDividend) return; // 公司行为现金分红与债券付息共用既有待实现/支付链路:公司行为步骤只把金额 // 累加到 PosiDividendSum,这里仍按交易约定的 DividendPayDate 生成支付流水。 // 公司行为不会调整 Stock/Fund 的期初价格;因此不能再把现金分红从 PosiMtmPnL // 中剥离或当作已实现收益提前写入。 var dividendPayDateOffset = tradeExtend?.ExtendObj?.DividendPayDate ?? 1; if (dividendPayDateOffset <= 0) return; var payDays = dividendPayDateOffset - 1; var dividendPayDate = QdpCalendarHelper.GetNonHoliday(settleDate.AddDays(payDays)); List dividendEvents = new List(); decimal dividendTotal = 0; foreach (var eodPosi in curEodPositions.Where(x => x.PosiDividendSum != 0)) { var dividendEvent = new swap_flow_event { SwapTradeId = td.id, SwapTradeNo = td.TradeNumber, EventType = (int)SwapEventTypeEnum.自动互换, EventReason = "系统操作-分红", EventDate = settleDate, UnwindDate = settleDate, PayDate = dividendPayDate, PositionId = eodPosi.PositionId, UnderlyingCode = eodPosi.UnderlyingCode, UnderlyingInstrumentType = eodPosi.UnderlyingInstrumentType, PayDirection = eodPosi.PosiDirection, PositionType = eodPosi.PositionType, PositionQty = eodPosi.PosiQuantity, Quantity = 0, ContractSize = eodPosi.ContractSize, TradingAmountAvg = eodPosi.PosiGrossPrice, TradingAmountNetAvg = eodPosi.PosiNetNoFeePrice, PosiGrossPrice = eodPosi.PosiGrossPrice, PosiNetPrice = eodPosi.PosiNetPrice, MarkClosePnl = 0,//当日盯市不要计算分红 DividendIn = eodPosi.PosiDividendSum, CloseFee = 0, TradingFee = 0, TradingFeePending = 0, ClientId = td.ClientId, DataState = (int)SwapFlowDateStateEnum.完成, }; dividendEvents.Add(dividendEvent); dividendTotal += eodPosi.PosiDividendSum; eodPosi.TdCloseDividend += eodPosi.PosiDividendSum; //当日也要展示 eodPosi.TdPosiDividend = 0; eodPosi.RealizedDividend += eodPosi.PosiDividendSum; //互换持仓价值要去掉已实现的 eodPosi.SwapPositionValue -= eodPosi.PosiDividendSum; //已实现盈亏要加上已实现的 eodPosi.RealizedPnl += eodPosi.PosiDividendSum; eodPosi.PosiDividendSum = 0; eodPosi.PosiProfitSum -= eodPosi.TdCloseDividend; } UnwindData unwindData = new UnwindData(); unwindData.SwapTradeId = td.id; unwindData.ValueDate = settleDate; unwindData.StartDate = td.StartDate.Value; unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); unwindData.PosiNotionalValue = curEodPositions.Sum(x => x.PosiNotionalValue); unwindData.PayDate = dividendPayDate; unwindData.SwapDividendPnl = dividendTotal; unwindData.SwapCloseAmount = 0; unwindData.SwapMarginRebatePnl = 0; unwindData.SwapRealizedPnL = dividendTotal; SaveAutoSwapDeal(td,null , unwindData,null, dividendEvents:dividendEvents); } /// /// 保存自动互换数据信息 /// /// /// /// 自动互换观察日信息,用于获取结算日期 private long SaveAutoSwapDeal(trade td, List flowEvents, UnwindData unwindData, IntervalModel interval, List dividendEvents = null) { //td.UnWindDate = unwindData.ValueDate; //优先使用 interval.SettlementDate 作为资金记录发生日期,如果没有则使用 ValueDate var cashHappenDate = interval?.SettlementDate ?? unwindData.ValueDate; int clientCashId = 0; var clientCashIds = new List(); // 利息腿:插入资金记录(使用系统操作_互换) if (unwindData.SwapCloseAmount != 0) { clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, cashHappenDate); clientCashIds.Add(clientCashId); } // 预付金腿:单独插入一条资金记录(系统操作_预付金返息) // R4 §2.4:返息按腿 FundTag 分流——授信部分不进资金(授信不产生流水), // 只对现金部分(含无标签存量)产生返息资金记录 if (unwindData.SwapMarginRebatePnl != 0) { var cashRebate = GetAutoSwapCashRebate(td, flowEvents, unwindData.SwapMarginRebatePnl); if (cashRebate != 0) { clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-cashRebate), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); clientCashIds.Add(clientCashId); } } unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL;//需要算上预付金利息 和 分红; 只是不算预付金返还 // 分红:使用派息支付日偏移记录资金记录 if (unwindData.SwapDividendPnl != 0) { var dividendPayDate = (dividendEvents != null && dividendEvents.Count > 0) ? dividendEvents.First().PayDate.Value : unwindData.ValueDate; clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapDividendPnl), ClientCashInCashOut.系统操作_互换, dividendPayDate); clientCashIds.Add(clientCashId); } unwindData.ClientCashIds = clientCashIds; string data = JsonConvert.SerializeObject(unwindData); // 走虚方法 AddSwapEvent(与 ComposePage:800 一致),让测试可 override 捕获事件; // 默认实现仍是 new SwapEventService(this).AddSwapEventDate,生产行为不变。 var swapEvent = AddSwapEvent(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件 if (flowEvents!=null) { flowEvents.ForEach(x => { x.EventId = swapEvent.id; PersistFlowEvent(x); }); UpdateInitalPostion(flowEvents, td.id); } // 保存分红事件 if (dividendEvents != null) { dividendEvents.ForEach(x => { x.EventId = swapEvent.id; PersistFlowEvent(x); }); UpdateInitalPostion(dividendEvents, td.id); } return swapEvent.id; } /// /// R4 §2.4:自动互换预付金返息按腿 FundTag 分流,返回现金部分返息。 /// 授信腿返息不进资金(授信不产生流水);无标签存量/无预付金腿事件时全额现金。 /// virtual 供纯内存测试 stub 为全额现金(见 TestableSwapEodPositionService)。 /// protected virtual decimal GetAutoSwapCashRebate(trade td, List flowEvents, decimal totalRebate) { if (flowEvents == null || flowEvents.Count == 0) { return totalRebate; } var premiumModes = MarginModes.ForLinq; var legs = flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && premiumModes.Contains(x.InterestMode)).ToList(); if (legs.Count == 0) { return totalRebate; } //GetSettlements 与 legs 同谓词同序过滤,settlements[i] 与 legs[i] 一一对应 var settlements = new SwapFundTagService(this).GetSettlements(legs); for (var i = 0; i < legs.Count; i++) { //对齐 DealAutoInterests 返息符号口径:InterestClosePnL × −ReceivePay(方向); //自动互换只结返息,保证金本金不在此返还(不写释放记录) settlements[i].MarginAmount = 0m; settlements[i].RebateAmount = legs[i].InterestClosePnL * -DirectionRatio.ReceivePay(legs[i].InterestDirection); } var split = FundTagCalc.SplitUnwindByTag(settlements); return Math.Round(totalRebate - split.CreditRebate, 2, MidpointRounding.AwayFromZero); } /// /// 互换更新实时持仓信息 /// /// private void UpdateInitalPostion(List flowEvents, int swapTradeId) { var positions = DbContext.swap_position.Where(x => !x.IsInitial && x.SwapTradeId == swapTradeId && !x.Invalid); foreach (var position in positions) { var interest = flowEvents.FirstOrDefault(x => x.PositionId == position.id); if (interest != null) { position.InterestAmount += interest.InterestAmount; UpdateDbOption(position); } } } /// /// 分页合成互换流水开平仓事件,暂时只按加权平均处理 /// /// 互换交易id /// 清算日期 protected void ComposePage(int swapTradeId, List flowEvents, DateTime tradeDate, bool needTrans = true) { // 同一标的 事件编码一致 var eventQuery = flowEvents.OrderBy(o => o.PositionId).ToList(); if (eventQuery.Count == 0) { return; } SwapTradeService swapTradeService = new SwapTradeService(this); var trans = needTrans ? DbContext.Database.BeginTransaction() : null; try { UnwindData unwindData = new UnwindData(); unwindData.SwapTradeId = swapTradeId; var swapEvent = AddSwapEvent(tradeDate, swapTradeId, (int)SwapEventTypeEnum.合成持仓, string.Empty, 0, true, "系统操作-自动合成持仓"); var td = FindTrade(swapTradeId); var preSettleDate = GetPreValueDate(tradeDate);//上一交易日期 List eventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; List removeEventTyps = new List() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; ClearSwapPositionsForCompose(td, tradeDate, removeEventTyps); td.trade_extend = FindTradeExtend(swapTradeId); var allEodPositions = FindEodSwapPositions(swapTradeId, preSettleDate); var eodPositions = allEodPositions.Where(x => x.ValueDate == preSettleDate).ToList();//上一日终持仓信息 var positions = FindSwapPositions(swapTradeId); var oriPositions = positions.Where(x => x.IsInitial).ToList(); var realPositions = positions.Where(x => !x.IsInitial).ToList(); var fpositions = positions.Where(x => x.PosiDirection > 0).ToList(); decimal tdCloseQty = 0; decimal totalPosiNotionalValue = 0;//总剩余名义本金 decimal tdCloseNotionalValue = 0;//当日平仓名义本金 var preDealDate = GetPreDealDate(swapTradeId, tradeDate, eventTyps);//上一次平仓/互换/自动互换事件日期 var lastEodSwap = FindEodSwap(swapTradeId, preSettleDate); decimal stockEqvNotional = lastEodSwap == null ? Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0) : lastEodSwap.NotionalValue;//上一日名义本金 unwindData.NotionalValue = stockEqvNotional; List longshortCloseInterests = new List(); decimal tradePrice = 0;//开仓费 decimal allPosiNotionalValue = 0; decimal longNotionalValue = realPositions.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);//剩余多头名义本金规模 decimal shortNotionalValue = realPositions.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue);//剩余空头名义本金规模 foreach (var eventGroup in eventQuery.GroupBy(g => g.PositionId))//持仓标的腿合成持仓 { var eventList = eventGroup.ToList(); var eventFlow = eventGroup.Last(); var fposition = fpositions.FirstOrDefault(n => n.PositionId == eventGroup.Key); var position = fpositions.FirstOrDefault(n => n.id == eventGroup.Key); var eodPayPosition = allEodPositions.Where(x => x.PositionId == eventFlow.PositionId).OrderByDescending(o => o.ValueDate).FirstOrDefault();//浮动腿 日终持仓信息 decimal netPrice = fposition == null ? 0 : fposition.PosiNetPrice;//持仓均价 decimal grossPrice = fposition == null ? 0 : fposition.PosiGrossPrice;//持仓均价-不含费 decimal netFeePrice = fposition == null ? 0 : fposition.PosiNetFeePrice ?? 0;//持仓净价-含费 decimal netNoFeePrice = fposition == null ? 0 : fposition.PosiNetNoFeePrice ?? 0;//持仓净价-不含费 decimal tradingFee = fposition == null ? 0 : fposition.PosiTradingFeePending;//持仓交易费用 decimal payQty = fposition == null ? 0 : fposition.PosiQuantity;//持仓数量 decimal posiNotionalValue = fposition == null ? 0 : fposition.PosiNotionalValue;//剩余名义本金 decimal dividendIn = 0;//当日浮动端分红 decimal tdDividendIn = 0;//当日浮动端平仓盈亏分红 decimal openQty = fposition == null ? 0 : fposition.PosiQuantity;//开仓数量 decimal openAmount = fposition == null ? 0 : openQty * grossPrice;//开仓累计成交金额不含费 decimal openAmountFee = fposition == null ? 0 : openQty * netPrice;//开仓累计成交金额含费 decimal openAmountNetFee = fposition == null ? 0 : openQty * netFeePrice;//开仓累计成交净价金额含费 decimal openAmountNet = fposition == null ? 0 : openQty * netNoFeePrice;//开仓累计成交净价金额不含费 decimal closeQty = 0;//当日平仓数量 decimal closeFee = 0;//当日平仓费用 decimal closeMtmPnl = 0;//当日平仓盈亏 var posiType = fposition == null ? 0 : fposition.PositionType; eventList.ForEach(x => { x.EventId = swapEvent.id; decimal ratio = x.EventType == 1 ? 1 : -1;//开仓为加法,平仓为减法 tradingFee = tradingFee + x.TradingFeePending;//开仓累计 if (x.EventType == 1) { openAmountFee = openAmountFee + x.TradingAmountFeeAvg * x.Quantity; openAmount = openAmount + x.TradingAmountAvg * x.Quantity; openAmountNetFee = openAmountNetFee + (x.TradingAmountNetFeeAvg * x.Quantity) ?? 0; openAmountNet = openAmountNet + (x.TradingAmountNetAvg * x.Quantity) ?? 0; openQty = openQty + x.Quantity; if (posiType != x.PositionType) { payQty = x.Quantity; posiType = x.PositionType; } else { payQty = payQty + x.Quantity; } } else { decimal amount = x.MarkClosePnl + x.CloseFee + x.DividendIn; //记录资金记录 if (amount != 0) { int clientCashId = AddClientCash(td, Convert.ToDouble(x.MarkClosePnl), ClientCashInCashOut.系统操作_平仓费, x.UnwindDate.Value); x.ClientCashId = clientCashId; } payQty = payQty - x.Quantity; closeQty = closeQty + x.Quantity; closeFee = closeFee + x.CloseFee; tdDividendIn = tdDividendIn + x.DividendIn; tdCloseNotionalValue = tdCloseNotionalValue + x.TradingAmount; } closeMtmPnl = closeMtmPnl + x.MarkClosePnl; x.PositionQty = payQty; dividendIn = dividendIn + x.DividendIn; x.DataState = (int)SwapFlowDateStateEnum.完成; }); tdCloseQty += closeQty; if (eventFlow.EventType == 1)//最后一条是开仓 { payQty = eventFlow.Quantity; } bool newOpen = openQty != 0 && closeQty == 0; if (openQty != 0 && closeQty == 0)//只有开仓,价格加权平均 { netPrice = openAmountFee / openQty;//持仓均价=((上一日持仓含费均价*上一日持仓数量)+(开仓成交均价*开仓数量))/所有开仓数量 grossPrice = openAmount / openQty;//持仓均价-不含费=((上一日持仓不含费均价*上一日持仓数量)+(开仓成交均价-不含费*开仓数量))/所有开仓数量 netFeePrice = openAmountNetFee / openQty; netNoFeePrice = openAmountNet / openQty; } else if (posiType != fposition?.PositionType)//平仓完新开仓 { netPrice = eventFlow.TradingAmountFeeAvg; grossPrice = eventFlow.TradingAmountAvg; netFeePrice = eventFlow.TradingAmountNetFeeAvg ?? 0; netNoFeePrice = eventFlow.TradingAmountNetAvg ?? 0; } if (eodPayPosition == null)//无日终持仓 { eodPayPosition = new eod_swap_position(); eodPayPosition.PosiStartDate = eventFlow.PayDate.Value; eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; eodPayPosition.ContractSize = eventFlow.ContractSize; } posiNotionalValue = eventFlow.ContractSize * netPrice * Math.Abs(payQty); totalPosiNotionalValue = totalPosiNotionalValue + posiNotionalValue; allPosiNotionalValue += posiNotionalValue; tradePrice += SaveEodPosition(eodPayPosition, td, eventFlow, netPrice, grossPrice, netFeePrice, netNoFeePrice, payQty, tradingFee, posiNotionalValue, dividendIn, tdDividendIn, closeQty, closeFee, closeMtmPnl, posiType, fposition == null); } if (tdCloseQty != 0) { td.HasPartialUnWind = 1; } // td.StockEqvNotional += Convert.ToDouble(totalPosiNotionalValue); td.TradePrice += Convert.ToDouble(tradePrice); unwindData.PosiNotionalValue = allPosiNotionalValue; unwindData.CloseNotionalValue = tdCloseNotionalValue; swapEvent.EventData = JsonHelper.Serialize(unwindData); SaveAllChanges(); SaveEodSwapRecord(td, tradeDate, preSettleDate); SaveAllChanges(); trans?.Commit(); } catch (Exception ex) { trans?.Rollback(); throw new Exception(ex.Message, ex); } finally { trans?.Dispose(); } } /// /// 利息腿字段拷贝(SaveEodInterestPosition / SaveAutoEodInterestPosition / SaveAutoEodWithCloseInterestPosition 共用)。 /// FloatRate 来源随场景不同(手工互换=当日流水;自动互换/平仓=计息结果),由调用方算好传入,勿在本方法内统一。 /// 场景差异字段(PosiStatus / InterestFeePending / TdInterestPrincipal / TdInterestRate)留在各调用点。 /// private static void CopyInterestLegFields(eod_swap_position newEodPayPosition, swap_position position, decimal floatRate) { newEodPayPosition.InterestDirection = position.InterestDirection; newEodPayPosition.InterestMode = position.InterestMode; newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; newEodPayPosition.InterestRateDefault = position.InterestRateDefault; newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval; newEodPayPosition.IsAnnualized = position.IsAnnualized; newEodPayPosition.HappenDate = position.HappenDate; newEodPayPosition.Currency = position.Currency; newEodPayPosition.InterestType = position.InterestType; newEodPayPosition.interest_rest_days = position.interest_rest_days; newEodPayPosition.interest_rule = position.interest_rule; newEodPayPosition.FloatRate = floatRate; newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; } /// /// 利息腿日终滚存收尾(四个 Save* 共用):RollRealized 滚累计已实现 → SetFixedLegRealizedPnl → 汇率 → TdCurrency。 /// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。 /// interestDirection 是 RateType 的方向来源——三个方法取 position.InterestDirection, /// SaveEodInterestPositionCopy 取 eodPayPosition.InterestDirection(现状差异,勿统一)。 /// PersistEodSwapPosition 与各自日志留在调用点(持久化边界 + 日志顺序各不相同)。 /// private void FinalizeInterestEodRoll(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, int ratio, trade td, DateTime valueDate, int interestDirection) { var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); newEodPayPosition.RealizedInterest = rolled.Interest; newEodPayPosition.RealizedInterestFee = rolled.Fee; SetFixedLegRealizedPnl(newEodPayPosition); var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, DirectionRatio.RateType(interestDirection)); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); } /// /// 产生互换用 /// /// 上一日日终归档 /// 当日归档 /// 持仓腿 /// 主体交易 /// 计息开始日 /// 计息结束日 /// 平仓金额 protected void SaveEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, List flowEvents) { Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); if (eodPayPosition == null) { eodPayPosition = new eod_swap_position(); eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; eodPayPosition.PosiStartDate = position.PosiStartDate; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; } var tradeExtend = td.trade_extend.ExtendObj; var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); if (newEodPayPosition == null) { newEodPayPosition = new eod_swap_position(); newEodPayPosition.ClientId = td.ClientId; newEodPayPosition.SwapTradeId = td.id; newEodPayPosition.PosiStartDate = position.PosiStartDate; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; } newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; newEodPayPosition.ClientId = td.ClientId; newEodPayPosition.SwapTradeId = td.id; UpdateDbOption(newEodPayPosition); newEodPayPosition.PosiStatus = 0; newEodPayPosition.Invalid = false; //持仓内容-利息腿(FloatRate 取当日互换/平仓流水) CopyInterestLegFields(newEodPayPosition, position, flowEvents.FirstOrDefault()?.FloatRate ?? 0); newEodPayPosition.InterestFeePending = 0; //利息端估值用信息 newEodPayPosition.TdInterestPrincipal = flowEvents.FirstOrDefault()?.InterestPrincipal ?? 0; newEodPayPosition.TdInterestRate = flowEvents.FirstOrDefault()?.InterestRate ?? 0; //当日已实现 newEodPayPosition.TdInterestFee = flowEvents.Sum(x => x.InterestFee); newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; //持仓内容-利息腿-损益统计(本方视角) var intersetAcmount = InterestIncomeCalc.DailyAccrual( newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息 var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome; var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee; var isMaturityFinalSettlement = valueDate.Date >= td.ExerciseDate.Value.Date && flowEvents.Any() && EodPnlCalculator.RoundMoney(interestIncomeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest) && EodPnlCalculator.RoundMoney(interestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee); if (isMaturityFinalSettlement) { // 到期日不是清零的充分条件。只有当前事件按金额两位覆盖本腿全部可结金额, // 才能确认是最终结算;否则保留尾差,避免手工互换少结时永久丢失待实现。 newEodPayPosition.InterestIncomeSum = 0; newEodPayPosition.InterestFeeSum = 0; } else { newEodPayPosition.InterestIncomeSum = EodPnlCalculator.RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest); newEodPayPosition.InterestFeeSum = EodPnlCalculator.RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); } newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现(滚存收尾见 FinalizeInterestEodRoll) FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); PersistEodSwapPosition(newEodPayPosition); } /// /// EOD 追保腿化·方案A:SettleAdditionalMargin 在当日 SwapPositionCompose/SwapEodCompose 之后生成 mode6 追保腿 /// (增量依赖当日 trade_span,无法前移),当日快照已落表,显式补写当日 eod_swap_position 行, /// 使当日报表明细(风险页 PostionMarginGain 等按快照腿汇总的列)不漏计。 /// 字段填充参照 SaveEodInterestPosition 新腿形态(无当日流水、无上日归档:利息/损益字段为 0,TdCurrency 取当日汇率)。 /// 幂等:先清(PositionId+ValueDate)后建;下一结算日 SwapPositionCompose 先清(ClearSwapPositions ValueDate>=当日) /// 再从实时腿重建,补写行不会跨日残留。 /// public void SaveEodAdditionalMarginPosition(trade td, swap_position leg, DateTime valueDate) { var existing = DbContext.eod_swap_position .Where(x => x.SwapTradeId == td.id && x.PositionId == leg.id && x.ValueDate == valueDate) .ToList(); DbContext.eod_swap_position.RemoveRange(existing); var row = new eod_swap_position { ClientId = td.ClientId, SwapTradeId = td.id, PosiStartDate = leg.PosiStartDate, PosiMatuirityDate = td.ExerciseDate, ValueDate = valueDate, PositionId = leg.id, PosiStatus = 0, Invalid = false }; UpdateDbOption(row); //持仓内容-利息腿(无当日流水,FloatRate 取 0) CopyInterestLegFields(row, leg, 0); row.InterestFeePending = 0; var ratio = DirectionRatio.InterestLegPnl(leg.InterestDirection, leg.InterestMode); row.SwapPositionValue = PositionValueCalc.Calc(row.InterestProfitSum, row.PosiProfitSum, ratio); row.RealizedPnl = row.RealizedInterest + row.RealizedInterestFee; row.TdCurrency = Convert.ToDecimal(GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, DirectionRatio.RateType(leg.InterestDirection))); PersistEodSwapPosition(row); SaveAllChanges(); } /// /// 自动互换用,当日无互换,当日无平仓 /// /// 上一日日终持仓 /// 当前收盘日日终持仓 /// 利息腿信息 /// 框架合约 /// 当前收盘日 /// 当前观察日 /// 上一平仓/互换日期 /// 当日平仓金额 /// 上一日终框架合约估值 protected List SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiTotalNotional, decimal grossPrice, decimal orginPv) { Log.Info($"[SaveAutoEodInterestPosition] 开始执行 - valueDate: {valueDate:yyyy-MM-dd}, td.id: {td?.id}, position.id: {position?.id}"); // 详细的参数验证 if (td == null) { Log.Info("[SaveAutoEodInterestPosition] 参数验证: td (trade) 为 null"); throw new ArgumentNullException(nameof(td), "交易对象不能为null"); } if (position == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: position 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(position), "持仓对象不能为null"); } if (interval == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: interval 为 null, td.id: {td.id}, position.id: {position.id}"); throw new ArgumentNullException(nameof(interval), "观察日信息不能为null"); } if (td.trade_extend == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend 为 null, td.id: {td.id}, td.TradeNumber: {td.TradeNumber}"); throw new ArgumentNullException("td.trade_extend", "交易扩展信息不能为null"); } Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); // 验证 ExtendObj if (td.trade_extend.ExtendObj == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend.ExtendObj 为 null, td.id: {td.id}"); throw new ArgumentNullException("td.trade_extend.ExtendObj", "交易扩展对象不能为null"); } var tradeExtend = td.trade_extend.ExtendObj; decimal posiNotionalValue = posiTotalNotional; decimal closePercent = 1; var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); if (eodPayPosition == null) { eodPayPosition = new eod_swap_position(); eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; eodPayPosition.PosiStartDate = td.StartDate.Value; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; } if (newEodPayPosition == null) { newEodPayPosition = eodPayPosition.Clone(); newEodPayPosition.id = 0; } List positions = new List(); positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); // orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值, // 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。 var interestModes = MarginModes.FixedAmountAndMargin; if (interestModes.Contains(position.InterestMode)) { orginPv = eodPayPosition.InterestPrincipalFix; } var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, orginPv, true); decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount); decimal tdInterestAmount = interests.Sum(x => x.TdInterestAmount); // 自动互换的流水和客户资金都由 InterestClosePnL 汇总。先把实际结算收敛到两位, // 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。 interests.ForEach(x => { x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount); x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL); }); decimal settledInterestAmount = interests.Sum(x => x.InterestAmount); newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); newEodPayPosition.Invalid = false; //持仓内容-利息腿(FloatRate 取计息结果) CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0); newEodPayPosition.InterestFeePending = 0; //利息端估值用信息 newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiNotionalValue; newEodPayPosition.TdInterestRate = interval.Rate; //当日已实现 //newEodPayPosition.TdInterestFee = 0; newEodPayPosition.TdCloseInterest = settledInterestAmount; // newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; //持仓内容-利息腿-损益统计(本方视角) newEodPayPosition.TdInterestIncome = tdInterestAmount; Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee},TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); var isMaturityFinalAutoSettlement = valueDate.Date >= td.ExerciseDate.Value.Date; // 到期自动互换是最后一次自动结算:两位实际金额已落流水/资金,待实现不再滚入下一日。 newEodPayPosition.InterestIncomeSum = isMaturityFinalAutoSettlement ? 0 : EodPnlCalculator.RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount); newEodPayPosition.InterestFeeSum = isMaturityFinalAutoSettlement ? 0 : EodPnlCalculator.RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee); newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现(滚存收尾见 FinalizeInterestEodRoll) FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); PersistEodSwapPosition(newEodPayPosition); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); return interests; } /// /// 将平仓/自动互换的盘中利息结果写成当日日终利息腿。 /// 字段完整口径和逐日示例见《收益互换日终收盘总流程与当前代码审查》7.2、16.7、16.13 节。 /// /// /// 关键状态链:上日待实现 + 当日新增 - 当日结息 = 当日待实现; /// 上日累计已实现 + 当日结息(按收付方向)= 当日累计已实现。 /// /// 上一日日终持仓 /// 当前收盘日日终持仓 不可能为空 /// 利息腿信息 /// 框架合约 /// 当前收盘日 /// 当前观察日 /// 上一平仓/互换日期 /// 当日平仓金额 /// 上一日终框架合约估值 /// 平仓主信息 protected List SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiTotalNotional, List flowEvents, decimal closeNational, bool autoSwap, decimal grossPrice, decimal orginPv) { Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); var tradeExtend = td.trade_extend.ExtendObj; // oriPosiNotionalValue 是平仓前规模,posiNotionalValue 是收盘后剩余规模,closeNational 是本次关闭规模。 // 例如 30% 平仓:303139117.80 = 212197382.46 + 90941735.34。 // 注意:此处的 posiNotionalValue 与盘中 GetUnwindInterests 传给 GetInterests 的语义不同: // 盘中传平仓前的当前剩余本金,EOD 此处传平仓后的剩余本金;后面又以 closePercent=1 // 调用共享计息器。因此策略的 "posiNotional × closePercent" 在本例会得到 212197382.46, // 而本次实际应结的平仓本金是 closeNational=90941735.34。该语义错位由 // SwapDealService.GetInterests 的模式2无条件修正、模式9全平零值兜底分流处理,不能删除。 decimal oriPosiNotionalValue = posiTotalNotional + closeNational; decimal posiNotionalValue = posiTotalNotional; // ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。 var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); // 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。 // 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum // 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。 var hasPreviousEod = eodPayPosition != null && eodPayPosition.id != 0; // InterestIncomeSum 是尚未结算的高精度利息;RealizedInterest 是生命周期累计已结利息。 // 二者不能相互替代,也不能在部分平仓后重新从 0 开始。 var lastInterestIncomeSum = eodPayPosition?.InterestIncomeSum ?? 0m; var lastInterestFeeSum = eodPayPosition?.InterestFeeSum ?? 0m; var lastRealizedInterest = eodPayPosition?.RealizedInterest ?? 0m; var lastRealizedInterestFee = eodPayPosition?.RealizedInterestFee ?? 0m; // 先保留平仓前的复利本金;后面 interests.First().InterestPrincipal 是本次已平部分, // 不能用它代表平仓前全额本金计算当日总利息。 var lastTdInterestPrincipal = eodPayPosition?.TdInterestPrincipal ?? 0m; // 保留上一日日终标识和计息上下文,部分平仓只从 ValueDate 之后续算,不能重置到交易起始日。 eodPayPosition = eodPayPosition?.Clone() ?? new eod_swap_position(); eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; // CalcSwapInterests 按 PositionId 匹配上一日日终。 eodPayPosition.PositionId = position.id; eodPayPosition.PosiStartDate = td.StartDate.Value; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; eodPayPosition.InterestIncomeSum = lastInterestIncomeSum; eodPayPosition.InterestFeeSum = lastInterestFeeSum; // 盘中计息结果 InterestAmount 只承接利息待实现;费用仍由 InterestFeeSum // 独立滚存,避免后续汇总 InterestProfitSum 时把历史费用重复计算。 eodPayPosition.InterestProfitSum = lastInterestIncomeSum; eodPayPosition.RealizedInterest = lastRealizedInterest; eodPayPosition.RealizedInterestFee = lastRealizedInterestFee; if (newEodPayPosition == null) { newEodPayPosition = eodPayPosition.Clone(); newEodPayPosition.id = 0; } // orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值, // 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。 var interestModes = MarginModes.FixedAmountAndMargin; if (interestModes.Contains(position.InterestMode)) { orginPv = eodPayPosition.InterestPrincipalFix; } else { orginPv = posiNotionalValue; } // closePercent 描述本次关闭占平仓前仓位的比例;上例为 90941735.34 / 303139117.80 = 30%。 decimal closePercent = oriPosiNotionalValue == 0 ? 0 : closeNational / oriPosiNotionalValue; var eventType = autoSwap ? (int)SwapEventTypeEnum.自动互换 : (int)SwapEventTypeEnum.平仓; bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString(); if (!autoSwap) { closePercent = oriPosiNotionalValue == 0 ? 0 : closeNational / oriPosiNotionalValue; if (longShort) { closePercent = 0; } } List positions = new List(); positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); var calcLast = tradeExtend?.CalcLast ?? true; // 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。 // 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。 // 恒1 重算的 InterestAmount 是结算现金流的直接输入(非无害中间值):系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。 // 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着 // SwapCalcTrace 分段过程日志追计算;autoSwap=观察日结现路径。 Log.Info($"[EOD平仓后收盘结息] tradeId={td.id} valueDate={valueDate:yyyy-MM-dd} autoSwap={autoSwap} " + $"口径=全额结息(恒1惯例) " + $"oriPosi(平仓前)={oriPosiNotionalValue} posi(剩余)={posiNotionalValue} close(平掉)={closeNational}"); var interests = CalcEodPostCloseSettleInterests(InterestCalcRequest.EodPostCloseSettle( td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, closeNational, eventType, tdClose: false, orginPv, add: true, newCalcLast: autoSwap || calcLast)); // TdInterestAmount:计息器返回的全腿当日/累计参考值,用于拆出 EOD 的当日新增。 // interestAmountBeforeSettlement:本次事件发生前理论应结的高精度利息。 // manualSettledInterestAmount:swap_flow_event 实际落库的手工结息,金额已按分处理。 decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount); decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount); decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount); decimal autoSettledInterestAmount = 0m; if (autoSwap && interests.Count > 0) { autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount); var autoInterest = interests[0]; autoInterest.InterestAmount = autoSettledInterestAmount; autoInterest.InterestClosePnL = autoSettledInterestAmount * DirectionRatio.ReceivePay(autoInterest.InterestDirection); } newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); newEodPayPosition.Invalid = false; //持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值: // ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。 CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0); // ── 持仓延续腿重置日再定盘(EQD-6968 自洽化)── // 排除日取价已收口为"纯跳过":事件利率=末段已消费利率。但剩余持仓自当日起进入新计息周期, // 快照 FloatRate 是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的 // 利率载体——平仓日恰为重置日时必须显式取当日新定盘(与 ByEod 日增路径的重置日行为同构)。 // 全平(剩余=0)/算尾(事件利率已是新定盘)/观察日(autoSwap 恒1已含当日)无需再定盘。 if (!autoSwap && !calcLast && posiNotionalValue > 0m && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode) && SwapDealService.IsResetDay(valueDate, td.StartDate.Value, position.interest_rest_days ?? 1)) { var ongoingFixing = ResolveOngoingResetFixing(position, valueDate); SwapCalcTrace.Critical( $"FIX EodCloseRefix 融资腿{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}"); newEodPayPosition.FloatRate = ongoingFixing; } //利息端估值用信息 // TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。 // 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。 newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? position.InterestPrincipalFix : position.InterestMode == (int)InterestModeEnum.标的期初全价 && position.InterestType != (int)InterestTypeEnum.复利 ? posiNotionalValue : interests.Count > 0 ? interests.First().InterestPrincipal : 0; if (interval != null) { newEodPayPosition.TdInterestRate = interval.Rate; } else { newEodPayPosition.TdInterestRate = flowEvents.FirstOrDefault()?.InterestRate ?? 0; } //当日已实现,平仓时已处理 newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; // TdCloseInterest 只表示当天真正结算出去的金额;部分平仓未结部分继续留在 InterestIncomeSum。 newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount; // intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用 // 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。 var intersetAcmount = InterestIncomeCalc.DailyAccrual( newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = autoSwap ? intersetAcmount : !hasPreviousEod ? interestAmountBeforeSettlement : posiNotionalValue == 0m ? interestAmountBeforeSettlement - lastInterestIncomeSum : lastRealizedInterest != 0m || lastRealizedInterestFee != 0m || !calcLast ? intersetAcmount : TdInterestAmount - lastInterestIncomeSum; if (!autoSwap && closePercent > 0m && closePercent < 1m && posiNotionalValue > 0m && position.InterestType == (int)InterestTypeEnum.复利 && (position.InterestMode == (int)InterestModeEnum.合约名义本金规模 || position.InterestMode == (int)InterestModeEnum.标的期初全价)) { // 模式2(合约名义本金规模)和模式9(标的期初全价)都以名义本金 // 作为复利基数;算尾用平仓前全额当日利息再扣实际结算, // 不算尾只计剩余本金,避免已平部分利息进入后续复利。 // fullPrincipal 是平仓前动态复利本金,仅用于判断平仓日应按全额还是剩余额计息。 var fullPrincipal = lastTdInterestPrincipal > 0m ? lastTdInterestPrincipal : oriPosiNotionalValue; // 当日计提按平仓前全额动态本金;跨日携带必须只留剩余仓位。 // calcLast=true 时,模式2返回本次已平部分本金,需反推剩余本金; // 模式9返回的已是剩余本金,不能再次按比例放大(GLMS-20260421-0004)。 // calcLast=false 快速路径返回上一 EOD 全额本金,保留原剩余比例缩放。 var usesFullPreviousEodPrincipal = !calcLast && hasPreviousEod && (valueDate - eodPayPosition.ValueDate).Days == 1 && (valueDate - position.PosiStartDate).Days % (position.interest_rest_days ?? 1) != 0; if (calcLast && position.InterestMode == (int)InterestModeEnum.合约名义本金规模) { // 模式2的 InterestPrincipal 是已平部分,需反推平仓前全额后再取剩余; // 模式9已直接返回剩余动态本金,再反推会把 30% 平仓后的本金放大 7/3 倍。 // 例如模式9的 212135529.97 已是剩余本金,错误反推会变成 494982903.27。 newEodPayPosition.TdInterestPrincipal *= (1m - closePercent) / closePercent; } else if (usesFullPreviousEodPrincipal) { newEodPayPosition.TdInterestPrincipal *= 1m - closePercent; } // 不算尾时,TdInterestPrincipal 已由计息器完成重置日待实现利息结转, // 并在非重置日分支按剩余仓位调整;若再次用上日本金乘剩余比例, // 会漏掉重置后已并入本金的待实现利息(如 2026-08-04 两笔 JIATT 交易)。 var accrualPrincipal = calcLast ? fullPrincipal : newEodPayPosition.TdInterestPrincipal; newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual( accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); } if (!autoSwap && closePercent > 0m && closePercent < 1m && posiNotionalValue > 0m && position.InterestType == (int)InterestTypeEnum.单利 && (position.InterestMode == (int)InterestModeEnum.合约名义本金规模 || position.InterestMode == (int)InterestModeEnum.标的期初全价)) { // 单利算尾当日仍按平仓前全额计提,跨日 EOD 本金只携带剩余持仓。 newEodPayPosition.TdInterestPrincipal = posiNotionalValue; } Log.Info($"InterestIncomeSum is {lastInterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" + $",TdCloseInterest is {newEodPayPosition.TdCloseInterest}"); Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); // pendingInterestBeforeSettlement 是“扣款前待实现”。普通平仓按上日待实现 + 当日新增; // 自动互换的 interestAmountBeforeSettlement 已经是完整理论应结,不能再加一次上日值。 var pendingInterestBeforeSettlement = autoSwap ? interestAmountBeforeSettlement : lastInterestIncomeSum + newEodPayPosition.TdInterestIncome; var pendingInterestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee; // InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。 // 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。 newEodPayPosition.InterestIncomeSum = closePercent == 1 && EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest) ? 0m : EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest); newEodPayPosition.InterestFeeSum = closePercent == 1 && EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee) ? 0m : EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); //持仓内容-利息腿-损益统计(本方视角) // InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。 newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); Log.Info($"InterestIncomeSum is {eodPayPosition.InterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" + $",TdCloseInterest is {newEodPayPosition.TdCloseInterest}"); Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); //累计已实现(滚存语义见 FinalizeInterestEodRoll:只增不回滚) FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition); return interests; } /// /// 利息腿复制上一日终归档信息 /// /// 上一日终利息腿持仓信息 /// 利息腿信息 /// 上一交易日 /// 当前结算日 /// 互换交易主干 protected void SaveEodInterestPositionCopy(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, DateTime valueDate, trade td, swap_position position, eod_swap lastEodSwap, bool needPrice, decimal posiTotalNotional, decimal grossPrice, decimal orginPv) { Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); List intervals = position.SwapIntervalList; var tradeExtend = td.trade_extend.ExtendObj; // orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值, // 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。 var interestModes = MarginModes.FixedAmountAndMargin; if (eodPayPosition == null) { //if (position.PosiStartDate > valueDate) //{ // return; //} eodPayPosition = new eod_swap_position(); eodPayPosition.InterestDirection = position.InterestDirection; eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; //eodPayPosition.PositionId = position.id; 为了算利息时找不到给日期重新赋值 eodPayPosition.InterestMode = position.InterestMode; eodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; eodPayPosition.InterestRateDefault = position.InterestRateDefault; eodPayPosition.InterestSwapInterval = position.InterestSwapInterval; eodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiTotalNotional; eodPayPosition.PosiStartDate = td.StartDate.Value; eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; eodPayPosition.IsAnnualized = position.IsAnnualized; eodPayPosition.HappenDate = position.HappenDate; eodPayPosition.Currency = position.Currency; eodPayPosition.InterestType = position.InterestType; eodPayPosition.FloatRate = position.FloatRate; eodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; eodPayPosition.interest_rest_days = position.interest_rest_days; eodPayPosition.interest_rule = position.interest_rule; } if (newEodPayPosition == null) { newEodPayPosition = eodPayPosition.Clone(); newEodPayPosition.id = 0; newEodPayPosition.PositionId = position.id; } if (interestModes.Contains(position.InterestMode)) { orginPv = eodPayPosition.InterestPrincipalFix; } bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString(); decimal oriPosiNotionalValue = posiTotalNotional; decimal posiNotionalValue = oriPosiNotionalValue; if (lastEodSwap == null) { lastEodSwap = new eod_swap(); } decimal closePercent = oriPosiNotionalValue == 0 ? 0 : posiNotionalValue / oriPosiNotionalValue; if (longShort) { closePercent = 0; } if (td.ExerciseDate == valueDate) { closePercent = 1; } var ratio = DirectionRatio.InterestLegPnl(eodPayPosition.InterestDirection, position.InterestMode); List positions = new List { position }; List preEodPositions = new List(); if (eodPayPosition.id != 0) { preEodPositions.Add(eodPayPosition); } var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiNotionalValue, closePercent, 0, false, orginPv); UpdateDbOption(newEodPayPosition); newEodPayPosition.PosiStatus = 0; newEodPayPosition.Invalid = false; newEodPayPosition.ValueDate = valueDate; decimal InterestAmount = interests.Sum(s => s.InterestAmount); decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount); //利息端估值用信息 newEodPayPosition.TdInterestPrincipal = interests.Count > 0 ? interests.First().InterestPrincipal : 0; newEodPayPosition.TdInterestRate = interests.Count > 0 ? interests.First().InterestRate : 0; newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0; //当日已实现 newEodPayPosition.TdCloseInterest = 0; newEodPayPosition.TdCloseInterestFee = 0; //持仓内容-利息腿-损益统计(本方视角) newEodPayPosition.TdInterestIncome = TdInterestAmount; newEodPayPosition.TdInterestFee = 0; Log.Info($"InterestIncomeSum is {eodPayPosition.InterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" + $",TdCloseInterest is {newEodPayPosition.TdCloseInterest}"); Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome - newEodPayPosition.TdCloseInterest; newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现(滚存收尾见 FinalizeInterestEodRoll;方向源=eodPayPosition,与其他三方法不同,勿统一) FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, eodPayPosition.InterestDirection); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition); } /// /// 持仓腿生成期初持仓及归档信息 /// /// 上一日终持仓信息 /// 合约框架 /// 最后一条事件流水 /// 期初价格 /// 期初价格不含费 /// 剩余持仓数量 /// 开仓交易佣金费用 /// 当前剩余名义本金 /// 当日浮动端分红 /// 当日浮动端平仓盈亏分红 /// 当日平仓数量 /// 当日平仓费用 /// 当日浮动盈亏 protected virtual decimal SaveEodPosition(eod_swap_position newEodPayPosition, trade td, swap_flow_event eventFlow, decimal netPrice, decimal grossPrice, decimal netFeePrice, decimal netNoFeePrice, decimal payQty, decimal tradingFee, decimal posiNotionalValue, decimal dividendIn, decimal tdDividendIn, decimal closeQty, decimal closeFee, decimal closeMtmPnl, int posiType, bool open) { payQty = Math.Abs(payQty); int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负 int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType); newEodPayPosition.ValueDate = eventFlow.PayDate.Value; newEodPayPosition.PositionId = eventFlow.PositionId; newEodPayPosition.ClientId = td.ClientId; newEodPayPosition.SwapTradeId = td.id; //持仓内容-浮动收益腿 newEodPayPosition.PosiDirection = eventFlow.PayDirection; newEodPayPosition.PositionType = posiType; newEodPayPosition.UnderlyingCode = eventFlow.UnderlyingCode; newEodPayPosition.UnderlyingInstrumentType = eventFlow.UnderlyingInstrumentType; newEodPayPosition.ContractSize = eventFlow.ContractSize; newEodPayPosition.CountRatio = eventFlow.CountRatio; newEodPayPosition.PosiNetPrice = netPrice; newEodPayPosition.PosiGrossPrice = Math.Round( grossPrice, GetStorageDeliveryPriceRound(eventFlow.UnderlyingInstrumentType, eventFlow.UnderlyingCode), MidpointRounding.AwayFromZero); newEodPayPosition.PosiNetFeePrice = netFeePrice; newEodPayPosition.PosiNetNoFeePrice = netNoFeePrice; newEodPayPosition.PosiQuantity = payQty; newEodPayPosition.PosiNotionalValue = posiNotionalValue; newEodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; if (newEodPayPosition.PosiQuantity == 0) { newEodPayPosition.PosiMatuirityDate = eventFlow.PayDate.Value; } //else //{ // newEodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; //} newEodPayPosition.PosiFeePending = tradingFee; //浮动端估值用信息 newEodPayPosition.UnderlyingPrice = UnderlyingCodePrice(newEodPayPosition.UnderlyingCode, eventFlow.EventDate, out decimal vobp); newEodPayPosition.dv01 = Dv01Helper.CalcDv01(newEodPayPosition.UnderlyingCode, newEodPayPosition.PosiQuantity, newEodPayPosition.PosiDirection, newEodPayPosition.PositionType, vobp); newEodPayPosition.UnderlyingMarketValue = MtmCalc.MarketValue(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio); //当日已实现 newEodPayPosition.TdCloseQty = closeQty; newEodPayPosition.TdChangedQty = 0; newEodPayPosition.TdCloseMtmPnl = closeMtmPnl * ratio; newEodPayPosition.TdCloseDividend = tdDividendIn * ratio; newEodPayPosition.TdCloseFee = closeFee * ratio; //持仓内容-浮动收益腿-损益统计(本方视角 newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2); newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio); newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2); newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending); //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum); //累计已实现 newEodPayPosition.RealizedFee = closeFee; newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl; newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend; EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition); newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0; UpdateDbOption(newEodPayPosition); newEodPayPosition.Invalid = false; var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, eventFlow.EventDate , seekPreday: true, currencyRateType: posiNotionalValue < 0 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); decimal posiTradingFee = 0; if (open)//更新新开仓持仓腿信息,因为在生成开仓事件时,先生成了空的持仓腿信息 { if (td.trade_extend.ExtendObj.NeedOpenFee && td.TradeDate == eventFlow.EventDate)//开仓 { posiTradingFee = Math.Abs(newEodPayPosition.PosiTradingFee) * Convert.ToDecimal(currencyRate); } UpdateSwapPosition(newEodPayPosition, td.TradeNumber); } UpdateSwapPositionWithRealTime(newEodPayPosition); PersistEodSwapPosition(newEodPayPosition); return posiTradingFee; } /// ///当日无平仓,无互换,生成持仓腿日终归档,适用于上一日终存在 /// /// 上一日日终归档信息 /// 当日日终归档信息 /// 当日平仓/互换事件信息 /// 交易信息 protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, decimal? corporateActionBeforeQuantity = null) { if (curretEod == null) { curretEod = eod.Clone(); curretEod.id = 0; curretEod.ValueDate = valueDate; } var um = GetUnderlyingData(eod.UnderlyingCode); if (um == null) { return curretEod; } var dealDate = curretEod.ValueDate; int shortRatio = DirectionRatio.LongShort(eod.PositionType); int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); decimal tax = um.ValueAddedTax ?? 0; if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0) { decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax); } curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0; // 分红递推过程常驻记录(快速定位):窗口/数量/税率/当日新计/累计前后值—— // 配合 BondPaymentService 的[分红-登记日口径]窗口命中日志,构成"命中哪些登记日→算出多少→账滚到多少"全链 Log.Info($"[分红-EOD计提Copy] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " + $"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " + $"TdPosiDividend={curretEod.TdPosiDividend} PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}"); curretEod.PosiQuantity = eod.PosiQuantity; if (curretEod.PosiStatus == 1) { curretEod.PosiNotionalValue = 0; } curretEod.UnderlyingPrice = price; curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio); curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); //curretEod.TdPosiDividend = 0; //curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend; curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.TdCloseFee = 0; curretEod.TdCloseQty = 0; curretEod.TdCloseMtmPnl = 0; // 需要计算平仓盈亏分红 curretEod.TdCloseDividend = 0; curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend; curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; EodPnlCalculator.SetFloatingRealizedPnl(curretEod); var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); UpdateDbOption(curretEod); curretEod.Invalid = false; if (curretEod.id == 0) { PersistEodSwapPosition(curretEod); } return curretEod; } /// /// 更新虚拟交易费用 /// /// private void UpdateVTradingFee(eod_swap_position curretEod) { //int ratio = curretEod.PositionType == (int)PositionTypeFlag.Long ? -1 : 1; //int shortRatio = curretEod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; //var priceFee = curretEod.PosiQuantity == 0 ? 0 : curretEod.UnderlyingPrice + curretEod.PosiFeePending / curretEod.PosiQuantity * ratio; //curretEod.VTradingFee = -(priceFee - curretEod.PosiNetPrice - curretEod.UnderlyingPrice + curretEod.PosiGrossPrice) * curretEod.PosiNotionalValue * shortRatio; curretEod.VTradingFee = curretEod.PosiFeePending * 2; } /// /// 当日有平仓/互换,生成持仓腿日终归档,适用于上一日终存在 /// /// /// /// /// protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents, decimal? corporateActionBeforeQuantity = null) { if (curretEod == null) { curretEod = eod.Clone(); curretEod.TdPosiDividend = 0; curretEod.id = 0; curretEod.ValueDate = valueDate; } var um = GetUnderlyingData(eod.UnderlyingCode); if (um == null) { return curretEod; } var dealDate = curretEod.ValueDate; int shortRatio = DirectionRatio.LongShort(eod.PositionType); int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); // 历史遗留死代码已删(2026-08-16,论证+边界测试见 DividendEodNoDoubleCountTest.脏数据边界_*): // todayConsumedDividend / originNotional / totalPayment / totalInterest 自 0910969e(2026-07-02 // 改递推式) 起计算结果从未被消费,仅残留一次全历史 CalcBondPayment 只读查询+日志副作用, // 且构成脏数据(OriginalStockEqvNotional=null/PosiNetPrice=0)下的 EOD 崩溃点。回退=git revert 本提交。 decimal tax = um.ValueAddedTax ?? 0; SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition); curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); curretEod.UnderlyingPrice = price; curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio); curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); curretEod.TdPosiDividend = 0; // 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红 // 修改,互换事件会影响待实现的分红的,现在要算上 if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0)) { decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax); } curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; // 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红) curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn); curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var closeQty = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList().Sum(s => s.Quantity); curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend; // 分红与互换解耦:持仓>0时待实现分红用递增模式(前日+当天新计-当天实现), // 与 CopyEodPosition 的逐天递增口径一致,避免从头重算的舍入累积差异。 if (curretEod.PosiQuantity > 0) { curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend - curretEod.TdCloseDividend; } else { curretEod.PosiDividendSum = 0; } // 分红递推过程常驻记录(快速定位):当日事件路径含实现扣减(前日+新计-当日实现) Log.Info($"[分红-EOD计提Update] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " + $"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " + $"TdPosiDividend={curretEod.TdPosiDividend} TdCloseDividend={curretEod.TdCloseDividend} " + $"PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}"); EodPnlCalculator.SetFloatingRealizedPnl(curretEod); curretEod.SwapPositionValue -= curretEod.TdCloseDividend; curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); if (curretEod.PosiStatus == 1) { curretEod.PosiNotionalValue = 0; } var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); UpdateDbOption(curretEod); curretEod.Invalid = false; if (curretEod.id == 0) { PersistEodSwapPosition(curretEod); } return curretEod; } /// /// 根据开平仓事件算价格及后付费用 /// /// /// /// public void SetPriceInfoByFlowEvent(eod_swap_position eod, eod_swap_position curretEod, List unwindEvents, swap_position position) { if (eod.PosiDirection == 0) { return; } int shortRatio = DirectionRatio.LongShort(eod.PositionType); int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList(); decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity); decimal openQty = openFlowEvents.Sum(s => s.Quantity); curretEod.PosiQuantity = QtyRollforward.Calc(eod.PosiQuantity, openQty, unwindQty); if (unwindEvents.Count == 0) { curretEod.PosiNetPrice = position.PosiNetPrice; curretEod.PosiGrossPrice = position.PosiGrossPrice; curretEod.PosiNetFeePrice = position.PosiNetFeePrice; curretEod.PosiNetNoFeePrice = position.PosiNetNoFeePrice; curretEod.PosiQuantity = position.PosiQuantity; curretEod.PosiFeePending = -position.PosiTradingFeePending * directionRatio; } else { var eventTradingFee = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓 || x.EventType == (int)SwapFlowEventTypeEnum.平仓).Sum(s => s.TradingFeePending * (s.EventType == (int)SwapFlowEventTypeEnum.开仓 ? 1m : -1m)); curretEod.PosiFeePending = eod.PosiFeePending + eventTradingFee; if (openFlowEvents.Count() == 0) { curretEod.PosiNetPrice = eod.PosiNetPrice; curretEod.PosiGrossPrice = eod.PosiGrossPrice; curretEod.PosiNetFeePrice = eod.PosiNetFeePrice; curretEod.PosiNetNoFeePrice = eod.PosiNetNoFeePrice; } else //平仓数量一定<持仓数量 { var posiQty = eod.PosiQuantity - unwindQty; if (posiQty < 0) { posiQty = 0; } curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty); curretEod.PosiGrossPrice = Math.Round( curretEod.PosiGrossPrice, GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode), MidpointRounding.AwayFromZero); curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty); curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty); curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty); curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize; curretEod.PosiNotionalValue = Math.Round(curretEod.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); curretEod.TdCloseDividend = unwindEvents.Sum(x => x.DividendIn); curretEod.TdCloseFee = unwindFlowEvents.Sum(x => x.TradingFee + x.TradingFeePending); curretEod.TdCloseQty = unwindQty; curretEod.TdCloseMtmPnl = unwindEvents.Sum(x => x.MarkClosePnl); } if (curretEod.PosiQuantity == 0) { curretEod.PosiFeePending = 0; } UpdateVTradingFee(curretEod); } /// /// 单标的浮动腿 首次归档 /// /// /// /// protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List unwindEvents, decimal? corporateActionBeforeQuantity = null) { eod_swap_position curretEod = new eod_swap_position(); var um = GetUnderlyingData(position.UnderlyingCode); if (um == null) { return curretEod; } var dealDate = settleDate; curretEod.ValueDate = settleDate; curretEod.PosiStartDate = position.PosiStartDate; curretEod.PosiMatuirityDate = td.ExerciseDate.Value; curretEod.SwapTradeId = td.id; curretEod.PositionId = position.id; curretEod.ClientId = td.ClientId; int shortRatio = DirectionRatio.LongShort(position.PositionType); int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection); curretEod.PositionType = position.PositionType; var eod = new eod_swap_position() { ContractSize = position.ContractSize, PositionType = position.PositionType, PosiDirection = position.PosiDirection, PosiFeePending = 0, PosiNetPrice = position.PosiNetPrice, PosiGrossPrice = position.PosiGrossPrice, PosiNetFeePrice = position.PosiNetFeePrice, PosiNetNoFeePrice = position.PosiNetNoFeePrice, }; curretEod.PosiDirection = position.PosiDirection; curretEod.UnderlyingCode = position.UnderlyingCode; curretEod.UnderlyingInstrumentType = position.UnderlyingInstrumentType; curretEod.SwapTradeId = position.SwapTradeId; curretEod.ContractSize = position.ContractSize; curretEod.CountRatio = position.CountRatio; curretEod.PosiTradingFee = position.PosiTradingFee; curretEod.UnderlyingPrice = GetSwapValuationPrice(position.UnderlyingCode, dealDate, out decimal vobp); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, position); curretEod.dv01 = Dv01Helper.CalcDv01(curretEod.UnderlyingCode, curretEod.PosiQuantity, curretEod.PosiDirection, curretEod.PositionType, vobp); //if (settleDate == td.TradeDate) //{ // curretEod.UnderlyingPrice = curretEod.PosiGrossPrice; // //curretEod.TdCloseMtmPnl = 0; // //curretEod.TdCloseFee = 0; //} // TdCloseDividend 已由 SetPriceInfoByFlowEvent 设置 // 当日新增分红及待实现分红(有互换全量归0,开仓首日两者相同) curretEod.TdPosiDividend = 0; var hasSwapEvent = unwindEvents.Any(e => e.EventType == (int)SwapFlowEventTypeEnum.互换 || e.EventType == (int)SwapFlowEventTypeEnum.自动互换); if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0) { decimal tax = um.ValueAddedTax ?? 0; decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity); payment = DividendCalc.AfterTax(payment, tax); //var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红 curretEod.TdPosiDividend = payment; curretEod.PosiDividendSum = payment; } curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio); curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = curretEod.TdCloseDividend; curretEod.RealizedFee = curretEod.TdCloseFee; EodPnlCalculator.SetFloatingRealizedPnl(curretEod); curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; if (curretEod.PosiStatus == 1) { curretEod.PosiNotionalValue = 0; } //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); UpdateDbOption(curretEod); curretEod.Invalid = false; PersistEodSwapPosition(curretEod); return curretEod; } /// /// 获取标的收盘价格 /// /// 标的代码 /// 收盘日 /// public decimal UnderlyingCodePrice(string code, DateTime settleDate, out decimal vobp) { vobp = 0; var data = DataCacheProvider.GetUnderlyingDataSource().GetData(code); if (data == null) { return 0; } if (data.IsBond()) { return Math.Round(BondPrice(data, settleDate, out vobp), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } var price = data.Price ?? 0; if (EodPriceQueryService.TryGetEodPrice(settleDate, code, out var eodPrice)) { price = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice); } return Math.Round(Convert.ToDecimal(price), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } /// /// 获取债券收盘价格 /// /// /// /// public decimal BondPrice(underlying_manager data, DateTime settleDate, out decimal vobp) { vobp = 0; var price = data.Price ?? 0; if (EodPriceQueryService.TryGetBondEodPrice(settleDate, data.UnderlyingCode, out var eodPrice)) { price = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice); vobp = eodPrice.Vobp ?? 0; } else { price = price * Convert.ToDouble(ConsGlobal.bondPriceMultiple); } return Convert.ToDecimal(price); } /// /// 框架合约估值 /// /// 互换交易 /// 收盘日 private void SaveEodSwap(trade td, DateTime settleDate, DateTime preSettleDate) { var eod_Swaps = DbContext.eod_swap.Where(x => x.SwapTradeId == td.id && x.ValueDate >= preSettleDate && x.ValueDate <= settleDate).ToList(); var eod_Swap = eod_Swaps.FirstOrDefault(x => x.ValueDate == settleDate); var preEodSwap = eod_Swaps.FirstOrDefault(x => x.ValueDate == preSettleDate); if (eod_Swap == null) { eod_Swap = new eod_swap(); } var tradeSpan = DbContext.trade_span.FirstOrDefault(x => x.TradeId == td.id && x.ValueDate == settleDate); // eod_swap 是交易级汇总;eod_swap_position 是浮动腿、利息腿和保证金腿的明细。 // 以下先按日终明细拆腿,再按框架合约展示口径汇总。 var eodSwapPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(td.id, settleDate).ToList(); var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿 var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 // 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模, // 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。 eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); eod_Swap.SwapTradeId = td.id; eod_Swap.SwapTradeNo = td.TradeNumber; eod_Swap.ClientId = td.ClientId; eod_Swap.BookId = td.AssetId; eod_Swap.ValueDate = settleDate; eod_Swap.StructureType = td.StructureType; EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions); eod_Swap.InterestPnL = EodPnlCalculator.SumInterestPnL(interestPositions); eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue); // 保证金腿的利息现金流方向与保证金本金方向相反。 // 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的 // 利息作为收益相加。逐腿按利息方向转换后再生成框架合约已实现收益。 eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl); eod_Swap.TdRealizedPnL = eod_Swap.RealizedPnL - (preEodSwap?.RealizedPnL ?? 0); eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty); var initMargin = Convert.ToDecimal(tradeSpan?.InitialMargin ?? 0); var maintainMargin = Convert.ToDecimal(tradeSpan?.WorstCastClientPayable ?? 0); if (initMargin < 0) { eod_Swap.InitMarginLoss = Math.Abs(initMargin); } else { eod_Swap.InitMarginGain = Math.Abs(initMargin); } if (maintainMargin < 0) { eod_Swap.PostionMarginLoss = Math.Abs(maintainMargin); } else { eod_Swap.PostionMarginGain = Math.Abs(maintainMargin); } UpdateDbOption(eod_Swap); if (eod_Swap.id == 0) { DbContext.eod_swap.Add(eod_Swap); } } /// /// 单标的修改当天 框架合约信息 /// /// /// private void UpdateEodSwap(trade td, DateTime settleDate) { eod_swap eod_Swap = DbContext.eod_swap.FirstOrDefault(x => x.SwapTradeId == td.id && x.ValueDate == settleDate); if (eod_Swap == null) { eod_Swap = new eod_swap(); eod_Swap.SwapTradeId = td.id; eod_Swap.ValueDate = settleDate; DbContext.eod_swap.Add(eod_Swap); } // 单标的调整与首次归档使用同一套框架合约汇总口径,避免重算后多空和名义本金展示不一致。 var eodSwapPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(td.id, settleDate).ToList(); var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿 var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions); eod_Swap.InterestPnL += EodPnlCalculator.SumInterestPnL(interestPositions); eodSwapPositions.ForEach(x => { var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee; }); eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue); eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl); var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id); var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.HappenDate == settleDate).ToList(); var addMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.HappenDate == settleDate).ToList(); eod_Swap.InitMarginGain += initMarginList.Where(s => s.InterestDirection == (int)SwapDirectionEnum.收取).ToList().Sum(s => s.InterestPrincipalFix); eod_Swap.InitMarginLoss += initMarginList.Where(s => s.InterestDirection == (int)SwapDirectionEnum.支付).ToList().Sum(s => s.InterestPrincipalFix); eod_Swap.PostionMarginGain += addMarginList.Where(s => s.InterestDirection == (int)SwapDirectionEnum.收取).ToList().Sum(s => s.InterestPrincipalFix); eod_Swap.PostionMarginLoss += addMarginList.Where(s => s.InterestDirection == (int)SwapDirectionEnum.支付).ToList().Sum(s => s.InterestPrincipalFix); UpdateDbOption(eod_Swap); DbContext.SaveChanges(); } /// /// 汇总单条日终腿的我方已实现收益。 /// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息 /// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为 /// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。 /// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。 /// public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position); /// /// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest /// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负; /// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。 /// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。 /// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。 /// public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position); /// /// 获取多空组合 平仓详细 /// /// /// /// public SwapLongShortCloseModel GetCloseDetails(int tradeId, DateTime valueDate) { SwapLongShortCloseModel closeModel = new SwapLongShortCloseModel(); var eodPositions = DbContext.eod_swap_position.ActiveByTradeAndDate(tradeId, valueDate).ToList(); var flowEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.EventDate == valueDate && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.EventType == (int)SwapEventTypeEnum.平仓 && string.IsNullOrEmpty(x.UnderlyingCode)).ToList(); closeModel.DealPositions = eodPositions.Where(x => x.TdCloseQty != 0).ToList(); closeModel.DealInterests = flowEvents; return closeModel; } /// /// 查询互换日终持仓风险-明细 /// /// /// public SearchListResult SearchEodPositionList(EodSwapPositionQueryRequest req) { var predicate = PredicateBuilder.Create(n => !n.Invalid); var tradePredicate = PredicateBuilder.Create(n => n.TradeType == "收益互换" && n.ValidState != "InValid"); if (req.ValueDate.HasValue) { predicate = predicate.And(n => n.ValueDate == req.ValueDate); //tradePredicate = tradePredicate.And(n => n.StartDate <= req.ValueDate); } if (!string.IsNullOrEmpty(req.TradeNumber)) { tradePredicate = tradePredicate.And(n => n.TradeNumber.Contains(req.TradeNumber.Trim())); } if (req.UserAssets != null || req.UserClients != null) { tradePredicate = tradePredicate.And(q => req.UserAssets.Contains(q.AssetId) || req.UserClients.Contains(q.ClientId)); } if (req.ClientIds != null && req.ClientIds.Any(x => x > 0)) { tradePredicate = tradePredicate.And(q => req.ClientIds.Contains(q.ClientId)); } if (req.BookIds != null && req.BookIds.Any(x => x > 0)) { tradePredicate = tradePredicate.And(q => req.BookIds.Contains(q.AssetId)); } if (req.UnderlyingIds != null) { var underlyingCodes = new List(); foreach (var id in req.UnderlyingIds) { var un = UnderlyingDataProvider.GetUnderlying(id); if (un != null) { underlyingCodes.Add(un.UnderlyingCode); } } predicate = predicate.And(n => underlyingCodes.Contains(n.UnderlyingCode)); } var positionQuery = DbContext.eod_swap_position.Where(predicate); var tradeQuery = DbContext.trade.Where(tradePredicate); var query = from position in positionQuery join td in tradeQuery on position.SwapTradeId equals td.id select new SwapPositionResponse { eodPosition = position, TradeDate = td.StartDate.Value, SwapTradeNo = td.TradeNumber, StructureType = td.StructureType, ClientName = td.ClientName, ClientId = td.ClientId, }; if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "SwapTradeNo,eodPosition.id"; req.sord = "asc"; } var retListResult = query.ToSearchList(req); foreach (var item in retListResult.rows) { var client = DataCacheProvider.GetClientDataSource().GetData(item.ClientId); item.SwapTradeTypeStr = client?.SwapTradeTypeStr; var multiplier = ConsGlobal.InstrumentType.IsBond(item.eodPosition.UnderlyingInstrumentType) ? 100 : 1; item.eodPosition.PosiNetPrice *= multiplier; item.eodPosition.PosiGrossPrice *= multiplier; item.eodPosition.PosiNetFeePrice *= multiplier; item.eodPosition.PosiNetNoFeePrice *= multiplier; item.eodPosition.UnderlyingPrice *= multiplier; // 历史数据的 TdCloseInterest、RealizedInterest 存在两种符号口径, // 风险报表统一按绝对金额和业务方向还原,并重算 RealizedPnl。 NormalizeInterestSignForReport(item.eodPosition); } return retListResult; } /// /// 查询互换日终持仓风险-框架合约 /// /// /// public SearchListResult SearchEodSwapList(EodSwapQueryRequest req) { var predicate = PredicateBuilder.Create(n => 1 == 1); var tradePredicate = PredicateBuilder.Create(n => n.TradeType == "收益互换" && n.ValidState != "InValid"); if (req.ValueDate.HasValue) { predicate = predicate.And(n => n.ValueDate == req.ValueDate); // tradePredicate = tradePredicate.And(n=>n.StartDate<=req.ValueDate); } if (!string.IsNullOrEmpty(req.TradeNumber)) { tradePredicate = tradePredicate.And(n => n.TradeNumber.Contains(req.TradeNumber.Trim())); } if (req.UserAssets != null || req.UserClients != null) { tradePredicate = tradePredicate.And(q => req.UserAssets.Contains(q.AssetId) || req.UserClients.Contains(q.ClientId)); } if (req.ClientIds != null && req.ClientIds.Any(x => x > 0)) { tradePredicate = tradePredicate.And(q => req.ClientIds.Contains(q.ClientId)); } if (req.BookIds != null && req.BookIds.Any(x => x > 0)) { tradePredicate = tradePredicate.And(q => req.BookIds.Contains(q.AssetId)); } var positionQuery = DbContext.eod_swap.Where(predicate); var tradeQuery = DbContext.trade.Where(tradePredicate); var query = from position in positionQuery join td in tradeQuery on position.SwapTradeId equals td.id select new EodSwapResponse { position = position, TradeDate = td.StartDate.Value, SwapTradeNo = td.TradeNumber, StructureType = td.StructureType, ClientName = td.ClientName, AssetBookName = td.AssetBookName, ClientId = td.ClientId }; if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "SwapTradeNo,position.id"; req.sord = "asc"; } DbContext.SetDebugLog(); var retListResult = query.ToSearchList(req); var tradeIds = retListResult.rows.Select(x => x.position.SwapTradeId).Distinct().ToList(); var valueDates = retListResult.rows.Select(x => x.position.ValueDate).Distinct().ToList(); var tradeNotionals = DbContext.trade .Where(x => tradeIds.Contains(x.id)) .Select(x => new { x.id, x.OriginalStockEqvNotional, x.StockEqvNotional }) .ToDictionary(x => x.id); var eodPositionDetails = DbContext.eod_swap_position .Where(x => tradeIds.Contains(x.SwapTradeId) && valueDates.Contains(x.ValueDate) && !x.Invalid) .ToList(); var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList(); var underlyingDataSource = DataCacheProvider.GetUnderlyingDataSource(); var varietyDataSource = DataCacheProvider.GetVarietyDataSource(); foreach (var item in retListResult.rows) { item.position.NotionalValueShort = -Math.Abs(item.position.NotionalValueShort); if (tradeNotionals.TryGetValue(item.position.SwapTradeId, out var tradeNotional)) { item.position.NotionalValue = Convert.ToDecimal(tradeNotional.OriginalStockEqvNotional ?? tradeNotional.StockEqvNotional); } var client = DataCacheProvider.GetClientDataSource().GetData(item.ClientId); item.SwapTradeTypeStr = client?.SwapTradeTypeStr; var details = eodPositionDetails .Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate) .ToList(); var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList(); var marginLegs = details.Where(x => MarginModes.Contains(x.InterestMode)).ToList(); var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId); var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1; item.UnderlyingType = string.Join(",", floatingLegs .Select(x => { var underlying = underlyingDataSource.GetData(x.UnderlyingCode); return varietyDataSource.GetData(underlying?.UnderlyingTypeId ?? 0)?.AssetType ?? underlying?.UnderlyingInstrumentTypeCn ?? underlying?.UnderlyingType; }) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct()); item.PeriodAmount = floatingLegs.Sum(x => x.RealizedDividend + x.PosiDividendSum); // PosiProfitSum = 标的盯市收益 + 未结交易费用 + 待实现付息/分红。 // 风险页的“合约浮动端待实现收益”需要保留未结交易费用, // 但期间付息/分红由 PeriodAmount 单列展示并参与对应估值口径, // 因此仅扣除 PosiDividendSum,不能直接使用 PosiMtmPnL。 item.FloatingUnrealizedPnl = floatingLegs.Sum(x => x.PosiProfitSum - x.PosiDividendSum); item.InterestPaymentMethod = dividendPayDate == 0 ? "到期轧差" : "派息日支付"; if (dividendPayDate == 0) { item.MaturityNettingValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL + item.PeriodAmount; } else { item.PeriodPaymentValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL; } // eod_swap 的保证金本金来自 trade_span;缺少 span 数据时会被保存为 0。 // 本风险页改按日终保证金腿的实际本金展示。 item.position.InitMarginGain = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.InterestDirection == (int)SwapDirectionEnum.收取) .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.InitMarginLoss = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.InterestDirection == (int)SwapDirectionEnum.支付) .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.PostionMarginGain = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.InterestDirection == (int)SwapDirectionEnum.收取) .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.PostionMarginLoss = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.InterestDirection == (int)SwapDirectionEnum.支付) .Sum(x => Math.Abs(x.InterestPrincipalFix)); // 保证金本金方向与我方的利息现金流方向相反:原始“收取”保证金 // 表示我方占用客户资金,应向客户支付利息;支付金额按负数展示。 item.MarginInterestGain = marginLegs .Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付) .Sum(x => Math.Abs(x.InterestIncomeSum)); item.MarginInterestLoss = marginLegs .Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取) .Sum(x => -Math.Abs(x.InterestIncomeSum)); } var dv01 = query.Sum(O => O.position.dv01??0); retListResult.Sum = new {DV = dv01 }; return retListResult; } /// /// 查询 EQD-7084 新“框架合约”字段。 /// 旧查询负责筛选、排序、分页及旧字段计算;新字段只基于当前页对应的日终腿补充计算, /// 避免改变旧接口的返回口径。 /// public SearchListResult SearchEodSwapNewList(EodSwapQueryRequest req) { // 新 Tab 与旧 Tab 共享同一套权限、筛选、排序和分页边界;先复用旧查询, // 再只替换需求明确调整的展示字段,避免新接口悄然改变旧口径或查询范围。 var oldResult = SearchEodSwapList(req); var oldRows = oldResult.rows?.ToList() ?? new List(); var tradeIds = oldRows.Select(x => x.position.SwapTradeId).Distinct().ToList(); var valueDates = oldRows.Select(x => x.position.ValueDate).Distinct().ToList(); if (tradeIds.Count == 0) { return new SearchListResult(oldResult, Enumerable.Empty()); } // 当前页的交易、日终明细和扩展信息各批量读取一次,随后在内存按“交易 + 日终日”配对。 // 不在 rows.Select 内查询数据库,避免分页结果产生 N+1 查询。 var trades = DbContext.trade .Where(x => tradeIds.Contains(x.id)) .Select(x => new { x.id, x.StartDate, x.ExerciseDate }) .ToDictionary(x => x.id); var eodPositionDetails = DbContext.eod_swap_position .Where(x => tradeIds.Contains(x.SwapTradeId) && valueDates.Contains(x.ValueDate) && !x.Invalid) .ToList(); var tradeExtends = DbContext.trade_extend .Where(x => tradeIds.Contains(x.TradeId)) .ToList(); var rows = oldRows.Select(item => { // 同一交易可出现在多个日终日;必须同时匹配 ValueDate,不能把其他日期的腿混入本行。 var details = eodPositionDetails .Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate) .ToList(); var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList(); var interestLegs = details.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList(); var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId); // 缺少扩展信息时按“期间支付”处理,和旧接口的默认值保持一致。 var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1; trades.TryGetValue(item.position.SwapTradeId, out var tradeInfo); return new EodSwapRiskNewResponse { position = item.position, TradeDate = item.TradeDate, SwapTradeNo = item.SwapTradeNo, ClientName = item.ClientName, StructureType = item.StructureType, AssetBookName = item.AssetBookName, ClientId = item.ClientId, SwapTradeTypeStr = item.SwapTradeTypeStr, UnderlyingType = item.UnderlyingType, PeriodAmount = item.PeriodAmount, FloatingUnrealizedPnl = item.FloatingUnrealizedPnl, InterestPaymentMethod = item.InterestPaymentMethod, MaturityNettingValuation = item.MaturityNettingValuation, PeriodPaymentValuation = item.PeriodPaymentValuation, MarginInterestGain = item.MarginInterestGain, MarginInterestLoss = item.MarginInterestLoss, // 所有 EQD-7084 差异集中在 NewFields;上方复制的旧字段用于保留原报表的 // 基本信息、DV、期间金额及已实现收益,前端再将六个差异列绑定到 NewFields。 NewFields = CalculateEodSwapRiskNewFields( floatingLegs, interestLegs, item.StructureType, item.position.NotionalValue, tradeInfo?.StartDate, tradeInfo?.ExerciseDate, item.PeriodAmount, dividendPayDate) }; }).ToList(); return new SearchListResult(oldResult, rows); } /// /// 获取互换交易日终持仓数据 /// /// /// /// public List GetPreEodPositions(int tradeId, DateTime valueDate) { return DbContext.eod_swap_position.ActiveByTradeAndDate(tradeId, valueDate).ToList(); } /// /// 获取互换交易日终持仓数据集合 /// /// /// /// public List GetEodSwaps(List tradeIds, DateTime valueDate) { return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate == valueDate).ToList(); } /// /// 获取互换交易某交易日区间框架合约数据 /// /// /// /// /// public List GetEodSwaps(List tradeIds, DateTime valueDate, DateTime preValueDate) { return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate <= valueDate && x.ValueDate >= preValueDate).ToList(); } /// /// 获取互换交易某日终持仓数据 /// /// /// /// /// public List GetEodPositions(List tradeIds, DateTime valueDate, DateTime preValueDate) { return DbContext.eod_swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && !string.IsNullOrEmpty(x.UnderlyingCode) && x.ValueDate <= valueDate && x.ValueDate >= preValueDate && !x.Invalid).ToList(); } /// /// 获取互换交易某区间日终持仓估值-按产品要求 /// /// /// public SearchListResult SearchPositionList(ClientSwapPositionRequest req) { var retListResult = GetSearchPositionList(req); var clientDataSource = DataCacheProvider.GetClientDataSource(); var underlyDataSource = DataCacheProvider.GetUnderlyingDataSource(); foreach (var item in retListResult.rows) { var client = clientDataSource.GetData(item.ClientId); item.ClientNumber = client.Number; if (!string.IsNullOrEmpty(item.eodPosition.UnderlyingCode)) { var underly = underlyDataSource.GetData(item.eodPosition.UnderlyingCode); if (underly != null) { item.eodPosition.UnderlyingName = underly.UnderlyingName; } } } return retListResult; } /// /// 获取互换交易某区间日终持仓估值-按山证要求 /// /// /// public SearchListResult SearchEodPositionList(ClientSwapPositionRequest req) { var retListResult = GetSearchEodPositionList(req); return retListResult; } /// /// 计算 EQD-7084 新“框架合约”Tab 的字段口径。 /// 纯函数只依赖日终浮动腿、利息腿和交易级展示参数,供查询接口及无库单测共用。 /// public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields( IEnumerable floatingLegs, IEnumerable interestLegs, string structureType, decimal notionalValue, DateTime? startDate, DateTime? ExerciseDate, decimal periodAmount, int dividendPayDate) => EodPnlCalculator.CalculateEodSwapRiskNewFields( floatingLegs, interestLegs, structureType, notionalValue, startDate, ExerciseDate, periodAmount, dividendPayDate); /// /// 互换持仓明细查询 /// /// /// private SearchListResult GetSearchPositionList(ClientSwapPositionRequest req) { var predicate = PredicateBuilder.Create(n => !n.Invalid && n.PositionType > 0 && n.PosiStatus == 0); var tradePredicate = PredicateBuilder.Create(n => n.TradeType == "收益互换" && n.ValidState != "InValid"); if (req.ClientId > 0) { predicate = predicate.And(x => x.ClientId == req.ClientId); } if (req.ValueDate != null) { predicate = predicate.And(x => x.ValueDate == req.ValueDate); tradePredicate = tradePredicate.And(n => n.StartDate <= req.ValueDate); } var positionQuery = DbContext.eod_swap_position.Where(predicate); var tradeQuery = DbContext.trade.Where(tradePredicate); var query = from position in positionQuery join td in tradeQuery on position.SwapTradeId equals td.id select new SwapPositionResponse { eodPosition = position, TradeDate = td.StartDate.Value, SwapTradeNo = td.TradeNumber, StructureType = td.StructureType, ClientName = td.ClientName, ClientId = td.ClientId, InitialMarginDirection = position.PosiDirection }; if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "SwapTradeNo,eodPosition.id"; req.sord = "asc"; } var retListResult = query.ToSearchList(req); foreach (var item in retListResult.rows) { SetClientEodPosition(item.eodPosition); SetPosiPrice(item.eodPosition); } return retListResult; } private SearchListResult GetSearchEodPositionList(ClientSwapPositionRequest req) { // 每日估值报告以有数量的浮动腿为主记录;利息腿和保证金腿仅作为同交易、同估值日的辅助数据参与汇总。 var predicate = PredicateBuilder.Create(n => !n.Invalid && n.PosiQuantity > 0); var interestPredicate = PredicateBuilder.Create(n => !n.Invalid && n.InterestDirection > 0); var tradePredicate = PredicateBuilder.Create(n => n.ValidState != "InValid"); // TODO 这里暂时忽略前端传的值 暂时使用临时方案:同时查普通债券类收益互换和普通收益互换 tradePredicate = tradePredicate.And(n => n.StructureType == "普通债券类收益互换" || n.StructureType == "普通收益互换"); if (req.ClientId > 0) { predicate = predicate.And(x => x.ClientId == req.ClientId); } if (req.BookId > 0) { tradePredicate = tradePredicate.And(x => x.AssetId == req.BookId.Value); } // ValueDateFrom 保留在请求模型中,但当前互换估值查询按 ValueDate 单日取数。 // if (req.ValueDateFrom != null) // { // predicate = predicate.And(x => x.ValueDate >= req.ValueDateFrom); // } if (req.ValueDate != null) { predicate = predicate.And(x => x.ValueDate == req.ValueDate); tradePredicate = tradePredicate.And(x => req.ValueDate >= x.TradeDate); } var positionQuery = DbContext.eod_swap_position.Where(predicate); var tradeQuery = DbContext.trade.Where(tradePredicate); var query = from position in positionQuery join td in tradeQuery on position.SwapTradeId equals td.id join tcrConfirm in DbContext.trade_contract_r.Where(O => O.Type == ContractTypeEnum.Trade && O.IsValid) on td.id equals tcrConfirm.TradeId into tcrConfirms from tcrConfirm in tcrConfirms.DefaultIfEmpty() select new EodSwapPositionResponse { position = position, ClientName = td.ClientName, ConfrimNo = tcrConfirm.ContractCode, TradeNumber = td.TradeNumber, StructureType = td.StructureType, UnwindDate = td.UnWindDate, TradeStatus = td.TradeStatus, InitYtm = td.InitYtm }; if (string.IsNullOrEmpty(req.sidx)) { req.sidx = "position.id"; req.sord = "asc"; } var retListResult = query.ToSearchList(req); var tradeIds = retListResult.rows.Select(s => s.position.SwapTradeId).ToList(); if (!tradeIds.Any()) { return retListResult; } interestPredicate = interestPredicate.And(x => tradeIds.Contains(x.SwapTradeId)); var valueDates = retListResult.rows.Select(s => s.position.ValueDate).Distinct().ToList(); interestPredicate = interestPredicate.And(x => valueDates.Contains(x.ValueDate)); // 主查询分页后再取同交易、同估值日的全部辅助腿,避免利息/保证金归集跨估值日串数据。 var eodPositions = DbContext.eod_swap_position.Where(interestPredicate).ToList(); var marginPositions = DbContext.swap_position .Where(x => tradeIds.Contains(x.SwapTradeId) && MarginModes.ForLinq.Contains(x.InterestMode) && x.IsInitial && !x.Invalid) .ToList(); var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList(); Dictionary tradeDic = new Dictionary(); foreach (var item in retListResult.rows) { var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId); // 到期结算日按合同到期日展示;实际期限按自然日且包含起始日,二者均不使用结算规则偏移。 item.MaturitySettlementDate = item.position.PosiMatuirityDate; item.DayCount = Math.Max(0, (item.position.ValueDate - item.position.PosiStartDate).Days + 1); //item.position.PosiProfitSum += item.position.VTradingFee-item.position.PosiFeePending; SetClientEodPosition(item.position); //item.position.PosiProfitSum += item.TradingFee; var posiProfitSum = item.position.PosiProfitSum; //item.position.PosiProfitSum 不需要加交易费用 // PosiProfitSum 原值包含交易费用和期间付息/分红。先拆出这两部分, // 使“浮动收益金额”仅反映标的盯市收益,后续净额公式再按支付方式决定是否加回期间金额。 var pendingDividend = item.position.PosiDividendSum; item.position.PosiProfitSum = item.position.PosiProfitSum - item.position.PosiFeePending - pendingDividend; // 现券仅展示期间付息和期初成交收益率;ETF(标的主数据类型 Fund)仅展示期间分红。 // 其余标的的三列均不适用,返回 null 使页面和 Excel 模板保持空白,而不是展示 0。 var isCashBond = ConsGlobal.InstrumentType.IsBond(item.position.UnderlyingInstrumentType); var isEtf = ConsGlobal.InstrumentType.Fund.Equals( item.position.UnderlyingInstrumentType, StringComparison.OrdinalIgnoreCase); if (isCashBond) { item.PeriodAmount = pendingDividend; item.DividendAmount = null; // 期初标的成交收益率是债券现券成交口径,非现券不展示该交易录入值。 } else if (isEtf) { item.PeriodAmount = null; item.DividendAmount = -pendingDividend; // 每日估值报告是客户视角 取值与日终持仓风险相反 } else { item.PeriodAmount = null; item.DividendAmount = null; } if (!isCashBond) { item.InitYtm = null; } // 预付金本金和利率来自交易腿,并以发生日判断在估值日是否已生效; // 预付金利息则来自当日日终腿,以获得截至估值日的 InterestIncomeSum。 var tradeMargins = marginPositions .Where(x => x.SwapTradeId == item.position.SwapTradeId && (!x.HappenDate.HasValue || x.HappenDate.Value <= item.position.ValueDate)) .ToList(); var interests = eodPositions.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate); var eodMargins = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList(); var eodInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList(); var initialMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList(); var additionalMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金).ToList(); var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault(); item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode; item.position.FloatRate = floatRateInterest?.FloatRate ?? 0; item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins); item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins); item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection))); item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault); // 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。 var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m; item.NetSettmentAmount = item.InterestAmount + item.position.PosiProfitSum + item.position.PosiFeePending + item.MarginInterestAmount + nettingDividend; item.NetSettmentAmount = Math.Round(item.NetSettmentAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); item.TrsValue = Math.Round(item.NetSettmentAmount + item.OpenMarginAmount + item.AdditionalMarginAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (item.position.PosiNotionalValue != 0 && item.position.PosiNetPrice != 0) { item.FloatRateAbs = item.position.PosiNotionalValue == 0 ? 0 : item.InterestAmount / item.position.PosiNotionalValue; } // 是否 ×100 由标的资产类型决定(债券价格以小数保存,展示时转为百分比价格), // 与存储层 GetStorageDeliveryPriceRound / GetSwapValuationPrice 的 IsBond 口径一致, // 不依赖簿记结构类型 StructureType。 SetPosiPrice(item.position); } return retListResult; } /// /// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额, /// 按收取为正、支付为负直接轧差求和,不做本金加权。 /// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。 /// public static decimal CalculateWeightedMarginInterest(IEnumerable margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins); /// /// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。 /// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致, /// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest), /// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。 /// public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position); /// /// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。 /// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成, /// 否则页面、Excel 和净额结算金额会出现相反符号。 /// /// private void SetClientEodPosition(eod_swap_position position) { position.TdCloseDividend = -position.TdCloseDividend; position.TdCloseMtmPnl = -position.TdCloseMtmPnl; position.TdCloseFee = -position.TdCloseFee; position.TdCloseInterest = -position.TdCloseInterest; position.TdCloseInterestFee = -position.TdCloseInterestFee; position.RealizedMtmPnL = -position.RealizedMtmPnL; position.RealizedDividend = -position.RealizedDividend; position.RealizedFee = -position.RealizedFee; position.RealizedInterest = -position.RealizedInterest; position.RealizedInterestFee = -position.RealizedInterestFee; position.RealizedPnl = -position.RealizedPnl; position.InterestProfitSum = -position.InterestProfitSum; position.PosiProfitSum = -position.PosiProfitSum; position.VTradingFee = -position.VTradingFee; position.PosiFeePending = -position.PosiFeePending; position.SwapPositionValue = -position.SwapPositionValue; position.PosiDividendSum = -position.PosiDividendSum; } private void SetPosiPrice(eod_swap_position position) { var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode); if (um != null && um.IsBond()) { position.PosiNetPrice *= 100; position.UnderlyingPrice *= 100; position.PosiGrossPrice *= 100; position.PosiNetFeePrice *= 100; position.PosiNetNoFeePrice *= 100; return; } } /// /// 获取客户互换持仓信息 /// /// /// /// public List GetSwapPositions(int clientId, DateTime valueDate) { var trades = DbContext.trade.Where(x => x.TradeType == "收益互换" && x.ClientId == clientId && ConsTrade.LiveTradeStatusList.Contains(x.TradeStatus) && x.ValidState != "InValid").ToList(); var tradeIds = trades.Select(s => s.id).ToList(); var eodSwaps = DbContext.eod_swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate == valueDate).ToList(); var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList(); return ConvertEodPnl(eodSwaps, trades, tradeExtends); } private List ConvertEodPnl(List eodSwaps, List trades, List tradeExtends) { List list = new List(); foreach (var item in eodSwaps) { var tradeOrigin = trades.First(x => x.id == item.SwapTradeId); var realizedPnL = item.RealizedMtmPnL + item.RealizedInterest; var tdExtend = tradeExtends.First(x => x.TradeId == item.SwapTradeId); eod_position model = new eod_position() { TradeId = tradeOrigin.id, TradeType = tradeOrigin.TradeType, ClientId = tradeOrigin.ClientId, TradeNumber = tradeOrigin.TradeNumber, TradeDate = tradeOrigin.TradeDate, ExerciseDate = tradeOrigin.ExerciseDate, PrincipalRate = tradeOrigin.PrincipalRate ?? 0, BasisUnderlyingCode = tradeOrigin.BasisUnderlyingCode, UnderlyingCode = item.UnderlyingCode, BasisGap = tradeOrigin.BasisGap ?? 0, Lots = Convert.ToDouble(item.PosiQuantity), ParticipationRate = tradeOrigin.ParticipationRate ?? 0, NoRiskRate = tradeOrigin.NoRiskRate ?? 0, UnderlyingPrice = Convert.ToDouble(item.UnderlyingPrice), Pv = Convert.ToDouble(item.UnderlyingMarketValue) * -1, RoundedPv = Math.Round(Convert.ToDouble(item.UnderlyingMarketValue), 2) * -1, Pnl = Convert.ToDouble(realizedPnL) * -1, RoundedPnl = Math.Round(Convert.ToDouble(realizedPnL), 2) * -1, ValueDate = item.ValueDate, PvDouble = Convert.ToDouble(item.UnderlyingMarketValue), PnlDouble = Convert.ToDouble(realizedPnL), PositionRelizedAmount = Convert.ToDouble(realizedPnL) * -1, InstrumentType = tradeOrigin.UnderlyingInstrumentType, IsGroup = tradeOrigin.IsGroup, SettlementType = tradeOrigin.SettlementType, SettlementFlag = tradeOrigin.SettlementFlag, tradeOrigin = tradeOrigin.Clone(), Vol = 0, Delta = 0, Gamma = 0, Theta = 0, Vega = 0, Rho = 0, GammaCash = 0 }; SetDicValue(model, item, tdExtend.ExtendObj.AnnualDays); list.Add(model); } return list; } /// /// 设置持仓导出字典信息 /// /// /// /// private void SetDicValue(eod_position model, eod_swap_position item, int annualDays) { var extDic = model.trade.MetaDic; if (!string.IsNullOrEmpty(item.UnderlyingCode)) { var underlyingAssetName = DataCacheProvider.GetUnderlyingDataSource().GetData(item.UnderlyingCode)?.UnderlyingName; decimal posiTradingFeeUnit = 0; if (item.PosiQuantity != 0) { posiTradingFeeUnit = item.PosiTradingFee / item.PosiQuantity; } if (item.PosiDirection == (int)SwapDirectionEnum.支付) { extDic["互换_支付方标的代码"] = item.UnderlyingCode; extDic["互换_支付方标的名称"] = underlyingAssetName; extDic["互换_支付方期初标的价格"] = item.PosiGrossPrice.OtcFormatUmPrice(); extDic["互换_支付方交易数量"] = item.PosiQuantity.OtcFormatNotional(); extDic["互换_支付方到期标的价格"] = item.UnderlyingPrice.OtcFormatUmPrice(); extDic["互换_支付方单位交易费用"] = posiTradingFeeUnit.OtcFormatUmPrice(); extDic["互换_支付方初始预付金"] = item.RealizedFee.OtcFormatPercent(); extDic["互换_支付方交易费用"] = item.PosiTradingFee.OtcFormatUmPrice(); extDic["互换_支付方多空方向"] = item.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头"; } else { extDic["互换_收取方标的代码"] = item.UnderlyingCode; extDic["互换_收取方标的名称"] = underlyingAssetName; extDic["互换_收取方期初标的价格"] = item.PosiGrossPrice.OtcFormatUmPrice(); extDic["互换_收取方交易数量"] = item.PosiQuantity.OtcFormatNotional(); extDic["互换_收取方到期标的价格"] = item.UnderlyingPrice.OtcFormatUmPrice(); extDic["互换_收取方单位交易费用"] = posiTradingFeeUnit.OtcFormatUmPrice(); extDic["互换_收取方初始预付金"] = ""; extDic["互换_收取方交易费用"] = item.PosiTradingFee.OtcFormatUmPrice(); extDic["互换_收取方多空方向"] = item.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头"; } } else { if (item.InterestDirection == (int)SwapDirectionEnum.支付) { extDic["互换_支付方互换利率"] = item.InterestRateDefault.OtcFormatPercent(); extDic["互换_支付方固定收益"] = ""; } else { extDic["互换_收取方互换利率"] = item.InterestRateDefault.OtcFormatPercent(); extDic["互换_收取方固定收益"] = ""; } } extDic["互换_互换日期"] = ""; extDic["年化天数"] = annualDays.ToString(); } } }