4018 lines
232 KiB
C#
4018 lines
232 KiB
C#
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。严格使用 < 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 快照,作为盘中操作的日初基线。
|
||
/// 必须严格使用 < valueDate:试算日当天的 EOD 可能尚未完成,或是重收盘留下的待重建数据,
|
||
/// 不能反向覆盖盘中实时持仓。例:D 日 10 送 10 后 EOD 为 2000 份/50,D+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);
|
||
}
|
||
|
||
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate,
|
||
decimal qty, int shortRatio, int directionRatio, decimal? corporateActionQty)
|
||
{
|
||
if (!corporateActionQty.HasValue)
|
||
{
|
||
return CalcBondPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
|
||
}
|
||
|
||
var service = new BondPaymentService(UserInfo);
|
||
var payments = service.GetBondPayments(underlyingCode, fromDate, toDate);
|
||
return service.CalcPayment(payments, qty, shortRatio, directionRatio, corporateActionQty);
|
||
}
|
||
|
||
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
|
||
|
||
/// <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;不能在 除权日 EOD 误取 除权日收盘价重算 登记日
|
||
/// 登记日形成的系数。测试实现可以返回快照中的回退值,生产实现从登记日行情读取。
|
||
/// </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}】存在多条有效登记日记录");
|
||
}
|
||
|
||
// 根据标的代码 创建map
|
||
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);
|
||
var corporateActionCashDividendBeforePositions = corporateActionBeforePositions
|
||
.Where(position => !string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||
&& exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividend)
|
||
&& dividend.GiveCashAmount != 0m)
|
||
.ToList();
|
||
|
||
// 交易首日恰逢 EffectiveDate 时,在内存克隆上生成除权后的开盘基线,应用生效日公司行为。
|
||
// 有上一份 EOD 时沿用 PrepareFundOpeningEodPositions,避免重复套系数。
|
||
var floatPositionsForCompose = eodPositions.Count == 0
|
||
? PrepareInitialCorporateActionPositions(posiList, exDividendByCode, settleDate)
|
||
: posiList;
|
||
|
||
// 处理浮动腿归档
|
||
var curEodPosis = DealFloatPositions(
|
||
floatPositionsForCompose, // 初始腿
|
||
realPosiList, // 实时腿
|
||
openingEodPositions, // 开盘基线
|
||
todyEodPositions, // 当日终持仓
|
||
settleDate, // 收盘日期
|
||
td, // 交易
|
||
preSettleDate, // 上一交易日
|
||
flowEvents, // 流水事件
|
||
corporateActionCashDividendBeforePositions);
|
||
|
||
// 现金分红不在登记日直接累加;Copy/Update EOD 通过 CalcBondPayment
|
||
// 读取 EffectiveDate 命中的 ex_dividend_info,并生成 TdPosiDividend。
|
||
// 这样登记日快照不提前变化,且公司行为分红与债券付息共用同一待实现余额。
|
||
// 公司行为事件
|
||
RecordCorporateActionEvents(
|
||
td,
|
||
curEodPosis,
|
||
corporateActionBeforePositions,
|
||
registrationInfos,
|
||
exDividendInfos,
|
||
settleDate);
|
||
// 登记日 EOD 仍保存除权前快照,
|
||
// 但下一交易日开盘读取的实时浮动腿需要先切换到生效后的 Q/P。
|
||
// 该更新基于当日 EOD 恢复后再套系数,重收盘不会重复放大。
|
||
UpdateRealtimeCorporateActionPositions(td, curEodPosis, registrationInfos, exDividendInfos, settleDate);
|
||
var posiLongNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||
var posiShortNotional = curEodPosis.Where(s => s.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue);
|
||
var closePosiNotional = curEodPosis.Where(s => s.TdCloseQty > 0).Sum(s => s.TdCloseQty * s.ContractSize * s.PosiGrossPrice);
|
||
var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0;
|
||
//处理利息腿
|
||
DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv);
|
||
//获取自动互换的观察日信息,用于确定结算日期
|
||
IntervalModel observationInterval = null;
|
||
foreach (var interest in interestList)
|
||
{
|
||
observationInterval = InterestEodScenarioDispatch.FindObservationInterval(interest, settleDate);
|
||
if (observationInterval != null)
|
||
break;
|
||
}
|
||
// 自动互换(仅利息/预付金,不含分红)
|
||
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, observationInterval);
|
||
// 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关
|
||
DealDividends(curEodPosis, td, settleDate, tradeExtend);
|
||
//多空组合判断是否已到到期日且无持仓信息
|
||
if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty == 0)
|
||
{
|
||
td.TradeStatus = "已到期";
|
||
td.UnWindDate = settleDate;
|
||
}
|
||
SaveAllChanges();
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把上一实际 EOD 复制成“当日开盘基线”,并在需要时套用当日生效的 Stock/Fund 公司行为。
|
||
/// 原始上一 EOD 只读保留在数据库中,确保登记日 EOD 报表仍展示除权前 Q/P。
|
||
/// 例如 1000 份/100 元、10 送 10 的记录在 登记日 EOD 仍是 1000/100;
|
||
/// 除权日处理当日流水前,内存基线先转为 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)
|
||
{
|
||
// 不是浮动腿 或者 不是 Fund Stock类型的标的 或者 没有除权信息 或者 除权日不是结算日 - 跳过
|
||
if (position.PosiDirection <= 0
|
||
|| !IsTrsCorporateActionInstrument(position.UnderlyingInstrumentType)
|
||
|| string.IsNullOrWhiteSpace(position.UnderlyingCode)
|
||
|| !exDividendByCode.TryGetValue(position.UnderlyingCode, out var dividendInfo)
|
||
|| !dividendInfo.EffectiveDate.HasValue
|
||
|| dividendInfo.EffectiveDate.Value.Date != settleDate.Date)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 获取除权参考价 - 登记日收盘价
|
||
var corporateActionClosePrice = GetFundCorporateActionClosePrice(
|
||
dividendInfo,
|
||
position.UnderlyingPrice);
|
||
if (corporateActionClosePrice <= 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"Stock/Fund 标的【{position.UnderlyingCode}】登记日【{dividendInfo.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
|
||
}
|
||
|
||
// Excel 公式 口径:PriceRatio 是“登记日收盘价 / 除权参考价”,
|
||
// 因此期初价格和持仓数量都使用同一个系数:P' = P / M,Q' = Q * M。
|
||
// 配股已经进入 价格参考价,所以即使没有送股,配股也会调整 TRS 数量;
|
||
// 现金分红不影响 TRS Stock/Fund 期初价格,现金权益由独立分红字段处理。
|
||
var originalQuantity = position.PosiQuantity;
|
||
// 计算公司行为发生后的 Q/P
|
||
var adjusted = CalculateCorporateActionValues(
|
||
position.PosiQuantity,
|
||
position.PosiGrossPrice,
|
||
position.PosiNetPrice,
|
||
position.PosiNetFeePrice,
|
||
position.PosiNetNoFeePrice,
|
||
dividendInfo,
|
||
corporateActionClosePrice,
|
||
dividendTaxRate,
|
||
GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode));
|
||
position.PosiQuantity = adjusted.Quantity;
|
||
position.TdChangedQty = position.PosiQuantity - originalQuantity;
|
||
|
||
position.PosiGrossPrice = adjusted.GrossPrice;
|
||
position.PosiNetPrice = adjusted.NetPrice;
|
||
position.PosiNetFeePrice = adjusted.NetFeePrice;
|
||
position.PosiNetNoFeePrice = adjusted.NetNoFeePrice;
|
||
|
||
// 多空方向
|
||
var shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||
// 收付方向
|
||
var directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||
// 处理价格的正负号(收支方向)
|
||
position.PosiNotionalValue = Math.Round(
|
||
position.PosiGrossPrice * position.PosiQuantity * position.ContractSize,
|
||
ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero);
|
||
position.UnderlyingMarketValue = MtmCalc.MarketValue(
|
||
position.UnderlyingPrice,
|
||
position.PosiQuantity,
|
||
position.ContractSize,
|
||
shortRatio);
|
||
position.PosiMtmPnL = EodPnlCalculator.RoundMoney(MtmCalc.UnrealizedPnl(
|
||
position.UnderlyingPrice,
|
||
position.PosiGrossPrice,
|
||
position.PosiQuantity,
|
||
position.ContractSize,
|
||
shortRatio,
|
||
directionRatio));
|
||
position.PosiProfitSum = EodPnlCalculator.RoundMoney(MtmCalc.ReturnLegProfitSum(
|
||
position.PosiMtmPnL,
|
||
position.PosiDividendSum,
|
||
position.PosiFeePending));
|
||
position.SwapPositionValue = EodPnlCalculator.RoundMoney(PositionValueCalc.Calc(
|
||
position.InterestProfitSum,
|
||
position.PosiProfitSum));
|
||
position.PosiStatus = position.PosiQuantity == 0 ? 1 : 0;
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
// 对每条当日 EOD 浮动腿,按标的代码在 pendingInfos 中找匹配的公司行为。
|
||
var pending = pendingInfos.FirstOrDefault(x => string.Equals(
|
||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||
if (pending != null)
|
||
{
|
||
// 必须从登记日 EOD 基线生成下一交易日 BOD,而不是在旧实时腿上继续套系数;
|
||
// 这样 100000/100 只会变成一次 200000/50,并且不会把初始腿改掉。
|
||
// 先将实时腿恢复为登记日 EOD 的旧基线,再只对实时腿应用一次公司行为。
|
||
// EOD 仍保持除权前快照;因此 7/10 EOD=100000/100,而 7/13 BOD=200000/50。
|
||
var baseline = eod.Clone();
|
||
UpdateSwapPositionWithRealTime(baseline);
|
||
var closePrice = GetFundCorporateActionClosePrice(pending, baseline.PosiGrossPrice);
|
||
ApplyCorporateActionToPosition(realtime, pending, closePrice, 0m);
|
||
continue;
|
||
}
|
||
|
||
var applied = appliedInfos.FirstOrDefault(x => string.Equals(
|
||
x.UnderlyingCode, eod.UnderlyingCode, StringComparison.OrdinalIgnoreCase));
|
||
if (applied != null)
|
||
{
|
||
// 生效日 EOD 已经完成 Q/P 调整,实时腿直接同步最终快照,不再二次套系数。
|
||
UpdateSwapPositionWithRealTime(eod.Clone());
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
// 跟据交易id查当前交易关联事件
|
||
var existingEvents = FindCorporateActionEvents(td.id);
|
||
foreach (var current in currentPositions.Where(x => x != null && x.PosiDirection > 0
|
||
&& IsTrsCorporateActionInstrument(x.UnderlyingInstrumentType)))
|
||
{
|
||
var info = infos.FirstOrDefault(x => string.Equals(
|
||
x.UnderlyingCode,
|
||
current.UnderlyingCode,
|
||
StringComparison.OrdinalIgnoreCase));
|
||
if (info == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 公司行为事件只使用“公司行为记录主键 + PositionId”作为幂等键。
|
||
var matchingEvents = existingEvents
|
||
.Select(x => new { Event = x, Data = DeserializeCorporateActionEventData(x.EventData) })
|
||
.Where(x => x.Data != null
|
||
&& info.id > 0
|
||
&& x.Data.ExDividendInfoId == info.id
|
||
&& x.Data.PositionId == current.PositionId)
|
||
.ToList();
|
||
// 寻找applied = false的(登记日记录的)
|
||
var eventData = matchingEvents.FirstOrDefault(x => !x.Data.Applied)
|
||
?? matchingEvents.FirstOrDefault();
|
||
var previous = previousPositions?.FirstOrDefault(x => x != null
|
||
&& x.PositionId == current.PositionId);
|
||
// 登记日 false 除权日 true
|
||
var isEffective = info.EffectiveDate.HasValue
|
||
&& info.EffectiveDate.Value.Date <= settleDate.Date
|
||
&& effectiveInfos != null
|
||
&& effectiveInfos.Any(x => x.id == info.id);
|
||
|
||
// 如果没有匹配到事件或今天不是除权日 但找到的事件的applied=true(异常事件/重收盘),则创建新事件。
|
||
if (eventData == null || (!isEffective && eventData.Data.Applied))
|
||
{
|
||
// 创建新事件
|
||
var pending = BuildCorporateActionEventData(
|
||
info,
|
||
previous ?? current,
|
||
isEffective ? current : null,
|
||
applied: isEffective);
|
||
// 生命周期事件的发生日固定为登记日,EffectiveDate 只表示 Q/P 基线切换日。
|
||
// 这样回退后重收盘仍能按原登记日排序和追溯,不会把同一事件拆成两条历史。
|
||
var eventDate = info.ExDividendDate?.Date
|
||
?? info.EffectiveDate?.Date
|
||
?? settleDate.Date;
|
||
var created = AddSwapEvent(
|
||
eventDate,
|
||
td.id,
|
||
(int)SwapEventTypeEnum.公司行为,
|
||
JsonConvert.SerializeObject(pending),
|
||
0,
|
||
false,
|
||
BuildCorporateActionReason(pending));
|
||
if (created == null)
|
||
{
|
||
created = new swap_event();
|
||
}
|
||
// 测试接缝和历史实现可能返回只带 id 的实体;统一补齐字段,
|
||
// 确保同一收盘事务内的生效步骤能找到刚创建的事件。
|
||
created.EventType = (int)SwapEventTypeEnum.公司行为;
|
||
created.SwapTradeId = td.id;
|
||
created.ValueDate = eventDate;
|
||
created.EventData = JsonConvert.SerializeObject(pending);
|
||
created.EventReason = BuildCorporateActionReason(pending);
|
||
existingEvents.Add(created);
|
||
continue;
|
||
}
|
||
|
||
// 如果不是生效日或事件已生效,则跳过。
|
||
if (!isEffective || eventData.Data.Applied)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 生效日只补齐同一事件的 Before/After 快照,不重新套系数:Before* 来自
|
||
// 调整前 EOD,After* 来自生效日当前 EOD,current 已由开盘基线处理完成。
|
||
eventData.Data.BeforeNotional = previous?.PosiNotionalValue ?? eventData.Data.BeforeNotional;
|
||
eventData.Data.BeforePrice = previous?.PosiGrossPrice ?? eventData.Data.BeforePrice;
|
||
eventData.Data.BeforeQuantity = previous?.PosiQuantity ?? eventData.Data.BeforeQuantity;
|
||
eventData.Data.BeforePendingDividend = previous?.PosiDividendSum ?? eventData.Data.BeforePendingDividend;
|
||
eventData.Data.AfterNotional = current.PosiNotionalValue;
|
||
eventData.Data.AfterPrice = current.PosiGrossPrice;
|
||
eventData.Data.AfterQuantity = current.PosiQuantity;
|
||
eventData.Data.AfterPendingDividend = current.PosiDividendSum;
|
||
eventData.Data.CashFlowChange = current.RealizedDividend - (previous?.RealizedDividend ?? current.RealizedDividend);
|
||
eventData.Data.Applied = true;
|
||
eventData.Event.EventData = JsonConvert.SerializeObject(eventData.Data);
|
||
eventData.Event.EventReason = BuildCorporateActionReason(eventData.Data);
|
||
UpdateCorporateActionEventRecord(eventData.Event);
|
||
}
|
||
}
|
||
|
||
public static CorporateActionEventData BuildCorporateActionEventData(
|
||
ex_dividend_info info,
|
||
eod_swap_position previous,
|
||
eod_swap_position current,
|
||
bool applied)
|
||
{
|
||
return new CorporateActionEventData
|
||
{
|
||
ExDividendInfoId = info.id,
|
||
PositionId = (current ?? previous).PositionId,
|
||
UnderlyingCode = (current ?? previous).UnderlyingCode,
|
||
ExDividendDate = info.ExDividendDate,
|
||
EffectiveDate = info.EffectiveDate,
|
||
GiveCashAmount = info.GiveCashAmount,
|
||
GiveShareAmount = info.GiveShareAmount,
|
||
Split = info.Split,
|
||
RationedSharesAmount = info.RationedSharesAmount,
|
||
RationedSharesPrice = info.RationedSharesPrice,
|
||
BeforeNotional = previous?.PosiNotionalValue ?? 0m,
|
||
BeforePrice = previous?.PosiGrossPrice ?? 0m,
|
||
BeforeQuantity = previous?.PosiQuantity ?? 0m,
|
||
AfterNotional = applied ? current?.PosiNotionalValue ?? 0m : 0m,
|
||
AfterPrice = applied ? current?.PosiGrossPrice ?? 0m : 0m,
|
||
AfterQuantity = applied ? current?.PosiQuantity ?? 0m : 0m,
|
||
BeforePendingDividend = previous?.PosiDividendSum ?? 0m,
|
||
AfterPendingDividend = applied ? current?.PosiDividendSum ?? 0m : 0m,
|
||
CashFlowChange = applied ? (current?.RealizedDividend ?? 0m) - (previous?.RealizedDividend ?? 0m) : 0m,
|
||
Applied = applied,
|
||
};
|
||
}
|
||
|
||
public static bool ShouldCreateCorporateActionEvent(
|
||
IEnumerable<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)
|
||
{
|
||
// 计算除权系数 - adjustCashDividendPrice = false (现金分红模式)
|
||
var factors = DividendService.CalculateCorporateActionFactors(
|
||
dividendInfo,
|
||
closePrice,
|
||
dividendTaxRate,
|
||
adjustCashDividendPrice: false);
|
||
if (factors.PriceRatio <= 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"标的【{dividendInfo?.UnderlyingCode}】计算得到无效除权系数");
|
||
}
|
||
|
||
var adjustedQuantity = Math.Round(quantity * factors.PriceRatio, 12, MidpointRounding.AwayFromZero);
|
||
var adjustedGrossPrice = Math.Round(grossPrice / factors.PriceRatio, grossPriceRound, MidpointRounding.AwayFromZero);
|
||
var adjustedNetPrice = Math.Round(netPrice / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
var adjustedNetFeePrice = netFeePrice.HasValue
|
||
? Math.Round(netFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||
: (decimal?)null;
|
||
var adjustedNetNoFeePrice = netNoFeePrice.HasValue
|
||
? Math.Round(netNoFeePrice.Value / factors.PriceRatio, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero)
|
||
: (decimal?)null;
|
||
return new CorporateActionValues(
|
||
adjustedQuantity,
|
||
adjustedGrossPrice,
|
||
adjustedNetPrice,
|
||
adjustedNetFeePrice,
|
||
adjustedNetNoFeePrice);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将一条真实生效日公司行为应用到盘中实时 TRS Stock/Fund 浮动腿。
|
||
/// 盘中先复制严格早于 valueDate 的 EOD,再调用此方法;因此重复调用时每次都会
|
||
/// 从同一份除权前 EOD 重新恢复,不会把 1000/100 重复变成 4000/25。
|
||
/// 例:8 月 14 日 EOD 为 1000/100,8 月 17 日生效的 10 送 10 会得到 2000/50。
|
||
/// 现金模式调用公式时使用 adjustCashDividendPrice=false,现金权益只进入分红字段,
|
||
/// 不改变 Stock/Fund 的期初价格。
|
||
/// </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}");
|
||
// 分派优先级与粒度说明见 ResolveInterestScenario;8 组合表驱动覆盖见 InterestEodScenarioDispatchTest。
|
||
// 仅观察日两个分支把返回值收进 autoInterests(→资金记录)——分派错序=静默少结。
|
||
if (observationInterval != null && !hasSwap)
|
||
{
|
||
if (!hasClose)//当日无平仓
|
||
{
|
||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
|
||
if (_autoInterests.Count > 0)
|
||
{
|
||
autoInterests.AddRange(_autoInterests);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: true, grossPrice, orginPv);
|
||
if (_autoInterests.Count > 0)
|
||
{
|
||
autoInterests.AddRange(_autoInterests);
|
||
}
|
||
}
|
||
}
|
||
else if (hasSwap)//当日有互换,根据互换事件重新生成
|
||
{
|
||
SaveEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, swapEvents);
|
||
}
|
||
else if (hasClose)
|
||
{
|
||
SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: false, grossPrice, orginPv);
|
||
}
|
||
else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息
|
||
{
|
||
SaveEodInterestPositionCopy(eodPosition, tdEodPosition, settleDate, td, interest, lastEodSwap, true, posiTotalNotional, grossPrice, orginPv);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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,
|
||
IReadOnlyCollection<eod_swap_position> corporateActionBeforePositions = null)
|
||
{
|
||
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);
|
||
var corporateActionBeforeQuantity = corporateActionBeforePositions?
|
||
.FirstOrDefault(x => x.PositionId == posi.id)?.PosiQuantity;
|
||
eod_swap_position eodPosi = new eod_swap_position();
|
||
if (eodPosition == null)
|
||
{
|
||
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||
}
|
||
else if (unwindEvents.Count() == 0)
|
||
{
|
||
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate, corporateActionBeforeQuantity);
|
||
}
|
||
else
|
||
{
|
||
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents, corporateActionBeforeQuantity);
|
||
}
|
||
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
|
||
list.Add(eodPosi);
|
||
}
|
||
Log.Info($"================{settleDateStr}浮动腿归档结束==================");
|
||
return list;
|
||
}
|
||
/// <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>
|
||
/// EOD 追保腿化·方案A:SettleAdditionalMargin 在当日 SwapPositionCompose/SwapEodCompose 之后生成 mode6 追保腿
|
||
/// (增量依赖当日 trade_span,无法前移),当日快照已落表,显式补写当日 eod_swap_position 行,
|
||
/// 使当日报表明细(风险页 PostionMarginGain 等按快照腿汇总的列)不漏计。
|
||
/// 字段填充参照 SaveEodInterestPosition 新腿形态(无当日流水、无上日归档:利息/损益字段为 0,TdCurrency 取当日汇率)。
|
||
/// 幂等:先清(PositionId+ValueDate)后建;下一结算日 SwapPositionCompose 先清(ClearSwapPositions ValueDate>=当日)
|
||
/// 再从实时腿重建,补写行不会跨日残留。
|
||
/// </summary>
|
||
public void SaveEodAdditionalMarginPosition(trade td, swap_position leg, DateTime valueDate)
|
||
{
|
||
var existing = DbContext.eod_swap_position
|
||
.Where(x => x.SwapTradeId == td.id && x.PositionId == leg.id && x.ValueDate == valueDate)
|
||
.ToList();
|
||
DbContext.eod_swap_position.RemoveRange(existing);
|
||
var row = new eod_swap_position
|
||
{
|
||
ClientId = td.ClientId,
|
||
SwapTradeId = td.id,
|
||
PosiStartDate = leg.PosiStartDate,
|
||
PosiMatuirityDate = td.ExerciseDate,
|
||
ValueDate = valueDate,
|
||
PositionId = leg.id,
|
||
PosiStatus = 0,
|
||
Invalid = false
|
||
};
|
||
UpdateDbOption(row);
|
||
//持仓内容-利息腿(无当日流水,FloatRate 取 0)
|
||
CopyInterestLegFields(row, leg, 0);
|
||
row.InterestFeePending = 0;
|
||
var ratio = DirectionRatio.InterestLegPnl(leg.InterestDirection, leg.InterestMode);
|
||
row.SwapPositionValue = PositionValueCalc.Calc(row.InterestProfitSum, row.PosiProfitSum, ratio);
|
||
row.RealizedPnl = row.RealizedInterest + row.RealizedInterestFee;
|
||
row.TdCurrency = Convert.ToDecimal(GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
|
||
DirectionRatio.RateType(leg.InterestDirection)));
|
||
PersistEodSwapPosition(row);
|
||
SaveAllChanges();
|
||
}
|
||
/// <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:本次事件发生前理论应结的高精度利息。
|
||
// manualSettledInterestAmount:swap_flow_event 实际落库的手工结息,金额已按分处理。
|
||
decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount);
|
||
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
|
||
decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount);
|
||
decimal autoSettledInterestAmount = 0m;
|
||
if (autoSwap && interests.Count > 0)
|
||
{
|
||
autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||
var autoInterest = interests[0];
|
||
autoInterest.InterestAmount = autoSettledInterestAmount;
|
||
autoInterest.InterestClosePnL = autoSettledInterestAmount
|
||
* DirectionRatio.ReceivePay(autoInterest.InterestDirection);
|
||
}
|
||
newEodPayPosition.ValueDate = valueDate;
|
||
newEodPayPosition.PositionId = position.id;
|
||
UpdateDbOption(newEodPayPosition);
|
||
newEodPayPosition.Invalid = false;
|
||
//持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值:
|
||
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。
|
||
CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0);
|
||
// ── 持仓延续腿重置日再定盘(EQD-6968 自洽化)──
|
||
// 排除日取价已收口为"纯跳过":事件利率=末段已消费利率。但剩余持仓自当日起进入新计息周期,
|
||
// 快照 FloatRate 是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的
|
||
// 利率载体——平仓日恰为重置日时必须显式取当日新定盘(与 ByEod 日增路径的重置日行为同构)。
|
||
// 全平(剩余=0)/算尾(事件利率已是新定盘)/观察日(autoSwap 恒1已含当日)无需再定盘。
|
||
if (!autoSwap && !calcLast && posiNotionalValue > 0m
|
||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
|
||
&& SwapDealService.IsResetDay(valueDate, td.StartDate.Value, position.interest_rest_days ?? 1))
|
||
{
|
||
var ongoingFixing = ResolveOngoingResetFixing(position, valueDate);
|
||
SwapCalcTrace.Critical(
|
||
$"FIX EodCloseRefix 融资腿{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}");
|
||
newEodPayPosition.FloatRate = ongoingFixing;
|
||
}
|
||
//利息端估值用信息
|
||
// TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。
|
||
// 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。
|
||
newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode)
|
||
? position.InterestPrincipalFix
|
||
: position.InterestMode == (int)InterestModeEnum.标的期初全价
|
||
&& position.InterestType != (int)InterestTypeEnum.复利
|
||
? posiNotionalValue
|
||
: interests.Count > 0 ? interests.First().InterestPrincipal : 0;
|
||
if (interval != null)
|
||
{
|
||
newEodPayPosition.TdInterestRate = interval.Rate;
|
||
}
|
||
else
|
||
{
|
||
newEodPayPosition.TdInterestRate = flowEvents.FirstOrDefault()?.InterestRate ?? 0;
|
||
}
|
||
//当日已实现,平仓时已处理
|
||
newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee);
|
||
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
|
||
// TdCloseInterest 只表示当天真正结算出去的金额;部分平仓未结部分继续留在 InterestIncomeSum。
|
||
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
|
||
// intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用
|
||
// 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。
|
||
var intersetAcmount = InterestIncomeCalc.DailyAccrual(
|
||
newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
|
||
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
|
||
newEodPayPosition.TdInterestIncome = autoSwap
|
||
? intersetAcmount
|
||
: !hasPreviousEod
|
||
? interestAmountBeforeSettlement
|
||
: posiNotionalValue == 0m
|
||
? interestAmountBeforeSettlement - lastInterestIncomeSum
|
||
: lastRealizedInterest != 0m || lastRealizedInterestFee != 0m || !calcLast
|
||
? intersetAcmount
|
||
: TdInterestAmount - lastInterestIncomeSum;
|
||
if (!autoSwap
|
||
&& closePercent > 0m && closePercent < 1m
|
||
&& posiNotionalValue > 0m
|
||
&& position.InterestType == (int)InterestTypeEnum.复利
|
||
&& (position.InterestMode == (int)InterestModeEnum.合约名义本金规模
|
||
|| position.InterestMode == (int)InterestModeEnum.标的期初全价))
|
||
{
|
||
// 模式2(合约名义本金规模)和模式9(标的期初全价)都以名义本金
|
||
// 作为复利基数;算尾用平仓前全额当日利息再扣实际结算,
|
||
// 不算尾只计剩余本金,避免已平部分利息进入后续复利。
|
||
// fullPrincipal 是平仓前动态复利本金,仅用于判断平仓日应按全额还是剩余额计息。
|
||
var fullPrincipal = lastTdInterestPrincipal > 0m
|
||
? lastTdInterestPrincipal
|
||
: oriPosiNotionalValue;
|
||
// 当日计提按平仓前全额动态本金;跨日携带必须只留剩余仓位。
|
||
// calcLast=true 时,模式2返回本次已平部分本金,需反推剩余本金;
|
||
// 模式9返回的已是剩余本金,不能再次按比例放大(GLMS-20260421-0004)。
|
||
// calcLast=false 快速路径返回上一 EOD 全额本金,保留原剩余比例缩放。
|
||
var usesFullPreviousEodPrincipal = !calcLast
|
||
&& hasPreviousEod
|
||
&& (valueDate - eodPayPosition.ValueDate).Days == 1
|
||
&& (valueDate - position.PosiStartDate).Days % (position.interest_rest_days ?? 1) != 0;
|
||
if (calcLast
|
||
&& position.InterestMode == (int)InterestModeEnum.合约名义本金规模)
|
||
{
|
||
// 模式2的 InterestPrincipal 是已平部分,需反推平仓前全额后再取剩余;
|
||
// 模式9已直接返回剩余动态本金,再反推会把 30% 平仓后的本金放大 7/3 倍。
|
||
// 例如模式9的 212135529.97 已是剩余本金,错误反推会变成 494982903.27。
|
||
newEodPayPosition.TdInterestPrincipal *= (1m - closePercent) / closePercent;
|
||
}
|
||
else if (usesFullPreviousEodPrincipal)
|
||
{
|
||
newEodPayPosition.TdInterestPrincipal *= 1m - closePercent;
|
||
}
|
||
// 不算尾时,TdInterestPrincipal 已由计息器完成重置日待实现利息结转,
|
||
// 并在非重置日分支按剩余仓位调整;若再次用上日本金乘剩余比例,
|
||
// 会漏掉重置后已并入本金的待实现利息(如 2026-08-04 两笔 JIATT 交易)。
|
||
var accrualPrincipal = calcLast
|
||
? fullPrincipal
|
||
: newEodPayPosition.TdInterestPrincipal;
|
||
newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual(
|
||
accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
|
||
newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
|
||
}
|
||
if (!autoSwap
|
||
&& closePercent > 0m && closePercent < 1m
|
||
&& posiNotionalValue > 0m
|
||
&& position.InterestType == (int)InterestTypeEnum.单利
|
||
&& (position.InterestMode == (int)InterestModeEnum.合约名义本金规模
|
||
|| position.InterestMode == (int)InterestModeEnum.标的期初全价))
|
||
{
|
||
// 单利算尾当日仍按平仓前全额计提,跨日 EOD 本金只携带剩余持仓。
|
||
newEodPayPosition.TdInterestPrincipal = posiNotionalValue;
|
||
}
|
||
Log.Info($"InterestIncomeSum is {lastInterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" +
|
||
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
|
||
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
|
||
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
|
||
// pendingInterestBeforeSettlement 是“扣款前待实现”。普通平仓按上日待实现 + 当日新增;
|
||
// 自动互换的 interestAmountBeforeSettlement 已经是完整理论应结,不能再加一次上日值。
|
||
var pendingInterestBeforeSettlement = autoSwap
|
||
? interestAmountBeforeSettlement
|
||
: lastInterestIncomeSum + newEodPayPosition.TdInterestIncome;
|
||
var pendingInterestFeeBeforeSettlement = eodPayPosition.InterestFeeSum
|
||
+ newEodPayPosition.TdInterestFee;
|
||
// InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。
|
||
// 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。
|
||
newEodPayPosition.InterestIncomeSum = closePercent == 1
|
||
&& EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
|
||
? 0m
|
||
: EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||
newEodPayPosition.InterestFeeSum = closePercent == 1
|
||
&& EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee)
|
||
? 0m
|
||
: EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||
//持仓内容-利息腿-损益统计(本方视角)
|
||
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
|
||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||
//持仓价值
|
||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||
|
||
Log.Info($"InterestIncomeSum is {eodPayPosition.InterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" +
|
||
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
|
||
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
|
||
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
|
||
//累计已实现(滚存语义见 FinalizeInterestEodRoll:只增不回滚)
|
||
FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection);
|
||
Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||
PersistEodSwapPosition(newEodPayPosition);
|
||
return interests;
|
||
}
|
||
|
||
/// <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, decimal? corporateActionBeforeQuantity = null)
|
||
{
|
||
if (curretEod == null)
|
||
{
|
||
curretEod = eod.Clone();
|
||
curretEod.id = 0;
|
||
curretEod.ValueDate = valueDate;
|
||
}
|
||
var um = GetUnderlyingData(eod.UnderlyingCode);
|
||
if (um == null)
|
||
{
|
||
return curretEod;
|
||
}
|
||
var dealDate = curretEod.ValueDate;
|
||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
|
||
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
|
||
decimal tax = um.ValueAddedTax ?? 0;
|
||
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||
{
|
||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||
}
|
||
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
|
||
// 分红递推过程常驻记录(快速定位):窗口/数量/税率/当日新计/累计前后值——
|
||
// 配合 BondPaymentService 的[分红-登记日口径]窗口命中日志,构成"命中哪些登记日→算出多少→账滚到多少"全链
|
||
Log.Info($"[分红-EOD计提Copy] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
|
||
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
|
||
$"TdPosiDividend={curretEod.TdPosiDividend} PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
|
||
curretEod.PosiQuantity = eod.PosiQuantity;
|
||
if (curretEod.PosiStatus == 1)
|
||
{
|
||
curretEod.PosiNotionalValue = 0;
|
||
}
|
||
curretEod.UnderlyingPrice = price;
|
||
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
|
||
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
|
||
//curretEod.TdPosiDividend = 0;
|
||
//curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend;
|
||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||
curretEod.TdCloseFee = 0;
|
||
curretEod.TdCloseQty = 0;
|
||
curretEod.TdCloseMtmPnl = 0;
|
||
// 需要计算平仓盈亏分红
|
||
curretEod.TdCloseDividend = 0;
|
||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||
curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend;
|
||
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
|
||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||
//持仓价值
|
||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||
UpdateDbOption(curretEod);
|
||
curretEod.Invalid = false;
|
||
if (curretEod.id == 0)
|
||
{
|
||
PersistEodSwapPosition(curretEod);
|
||
}
|
||
return curretEod;
|
||
}
|
||
|
||
/// <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, decimal? corporateActionBeforeQuantity = null)
|
||
{
|
||
if (curretEod == null)
|
||
{
|
||
curretEod = eod.Clone();
|
||
curretEod.TdPosiDividend = 0;
|
||
curretEod.id = 0;
|
||
curretEod.ValueDate = valueDate;
|
||
}
|
||
|
||
var um = GetUnderlyingData(eod.UnderlyingCode);
|
||
if (um == null)
|
||
{
|
||
return curretEod;
|
||
}
|
||
var dealDate = curretEod.ValueDate;
|
||
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
|
||
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
|
||
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
|
||
// 历史遗留死代码已删(2026-08-16,论证+边界测试见 DividendEodNoDoubleCountTest.脏数据边界_*):
|
||
// todayConsumedDividend / originNotional / totalPayment / totalInterest 自 0910969e(2026-07-02
|
||
// 改递推式) 起计算结果从未被消费,仅残留一次全历史 CalcBondPayment 只读查询+日志副作用,
|
||
// 且构成脏数据(OriginalStockEqvNotional=null/PosiNetPrice=0)下的 EOD 崩溃点。回退=git revert 本提交。
|
||
decimal tax = um.ValueAddedTax ?? 0;
|
||
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition);
|
||
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
|
||
curretEod.UnderlyingPrice = price;
|
||
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
|
||
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
|
||
curretEod.TdPosiDividend = 0;
|
||
// 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红
|
||
// 修改,互换事件会影响待实现的分红的,现在要算上
|
||
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
|
||
{
|
||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
|
||
}
|
||
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
|
||
// 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红)
|
||
curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn);
|
||
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
|
||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||
var closeQty = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList().Sum(s => s.Quantity);
|
||
|
||
curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend;
|
||
|
||
// 分红与互换解耦:持仓>0时待实现分红用递增模式(前日+当天新计-当天实现),
|
||
// 与 CopyEodPosition 的逐天递增口径一致,避免从头重算的舍入累积差异。
|
||
if (curretEod.PosiQuantity > 0)
|
||
{
|
||
curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend - curretEod.TdCloseDividend;
|
||
}
|
||
else
|
||
{
|
||
curretEod.PosiDividendSum = 0;
|
||
}
|
||
// 分红递推过程常驻记录(快速定位):当日事件路径含实现扣减(前日+新计-当日实现)
|
||
Log.Info($"[分红-EOD计提Update] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
|
||
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
|
||
$"TdPosiDividend={curretEod.TdPosiDividend} TdCloseDividend={curretEod.TdCloseDividend} " +
|
||
$"PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
|
||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
|
||
|
||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||
if (curretEod.PosiStatus == 1)
|
||
{
|
||
curretEod.PosiNotionalValue = 0;
|
||
}
|
||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||
//持仓价值
|
||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||
UpdateDbOption(curretEod);
|
||
curretEod.Invalid = false;
|
||
if (curretEod.id == 0)
|
||
{
|
||
PersistEodSwapPosition(curretEod);
|
||
}
|
||
return curretEod;
|
||
}
|
||
|
||
/// <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, decimal? corporateActionBeforeQuantity = null)
|
||
{
|
||
eod_swap_position curretEod = new eod_swap_position();
|
||
var um = GetUnderlyingData(position.UnderlyingCode);
|
||
if (um == null)
|
||
{
|
||
return curretEod;
|
||
}
|
||
var dealDate = settleDate;
|
||
curretEod.ValueDate = settleDate;
|
||
curretEod.PosiStartDate = position.PosiStartDate;
|
||
curretEod.PosiMatuirityDate = td.ExerciseDate.Value;
|
||
curretEod.SwapTradeId = td.id;
|
||
curretEod.PositionId = position.id;
|
||
curretEod.ClientId = td.ClientId;
|
||
int shortRatio = DirectionRatio.LongShort(position.PositionType);
|
||
int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
|
||
curretEod.PositionType = position.PositionType;
|
||
var eod = new eod_swap_position()
|
||
{
|
||
ContractSize = position.ContractSize,
|
||
PositionType = position.PositionType,
|
||
PosiDirection = position.PosiDirection,
|
||
PosiFeePending = 0,
|
||
PosiNetPrice = position.PosiNetPrice,
|
||
PosiGrossPrice = position.PosiGrossPrice,
|
||
PosiNetFeePrice = position.PosiNetFeePrice,
|
||
PosiNetNoFeePrice = position.PosiNetNoFeePrice,
|
||
};
|
||
curretEod.PosiDirection = position.PosiDirection;
|
||
curretEod.UnderlyingCode = position.UnderlyingCode;
|
||
curretEod.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||
curretEod.SwapTradeId = position.SwapTradeId;
|
||
curretEod.ContractSize = position.ContractSize;
|
||
curretEod.CountRatio = position.CountRatio;
|
||
curretEod.PosiTradingFee = position.PosiTradingFee;
|
||
curretEod.UnderlyingPrice = GetSwapValuationPrice(position.UnderlyingCode, dealDate, out decimal vobp);
|
||
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, position);
|
||
curretEod.dv01 = Dv01Helper.CalcDv01(curretEod.UnderlyingCode, curretEod.PosiQuantity, curretEod.PosiDirection, curretEod.PositionType, vobp);
|
||
//if (settleDate == td.TradeDate)
|
||
//{
|
||
// curretEod.UnderlyingPrice = curretEod.PosiGrossPrice;
|
||
// //curretEod.TdCloseMtmPnl = 0;
|
||
// //curretEod.TdCloseFee = 0;
|
||
//}
|
||
// TdCloseDividend 已由 SetPriceInfoByFlowEvent 设置
|
||
|
||
// 当日新增分红及待实现分红(有互换全量归0,开仓首日两者相同)
|
||
curretEod.TdPosiDividend = 0;
|
||
var hasSwapEvent = unwindEvents.Any(e => e.EventType == (int)SwapFlowEventTypeEnum.互换 || e.EventType == (int)SwapFlowEventTypeEnum.自动互换);
|
||
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
|
||
{
|
||
decimal tax = um.ValueAddedTax ?? 0;
|
||
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio, corporateActionBeforeQuantity);
|
||
payment = DividendCalc.AfterTax(payment, tax);
|
||
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
|
||
curretEod.TdPosiDividend = payment;
|
||
curretEod.PosiDividendSum = payment;
|
||
}
|
||
|
||
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
|
||
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
|
||
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
|
||
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
|
||
curretEod.RealizedDividend = curretEod.TdCloseDividend;
|
||
curretEod.RealizedFee = curretEod.TdCloseFee;
|
||
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
|
||
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
|
||
if (curretEod.PosiStatus == 1)
|
||
{
|
||
curretEod.PosiNotionalValue = 0;
|
||
}
|
||
//持仓价值
|
||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
|
||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||
UpdateDbOption(curretEod);
|
||
curretEod.Invalid = false;
|
||
PersistEodSwapPosition(curretEod);
|
||
return curretEod;
|
||
}
|
||
/// <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>
|
||
/// 查询 EQD-7084 新“框架合约”字段。
|
||
/// 旧查询负责筛选、排序、分页及旧字段计算;新字段只基于当前页对应的日终腿补充计算,
|
||
/// 避免改变旧接口的返回口径。
|
||
/// </summary>
|
||
public SearchListResult<EodSwapRiskNewResponse> SearchEodSwapNewList(EodSwapQueryRequest req)
|
||
{
|
||
// 新 Tab 与旧 Tab 共享同一套权限、筛选、排序和分页边界;先复用旧查询,
|
||
// 再只替换需求明确调整的展示字段,避免新接口悄然改变旧口径或查询范围。
|
||
var oldResult = SearchEodSwapList(req);
|
||
var oldRows = oldResult.rows?.ToList() ?? new List<EodSwapResponse>();
|
||
var tradeIds = oldRows.Select(x => x.position.SwapTradeId).Distinct().ToList();
|
||
var valueDates = oldRows.Select(x => x.position.ValueDate).Distinct().ToList();
|
||
|
||
if (tradeIds.Count == 0)
|
||
{
|
||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult,
|
||
Enumerable.Empty<EodSwapRiskNewResponse>());
|
||
}
|
||
|
||
// 当前页的交易、日终明细和扩展信息各批量读取一次,随后在内存按“交易 + 日终日”配对。
|
||
// 不在 rows.Select 内查询数据库,避免分页结果产生 N+1 查询。
|
||
var trades = DbContext.trade
|
||
.Where(x => tradeIds.Contains(x.id))
|
||
.Select(x => new { x.id, x.StartDate, x.ExerciseDate })
|
||
.ToDictionary(x => x.id);
|
||
var eodPositionDetails = DbContext.eod_swap_position
|
||
.Where(x => tradeIds.Contains(x.SwapTradeId)
|
||
&& valueDates.Contains(x.ValueDate)
|
||
&& !x.Invalid)
|
||
.ToList();
|
||
var tradeExtends = DbContext.trade_extend
|
||
.Where(x => tradeIds.Contains(x.TradeId))
|
||
.ToList();
|
||
|
||
var rows = oldRows.Select(item =>
|
||
{
|
||
// 同一交易可出现在多个日终日;必须同时匹配 ValueDate,不能把其他日期的腿混入本行。
|
||
var details = eodPositionDetails
|
||
.Where(x => x.SwapTradeId == item.position.SwapTradeId
|
||
&& x.ValueDate == item.position.ValueDate)
|
||
.ToList();
|
||
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||
var interestLegs = details.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
|
||
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
|
||
// 缺少扩展信息时按“期间支付”处理,和旧接口的默认值保持一致。
|
||
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
|
||
trades.TryGetValue(item.position.SwapTradeId, out var tradeInfo);
|
||
|
||
return new EodSwapRiskNewResponse
|
||
{
|
||
position = item.position,
|
||
TradeDate = item.TradeDate,
|
||
SwapTradeNo = item.SwapTradeNo,
|
||
ClientName = item.ClientName,
|
||
StructureType = item.StructureType,
|
||
AssetBookName = item.AssetBookName,
|
||
ClientId = item.ClientId,
|
||
SwapTradeTypeStr = item.SwapTradeTypeStr,
|
||
UnderlyingType = item.UnderlyingType,
|
||
PeriodAmount = item.PeriodAmount,
|
||
FloatingUnrealizedPnl = item.FloatingUnrealizedPnl,
|
||
InterestPaymentMethod = item.InterestPaymentMethod,
|
||
MaturityNettingValuation = item.MaturityNettingValuation,
|
||
PeriodPaymentValuation = item.PeriodPaymentValuation,
|
||
MarginInterestGain = item.MarginInterestGain,
|
||
MarginInterestLoss = item.MarginInterestLoss,
|
||
// 所有 EQD-7084 差异集中在 NewFields;上方复制的旧字段用于保留原报表的
|
||
// 基本信息、DV、期间金额及已实现收益,前端再将六个差异列绑定到 NewFields。
|
||
NewFields = CalculateEodSwapRiskNewFields(
|
||
floatingLegs,
|
||
interestLegs,
|
||
item.StructureType,
|
||
item.position.NotionalValue,
|
||
tradeInfo?.StartDate,
|
||
tradeInfo?.ExerciseDate,
|
||
item.PeriodAmount,
|
||
dividendPayDate)
|
||
};
|
||
}).ToList();
|
||
|
||
return new SearchListResult<EodSwapRiskNewResponse>(oldResult, rows);
|
||
}
|
||
|
||
/// <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>
|
||
/// 计算 EQD-7084 新“框架合约”Tab 的字段口径。
|
||
/// 纯函数只依赖日终浮动腿、利息腿和交易级展示参数,供查询接口及无库单测共用。
|
||
/// </summary>
|
||
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
|
||
IEnumerable<eod_swap_position> floatingLegs,
|
||
IEnumerable<eod_swap_position> interestLegs,
|
||
string structureType,
|
||
decimal notionalValue,
|
||
DateTime? startDate,
|
||
DateTime? ExerciseDate,
|
||
decimal periodAmount,
|
||
int dividendPayDate)
|
||
=> EodPnlCalculator.CalculateEodSwapRiskNewFields(
|
||
floatingLegs,
|
||
interestLegs,
|
||
structureType,
|
||
notionalValue,
|
||
startDate,
|
||
ExerciseDate,
|
||
periodAmount,
|
||
dividendPayDate);
|
||
|
||
/// <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 由标的资产类型决定(债券价格以小数保存,展示时转为百分比价格),
|
||
// 与存储层 GetStorageDeliveryPriceRound / GetSwapValuationPrice 的 IsBond 口径一致,
|
||
// 不依赖簿记结构类型 StructureType。
|
||
SetPosiPrice(item.position);
|
||
}
|
||
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)
|
||
{
|
||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
|
||
if (um != null && um.IsBond())
|
||
{
|
||
position.PosiNetPrice *= 100;
|
||
position.UnderlyingPrice *= 100;
|
||
position.PosiGrossPrice *= 100;
|
||
position.PosiNetFeePrice *= 100;
|
||
position.PosiNetNoFeePrice *= 100;
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <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();
|
||
}
|
||
}
|
||
}
|