Files
zszq-trs/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
T
hjhan 7fff6e8236 fix(swap-eod): 用 MarginModes.ForLinq 替换 EF LINQ 中的 MarginModes.Contains
swap_position 的 EF 查询在 Where 里使用 MarginModes.Contains(x.InterestMode),
该静态方法无法被 EF Core 翻译成 SQL,导致有数据时抛 Translation failed。
改用 MarginModes.ForLinq(List<int> 形态)即可翻译为 IN (...)。
内存集合(details/interests 已 ToList)不受影响,无需改动。
2026-08-25 11:09:13 +08:00

3827 lines
221 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 互换流水日终归档服务
/// </summary>
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)
{
}
/// <summary>
/// 在一组日终快照中选择严格早于指定日期的最近日。
/// 回退到除权日 D 时必须得到 D 之前的基线;若使用 D 自身,除权后的 2000/50
/// 会被当成除权前状态,重收盘时就可能再次套用 10 送 10。严格使用 &lt; valueDate
/// 也覆盖周末、节假日:周一没有周日 EOD 时,直接选择上一个实际有快照的交易日。
/// </summary>
public static DateTime? SelectLatestEodDateBefore(
IEnumerable<eod_swap_position> 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();
}
/// <summary>
/// 返回指定交易在 valueDate 之前最近实际 EOD 日的全部有效明细。
/// 这是回退和纯单元测试共用的选择规则;调用方不得退化为 AddDays(-1),因为自然日
/// 不等于交易日。若没有快照返回空集合,表示只能保留当前实时持仓,不能伪造基线。
/// </summary>
public static List<eod_swap_position> SelectLatestEodPositionsBefore(
IEnumerable<eod_swap_position> eodPositions,
DateTime valueDate)
{
var latestDate = SelectLatestEodDateBefore(eodPositions, valueDate);
if (!latestDate.HasValue)
{
return new List<eod_swap_position>();
}
return eodPositions
.Where(x => x != null && !x.Invalid && x.ValueDate.Date == latestDate.Value.Date)
.ToList();
}
/// <summary>
/// 从已经确认的 Stock/Fund EOD 快照恢复实时浮动腿的有效基线。
/// 该方法只复制 EOD 已落库的数量、价格、名义本金及累计分红/待结费用,不再次计算
/// 公司行动系数,因此是幂等的。例:原 1000 份、期初价 100,10 送 10 后 EOD 为
/// 2000 份、50;下一日盘中直接恢复 2000/50,不能再变成 4000/25。
/// 非 Fund、空快照或标的腿不满足收取方向时返回 false,保持原有逻辑。
/// </summary>
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;
}
/// <summary>
/// 查询 valueDate 之前最近一份有效 Stock/Fund EOD 快照,作为盘中操作的日初基线。
/// 必须严格使用 &lt; valueDate:试算日当天的 EOD 可能尚未完成,或是重收盘留下的待重建数据,
/// 不能反向覆盖盘中实时持仓。例:D 日 10 送 10 后 EOD 为 2000 份/50D+1 盘中读取 D
/// D 日盘中只读取 D-1,不会误把 D 日半成品当成已生效基线。Invalid 明细始终排除。
/// 调用方还需检查该 EOD 之后是否已有完成流水,避免覆盖当日部分平仓结果。
/// </summary>
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();
}
/// <summary>
/// 回退/收益互换等路径需要的最近实际 EOD 快照集合;严格早于 valueDate,且过滤作废行。
/// </summary>
public virtual List<eod_swap_position> 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/外部调用,生产代码行为不变
/// <summary>持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表)</summary>
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);
}
}
/// <summary>保存所有变更(生产: DbContext.SaveChanges;测试: 计数)</summary>
protected virtual void SaveAllChanges()
{
DbContext.SaveChanges();
}
/// <summary>获取汇率(生产: EodCurrencyRateService;测试: 返回固定值)</summary>
protected virtual double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType)
{
return new EodCurrencyRateService(UserInfo).GetCurrencyRate(quoteCurrency, settlementCurrency, valueDate, seekPreday, currencyRateType);
}
/// <summary>
/// 计算利息腿利息明细(生产: new SwapDealService(this).GetInterests;测试: 用StubSwapDealService内存算)。
/// 参数与 SwapDealService.GetInterests 完全一致,保证行为不变。
/// needPrice/grossPrice 死参数已随 2026-08 收口删除,两侧同步。)
/// </summary>
protected virtual List<swap_flow_event> CalcSwapInterests(
trade td, trade_extend tradeExtend,
DateTime valueDate, DateTime unwindDate,
List<eod_swap_position> eodPositions, List<swap_position> positions,
decimal posiNotionalValue,
decimal closePosiNotionalValue, decimal closePrecent,
int eventType, bool tdClose,
decimal orginPv,
bool add = false, bool settment = true, bool newCalcLast = false,
List<swap_flow_event> closeList = null)
{
return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose,
orginPv, add, settment, newCalcLast, closeList);
}
/// <summary>
/// 【EOD 当日有平仓后的收盘结息】显式入口——原 SaveAutoEodWithCloseInterestPosition 直调
/// CalcSwapInterests(settment:false) 的具名封装(2026-08 显式化重构)。
/// 语义契约见 InterestCalcRequest.EodPostCloseSettle 工厂注释(平仓后剩余 + 实际平掉额 + 恒1全额结息,
/// 触发 GetInterests 内 mode2/mode9 本金修正)。计息走 CalcUnwindInterest 全区间重放。
/// 默认实现仍经 CalcSwapInterests 转发,保持既有测试替身对该虚接缝的拦截不变。
/// </summary>
protected virtual List<swap_flow_event> 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);
/// <summary>
/// 持仓延续腿重置日再定盘(EQD-6968 口径自洽化接缝)。
/// 生产:Fr007IndexFixer.GetFixingOrThrow——缺价抛异常,与 ByEod 重置日再定盘/EodCheckSettlePrice
/// 同口径(EOD 时点当日 FR007 已由收盘前检查把关);测试:override 注入受控定盘。
/// </summary>
protected virtual decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate)
=> Fr007IndexFixer.Instance.GetFixingOrThrow(valueDate, position.interest_rule, position.FloatRateUnderlyingCode);
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
/// <summary>查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典)</summary>
protected virtual trade_extend FindTradeExtend(int tradeId)
{
return DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
}
/// <summary>查找指定日期范围的 eod 持仓(生产: DbContext.eod_swap_position.Where;测试: 内存列表)</summary>
protected virtual List<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
{
return DbContext.eod_swap_position.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList();
}
/// <summary>查找交易持仓(生产: DbContext.swap_position.Where;测试: 内存列表)</summary>
protected virtual List<swap_position> FindSwapPositions(int swapTradeId)
{
return DbContext.swap_position.ActiveByTrade(swapTradeId).ToList();
}
/// <summary>查找框架合约日终汇总(生产: DbContext.eod_swap.FirstOrDefault;测试: 内存字典)</summary>
protected virtual eod_swap FindEodSwap(int swapTradeId, DateTime valueDate)
{
return DbContext.eod_swap.FirstOrDefault(x => x.SwapTradeId == swapTradeId && x.ValueDate == valueDate);
}
/// <summary>添加互换事件(生产: new SwapEventService(this).AddSwapEventDate;测试: 收集到列表)</summary>
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);
}
/// <summary>持久化互换流水事件(生产: DbContext.swap_flow_event.Add;测试: 收集到列表)</summary>
protected virtual void PersistFlowEvent(swap_flow_event flowEvent)
{
DbContext.swap_flow_event.Add(flowEvent);
}
/// <summary>在事务中执行(生产: BeginTransaction/Commit/Rollback;测试: 直接执行不包事务)</summary>
protected virtual void ExecuteInTransaction(Action action)
{
var trans = DbContext.Database.BeginTransaction();
try
{
action();
trans.Commit();
}
catch
{
trans.Rollback();
throw;
}
finally
{
trans.Dispose();
}
}
/// <summary>添加资金记录(生产: AddClientCashInCashOut;测试: 收集到计数器)</summary>
protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate)
{
return AddClientCashInCashOut(td, amount, action, valueDate);
}
/// <summary>保存框架合约日终汇总(生产: SaveEodSwap私有方法;测试: 收集到列表)</summary>
protected virtual void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate)
{
SaveEodSwap(td, settleDate, preSettleDate);
}
/// <summary>清理旧持仓事件(生产: ClearSwapPositions;测试: 空操作)</summary>
protected virtual void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List<int> eventTypes)
{
ClearSwapPositions(td, tradeDate, eventTypes, false);
}
/// <summary>获取标的市场价格(生产: UnderlyingCodePrice查缓存+中债估值;测试: 返回固定值)</summary>
protected virtual decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{
return UnderlyingCodePrice(code, settleDate, out vobp);
}
/// <summary>获取用于互换浮动腿盯市的标的价格。</summary>
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);
}
/// <summary>获取标的缓存数据(生产: DataCacheProvider;测试: 返回内存对象)</summary>
protected virtual underlying_manager GetUnderlyingData(string underlyingCode)
{
return DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode);
}
/// <summary>
/// 计算期间现金流(生产: BondPaymentService;测试: 返回固定值)。
/// BondPaymentService 会按每条付款记录的数据来源换算:bond_payment_info 原生期间付息
/// 按每 100 份,公司行为表补充的现金分红按每 10 份。不能只按 Fund/Stock 标的类型
/// 选择一个统一除数,否则两类记录同时命中时会错算。
/// </summary>
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);
}
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
protected virtual List<trade> FindActiveSwapTrades(DateTime settleDate, IEnumerable<int> clientIds)
{
var tradePredicate = PredicateBuilder.Create<trade>(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();
}
/// <summary>查找交易的所有持仓(含初始+实际,生产: DbContext.swap_position;测试: 内存列表)</summary>
protected virtual List<swap_position> FindAllSwapPositions(List<int> tradeIds)
{
return DbContext.swap_position.Where(t => tradeIds.Contains(t.SwapTradeId) && !t.Invalid).ToList();
}
/// <summary>批量查找交易扩展(生产: DbContext.trade_extend;测试: 内存列表)</summary>
protected virtual List<trade_extend> FindTradeExtends(List<int> tradeIds)
{
return DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
}
/// <summary>查找指定日期的日终汇总(生产: DbContext.eod_swap;测试: 内存列表)</summary>
protected virtual List<eod_swap> FindEodSwapsByDate(DateTime valueDate)
{
return DbContext.eod_swap.Where(x => x.ValueDate == valueDate).ToList();
}
/// <summary>查找交易在指定日期的完成流水事件(生产: DbContext.swap_flow_event;测试: 内存列表)</summary>
protected virtual List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate)
{
Expression<Func<swap_flow_event, bool>> eventExpression = x => x.SwapTradeId == swapTradeId
&& x.DataState == (int)SwapFlowDateStateEnum.完成
&& x.EventDate == settleDate;
return DbContext.swap_flow_event.Where(eventExpression).ToList();
}
protected virtual List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds)
{
return DbContext.swap_flow_event
.Where(x => tradeIds.Contains(x.SwapTradeId)
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
.ToList();
}
/// <summary>查询登记日或真实生效日命中的有效公司行为。</summary>
protected virtual List<ex_dividend_info> 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();
}
/// <summary>查询交易已有公司行为事件,用于登记日/生效日幂等匹配。</summary>
protected virtual List<swap_event> FindCorporateActionEvents(int swapTradeId)
{
return DbContext.swap_event
.Where(x => x.SwapTradeId == swapTradeId
&& x.EventType == (int)SwapEventTypeEnum.公司行为
&& !x.Invalid)
.ToList();
}
/// <summary>更新已存在的公司行为事件;默认只标记实体,统一由收盘事务保存。</summary>
protected virtual void UpdateCorporateActionEventRecord(swap_event swapEvent)
{
UpdateDbOption(swapEvent);
}
/// <summary>
/// 获取公司行为公式使用的收盘价。
/// EffectiveDate 是真正切换持仓基线的日期,但除权系数的收盘价仍属于登记日
/// ExDividendDate;不能在 8 月 17 日 EOD 误取 8 月 17 日收盘价重算 8 月 14 日
/// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。
/// </summary>
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
/// <summary>
/// 多空组合 互换流水合成持仓
/// </summary>
/// <param name="tradeDate">清算日期</param>
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, "互换流水合成持仓完成");
}
/// <summary>
/// 多空组合 互换流水合成持仓
/// </summary>
/// <param name="tradeDate">清算日期</param>
public void SwapFlowEventCompose(List<long> 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, "互换流水合成持仓完成");
}
/// <summary>
/// 收盘生成归档信息
/// </summary>
/// <param name="settleDate">结算日期</param>
public void SwapPositionCompose(DateTime settleDate, DateTime preSettleDate, IEnumerable<int> 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<ex_dividend_info>();
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}】存在多条有效登记日记录");
}
var exDividendByCode = exDividendInfos.ToDictionary(
x => x.UnderlyingCode,
x => x,
StringComparer.OrdinalIgnoreCase);
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
foreach (var td in tradeQueryList)
{
ExecuteInTransaction(() =>
{
List<int> removeEventTyps = new List<int>() { (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<swap_flow_event> autoInterests = new List<swap_flow_event>();//自动互换利息腿信息
// 处理浮动腿前先准备当日开盘基线:登记日 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);
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
var floatPositionsForCompose = eodPositions.Count == 0
? PrepareInitialCorporateActionPositions(posiList, exDividendByCode, settleDate)
: posiList;
// 处理浮动腿归档
var curEodPosis = DealFloatPositions(
floatPositionsForCompose,
realPosiList,
openingEodPositions,
todyEodPositions,
settleDate,
td,
preSettleDate,
flowEvents);
// 现金分红不在登记日直接累加;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();
});
}
}
/// <summary>
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
/// 例如 1000 份/100 元、10 送 10 的记录在 8 月 14 日 EOD 仍是 1000/100
/// 8 月 17 日处理当日流水前,内存基线先转为 2000/50,再平仓 300 份得到 1700/50。
/// </summary>
protected List<eod_swap_position> PrepareFundOpeningEodPositions(
IReadOnlyCollection<eod_swap_position> previousEodPositions,
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
DateTime settleDate)
{
// 首日收盘或者当前非生效日 跳过
if (previousEodPositions == null || previousEodPositions.Count == 0
|| exDividendByCode == null || exDividendByCode.Count == 0)
{
return previousEodPositions?.ToList() ?? new List<eod_swap_position>();
}
// Clone 后只调整本次收盘的内存输入,不改 DbContext 跟踪的上一日实体;
// 否则重收盘或报表读取会把登记日的 Q/P 永久变成除权后 Q/P。
var openingPositions = previousEodPositions
.Where(x => x != null)
.Select(x => x.Clone())
.ToList();
// 应用公司行为
ApplyCorporateActions(
openingPositions,
exDividendByCode,
settleDate);
return openingPositions;
}
/// <summary>
/// 对 TRS Stock/Fund 浮动腿应用一条已按 EffectiveDate 筛选的份额/价格公司行为。
/// 此方法用于直接测试/兼容已有调用方;正式收盘链路通过
/// PrepareFundOpeningEodPositions 在处理当日流水前执行同一动作。
/// 该步骤只改 EOD 持仓的份额/价格基线,不生成现金分红流水;现金模式下现金分红
/// 不下调期初价格,而是由同步任务写入 bond_payment_info,后续付息链路单独计入。
/// <para>
/// 幂等例子:原持仓 1000 份、期初价 100,每 10 份送 10 份。首次收盘得到 2000 份/50;
/// 同日重跑时,若该腿没有新流水,先从前一日 EOD 恢复 1000/100,再计算为 2000/50
/// 不能直接在当日结果上再次计算成 4000/25。
/// </para>
/// <para>
/// 有流水时不在这里强行恢复前一日数量,因为 DealFloatPositions 已把当日开平仓滚动到当前结果;
/// 盘中平仓不会再次套公式,而是读取严格早于 valueDate 的最近有效 EOD,必要时按当日
/// EffectiveDate 再生成开盘基线。
/// </para>
/// </summary>
protected void ApplyCorporateActions(
IEnumerable<eod_swap_position> positions,
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
DateTime settleDate)
{
if (exDividendByCode.Count == 0)
{
return;
}
// TODO: 现金分红税率接入后,仅价格调整模式需要读取税率;TRS 现金模式下不参与除权系数。
// var dividendTaxRate = GetDividendTaxRate();
var dividendTaxRate = 0m;
foreach (var position in positions)
{
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 / MQ' = 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;
}
}
/// <summary>
/// 构造审计事件的调整前快照。优先克隆上一 EOD,保证后续调整不会污染历史实体;
/// 交易首日没有 EOD 时才从初始持仓复制,并把累计分红/已实现字段初始化为 0。
/// </summary>
private static List<eod_swap_position> BuildCorporateActionBeforePositions(
IReadOnlyCollection<eod_swap_position> previousPositions,
IReadOnlyCollection<swap_position> initialPositions)
{
if (previousPositions != null && previousPositions.Count > 0)
{
return previousPositions
.Where(x => x != null)
.Select(x => x.Clone())
.ToList();
}
return (initialPositions ?? Array.Empty<swap_position>())
.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();
}
/// <summary>
/// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线。
/// 不直接修改初始持仓实体,避免重收盘或后续流程再次读取时重复套用系数。
/// </summary>
private List<swap_position> PrepareInitialCorporateActionPositions(
IReadOnlyCollection<swap_position> initialPositions,
IReadOnlyDictionary<string, ex_dividend_info> exDividendByCode,
DateTime settleDate)
{
var positions = (initialPositions ?? Array.Empty<swap_position>())
.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;
}
/// <summary>
/// 同步公司行为后的实时浮动腿。
/// 登记日只更新下一交易日 BOD 使用的实时 Q/P,不改当日已落库的 EOD;
/// 生效日则把已调整的 EOD 复制到实时腿。每次都先从当日 EOD 恢复,保证重跑幂等。
/// </summary>
private void UpdateRealtimeCorporateActionPositions(
trade td,
IReadOnlyCollection<eod_swap_position> currentEodPositions,
IReadOnlyCollection<ex_dividend_info> registrationInfos,
IReadOnlyCollection<ex_dividend_info> effectiveInfos,
DateTime settleDate)
{
if (td == null || currentEodPositions == null || currentEodPositions.Count == 0)
{
return;
}
// 登记日收盘后即切换实时 BOD。EffectiveDate 只用于确认这条记录仍是未来生效的
// 公司行为;无论登记日与生效日之间有一个还是多个非交易日,都不能漏掉这次切换。
var pendingInfos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
.Where(x => x.EffectiveDate.HasValue && x.EffectiveDate.Value.Date > settleDate.Date)
.ToList();
var appliedInfos = effectiveInfos ?? Array.Empty<ex_dividend_info>();
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;
}
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());
}
}
}
/// <summary>
/// 写入公司行为生命周期审计事件。
/// 登记日:保存调整前快照并标记 Applied=false
/// 真实除权日:使用上一 EOD 与当前 EOD 补齐调整后快照并标记 Applied=true。
/// 事件数据只追加/补齐,不删除已生效记录,
/// 便于交易回退后通过 BackId 关联新的回退记录。
/// </summary>
protected virtual void RecordCorporateActionEvents(
trade td,
IReadOnlyCollection<eod_swap_position> currentPositions,
IReadOnlyCollection<eod_swap_position> previousPositions,
IReadOnlyCollection<ex_dividend_info> registrationInfos,
IReadOnlyCollection<ex_dividend_info> effectiveInfos,
DateTime settleDate)
{
if (td == null || currentPositions == null)
{
return;
}
var infos = (registrationInfos ?? Array.Empty<ex_dividend_info>())
.Concat(effectiveInfos ?? Array.Empty<ex_dividend_info>())
.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;
}
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();
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);
// 如果没有匹配到事件或事件未生效,则创建新事件。
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* 来自
// 调整前 EODAfter* 来自生效日当前 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<swap_event> events,
ex_dividend_info info,
long positionId)
{
if (info == null)
{
return false;
}
// 幂等键与收盘事件匹配保持一致,只认 ExDividendInfoId + PositionId。
// 无法反序列化或缺少 ExDividendInfoId 的存量事件均不参与匹配。
return !(events ?? Enumerable.Empty<swap_event>()).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<CorporateActionEventData>(eventData);
}
catch (JsonException)
{
return null;
}
}
private static string BuildCorporateActionReason(CorporateActionEventData data)
{
return SwapEventService.BuildCorporateActionEventReason(data);
}
/// <summary>公司行为调整后的持仓 Q/P 结果,供 EOD、实时腿和盘中平仓共用。</summary>
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; }
}
/// <summary>
/// 统一计算公司行为后的 Q/P。EOD、实时腿和盘中平仓只负责提供基线,
/// 不再各自复制数量、毛价和净价的调整公式。
/// </summary>
private static CorporateActionValues CalculateCorporateActionValues(
decimal quantity,
decimal grossPrice,
decimal netPrice,
decimal? netFeePrice,
decimal? netNoFeePrice,
ex_dividend_info dividendInfo,
decimal closePrice,
decimal dividendTaxRate,
int grossPriceRound)
{
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);
}
/// <summary>
/// 将一条真实生效日公司行为应用到盘中实时 TRS Stock/Fund 浮动腿。
/// 盘中先复制严格早于 valueDate 的 EOD,再调用此方法;因此重复调用时每次都会
/// 从同一份除权前 EOD 重新恢复,不会把 1000/100 重复变成 4000/25。
/// 例:8 月 14 日 EOD 为 1000/1008 月 17 日生效的 10 送 10 会得到 2000/50。
/// 现金模式调用公式时使用 adjustCashDividendPrice=false,现金权益只进入分红字段,
/// 不改变 Stock/Fund 的期初价格。
/// </summary>
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;
}
/// <summary>
/// 框架合约汇总
/// </summary>
/// <param name="settleDate"></param>
/// <param name="preSettleDate"></param>
/// <param name="ClientIds"></param>
public void SwapEodCompose(DateTime settleDate, DateTime preSettleDate, IEnumerable<int> ClientIds)
{
Log.Info("SwapEodCompose:" + "settleDate:" + settleDate + " preSettleDate:" + preSettleDate + " ClientIds:" + JsonHelper.Serialize(ClientIds));
var tradePredicate = PredicateBuilder.Create<trade>(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();
}
/// <summary>
/// 处理利息腿归档
/// </summary>
/// <param name="interestList">利息腿持仓信息</param>
/// <param name="eodPositions">上一日终归档持仓信息</param>
/// <param name="todyEodPositions">当日归档持仓信息</param>
/// <param name="settleDate">当前结算日期</param>
/// <param name="td">互换交易主信息</param>
/// <param name="swapDeals">当日平仓/互换信息</param>
/// <param name="autoInterests">自动互换集合</param>
/// <param name="longshortCloseInterests">多空组合平仓利息腿信息</param>
/// <param name="lastEodSwap">上一日终框架合约</param>
protected virtual void DealInterests(List<swap_position> interestList,
List<eod_swap_position> eodPositions,
List<eod_swap_position> todyEodPositions,
DateTime settleDate,
trade td,
List<swap_flow_event> flowEvents,
List<swap_flow_event> 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<swap_flow_event> dealInterests = new List<swap_flow_event>();
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}");
// 分派优先级与粒度说明见 ResolveInterestScenario8 组合表驱动覆盖见 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);
}
}
}
/// <summary>
/// 处理浮动腿归档
/// </summary>
/// <param name="posiList"></param>
/// <param name="eodPositions"></param>
/// <param name="todyEodPositions"></param>
/// <param name="settleDate"></param>
/// <param name="td"></param>
/// <param name="longShort"></param>
/// <param name="flowEvents"></param>
/// <param name="unwindEvent"></param>
protected List<eod_swap_position> DealFloatPositions(List<swap_position> posiList,
List<swap_position> realPosiList,
List<eod_swap_position> eodPositions,
List<eod_swap_position> todyEodPositions,
DateTime settleDate,
trade td,
DateTime preSettleDate,
List<swap_flow_event> flowEvents)
{
string settleDateStr = settleDate.ToString("yyyy-MM-dd");
string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd");
Log.Info($"================开始处理{settleDateStr}浮动腿归档==================");
List<eod_swap_position> list = new List<eod_swap_position>();
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);
eod_swap_position eodPosi = new eod_swap_position();
if (eodPosition == null)
{
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents);
}
else if (unwindEvents.Count() == 0)
{
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate);
}
else
{
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents);
}
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
list.Add(eodPosi);
}
Log.Info($"================{settleDateStr}浮动腿归档结束==================");
return list;
}
/// <summary>
/// 处理自动互换数据
/// </summary>
/// <param name="autoInterests"></param>
/// <param name="td"></param>
/// <param name="settleDate"></param>
/// <param name="swapDeals"></param>
/// <param name="interval">自动互换观察日信息,用于获取结算日期</param>
/// <param name="curEodPositions">当日浮动端EOD持仓</param>
/// <param name="tradeExtend">交易扩展信息</param>
/// <summary>
/// 自动互换(仅利息/预付金,不含分红)
/// </summary>
private void DealAutoInterests(List<swap_flow_event> 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);
}
/// <summary>
/// 分红独立处理:当天有债券需要分红时,生成独立的分红自动互换事件
/// </summary>
private void DealDividends(List<eod_swap_position> 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<swap_flow_event> dividendEvents = new List<swap_flow_event>();
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);
}
/// <summary>
/// 保存自动互换数据信息
/// </summary>
/// <param name="td"></param>
/// <param name="swap_Deal"></param>
/// <param name="interval">自动互换观察日信息,用于获取结算日期</param>
private long SaveAutoSwapDeal(trade td, List<swap_flow_event> flowEvents, UnwindData unwindData, IntervalModel interval, List<swap_flow_event> dividendEvents = null)
{
//td.UnWindDate = unwindData.ValueDate;
//优先使用 interval.SettlementDate 作为资金记录发生日期,如果没有则使用 ValueDate
var cashHappenDate = interval?.SettlementDate ?? unwindData.ValueDate;
int clientCashId = 0;
var clientCashIds = new List<int>();
// 利息腿:插入资金记录(使用系统操作_互换)
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;
}
/// <summary>
/// R4 §2.4:自动互换预付金返息按腿 FundTag 分流,返回现金部分返息。
/// 授信腿返息不进资金(授信不产生流水);无标签存量/无预付金腿事件时全额现金。
/// virtual 供纯内存测试 stub 为全额现金(见 TestableSwapEodPositionService)。
/// </summary>
protected virtual decimal GetAutoSwapCashRebate(trade td, List<swap_flow_event> 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);
}
/// <summary>
/// 互换更新实时持仓信息
/// </summary>
/// <param name="flowEvents"></param>
private void UpdateInitalPostion(List<swap_flow_event> 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);
}
}
}
/// <summary>
/// 分页合成互换流水开平仓事件,暂时只按加权平均处理
/// </summary>
/// <param name="swapTradeId">互换交易id</param>
/// <param name="tradeDate">清算日期</param>
protected void ComposePage(int swapTradeId, List<swap_flow_event> 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<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
List<int> removeEventTyps = new List<int>() { (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<swap_flow_event> longshortCloseInterests = new List<swap_flow_event>();
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();
}
}
/// <summary>
/// 利息腿字段拷贝(SaveEodInterestPosition / SaveAutoEodInterestPosition / SaveAutoEodWithCloseInterestPosition 共用)。
/// FloatRate 来源随场景不同(手工互换=当日流水;自动互换/平仓=计息结果),由调用方算好传入,勿在本方法内统一。
/// 场景差异字段(PosiStatus / InterestFeePending / TdInterestPrincipal / TdInterestRate)留在各调用点。
/// </summary>
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;
}
/// <summary>
/// 利息腿日终滚存收尾(四个 Save* 共用):RollRealized 滚累计已实现 → SetFixedLegRealizedPnl → 汇率 → TdCurrency。
/// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
/// interestDirection 是 RateType 的方向来源——三个方法取 position.InterestDirection
/// SaveEodInterestPositionCopy 取 eodPayPosition.InterestDirection(现状差异,勿统一)。
/// PersistEodSwapPosition 与各自日志留在调用点(持久化边界 + 日志顺序各不相同)。
/// </summary>
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);
}
/// <summary>
/// 产生互换用
/// </summary>
/// <param name="eodPayPosition">上一日日终归档</param>
/// <param name="newEodPayPosition">当日归档</param>
/// <param name="position">持仓腿</param>
/// <param name="td">主体交易</param>
/// <param name="startDate">计息开始日</param>
/// <param name="valueDate">计息结束日</param>
/// <param name="closeAmount">平仓金额</param>
protected void SaveEodInterestPosition(eod_swap_position eodPayPosition,
eod_swap_position newEodPayPosition,
swap_position position,
trade td,
DateTime valueDate,
List<swap_flow_event> 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);
}
/// <summary>
/// 自动互换用,当日无互换,当日无平仓
/// </summary>
/// <param name="eodPayPosition">上一日日终持仓</param>
/// <param name="newEodPayPosition">当前收盘日日终持仓</param>
/// <param name="position">利息腿信息</param>
/// <param name="td">框架合约</param>
/// <param name="valueDate">当前收盘日</param>
/// <param name="interval">当前观察日</param>
/// <param name="preDealDate">上一平仓/互换日期</param>
/// <param name="closeAmount">当日平仓金额</param>
/// <param name="lastEodSwap">上一日终框架合约估值</param>
protected List<swap_flow_event> 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<swap_position> positions = new List<swap_position>();
positions.Add(position);
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
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;
}
/// <summary>
/// 将平仓/自动互换的盘中利息结果写成当日日终利息腿。
/// 字段完整口径和逐日示例见《收益互换日终收盘总流程与当前代码审查》7.2、16.7、16.13 节。
/// </summary>
/// <remarks>
/// 关键状态链:上日待实现 + 当日新增 - 当日结息 = 当日待实现;
/// 上日累计已实现 + 当日结息(按收付方向)= 当日累计已实现。
/// </remarks>
/// <param name="eodPayPosition">上一日日终持仓</param>
/// <param name="newEodPayPosition">当前收盘日日终持仓 不可能为空</param>
/// <param name="position">利息腿信息</param>
/// <param name="td">框架合约</param>
/// <param name="valueDate">当前收盘日</param>
/// <param name="interval">当前观察日</param>
/// <param name="preDealDate">上一平仓/互换日期</param>
/// <param name="closeAmount">当日平仓金额</param>
/// <param name="lastEodSwap">上一日终框架合约估值</param>
/// <param name="unwintotal">平仓主信息</param>
protected List<swap_flow_event> SaveAutoEodWithCloseInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, decimal posiTotalNotional, List<swap_flow_event> 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<swap_position> positions = new List<swap_position>();
positions.Add(position);
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
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:本次事件发生前理论应结的高精度利息。
// manualSettledInterestAmountswap_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;
}
/// <summary>
/// 利息腿复制上一日终归档信息
/// </summary>
/// <param name="eodPayPosition">上一日终利息腿持仓信息</param>
/// <param name="position">利息腿信息</param>
/// <param name="preSettleDate">上一交易日</param>
/// <param name="valueDate">当前结算日</param>
/// <param name="td">互换交易主干</param>
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<IntervalModel> 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<swap_position> positions = new List<swap_position>
{
position
};
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
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);
}
/// <summary>
/// 持仓腿生成期初持仓及归档信息
/// </summary>
/// <param name="eodPayPosition">上一日终持仓信息</param>
/// <param name="td">合约框架</param>
/// <param name="eventFlow">最后一条事件流水</param>
/// <param name="netPrice">期初价格</param>
/// <param name="grossPrice">期初价格不含费</param>
/// <param name="payQty">剩余持仓数量</param>
/// <param name="tradingFee">开仓交易佣金费用</param>
/// <param name="posiNotionalValue">当前剩余名义本金</param>
/// <param name="dividendIn">当日浮动端分红</param>
/// <param name="tdDividendIn">当日浮动端平仓盈亏分红</param>
/// <param name="closeQty">当日平仓数量</param>
/// <param name="closeFee">当日平仓费用</param>
/// <param name="closeMtmPnl">当日浮动盈亏</param>
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;
}
/// <summary>
///当日无平仓,无互换,生成持仓腿日终归档,适用于上一日终存在
/// </summary>
/// <param name="eod_Swap_Positions">上一日日终归档信息</param>
/// <param name="todayPositions">当日日终归档信息</param>
/// <param name="swap_Deals">当日平仓/互换事件信息</param>
/// <param name="td">交易信息</param>
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
{
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);
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;
}
/// <summary>
/// 更新虚拟交易费用
/// </summary>
/// <param name="curretEod"></param>
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;
}
/// <summary>
/// 当日有平仓/互换,生成持仓腿日终归档,适用于上一日终存在
/// </summary>
/// <param name="eod"></param>
/// <param name="curretEod"></param>
/// <param name="td"></param>
/// <param name="valueDate"></param>
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
{
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);
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;
}
/// <summary>
/// 根据开平仓事件算价格及后付费用
/// </summary>
/// <param name="eod"></param>
/// <param name="curretEod"></param>
/// <param name="unwindEvents"></param>
public void SetPriceInfoByFlowEvent(eod_swap_position eod, eod_swap_position curretEod, List<swap_flow_event> 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);
}
/// <summary>
/// 单标的浮动腿 首次归档
/// </summary>
/// <param name="position"></param>
/// <param name="td"></param>
/// <param name="settleDate"></param>
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
{
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);
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;
}
/// <summary>
/// 获取标的收盘价格
/// </summary>
/// <param name="code">标的代码</param>
/// <param name="settleDate">收盘日</param>
/// <returns></returns>
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);
}
/// <summary>
/// 获取债券收盘价格
/// </summary>
/// <param name="code"></param>
/// <param name="settleDate"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 框架合约估值
/// </summary>
/// <param name="td">互换交易</param>
/// <param name="settleDate">收盘日</param>
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);
}
}
/// <summary>
/// 单标的修改当天 框架合约信息
/// </summary>
/// <param name="td"></param>
/// <param name="settleDate"></param>
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();
}
/// <summary>
/// 汇总单条日终腿的我方已实现收益。
/// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
/// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
/// </summary>
public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position);
/// <summary>
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
/// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
/// </summary>
public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position);
/// <summary>
/// 获取多空组合 平仓详细
/// </summary>
/// <param name="tradeId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 查询互换日终持仓风险-明细
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<SwapPositionResponse> SearchEodPositionList(EodSwapPositionQueryRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid);
var tradePredicate = PredicateBuilder.Create<trade>(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<string>();
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;
}
/// <summary>
/// 查询互换日终持仓风险-框架合约
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<EodSwapResponse> SearchEodSwapList(EodSwapQueryRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap>(n => 1 == 1);
var tradePredicate = PredicateBuilder.Create<trade>(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;
}
/// <summary>
/// 获取互换交易日终持仓数据
/// </summary>
/// <param name="tradeId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_swap_position> GetPreEodPositions(int tradeId, DateTime valueDate)
{
return DbContext.eod_swap_position.ActiveByTradeAndDate(tradeId, valueDate).ToList();
}
/// <summary>
/// 获取互换交易日终持仓数据集合
/// </summary>
/// <param name="tradeId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_swap> GetEodSwaps(List<int> tradeIds, DateTime valueDate)
{
return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate == valueDate).ToList();
}
/// <summary>
/// 获取互换交易某交易日区间框架合约数据
/// </summary>
/// <param name="tradeIds"></param>
/// <param name="valueDate"></param>
/// <param name="preValueDate"></param>
/// <returns></returns>
public List<eod_swap> GetEodSwaps(List<int> tradeIds, DateTime valueDate, DateTime preValueDate)
{
return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate <= valueDate && x.ValueDate >= preValueDate).ToList();
}
/// <summary>
/// 获取互换交易某日终持仓数据
/// </summary>
/// <param name="tradeIds"></param>
/// <param name="valueDate"></param>
/// <param name="preValueDate"></param>
/// <returns></returns>
public List<eod_swap_position> GetEodPositions(List<int> 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();
}
/// <summary>
/// 获取互换交易某区间日终持仓估值-按产品要求
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<SwapPositionResponse> 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;
}
/// <summary>
/// 获取互换交易某区间日终持仓估值-按山证要求
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<EodSwapPositionResponse> SearchEodPositionList(ClientSwapPositionRequest req)
{
var retListResult = GetSearchEodPositionList(req);
return retListResult;
}
/// <summary>
/// 互换持仓明细查询
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
private SearchListResult<SwapPositionResponse> GetSearchPositionList(ClientSwapPositionRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.PositionType > 0 && n.PosiStatus == 0);
var tradePredicate = PredicateBuilder.Create<trade>(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<EodSwapPositionResponse> GetSearchEodPositionList(ClientSwapPositionRequest req)
{
// 每日估值报告以有数量的浮动腿为主记录;利息腿和保证金腿仅作为同交易、同估值日的辅助数据参与汇总。
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.PosiQuantity > 0);
var interestPredicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.InterestDirection > 0);
var tradePredicate = PredicateBuilder.Create<trade>(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<string, bool> tradeDic = new Dictionary<string, bool>();
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 转换。
SetPosiPrice(item.position, item.StructureType == "普通债券类收益互换");
}
return retListResult;
}
/// <summary>
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
/// </summary>
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins);
/// <summary>
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
/// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
/// </summary>
public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position);
/// <summary>
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,
/// 否则页面、Excel 和净额结算金额会出现相反符号。
/// </summary>
/// <param name="position"></param>
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, bool? useBondPriceScale = null)
{
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
if (useBondPriceScale ?? (um != null && um.IsBond()))
{
position.PosiNetPrice *= 100;
position.UnderlyingPrice *= 100;
position.PosiGrossPrice *= 100;
position.PosiNetFeePrice *= 100;
position.PosiNetNoFeePrice *= 100;
return;
}
}
/// <summary>
/// 获取客户互换持仓信息
/// </summary>
/// <param name="clientId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_position> 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<eod_position> ConvertEodPnl(List<eod_swap_position> eodSwaps, List<trade> trades, List<trade_extend> tradeExtends)
{
List<eod_position> list = new List<eod_position>();
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;
}
/// <summary>
/// 设置持仓导出字典信息
/// </summary>
/// <param name="model"></param>
/// <param name="item"></param>
/// <param name="annualDays"></param>
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();
}
}
}