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 - Convert.ToDecimal(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);
}
///
/// 自动互换用,当日无互换,当日无平仓
///
/// 上一日日终持仓
/// 当前收盘日日终持仓
/// 利息腿信息
/// 框架合约
/// 当前收盘日
/// 当前观察日
/// 上一平仓/互换日期
/// 当日平仓金额
/// 上一日终框架合约估值
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();
}
}
}