Files
zszq-trs/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
T
hjhan ef37c8e71d refactor(swap): 合并多空名义本金参数为单一 posiTotalNotional 并加固测试
- DealInterests 的 posiLongNotionalValue + posiShortNotionalValue 合并为 posiTotalNotional(调用点以 posiLongNotional+posiShortNotional 求和传入),净减一个参数

- SwapDealService / SwapEodPositionService / InterestCalcRequest 同步收敛多空死管道参数

- 19 个测试调用点适配新签名

- SwapEodPositionServiceIntegrationTest 参数计数断言由裸数字改为参数名集合断言(CollectionAssert.AreEquivalent,对增删/重排/改名敏感)
2026-08-14 17:30:46 +08:00

2933 lines
174 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BaseOUDAL;
using Newtonsoft.Json;
using NPOI.POIFS.Properties;
using System;
using System.Linq.Expressions;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Helpers;
using YLErp.Model;
using YLErp.Model.Enum;
using YLErp.Models;
using YLErp.Modules.DataProviderModule;
using YLErp.Modules.EodModule;
using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.SwapModule.ReturnLegs;
using YLErp.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)
{
}
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);
// 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.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).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;测试: 返回固定值)</summary>
protected virtual decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
return new BondPaymentService(UserInfo).CalcPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
}
// ---- SwapPositionCompose 路径专用 seam(借鉴 testable 分支)----
/// <summary>查找收盘所需的活跃互换交易(生产: DbContext.trade.Where;测试: 内存列表)</summary>
protected virtual List<trade> FindActiveSwapTrades(DateTime settleDate, IEnumerable<int> clientIds)
{
var tradePredicate = PredicateBuilder.Create<trade>(n => n.ValidState != ConsGlobal.InValid
&& n.TradeType == "收益互换"
&& n.TradeDate <= settleDate
&& n.ExerciseDate >= settleDate
&& (n.TradeStatus == ConsTrade.确认成交 || n.UnWindDate >= settleDate)
);
if (clientIds != null && clientIds.Any())
{
tradePredicate = tradePredicate.And(x => clientIds.Contains(x.ClientId));
}
return DbContext.trade.Where(tradePredicate).ToList();
}
/// <summary>查找交易的所有持仓(含初始+实际,生产: DbContext.swap_position;测试: 内存列表)</summary>
protected virtual List<swap_position> FindAllSwapPositions(List<int> tradeIds)
{
return DbContext.swap_position.Where(t => tradeIds.Contains(t.SwapTradeId) && !t.Invalid).ToList();
}
/// <summary>批量查找交易扩展(生产: DbContext.trade_extend;测试: 内存列表)</summary>
protected virtual List<trade_extend> FindTradeExtends(List<int> tradeIds)
{
return DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
}
/// <summary>查找指定日期的日终汇总(生产: DbContext.eod_swap;测试: 内存列表)</summary>
protected virtual List<eod_swap> FindEodSwapsByDate(DateTime valueDate)
{
return DbContext.eod_swap.Where(x => x.ValueDate == valueDate).ToList();
}
/// <summary>查找交易在指定日期的完成流水事件(生产: DbContext.swap_flow_event;测试: 内存列表)</summary>
protected virtual List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate)
{
Expression<Func<swap_flow_event, bool>> eventExpression = x => x.SwapTradeId == swapTradeId
&& x.DataState == (int)SwapFlowDateStateEnum.完成
&& x.EventDate == settleDate;
return DbContext.swap_flow_event.Where(eventExpression).ToList();
}
protected virtual List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds)
{
return DbContext.swap_flow_event
.Where(x => tradeIds.Contains(x.SwapTradeId)
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
.ToList();
}
#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);
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>();//自动互换利息腿信息
//处理浮动腿
var curEodPosis = DealFloatPositions(posiList, realPosiList, eodPositions, todyEodPositions, settleDate, td, preSettleDate, flowEvents);
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);
//获取自动互换的 interval 信息,用于确定结算日期
IntervalModel autoInterval = null;
foreach (var interest in interestList)
{
autoInterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);
if (autoInterval != null)
break;
}
// 自动互换(仅利息/预付金,不含分红)
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval);
// 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关
DealDividends(curEodPosis, td, settleDate, tradeExtend);
//多空组合判断是否已到到期日且无持仓信息
if (longShort && td.ExerciseDate.Value == settleDate && allPositionQty == 0)
{
td.TradeStatus = "已到期";
td.UnWindDate = settleDate;
}
SaveAllChanges();
});
}
}
/// <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 insterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);//自动互换观察日信息
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($"insterval is {insterval},hasSwap is {hasSwap},hasClose is {hasClose}");
if (insterval != null && !hasSwap)
{
if (!hasClose)//当日无平仓
{
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
if (_autoInterests.Count > 0)
{
autoInterests.AddRange(_autoInterests);
}
}
else
{
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiTotalNotional, swapEvents, closeNational, 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, insterval, posiTotalNotional, swapEvents, closeNational, false, grossPrice, orginPv);
}
else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息
{
SaveEodInterestPositionCopy(eodPosition, tdEodPosition, settleDate, td, interest, lastEodSwap, true, posiTotalNotional, grossPrice, orginPv);
}
}
}
/// <summary>
/// 处理浮动腿归档
/// </summary>
/// <param name="posiList"></param>
/// <param name="eodPositions"></param>
/// <param name="todyEodPositions"></param>
/// <param name="settleDate"></param>
/// <param name="td"></param>
/// <param name="longShort"></param>
/// <param name="flowEvents"></param>
/// <param name="unwindEvent"></param>
protected List<eod_swap_position> DealFloatPositions(List<swap_position> posiList,
List<swap_position> realPosiList,
List<eod_swap_position> eodPositions,
List<eod_swap_position> todyEodPositions,
DateTime settleDate,
trade td,
DateTime preSettleDate,
List<swap_flow_event> flowEvents)
{
string settleDateStr = settleDate.ToString("yyyy-MM-dd");
string preSettleDateStr = preSettleDate.ToString("yyyy-MM-dd");
Log.Info($"================开始处理{settleDateStr}浮动腿归档==================");
List<eod_swap_position> list = new List<eod_swap_position>();
Log.Info($"浮动腿归档各项参数如下:\n " +
$"settleDate为:{settleDateStr} \n" +
$"preSettleDate为:{preSettleDateStr} \n " +
$"td为:{td.id} \n " +
$"posiList为:{JsonHelper.Serialize(posiList)} \n " +
$"realPosiList为:{JsonHelper.Serialize(realPosiList)} \n " +
$"eodPositions为:{JsonHelper.Serialize(eodPositions)} \n " +
$"todyEodPositions为:{JsonHelper.Serialize(todyEodPositions)} \n " +
$"flowEvents为:{JsonHelper.Serialize(flowEvents)} \n ");
foreach (var posi in posiList)
{
Log.Info($"posi为:{JsonHelper.Serialize(posi, false)}");
var eodPosition = eodPositions.Where(x => x.PositionId == posi.id).FirstOrDefault();//上一日日终持仓信息
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == posi.id);//当前结算日日终持仓信息
var unwindEvents = flowEvents.Where(x => x.PositionId == posi.id).ToList();//当前日平仓信息
var realPosition = realPosiList.FirstOrDefault(s => s.PositionId == posi.id);
eod_swap_position eodPosi = new eod_swap_position();
if (eodPosition == null)
{
eodPosi = SaveCurrentEodInitalPosi(posi, td, settleDate, preSettleDate, unwindEvents);
}
else if (unwindEvents.Count() == 0)
{
eodPosi = CopyEodPosition(eodPosition, tdEodPosition, td, settleDate, preSettleDate);
}
else
{
eodPosi = UpdateEodPosition(posi, eodPosition, tdEodPosition, td, settleDate, preSettleDate, unwindEvents);
}
Log.Info($"eodPosi为:{JsonHelper.Serialize(eodPosi, false)}");
list.Add(eodPosi);
}
Log.Info($"================{settleDateStr}浮动腿归档结束==================");
return list;
}
/// <summary>
/// 处理自动互换数据
/// </summary>
/// <param name="autoInterests"></param>
/// <param name="td"></param>
/// <param name="settleDate"></param>
/// <param name="swapDeals"></param>
/// <param name="interval">自动互换观察日信息,用于获取结算日期</param>
/// <param name="curEodPositions">当日浮动端EOD持仓</param>
/// <param name="tradeExtend">交易扩展信息</param>
/// <summary>
/// 自动互换(仅利息/预付金,不含分红)
/// </summary>
private void DealAutoInterests(List<swap_flow_event> autoInterests, trade td, DateTime settleDate, DateTime? preDealDate, decimal StockEqvNotional, IntervalModel interval)
{
if (autoInterests.Count == 0) return;
UnwindData unwindData = new UnwindData();
unwindData.SwapTradeId = td.id;
unwindData.ValueDate = settleDate;
unwindData.StartDate = preDealDate ?? td.StartDate.Value;
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
unwindData.PosiNotionalValue = StockEqvNotional;
unwindData.PayDate = settleDate;
autoInterests.ForEach(x => x.PayDate = settleDate);
var premiumModes = new List<int>() { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
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;
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);
}
// 预付金腿:单独插入一条资金记录(系统操作_预付金返息)
if (unwindData.SwapMarginRebatePnl != 0)
{
clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), 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>
/// 互换更新实时持仓信息
/// </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;
var swapInsertEnum = SwapPushDataEnum.收益互换交易新增;
var swapPosiEnum = SwapPushDataEnum.互换资产交易新增;
if (fposition != null)
{
swapInsertEnum = SwapPushDataEnum.收益互换交易修改;
swapPosiEnum = SwapPushDataEnum.互换资产交易修改;
}
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>
/// 产生互换用
/// </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;
//持仓内容-利息腿
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 = flowEvents.FirstOrDefault()?.FloatRate ?? 0;
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
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);
//累计已实现
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(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
PersistEodSwapPosition(newEodPayPosition);
}
/// <summary>
/// 自动互换用,当日无互换,当日无平仓
/// </summary>
/// <param name="eodPayPosition">上一日日终持仓</param>
/// <param name="newEodPayPosition">当前收盘日日终持仓</param>
/// <param name="position">利息腿信息</param>
/// <param name="td">框架合约</param>
/// <param name="valueDate">当前收盘日</param>
/// <param name="interval">当前观察日</param>
/// <param name="preDealDate">上一平仓/互换日期</param>
/// <param name="closeAmount">当日平仓金额</param>
/// <param name="lastEodSwap">上一日终框架合约估值</param>
protected List<swap_flow_event> SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiTotalNotional, decimal grossPrice, decimal orginPv)
{
Log.Info($"[SaveAutoEodInterestPosition] 开始执行 - valueDate: {valueDate:yyyy-MM-dd}, td.id: {td?.id}, position.id: {position?.id}");
// 详细的参数验证
if (td == null)
{
Log.Info("[SaveAutoEodInterestPosition] 参数验证: td (trade) 为 null");
throw new ArgumentNullException(nameof(td), "交易对象不能为null");
}
if (position == null)
{
Log.Info($"[SaveAutoEodInterestPosition] 参数验证: position 为 null, td.id: {td.id}");
throw new ArgumentNullException(nameof(position), "持仓对象不能为null");
}
if (interval == null)
{
Log.Info($"[SaveAutoEodInterestPosition] 参数验证: interval 为 null, td.id: {td.id}, position.id: {position.id}");
throw new ArgumentNullException(nameof(interval), "观察日信息不能为null");
}
if (td.trade_extend == null)
{
Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend 为 null, td.id: {td.id}, td.TradeNumber: {td.TradeNumber}");
throw new ArgumentNullException("td.trade_extend", "交易扩展信息不能为null");
}
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
// 验证 ExtendObj
if (td.trade_extend.ExtendObj == null)
{
Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend.ExtendObj 为 null, td.id: {td.id}");
throw new ArgumentNullException("td.trade_extend.ExtendObj", "交易扩展对象不能为null");
}
var tradeExtend = td.trade_extend.ExtendObj;
decimal posiNotionalValue = posiTotalNotional;
decimal closePercent = 1;
var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
if (eodPayPosition == null)
{
eodPayPosition = new eod_swap_position();
eodPayPosition.ClientId = td.ClientId;
eodPayPosition.SwapTradeId = td.id;
eodPayPosition.PosiStartDate = td.StartDate.Value;
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
}
if (newEodPayPosition == null)
{
newEodPayPosition = eodPayPosition.Clone();
newEodPayPosition.id = 0;
}
List<swap_position> positions = new List<swap_position>();
positions.Add(position);
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
preEodPositions.Add(eodPayPosition);
// orginPv 在此仅对固定值腿(mode 1)生效;保证金腿(5/6)的 orginPv 虽在此赋值,
// 但 GetInterests 保证金分支已走 CalcMarginInterest(内部自算 orginPv=PreviousBalance),忽略此处传入值。
var interestModes = MarginModes.FixedAmountAndMargin;
if (interestModes.Contains(position.InterestMode))
{
orginPv = eodPayPosition.InterestPrincipalFix;
}
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiNotionalValue, closePercent, (int)SwapEventTypeEnum.自动互换, false, orginPv, true);
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
decimal tdInterestAmount = interests.Sum(x => x.TdInterestAmount);
// 自动互换的流水和客户资金都由 InterestClosePnL 汇总。先把实际结算收敛到两位,
// 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。
interests.ForEach(x =>
{
x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount);
x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL);
});
decimal settledInterestAmount = interests.Sum(x => x.InterestAmount);
newEodPayPosition.ValueDate = valueDate;
newEodPayPosition.PositionId = position.id;
UpdateDbOption(newEodPayPosition);
newEodPayPosition.Invalid = false;
//持仓内容-利息腿
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 = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0;
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
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);
//累计已实现
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(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
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?.InterestCalcMode?.EndsWith("1") ?? true;
// 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。
// 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。
var interests = CalcEodPostCloseSettleInterests(InterestCalcRequest.EodPostCloseSettle(
td, td.trade_extend, valueDate, valueDate, preEodPositions, positions,
posiNotionalValue, closeNational,
eventType, tdClose: false, orginPv, add: true, newCalcLast: autoSwap || calcLast));
// TdInterestAmount:计息器返回的全腿当日/累计参考值,用于拆出 EOD 的当日新增。
// interestAmountBeforeSettlement:本次事件发生前理论应结的高精度利息。
// manualSettledInterestAmountswap_flow_event 实际落库的手工结息,金额已按分处理。
decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount);
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount);
decimal autoSettledInterestAmount = 0m;
if (autoSwap && interests.Count > 0)
{
autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
var autoInterest = interests[0];
autoInterest.InterestAmount = autoSettledInterestAmount;
autoInterest.InterestClosePnL = autoSettledInterestAmount
* DirectionRatio.ReceivePay(autoInterest.InterestDirection);
}
newEodPayPosition.ValueDate = valueDate;
newEodPayPosition.PositionId = position.id;
UpdateDbOption(newEodPayPosition);
newEodPayPosition.Invalid = false;
//持仓内容-利息腿
newEodPayPosition.InterestDirection = position.InterestDirection;
newEodPayPosition.InterestMode = position.InterestMode;
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减。
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
// newEodPayPosition.InterestPrincipalFix *= (1 - closePercent);
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.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0;
newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode;
newEodPayPosition.interest_rest_days = position.interest_rest_days;
newEodPayPosition.interest_rule = position.interest_rule;
//利息端估值用信息
// 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}");
//累计已实现
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
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(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
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);
//累计已实现
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(eodPayPosition.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
PersistEodSwapPosition(newEodPayPosition);
}
/// <summary>
/// 持仓腿生成期初持仓及归档信息
/// </summary>
/// <param name="eodPayPosition">上一日终持仓信息</param>
/// <param name="td">合约框架</param>
/// <param name="eventFlow">最后一条事件流水</param>
/// <param name="netPrice">期初价格</param>
/// <param name="grossPrice">期初价格不含费</param>
/// <param name="payQty">剩余持仓数量</param>
/// <param name="tradingFee">开仓交易佣金费用</param>
/// <param name="posiNotionalValue">当前剩余名义本金</param>
/// <param name="dividendIn">当日浮动端分红</param>
/// <param name="tdDividendIn">当日浮动端平仓盈亏分红</param>
/// <param name="closeQty">当日平仓数量</param>
/// <param name="closeFee">当日平仓费用</param>
/// <param name="closeMtmPnl">当日浮动盈亏</param>
protected virtual decimal SaveEodPosition(eod_swap_position newEodPayPosition,
trade td,
swap_flow_event eventFlow,
decimal netPrice,
decimal grossPrice,
decimal netFeePrice,
decimal netNoFeePrice,
decimal payQty,
decimal tradingFee,
decimal posiNotionalValue,
decimal dividendIn,
decimal tdDividendIn,
decimal closeQty,
decimal closeFee,
decimal closeMtmPnl,
int posiType,
bool open)
{
payQty = Math.Abs(payQty);
int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负
int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType);
newEodPayPosition.ValueDate = eventFlow.PayDate.Value;
newEodPayPosition.PositionId = eventFlow.PositionId;
newEodPayPosition.ClientId = td.ClientId;
newEodPayPosition.SwapTradeId = td.id;
//持仓内容-浮动收益腿
newEodPayPosition.PosiDirection = eventFlow.PayDirection;
newEodPayPosition.PositionType = posiType;
newEodPayPosition.UnderlyingCode = eventFlow.UnderlyingCode;
newEodPayPosition.UnderlyingInstrumentType = eventFlow.UnderlyingInstrumentType;
newEodPayPosition.ContractSize = eventFlow.ContractSize;
newEodPayPosition.CountRatio = eventFlow.CountRatio;
newEodPayPosition.PosiNetPrice = netPrice;
newEodPayPosition.PosiGrossPrice = Math.Round(
grossPrice,
GetStorageDeliveryPriceRound(eventFlow.UnderlyingInstrumentType, eventFlow.UnderlyingCode),
MidpointRounding.AwayFromZero);
newEodPayPosition.PosiNetFeePrice = netFeePrice;
newEodPayPosition.PosiNetNoFeePrice = netNoFeePrice;
newEodPayPosition.PosiQuantity = payQty;
newEodPayPosition.PosiNotionalValue = posiNotionalValue;
newEodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
if (newEodPayPosition.PosiQuantity == 0)
{
newEodPayPosition.PosiMatuirityDate = eventFlow.PayDate.Value;
}
//else
//{
// newEodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
//}
newEodPayPosition.PosiFeePending = tradingFee;
//浮动端估值用信息
newEodPayPosition.UnderlyingPrice = UnderlyingCodePrice(newEodPayPosition.UnderlyingCode, eventFlow.EventDate, out decimal vobp);
newEodPayPosition.dv01 = Dv01Helper.CalcDv01(newEodPayPosition.UnderlyingCode, newEodPayPosition.PosiQuantity, newEodPayPosition.PosiDirection, newEodPayPosition.PositionType, vobp);
newEodPayPosition.UnderlyingMarketValue = MtmCalc.MarketValue(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio);
//当日已实现
newEodPayPosition.TdCloseQty = closeQty;
newEodPayPosition.TdChangedQty = 0;
newEodPayPosition.TdCloseMtmPnl = closeMtmPnl * ratio;
newEodPayPosition.TdCloseDividend = tdDividendIn * ratio;
newEodPayPosition.TdCloseFee = closeFee * ratio;
//持仓内容-浮动收益腿-损益统计(本方视角
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio);
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending);
//持仓价值
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum);
//累计已实现
newEodPayPosition.RealizedFee = closeFee;
newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl;
newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend;
EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition);
newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0;
UpdateDbOption(newEodPayPosition);
newEodPayPosition.Invalid = false;
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, eventFlow.EventDate
, seekPreday: true, currencyRateType: posiNotionalValue < 0 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
decimal posiTradingFee = 0;
if (open)//更新新开仓持仓腿信息,因为在生成开仓事件时,先生成了空的持仓腿信息
{
if (td.trade_extend.ExtendObj.NeedOpenFee && td.TradeDate == eventFlow.EventDate)//开仓
{
posiTradingFee = Math.Abs(newEodPayPosition.PosiTradingFee) * Convert.ToDecimal(currencyRate);
}
UpdateSwapPosition(newEodPayPosition, td.TradeNumber);
}
UpdateSwapPositionWithRealTime(newEodPayPosition);
PersistEodSwapPosition(newEodPayPosition);
return posiTradingFee;
}
/// <summary>
///当日无平仓,无互换,生成持仓腿日终归档,适用于上一日终存在
/// </summary>
/// <param name="eod_Swap_Positions">上一日日终归档信息</param>
/// <param name="todayPositions">当日日终归档信息</param>
/// <param name="swap_Deals">当日平仓/互换事件信息</param>
/// <param name="td">交易信息</param>
protected eod_swap_position CopyEodPosition(eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate)
{
if (curretEod == null)
{
curretEod = eod.Clone();
curretEod.id = 0;
curretEod.ValueDate = valueDate;
}
var um = GetUnderlyingData(eod.UnderlyingCode);
if (um == null)
{
return curretEod;
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
decimal tax = um.ValueAddedTax ?? 0;
if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
curretEod.PosiQuantity = eod.PosiQuantity;
if (curretEod.PosiStatus == 1)
{
curretEod.PosiNotionalValue = 0;
}
curretEod.UnderlyingPrice = price;
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
//curretEod.TdPosiDividend = 0;
//curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend;
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.TdCloseFee = 0;
curretEod.TdCloseQty = 0;
curretEod.TdCloseMtmPnl = 0;
// 需要计算平仓盈亏分红
curretEod.TdCloseDividend = 0;
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend;
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
UpdateDbOption(curretEod);
curretEod.Invalid = false;
if (curretEod.id == 0)
{
PersistEodSwapPosition(curretEod);
}
return curretEod;
}
/// <summary>
/// 更新虚拟交易费用
/// </summary>
/// <param name="curretEod"></param>
private void UpdateVTradingFee(eod_swap_position curretEod)
{
//int ratio = curretEod.PositionType == (int)PositionTypeFlag.Long ? -1 : 1;
//int shortRatio = curretEod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
//var priceFee = curretEod.PosiQuantity == 0 ? 0 : curretEod.UnderlyingPrice + curretEod.PosiFeePending / curretEod.PosiQuantity * ratio;
//curretEod.VTradingFee = -(priceFee - curretEod.PosiNetPrice - curretEod.UnderlyingPrice + curretEod.PosiGrossPrice) * curretEod.PosiNotionalValue * shortRatio;
curretEod.VTradingFee = curretEod.PosiFeePending * 2;
}
/// <summary>
/// 当日有平仓/互换,生成持仓腿日终归档,适用于上一日终存在
/// </summary>
/// <param name="eod"></param>
/// <param name="curretEod"></param>
/// <param name="td"></param>
/// <param name="valueDate"></param>
protected eod_swap_position UpdateEodPosition(swap_position swapPosition, eod_swap_position eod, eod_swap_position curretEod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
{
if (curretEod == null)
{
curretEod = eod.Clone();
curretEod.TdPosiDividend = 0;
curretEod.id = 0;
curretEod.ValueDate = valueDate;
}
var um = GetUnderlyingData(eod.UnderlyingCode);
if (um == null)
{
return curretEod;
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio);
decimal tax = um.ValueAddedTax ?? 0;
decimal totalInterest = DividendCalc.AfterTaxRaw(totalPayment, tax);
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
curretEod.UnderlyingPrice = price;
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
curretEod.TdPosiDividend = 0;
// 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红
// 修改,互换事件会影响待实现的分红的,现在要算上
if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0))
{
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio);
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
// 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红)
curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn);
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
var closeQty = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList().Sum(s => s.Quantity);
curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend;
// 分红与互换解耦:持仓>0时待实现分红用递增模式(前日+当天新计-当天实现),
// 与 CopyEodPosition 的逐天递增口径一致,避免从头重算的舍入累积差异。
if (curretEod.PosiQuantity > 0)
{
curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend - curretEod.TdCloseDividend;
}
else
{
curretEod.PosiDividendSum = 0;
}
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;
}
private decimal CalcConsumedDividend(eod_swap_position curretEod, List<swap_flow_event> events)
{
decimal consumedDividend = 0;
List<int> swapEventTypes = new List<int>() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
//这里要剔除掉平仓产生的分红
consumedDividend = events
.Where(x => x.SwapTradeId == curretEod.SwapTradeId
&& swapEventTypes.Contains(x.EventType)
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
.Sum(s => s.DividendIn);
return consumedDividend;
}
/// <summary>
/// 根据开平仓事件算价格及后付费用
/// </summary>
/// <param name="eod"></param>
/// <param name="curretEod"></param>
/// <param name="unwindEvents"></param>
public void SetPriceInfoByFlowEvent(eod_swap_position eod, eod_swap_position curretEod, List<swap_flow_event> unwindEvents, swap_position position)
{
if (eod.PosiDirection == 0)
{
return;
}
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList();
var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList();
decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity);
decimal openQty = openFlowEvents.Sum(s => s.Quantity);
curretEod.PosiQuantity = QtyRollforward.Calc(eod.PosiQuantity, openQty, unwindQty);
if (unwindEvents.Count == 0)
{
curretEod.PosiNetPrice = position.PosiNetPrice;
curretEod.PosiGrossPrice = position.PosiGrossPrice;
curretEod.PosiNetFeePrice = position.PosiNetFeePrice;
curretEod.PosiNetNoFeePrice = position.PosiNetNoFeePrice;
curretEod.PosiQuantity = position.PosiQuantity;
curretEod.PosiFeePending = -position.PosiTradingFeePending * directionRatio;
}
else
{
var eventTradingFee = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓 || x.EventType == (int)SwapFlowEventTypeEnum.平仓).Sum(s => s.TradingFeePending * (s.EventType == (int)SwapFlowEventTypeEnum.开仓 ? 1m : -1m));
curretEod.PosiFeePending = eod.PosiFeePending + eventTradingFee;
if (openFlowEvents.Count() == 0)
{
curretEod.PosiNetPrice = eod.PosiNetPrice;
curretEod.PosiGrossPrice = eod.PosiGrossPrice;
curretEod.PosiNetFeePrice = eod.PosiNetFeePrice;
curretEod.PosiNetNoFeePrice = eod.PosiNetNoFeePrice;
}
else //平仓数量一定<持仓数量
{
var posiQty = eod.PosiQuantity - unwindQty;
if (posiQty < 0)
{
posiQty = 0;
}
curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty);
curretEod.PosiGrossPrice = Math.Round(
curretEod.PosiGrossPrice,
GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode),
MidpointRounding.AwayFromZero);
curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty);
curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
}
curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize;
curretEod.PosiNotionalValue = Math.Round(curretEod.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
curretEod.TdCloseDividend = unwindEvents.Sum(x => x.DividendIn);
curretEod.TdCloseFee = unwindFlowEvents.Sum(x => x.TradingFee + x.TradingFeePending);
curretEod.TdCloseQty = unwindQty;
curretEod.TdCloseMtmPnl = unwindEvents.Sum(x => x.MarkClosePnl);
}
if (curretEod.PosiQuantity == 0)
{
curretEod.PosiFeePending = 0;
}
UpdateVTradingFee(curretEod);
}
/// <summary>
/// 单标的浮动腿 首次归档
/// </summary>
/// <param name="position"></param>
/// <param name="td"></param>
/// <param name="settleDate"></param>
protected eod_swap_position SaveCurrentEodInitalPosi(swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
{
eod_swap_position curretEod = new eod_swap_position();
var um = GetUnderlyingData(position.UnderlyingCode);
if (um == null)
{
return curretEod;
}
var dealDate = settleDate;
curretEod.ValueDate = settleDate;
curretEod.PosiStartDate = position.PosiStartDate;
curretEod.PosiMatuirityDate = td.ExerciseDate.Value;
curretEod.SwapTradeId = td.id;
curretEod.PositionId = position.id;
curretEod.ClientId = td.ClientId;
int shortRatio = DirectionRatio.LongShort(position.PositionType);
int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
curretEod.PositionType = position.PositionType;
var eod = new eod_swap_position()
{
ContractSize = position.ContractSize,
PositionType = position.PositionType,
PosiDirection = position.PosiDirection,
PosiFeePending = 0,
PosiNetPrice = position.PosiNetPrice,
PosiGrossPrice = position.PosiGrossPrice,
PosiNetFeePrice = position.PosiNetFeePrice,
PosiNetNoFeePrice = position.PosiNetNoFeePrice,
};
curretEod.PosiDirection = position.PosiDirection;
curretEod.UnderlyingCode = position.UnderlyingCode;
curretEod.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
curretEod.SwapTradeId = position.SwapTradeId;
curretEod.ContractSize = position.ContractSize;
curretEod.CountRatio = position.CountRatio;
curretEod.PosiTradingFee = position.PosiTradingFee;
curretEod.UnderlyingPrice = GetSwapValuationPrice(position.UnderlyingCode, dealDate, out decimal vobp);
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, position);
curretEod.dv01 = Dv01Helper.CalcDv01(curretEod.UnderlyingCode, curretEod.PosiQuantity, curretEod.PosiDirection, curretEod.PositionType, vobp);
//if (settleDate == td.TradeDate)
//{
// curretEod.UnderlyingPrice = curretEod.PosiGrossPrice;
// //curretEod.TdCloseMtmPnl = 0;
// //curretEod.TdCloseFee = 0;
//}
// TdCloseDividend 已由 SetPriceInfoByFlowEvent 设置
// 当日新增分红及待实现分红(有互换全量归0,开仓首日两者相同)
curretEod.TdPosiDividend = 0;
var hasSwapEvent = unwindEvents.Any(e => e.EventType == (int)SwapFlowEventTypeEnum.互换 || e.EventType == (int)SwapFlowEventTypeEnum.自动互换);
if (!hasSwapEvent && settleDate > td.StartDate.Value && curretEod.PosiQuantity > 0)
{
decimal tax = um.ValueAddedTax ?? 0;
decimal payment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio);
payment = DividendCalc.AfterTax(payment, tax);
//var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红
curretEod.TdPosiDividend = payment;
curretEod.PosiDividendSum = payment;
}
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = curretEod.TdCloseDividend;
curretEod.RealizedFee = curretEod.TdCloseFee;
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
if (curretEod.PosiStatus == 1)
{
curretEod.PosiNotionalValue = 0;
}
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
UpdateDbOption(curretEod);
curretEod.Invalid = false;
PersistEodSwapPosition(curretEod);
return curretEod;
}
/// <summary>
/// 获取标的收盘价格
/// </summary>
/// <param name="code">标的代码</param>
/// <param name="settleDate">收盘日</param>
/// <returns></returns>
public decimal UnderlyingCodePrice(string code, DateTime settleDate, out decimal vobp)
{
vobp = 0;
var data = DataCacheProvider.GetUnderlyingDataSource().GetData(code);
if (data == null)
{
return 0;
}
if (data.IsBond())
{
return Math.Round(BondPrice(data, settleDate, out vobp), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
}
var price = data.Price ?? 0;
if (EodPriceQueryService.TryGetEodPrice(settleDate, code, out var eodPrice))
{
price = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice);
}
return Math.Round(Convert.ToDecimal(price), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 获取债券收盘价格
/// </summary>
/// <param name="code"></param>
/// <param name="settleDate"></param>
/// <returns></returns>
public decimal BondPrice(underlying_manager data, DateTime settleDate, out decimal vobp)
{
vobp = 0;
var price = data.Price ?? 0;
if (EodPriceQueryService.TryGetBondEodPrice(settleDate, data.UnderlyingCode, out var eodPrice))
{
price = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice);
vobp = eodPrice.Vobp ?? 0;
}
else
{
price = price * Convert.ToDouble(ConsGlobal.bondPriceMultiple);
}
return Convert.ToDecimal(price);
}
/// <summary>
/// 框架合约估值
/// </summary>
/// <param name="td">互换交易</param>
/// <param name="settleDate">收盘日</param>
private void SaveEodSwap(trade td, DateTime settleDate, DateTime preSettleDate)
{
var eod_Swaps = DbContext.eod_swap.Where(x => x.SwapTradeId == td.id && x.ValueDate >= preSettleDate && x.ValueDate <= settleDate).ToList();
var eod_Swap = eod_Swaps.FirstOrDefault(x => x.ValueDate == settleDate);
var preEodSwap = eod_Swaps.FirstOrDefault(x => x.ValueDate == preSettleDate);
if (eod_Swap == null)
{
eod_Swap = new eod_swap();
}
var tradeSpan = DbContext.trade_span.FirstOrDefault(x => x.TradeId == td.id && x.ValueDate == settleDate);
// eod_swap 是交易级汇总;eod_swap_position 是浮动腿、利息腿和保证金腿的明细。
// 以下先按日终明细拆腿,再按框架合约展示口径汇总。
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).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.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).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.Where(x => x.SwapTradeId == tradeId && x.ValueDate == valueDate && !x.Invalid).ToList();
var flowEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.EventDate == valueDate && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.EventType == (int)SwapEventTypeEnum.平仓 && string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
closeModel.DealPositions = eodPositions.Where(x => x.TdCloseQty != 0).ToList();
closeModel.DealInterests = flowEvents;
return closeModel;
}
/// <summary>
/// 查询互换日终持仓风险-明细
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<SwapPositionResponse> SearchEodPositionList(EodSwapPositionQueryRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid);
var tradePredicate = PredicateBuilder.Create<trade>(n => n.TradeType == "收益互换"
&& n.ValidState != "InValid");
if (req.ValueDate.HasValue)
{
predicate = predicate.And(n => n.ValueDate == req.ValueDate);
//tradePredicate = tradePredicate.And(n => n.StartDate <= req.ValueDate);
}
if (!string.IsNullOrEmpty(req.TradeNumber))
{
tradePredicate = tradePredicate.And(n => n.TradeNumber.Contains(req.TradeNumber.Trim()));
}
if (req.UserAssets != null || req.UserClients != null)
{
tradePredicate = tradePredicate.And(q => req.UserAssets.Contains(q.AssetId) || req.UserClients.Contains(q.ClientId));
}
if (req.ClientIds != null && req.ClientIds.Any(x => x > 0))
{
tradePredicate = tradePredicate.And(q => req.ClientIds.Contains(q.ClientId));
}
if (req.BookIds != null && req.BookIds.Any(x => x > 0))
{
tradePredicate = tradePredicate.And(q => req.BookIds.Contains(q.AssetId));
}
if (req.UnderlyingIds != null)
{
var underlyingCodes = new List<string>();
foreach (var id in req.UnderlyingIds)
{
var un = UnderlyingDataProvider.GetUnderlying(id);
if (un != null)
{
underlyingCodes.Add(un.UnderlyingCode);
}
}
predicate = predicate.And(n => underlyingCodes.Contains(n.UnderlyingCode));
}
var positionQuery = DbContext.eod_swap_position.Where(predicate);
var tradeQuery = DbContext.trade.Where(tradePredicate);
var query = from position in positionQuery
join td in tradeQuery on position.SwapTradeId equals td.id
select new SwapPositionResponse
{
eodPosition = position,
TradeDate = td.StartDate.Value,
SwapTradeNo = td.TradeNumber,
StructureType = td.StructureType,
ClientName = td.ClientName,
ClientId = td.ClientId,
};
if (string.IsNullOrEmpty(req.sidx))
{
req.sidx = "SwapTradeNo,eodPosition.id";
req.sord = "asc";
}
var retListResult = query.ToSearchList(req);
foreach (var item in retListResult.rows)
{
var client = DataCacheProvider.GetClientDataSource().GetData(item.ClientId);
item.SwapTradeTypeStr = client?.SwapTradeTypeStr;
var multiplier = ConsGlobal.InstrumentType.IsBond(item.eodPosition.UnderlyingInstrumentType) ? 100 : 1;
item.eodPosition.PosiNetPrice *= multiplier;
item.eodPosition.PosiGrossPrice *= multiplier;
item.eodPosition.PosiNetFeePrice *= multiplier;
item.eodPosition.PosiNetNoFeePrice *= multiplier;
item.eodPosition.UnderlyingPrice *= multiplier;
// 历史数据的 TdCloseInterest、RealizedInterest 存在两种符号口径,
// 风险报表统一按绝对金额和业务方向还原,并重算 RealizedPnl。
NormalizeInterestSignForReport(item.eodPosition);
}
return retListResult;
}
/// <summary>
/// 查询互换日终持仓风险-框架合约
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<EodSwapResponse> SearchEodSwapList(EodSwapQueryRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap>(n => 1 == 1);
var tradePredicate = PredicateBuilder.Create<trade>(n => n.TradeType == "收益互换"
&& n.ValidState != "InValid");
if (req.ValueDate.HasValue)
{
predicate = predicate.And(n => n.ValueDate == req.ValueDate);
// tradePredicate = tradePredicate.And(n=>n.StartDate<=req.ValueDate);
}
if (!string.IsNullOrEmpty(req.TradeNumber))
{
tradePredicate = tradePredicate.And(n => n.TradeNumber.Contains(req.TradeNumber.Trim()));
}
if (req.UserAssets != null || req.UserClients != null)
{
tradePredicate = tradePredicate.And(q => req.UserAssets.Contains(q.AssetId) || req.UserClients.Contains(q.ClientId));
}
if (req.ClientIds != null && req.ClientIds.Any(x => x > 0))
{
tradePredicate = tradePredicate.And(q => req.ClientIds.Contains(q.ClientId));
}
if (req.BookIds != null && req.BookIds.Any(x => x > 0))
{
tradePredicate = tradePredicate.And(q => req.BookIds.Contains(q.AssetId));
}
var positionQuery = DbContext.eod_swap.Where(predicate);
var tradeQuery = DbContext.trade.Where(tradePredicate);
var query = from position in positionQuery
join td in tradeQuery on position.SwapTradeId equals td.id
select new EodSwapResponse
{
position = position,
TradeDate = td.StartDate.Value,
SwapTradeNo = td.TradeNumber,
StructureType = td.StructureType,
ClientName = td.ClientName,
AssetBookName = td.AssetBookName,
ClientId = td.ClientId
};
if (string.IsNullOrEmpty(req.sidx))
{
req.sidx = "SwapTradeNo,position.id";
req.sord = "asc";
}
DbContext.SetDebugLog();
var retListResult = query.ToSearchList(req);
var tradeIds = retListResult.rows.Select(x => x.position.SwapTradeId).Distinct().ToList();
var valueDates = retListResult.rows.Select(x => x.position.ValueDate).Distinct().ToList();
var tradeNotionals = DbContext.trade
.Where(x => tradeIds.Contains(x.id))
.Select(x => new { x.id, x.OriginalStockEqvNotional, x.StockEqvNotional })
.ToDictionary(x => x.id);
var eodPositionDetails = DbContext.eod_swap_position
.Where(x => tradeIds.Contains(x.SwapTradeId) && valueDates.Contains(x.ValueDate) && !x.Invalid)
.ToList();
var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
var underlyingDataSource = DataCacheProvider.GetUnderlyingDataSource();
var varietyDataSource = DataCacheProvider.GetVarietyDataSource();
foreach (var item in retListResult.rows)
{
item.position.NotionalValueShort = -Math.Abs(item.position.NotionalValueShort);
if (tradeNotionals.TryGetValue(item.position.SwapTradeId, out var tradeNotional))
{
item.position.NotionalValue = Convert.ToDecimal(tradeNotional.OriginalStockEqvNotional ?? tradeNotional.StockEqvNotional);
}
var client = DataCacheProvider.GetClientDataSource().GetData(item.ClientId);
item.SwapTradeTypeStr = client?.SwapTradeTypeStr;
var details = eodPositionDetails
.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate)
.ToList();
var floatingLegs = details.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();
var marginLegs = details.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
var dividendPayDate = tradeExtend?.ExtendObj?.DividendPayDate ?? 1;
item.UnderlyingType = string.Join(",", floatingLegs
.Select(x =>
{
var underlying = underlyingDataSource.GetData(x.UnderlyingCode);
return varietyDataSource.GetData(underlying?.UnderlyingTypeId ?? 0)?.AssetType
?? underlying?.UnderlyingInstrumentTypeCn
?? underlying?.UnderlyingType;
})
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct());
item.PeriodAmount = floatingLegs.Sum(x => x.RealizedDividend + x.PosiDividendSum);
// PosiProfitSum = 标的盯市收益 + 未结交易费用 + 待实现付息/分红。
// 风险页的“合约浮动端待实现收益”需要保留未结交易费用,
// 但期间付息/分红由 PeriodAmount 单列展示并参与对应估值口径,
// 因此仅扣除 PosiDividendSum,不能直接使用 PosiMtmPnL。
item.FloatingUnrealizedPnl = floatingLegs.Sum(x => x.PosiProfitSum - x.PosiDividendSum);
item.InterestPaymentMethod = dividendPayDate == 0 ? "到期轧差" : "派息日支付";
if (dividendPayDate == 0)
{
item.MaturityNettingValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL + item.PeriodAmount;
}
else
{
item.PeriodPaymentValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL;
}
// eod_swap 的保证金本金来自 trade_span;缺少 span 数据时会被保存为 0。
// 本风险页改按日终保证金腿的实际本金展示。
item.position.InitMarginGain = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金
&& x.InterestDirection == (int)SwapDirectionEnum.收取)
.Sum(x => Math.Abs(x.InterestPrincipalFix));
item.position.InitMarginLoss = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金
&& x.InterestDirection == (int)SwapDirectionEnum.支付)
.Sum(x => Math.Abs(x.InterestPrincipalFix));
item.position.PostionMarginGain = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金
&& x.InterestDirection == (int)SwapDirectionEnum.收取)
.Sum(x => Math.Abs(x.InterestPrincipalFix));
item.position.PostionMarginLoss = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金
&& x.InterestDirection == (int)SwapDirectionEnum.支付)
.Sum(x => Math.Abs(x.InterestPrincipalFix));
// 保证金本金方向与我方的利息现金流方向相反:原始“收取”保证金
// 表示我方占用客户资金,应向客户支付利息;支付金额按负数展示。
item.MarginInterestGain = marginLegs
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付)
.Sum(x => Math.Abs(x.InterestIncomeSum));
item.MarginInterestLoss = marginLegs
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取)
.Sum(x => -Math.Abs(x.InterestIncomeSum));
}
var dv01 = query.Sum(O => O.position.dv01??0);
retListResult.Sum = new {DV = dv01 };
return retListResult;
}
/// <summary>
/// 获取互换交易日终持仓数据
/// </summary>
/// <param name="tradeId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_swap_position> GetPreEodPositions(int tradeId, DateTime valueDate)
{
return DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId && x.ValueDate == valueDate && !x.Invalid).ToList();
}
/// <summary>
/// 获取互换交易日终持仓数据集合
/// </summary>
/// <param name="tradeId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_swap> GetEodSwaps(List<int> tradeIds, DateTime valueDate)
{
return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate == valueDate).ToList();
}
/// <summary>
/// 获取互换交易某交易日区间框架合约数据
/// </summary>
/// <param name="tradeIds"></param>
/// <param name="valueDate"></param>
/// <param name="preValueDate"></param>
/// <returns></returns>
public List<eod_swap> GetEodSwaps(List<int> tradeIds, DateTime valueDate, DateTime preValueDate)
{
return DbContext.eod_swap.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate <= valueDate && x.ValueDate >= preValueDate).ToList();
}
/// <summary>
/// 获取互换交易某日终持仓数据
/// </summary>
/// <param name="tradeIds"></param>
/// <param name="valueDate"></param>
/// <param name="preValueDate"></param>
/// <returns></returns>
public List<eod_swap_position> GetEodPositions(List<int> tradeIds, DateTime valueDate, DateTime preValueDate)
{
return DbContext.eod_swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && !string.IsNullOrEmpty(x.UnderlyingCode) && x.ValueDate <= valueDate && x.ValueDate >= preValueDate && !x.Invalid).ToList();
}
/// <summary>
/// 获取互换交易某区间日终持仓估值-按产品要求
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<SwapPositionResponse> SearchPositionList(ClientSwapPositionRequest req)
{
var retListResult = GetSearchPositionList(req);
var clientDataSource = DataCacheProvider.GetClientDataSource();
var underlyDataSource = DataCacheProvider.GetUnderlyingDataSource();
foreach (var item in retListResult.rows)
{
var client = clientDataSource.GetData(item.ClientId);
item.ClientNumber = client.Number;
if (!string.IsNullOrEmpty(item.eodPosition.UnderlyingCode))
{
var underly = underlyDataSource.GetData(item.eodPosition.UnderlyingCode);
if (underly != null)
{
item.eodPosition.UnderlyingName = underly.UnderlyingName;
}
}
}
return retListResult;
}
/// <summary>
/// 获取互换交易某区间日终持仓估值-按山证要求
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public SearchListResult<EodSwapPositionResponse> SearchEodPositionList(ClientSwapPositionRequest req)
{
var retListResult = GetSearchEodPositionList(req);
return retListResult;
}
/// <summary>
/// 互换持仓明细查询
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
private SearchListResult<SwapPositionResponse> GetSearchPositionList(ClientSwapPositionRequest req)
{
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.PositionType > 0 && n.PosiStatus == 0);
var tradePredicate = PredicateBuilder.Create<trade>(n => n.TradeType == "收益互换"
&& n.ValidState != "InValid");
if (req.ClientId > 0)
{
predicate = predicate.And(x => x.ClientId == req.ClientId);
}
if (req.ValueDate != null)
{
predicate = predicate.And(x => x.ValueDate == req.ValueDate);
tradePredicate = tradePredicate.And(n => n.StartDate <= req.ValueDate);
}
var positionQuery = DbContext.eod_swap_position.Where(predicate);
var tradeQuery = DbContext.trade.Where(tradePredicate);
var query = from position in positionQuery
join td in tradeQuery on position.SwapTradeId equals td.id
select new SwapPositionResponse
{
eodPosition = position,
TradeDate = td.StartDate.Value,
SwapTradeNo = td.TradeNumber,
StructureType = td.StructureType,
ClientName = td.ClientName,
ClientId = td.ClientId,
InitialMarginDirection = position.PosiDirection
};
if (string.IsNullOrEmpty(req.sidx))
{
req.sidx = "SwapTradeNo,eodPosition.id";
req.sord = "asc";
}
var retListResult = query.ToSearchList(req);
foreach (var item in retListResult.rows)
{
SetClientEodPosition(item.eodPosition);
SetPosiPrice(item.eodPosition);
}
return retListResult;
}
private SearchListResult<EodSwapPositionResponse> GetSearchEodPositionList(ClientSwapPositionRequest req)
{
// 每日估值报告以有数量的浮动腿为主记录;利息腿和保证金腿仅作为同交易、同估值日的辅助数据参与汇总。
var predicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.PosiQuantity > 0);
var interestPredicate = PredicateBuilder.Create<eod_swap_position>(n => !n.Invalid && n.InterestDirection > 0);
var tradePredicate = PredicateBuilder.Create<trade>(n => n.ValidState != "InValid");
// TODO 这里暂时忽略前端传的值 暂时使用临时方案:同时查普通债券类收益互换和普通收益互换
tradePredicate = tradePredicate.And(n => n.StructureType == "普通债券类收益互换" || n.StructureType == "普通收益互换");
if (req.ClientId > 0)
{
predicate = predicate.And(x => x.ClientId == req.ClientId);
}
if (req.BookId > 0)
{
tradePredicate = tradePredicate.And(x => x.AssetId == req.BookId.Value);
}
// ValueDateFrom 保留在请求模型中,但当前互换估值查询按 ValueDate 单日取数。
// if (req.ValueDateFrom != null)
// {
// predicate = predicate.And(x => x.ValueDate >= req.ValueDateFrom);
// }
if (req.ValueDate != null)
{
predicate = predicate.And(x => x.ValueDate == req.ValueDate);
tradePredicate = tradePredicate.And(x => req.ValueDate >= x.TradeDate);
}
var positionQuery = DbContext.eod_swap_position.Where(predicate);
var tradeQuery = DbContext.trade.Where(tradePredicate);
var query = from position in positionQuery
join td in tradeQuery on position.SwapTradeId equals td.id
join tcrConfirm in DbContext.trade_contract_r.Where(O => O.Type == ContractTypeEnum.Trade && O.IsValid) on td.id equals tcrConfirm.TradeId into tcrConfirms
from tcrConfirm in tcrConfirms.DefaultIfEmpty()
select new EodSwapPositionResponse
{
position = position,
ClientName = td.ClientName,
ConfrimNo = tcrConfirm.ContractCode,
TradeNumber = td.TradeNumber,
StructureType = td.StructureType,
UnwindDate = td.UnWindDate,
TradeStatus = td.TradeStatus,
InitYtm = td.InitYtm
};
if (string.IsNullOrEmpty(req.sidx))
{
req.sidx = "position.id";
req.sord = "asc";
}
var retListResult = query.ToSearchList(req);
var tradeIds = retListResult.rows.Select(s => s.position.SwapTradeId).ToList();
if (!tradeIds.Any())
{
return retListResult;
}
interestPredicate = interestPredicate.And(x => tradeIds.Contains(x.SwapTradeId));
var valueDates = retListResult.rows.Select(s => s.position.ValueDate).Distinct().ToList();
interestPredicate = interestPredicate.And(x => valueDates.Contains(x.ValueDate));
// 主查询分页后再取同交易、同估值日的全部辅助腿,避免利息/保证金归集跨估值日串数据。
var eodPositions = DbContext.eod_swap_position.Where(interestPredicate).ToList();
var marginPositions = DbContext.swap_position
.Where(x => tradeIds.Contains(x.SwapTradeId) && MarginModes.Contains(x.InterestMode) && x.IsInitial && !x.Invalid)
.ToList();
var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
Dictionary<string, bool> tradeDic = new Dictionary<string, bool>();
foreach (var item in retListResult.rows)
{
var tradeExtend = tradeExtends.FirstOrDefault(x => x.TradeId == item.position.SwapTradeId);
// 到期结算日按合同到期日展示;实际期限按自然日且包含起始日,二者均不使用结算规则偏移。
item.MaturitySettlementDate = item.position.PosiMatuirityDate;
item.DayCount = Math.Max(0, (item.position.ValueDate - item.position.PosiStartDate).Days + 1);
//item.position.PosiProfitSum += item.position.VTradingFee-item.position.PosiFeePending;
SetClientEodPosition(item.position);
//item.position.PosiProfitSum += item.TradingFee;
var posiProfitSum = item.position.PosiProfitSum;
//item.position.PosiProfitSum 不需要加交易费用
// PosiProfitSum 原值包含交易费用和期间付息/分红。先拆出这两部分,
// 使“浮动收益金额”仅反映标的盯市收益,后续净额公式再按支付方式决定是否加回期间金额。
var pendingDividend = item.position.PosiDividendSum;
item.position.PosiProfitSum = item.position.PosiProfitSum - item.position.PosiFeePending - pendingDividend;
// 现券仅展示期间付息和期初成交收益率;ETF(标的主数据类型 Fund)仅展示期间分红。
// 其余标的的三列均不适用,返回 null 使页面和 Excel 模板保持空白,而不是展示 0。
var isCashBond = ConsGlobal.InstrumentType.IsBond(item.position.UnderlyingInstrumentType);
var isEtf = ConsGlobal.InstrumentType.Fund.Equals(
item.position.UnderlyingInstrumentType,
StringComparison.OrdinalIgnoreCase);
if (isCashBond)
{
item.PeriodAmount = pendingDividend;
item.DividendAmount = null;
// 期初标的成交收益率是债券现券成交口径,非现券不展示该交易录入值。
}
else if (isEtf)
{
item.PeriodAmount = null;
item.DividendAmount = pendingDividend;
}
else
{
item.PeriodAmount = null;
item.DividendAmount = null;
}
if (!isCashBond)
{
item.InitYtm = null;
}
// 预付金本金和利率来自交易腿,并以发生日判断在估值日是否已生效;
// 预付金利息则来自当日日终腿,以获得截至估值日的 InterestIncomeSum。
var tradeMargins = marginPositions
.Where(x => x.SwapTradeId == item.position.SwapTradeId
&& (!x.HappenDate.HasValue || x.HappenDate.Value <= item.position.ValueDate))
.ToList();
var interests = eodPositions.Where(x => x.SwapTradeId == item.position.SwapTradeId && x.ValueDate == item.position.ValueDate);
var eodMargins = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList();
var eodInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList();
var initialMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
var additionalMargins = tradeMargins.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金).ToList();
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins);
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault);
// 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。
var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m;
item.NetSettmentAmount = item.InterestAmount
+ item.position.PosiProfitSum
+ item.position.PosiFeePending
+ item.MarginInterestAmount
+ nettingDividend;
item.NetSettmentAmount = Math.Round(item.NetSettmentAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
item.TrsValue = Math.Round(item.NetSettmentAmount + item.OpenMarginAmount + item.AdditionalMarginAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
if (item.position.PosiNotionalValue != 0 && item.position.PosiNetPrice != 0)
{
item.FloatRateAbs = item.position.PosiNotionalValue == 0 ? 0 : item.InterestAmount / item.position.PosiNotionalValue;
}
// 交易录入的债券类收益互换价格以小数保存,展示时转为百分比价格;
// 普通收益互换录入的是数量/原始数值,不做乘 100 转换。
SetPosiPrice(item.position, item.StructureType == "普通债券类收益互换");
}
return retListResult;
}
/// <summary>
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
/// </summary>
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins);
/// <summary>
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
/// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
/// </summary>
public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position);
/// <summary>
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,
/// 否则页面、Excel 和净额结算金额会出现相反符号。
/// </summary>
/// <param name="position"></param>
private void SetClientEodPosition(eod_swap_position position)
{
position.TdCloseDividend = -position.TdCloseDividend;
position.TdCloseMtmPnl = -position.TdCloseMtmPnl;
position.TdCloseFee = -position.TdCloseFee;
position.TdCloseInterest = -position.TdCloseInterest;
position.TdCloseInterestFee = -position.TdCloseInterestFee;
position.RealizedMtmPnL = -position.RealizedMtmPnL;
position.RealizedDividend = -position.RealizedDividend;
position.RealizedFee = -position.RealizedFee;
position.RealizedInterest = -position.RealizedInterest;
position.RealizedInterestFee = -position.RealizedInterestFee;
position.RealizedPnl = -position.RealizedPnl;
position.InterestProfitSum = -position.InterestProfitSum;
position.PosiProfitSum = -position.PosiProfitSum;
position.VTradingFee = -position.VTradingFee;
position.PosiFeePending = -position.PosiFeePending;
position.SwapPositionValue = -position.SwapPositionValue;
position.PosiDividendSum = -position.PosiDividendSum;
}
private void SetPosiPrice(eod_swap_position position, bool? useBondPriceScale = null)
{
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(position.UnderlyingCode);
if (useBondPriceScale ?? (um != null && um.IsBond()))
{
position.PosiNetPrice *= 100;
position.UnderlyingPrice *= 100;
position.PosiGrossPrice *= 100;
position.PosiNetFeePrice *= 100;
position.PosiNetNoFeePrice *= 100;
return;
}
}
/// <summary>
/// 获取客户互换持仓信息
/// </summary>
/// <param name="clientId"></param>
/// <param name="valueDate"></param>
/// <returns></returns>
public List<eod_position> GetSwapPositions(int clientId, DateTime valueDate)
{
var trades = DbContext.trade.Where(x => x.TradeType == "收益互换"
&& x.ClientId == clientId
&& ConsTrade.LiveTradeStatusList.Contains(x.TradeStatus)
&& x.ValidState != "InValid").ToList();
var tradeIds = trades.Select(s => s.id).ToList();
var eodSwaps = DbContext.eod_swap_position.Where(x => tradeIds.Contains(x.SwapTradeId) && x.ValueDate == valueDate).ToList();
var tradeExtends = DbContext.trade_extend.Where(x => tradeIds.Contains(x.TradeId)).ToList();
return ConvertEodPnl(eodSwaps, trades, tradeExtends);
}
private List<eod_position> ConvertEodPnl(List<eod_swap_position> eodSwaps, List<trade> trades, List<trade_extend> tradeExtends)
{
List<eod_position> list = new List<eod_position>();
foreach (var item in eodSwaps)
{
var tradeOrigin = trades.First(x => x.id == item.SwapTradeId);
var realizedPnL = item.RealizedMtmPnL + item.RealizedInterest;
var tdExtend = tradeExtends.First(x => x.TradeId == item.SwapTradeId);
eod_position model = new eod_position()
{
TradeId = tradeOrigin.id,
TradeType = tradeOrigin.TradeType,
ClientId = tradeOrigin.ClientId,
TradeNumber = tradeOrigin.TradeNumber,
TradeDate = tradeOrigin.TradeDate,
ExerciseDate = tradeOrigin.ExerciseDate,
PrincipalRate = tradeOrigin.PrincipalRate ?? 0,
BasisUnderlyingCode = tradeOrigin.BasisUnderlyingCode,
UnderlyingCode = item.UnderlyingCode,
BasisGap = tradeOrigin.BasisGap ?? 0,
Lots = Convert.ToDouble(item.PosiQuantity),
ParticipationRate = tradeOrigin.ParticipationRate ?? 0,
NoRiskRate = tradeOrigin.NoRiskRate ?? 0,
UnderlyingPrice = Convert.ToDouble(item.UnderlyingPrice),
Pv = Convert.ToDouble(item.UnderlyingMarketValue) * -1,
RoundedPv = Math.Round(Convert.ToDouble(item.UnderlyingMarketValue), 2) * -1,
Pnl = Convert.ToDouble(realizedPnL) * -1,
RoundedPnl = Math.Round(Convert.ToDouble(realizedPnL), 2) * -1,
ValueDate = item.ValueDate,
PvDouble = Convert.ToDouble(item.UnderlyingMarketValue),
PnlDouble = Convert.ToDouble(realizedPnL),
PositionRelizedAmount = Convert.ToDouble(realizedPnL) * -1,
InstrumentType = tradeOrigin.UnderlyingInstrumentType,
IsGroup = tradeOrigin.IsGroup,
SettlementType = tradeOrigin.SettlementType,
SettlementFlag = tradeOrigin.SettlementFlag,
tradeOrigin = tradeOrigin.Clone(),
Vol = 0,
Delta = 0,
Gamma = 0,
Theta = 0,
Vega = 0,
Rho = 0,
GammaCash = 0
};
SetDicValue(model, item, tdExtend.ExtendObj.AnnualDays);
list.Add(model);
}
return list;
}
/// <summary>
/// 设置持仓导出字典信息
/// </summary>
/// <param name="model"></param>
/// <param name="item"></param>
/// <param name="annualDays"></param>
private void SetDicValue(eod_position model, eod_swap_position item, int annualDays)
{
var extDic = model.trade.MetaDic;
if (!string.IsNullOrEmpty(item.UnderlyingCode))
{
var underlyingAssetName = DataCacheProvider.GetUnderlyingDataSource().GetData(item.UnderlyingCode)?.UnderlyingName;
decimal posiTradingFeeUnit = 0;
if (item.PosiQuantity != 0)
{
posiTradingFeeUnit = item.PosiTradingFee / item.PosiQuantity;
}
if (item.PosiDirection == (int)SwapDirectionEnum.支付)
{
extDic["互换_支付方标的代码"] = item.UnderlyingCode;
extDic["互换_支付方标的名称"] = underlyingAssetName;
extDic["互换_支付方期初标的价格"] = item.PosiGrossPrice.OtcFormatUmPrice();
extDic["互换_支付方交易数量"] = item.PosiQuantity.OtcFormatNotional();
extDic["互换_支付方到期标的价格"] = item.UnderlyingPrice.OtcFormatUmPrice();
extDic["互换_支付方单位交易费用"] = posiTradingFeeUnit.OtcFormatUmPrice();
extDic["互换_支付方初始预付金"] = item.RealizedFee.OtcFormatPercent();
extDic["互换_支付方交易费用"] = item.PosiTradingFee.OtcFormatUmPrice();
extDic["互换_支付方多空方向"] = item.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头";
}
else
{
extDic["互换_收取方标的代码"] = item.UnderlyingCode;
extDic["互换_收取方标的名称"] = underlyingAssetName;
extDic["互换_收取方期初标的价格"] = item.PosiGrossPrice.OtcFormatUmPrice();
extDic["互换_收取方交易数量"] = item.PosiQuantity.OtcFormatNotional();
extDic["互换_收取方到期标的价格"] = item.UnderlyingPrice.OtcFormatUmPrice();
extDic["互换_收取方单位交易费用"] = posiTradingFeeUnit.OtcFormatUmPrice();
extDic["互换_收取方初始预付金"] = "";
extDic["互换_收取方交易费用"] = item.PosiTradingFee.OtcFormatUmPrice();
extDic["互换_收取方多空方向"] = item.PositionType == (int)PositionTypeFlag.Long ? "多头" : "空头";
}
}
else
{
if (item.InterestDirection == (int)SwapDirectionEnum.支付)
{
extDic["互换_支付方互换利率"] = item.InterestRateDefault.OtcFormatPercent();
extDic["互换_支付方固定收益"] = "";
}
else
{
extDic["互换_收取方互换利率"] = item.InterestRateDefault.OtcFormatPercent();
extDic["互换_收取方固定收益"] = "";
}
}
extDic["互换_互换日期"] = "";
extDic["年化天数"] = annualDays.ToString();
}
}
}