- 将价差盈亏计算从使用CloseNotionalValue改为使用PositionQty和ContractSize - 更新前端JavaScript代码中的计算公式,按持仓数量和合约乘数计算价差盈亏 - 在UnwindInput模型中添加PositionQty和ContractSize字段 - 修正后台计算服务中的数据映射逻辑 - 添加FC_009测试用例验证债券价差按数量计算的正确性 - 更新现有测试用例的输入参数以匹配新的计算方式
1997 lines
109 KiB
C#
1997 lines
109 KiB
C#
using MoreLinq.Extensions;
|
||
using Newtonsoft.Json;
|
||
using Qdp.Pricing.Library.Base.Utilities;
|
||
using System.Linq.Expressions;
|
||
using YLErp.BLL;
|
||
using YLErp.BLL.Eod;
|
||
using YLErp.DBModels.Enums;
|
||
using YLErp.Helpers;
|
||
using YLErp.Modules.DataProviderModule;
|
||
using YLErp.Modules.EodModule;
|
||
using YLErp.Modules.TradeModule;
|
||
using YLErp.Modules.TradeModule.DealModule;
|
||
using YLErp.QdpModule;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
public class SwapDealService : SwapTradeBaseService
|
||
{
|
||
private static readonly IYcLogger Logger = LogFactory.GetLogger(nameof(SwapDealService));
|
||
protected virtual bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||
{
|
||
return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate);
|
||
}
|
||
|
||
#region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变
|
||
|
||
// FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复)
|
||
|
||
/// <summary>添加资金记录(生产: AddClientCashInCashOut;测试: 计数并记录金额)</summary>
|
||
protected virtual int AddClientCash(trade td, double amount, string action, DateTime valueDate)
|
||
{
|
||
return AddClientCashInCashOut(td, amount, action, valueDate);
|
||
}
|
||
|
||
/// <summary>保存互换/平仓事件(生产: 落库+建事件;测试: 收集 unwindData 入内存列表)。
|
||
/// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。</summary>
|
||
protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||
{
|
||
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
|
||
}
|
||
|
||
/// <summary>保存所有变更(生产: DbContext.SaveChanges;测试: 空操作)</summary>
|
||
protected virtual void SaveAllChanges()
|
||
{
|
||
DbContext.SaveChanges();
|
||
}
|
||
|
||
/// <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>保存互换交易资金记录(生产: new ClientCashInCashOutService;测试: 空操作)。
|
||
/// 仅 SwapUnwind 全平仓且 NeedOpenFee=false 时调用。</summary>
|
||
protected virtual void CallSaveSwapTradeClientCash(trade td, DateTime valueDate)
|
||
{
|
||
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
|
||
{
|
||
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, valueDate, 0);
|
||
}
|
||
}
|
||
|
||
/// <summary>触发互换实时持仓计算(生产: Task.Run 异步 RealtimePnlCalc;测试: 空操作)。
|
||
/// 仅 SwapUnwind 成功后调用。</summary>
|
||
protected virtual void TriggerRealtimeSwapPosition()
|
||
{
|
||
Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
RealtimePnlCalc.RealtimeSwapPosition(new OptUserInfo(0, "互换实时持仓服务", OptUserFrom.Service));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogFactory.GetLogger<SwapDealService>().Error("互换实时持仓服务计算失败", ex);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>查找待审核的互换/平仓事件(生产: DbContext.swap_event 查询;测试: 返回内存对象)</summary>
|
||
protected virtual swap_event FindSwapEvent(int tradeId, int eventType)
|
||
{
|
||
return DbContext.swap_event
|
||
.Where(x => x.SwapTradeId == tradeId && !x.Invalid && x.EventType == eventType)
|
||
.OrderByDescending(o => o.id).FirstOrDefault();
|
||
}
|
||
|
||
/// <summary>查找事件关联的流水事件(生产: DbContext.swap_flow_event 查询;测试: 返回内存列表)</summary>
|
||
protected virtual List<swap_flow_event> FindFlowEventsByEventId(long eventId)
|
||
{
|
||
return DbContext.swap_flow_event.Where(x => x.EventId == eventId).ToList();
|
||
}
|
||
|
||
/// <summary>平仓/互换审核的前置校验与状态设置(生产: new TradeUnwindService;测试: 空操作或计数)</summary>
|
||
protected virtual void CloseReCheckSetTrade(int swapTradeId, bool isSwap, bool needCheck)
|
||
{
|
||
new TradeUnwindService(this).CloseReCheck_SetTrade(swapTradeId, isSwap, needCheck);
|
||
}
|
||
|
||
protected virtual DateTime GetMaxIncomeValueDate(trade td)
|
||
{
|
||
return td.ExerciseDate.Value.AddDays(-1);
|
||
}
|
||
|
||
#endregion
|
||
|
||
public SwapDealService(OptUserInfo optUser) : base(optUser)
|
||
{
|
||
|
||
}
|
||
public SwapDealService(YLBaseService baseService) : base(baseService)
|
||
{
|
||
|
||
}
|
||
|
||
#region 前端盈亏只读校验(不阻断交易)
|
||
|
||
/// <summary>
|
||
/// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对,
|
||
/// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。
|
||
///
|
||
/// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底,
|
||
/// 为将来公式统一积累"前后端差异"数据。
|
||
/// </summary>
|
||
/// <param name="unwindData">前端算好传入的结算数据</param>
|
||
/// <param name="isIncome">true=结息页(income公式),false=平仓页(unwind公式)</param>
|
||
private void ValidateFrontendPnL(UnwindData unwindData, bool isIncome)
|
||
{
|
||
try
|
||
{
|
||
// 取浮动腿(有 UnderlyingCode 的),与前端 initDeal 取法一致
|
||
var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||
// PosiGrossPrice 是 [NotMapped],前端可能没传;为空/0 时跳过(避免误报)
|
||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 用 UnderlyingInstrumentType 推 Multiplier(债券=100,否则1)
|
||
bool isBond = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType);
|
||
int multiplier = isBond ? 100 : 1;
|
||
|
||
// 分类利息腿/预付金腿(InterestMode 初始预付金/追加预付金→Margin,否则→Interest)
|
||
var input = new UnwindInput
|
||
{
|
||
Multiplier = multiplier,
|
||
PosiGrossPrice = floatLeg.PosiGrossPrice, // EntryDirtyPrice
|
||
TradingAmountAvg = floatLeg.TradingAmountAvg, // ExitDirtyPrice(界面×multiplier形态)
|
||
CloseQty = unwindData.CloseQty,
|
||
PositionQty = unwindData.PositionQty,
|
||
ContractSize = floatLeg.ContractSize,
|
||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||
PayDirection = floatLeg.PayDirection,
|
||
PositionType = floatLeg.PositionType,
|
||
TradingFee = floatLeg.TradingFee.ToString(),
|
||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||
DividendIn = floatLeg.DividendIn.ToString(),
|
||
};
|
||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||
{
|
||
var target = (leg.InterestMode == (int)InterestModeEnum.初始预付金
|
||
|| leg.InterestMode == (int)InterestModeEnum.追加预付金)
|
||
? input.MarginLegs : input.InterestLegs;
|
||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||
}
|
||
|
||
var recalc = isIncome
|
||
? FrontendCalcReference.CalcIncome(input)
|
||
: FrontendCalcReference.CalcUnwind(input);
|
||
|
||
// 逐字段比对,差异 > 0.01 告警
|
||
const decimal threshold = 0.01m;
|
||
CheckDiff(nameof(recalc.SwapRealizedPnL), unwindData.SwapRealizedPnL, recalc.SwapRealizedPnL, threshold, unwindData.SwapTradeId, floatLeg);
|
||
CheckDiff(nameof(recalc.SwapCloseAmount), unwindData.SwapCloseAmount, recalc.SwapCloseAmount, threshold, unwindData.SwapTradeId, floatLeg);
|
||
CheckDiff("MarkClosePnl", floatLeg.MarkClosePnl, recalc.MarkClosePnl, threshold, unwindData.SwapTradeId, floatLeg);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 校验自身错误绝不阻断交易
|
||
Logger.Error($"[互换盈亏校验异常] tradeId={unwindData.SwapTradeId} isIncome={isIncome}", ex);
|
||
}
|
||
}
|
||
|
||
private void CheckDiff(string field, decimal frontendVal, decimal backendVal, decimal threshold, int tradeId, swap_flow_event floatLeg)
|
||
{
|
||
decimal diff = frontendVal - backendVal;
|
||
if (Math.Abs(diff) > threshold)
|
||
{
|
||
Logger.Error($"[互换盈亏校验分歧] tradeId={tradeId} field={field} frontend={frontendVal} backend={backendVal} diff={diff} " +
|
||
$"floatLeg=[gross={floatLeg.PosiGrossPrice} avg={floatLeg.TradingAmountAvg} qty={floatLeg.Quantity} payDir={floatLeg.PayDirection} posType={floatLeg.PositionType}]");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 平仓初始化
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public UnwindData InitUnwind(int tradeId)
|
||
{
|
||
var td = DbContext.trade.Find(tradeId);
|
||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
|
||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode);
|
||
bool commodity = ConsGlobal.InstrumentType.CalcTypeIsFutures(um.UnderlyingInstrumentType);
|
||
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.自动互换, (int)SwapEventTypeEnum.互换 };
|
||
var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
|
||
//CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||
td.trade_extend = tradeExtend;
|
||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||
var oriPosition = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && x.IsInitial).FirstOrDefault();
|
||
var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps);
|
||
var hasProcess = HasTradeProcess();
|
||
swap_flow_event floatEvent = new swap_flow_event();
|
||
UnwindData unwindData = new UnwindData();
|
||
if (((valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess)) && td.TradeStatus == ConsTrade.平仓待复核)
|
||
{
|
||
var swapEvent = GetSwapEvent(tradeId, (int)SwapEventTypeEnum.平仓);
|
||
if (swapEvent == null)
|
||
{
|
||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||
}
|
||
unwindData = swapEvent.unwindData;
|
||
}
|
||
else
|
||
{
|
||
unwindData.TradeStartDate = td.StartDate;
|
||
unwindData.CloseType = commodity ? 1 : 2;
|
||
unwindData.StartDate = td.TradeDate.Value;
|
||
if (preDealDate.HasValue)
|
||
{
|
||
unwindData.StartDate = preDealDate.Value;
|
||
}
|
||
unwindData.ValueDate = dealDate;
|
||
unwindData.UnwindDate = dealDate;
|
||
floatEvent.EventDate = dealDate;
|
||
floatEvent.UnwindDate = unwindData.UnwindDate;
|
||
floatEvent.PayDate = QdpCalendarHelper.GetNonHoliday(unwindData.UnwindDate.Value.AddDays(td.trade_extend.ExtendObj.SettlementRules));
|
||
unwindData.PayDate = floatEvent.PayDate;
|
||
floatEvent.SwapTradeId = tradeId;
|
||
floatEvent.SwapTradeNo = td.TradeNumber;
|
||
unwindData.SwapTradeId = tradeId;
|
||
unwindData.StructureType = td.StructureType;
|
||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||
unwindData.NotionalQty = positions.Where(x => x.IsInitial).Sum(s => s.PosiQuantity);
|
||
unwindData.PosiNotionalValue = Convert.ToDecimal(td.StockEqvNotional);
|
||
unwindData.PositionQty = position != null ? position.PosiQuantity : Convert.ToDecimal(td.TradeAmount);
|
||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
unwindData.CloseMethod = (int)CloseMethodEnum.全部平仓;
|
||
// 占期初(original)语义(A):默认"平掉剩余全部持仓" = 剩余名义本金/期初名义本金。
|
||
// 未平仓时 PosiNotionalValue==NotionalValue → 1(平100%);部分平仓后自动变为剩余比例(如已平10%则默认90%)。
|
||
// 与互换/提前终止 InitIncome(L447) 保持一致。
|
||
unwindData.ClosePercent = CalcDefaultInitClosePercent(unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||
unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||
unwindData.CloseQty = unwindData.PositionQty;
|
||
if (position != null)
|
||
{
|
||
floatEvent.PositionId = position.PositionId;
|
||
floatEvent.EventType = (int)SwapEventTypeEnum.平仓;
|
||
floatEvent.EventReason = "交易";
|
||
floatEvent.DividendIn = 0;
|
||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||
floatEvent.CloseFee = 0;
|
||
floatEvent.BeforeCloseFee = oriPosition.PosiTradingFeePending;
|
||
floatEvent.MarkClosePnl = 0;
|
||
floatEvent.PayDirection = position.PosiDirection;
|
||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||
floatEvent.PosiNetPrice = position.PosiNetPrice;
|
||
floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice;
|
||
floatEvent.PositionType = position.PositionType;
|
||
floatEvent.TradingAmountNetFeeAvg = position.PosiNetFeePrice;
|
||
floatEvent.Quantity = position.PosiQuantity;
|
||
floatEvent.PositionQty = 0;
|
||
floatEvent.ContractSize = position.ContractSize;
|
||
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
|
||
var ratio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? -1m : 1m;
|
||
floatEvent.TradingFeePending = position.PosiTradingFeePending;
|
||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
floatEvent.InterestMode = position.InterestMode;
|
||
floatEvent.ClientId = td.ClientId;
|
||
floatEvent.SetOpt(UserInfo);
|
||
}
|
||
unwindData.FlowEvents.Add(floatEvent);
|
||
}
|
||
return unwindData;
|
||
}
|
||
/// <summary>
|
||
/// 校验上日是否收盘
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
public void CheckEodTrade(int tradeId)
|
||
{
|
||
var td = DbContext.trade.Find(tradeId);
|
||
var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
|
||
//CheckLastEod(dealDate, td.StartDate.Value, tradeId);
|
||
}
|
||
/// <summary>
|
||
/// 校验收益结算操作(不检查收盘限制)
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
public void CheckEodTradeForIncome(int tradeId)
|
||
{
|
||
var td = DbContext.trade.Find(tradeId);
|
||
// 收益结算不检查收盘限制,只检查交易状态
|
||
if (td.TradeType != "收益互换")
|
||
{
|
||
throw new ServiceException("该交易不是收益互换类型");
|
||
}
|
||
if (td.ValidState == "InValid")
|
||
{
|
||
throw new ServiceException("该交易已无效");
|
||
}
|
||
if (td.TradeStatus != ConsTrade.确认成交 && td.TradeStatus != ConsTrade.提前终止拒绝)
|
||
{
|
||
throw new ServiceException($"该交易状态为【{td.TradeStatus}】,无法进行收益结算");
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 多空组合 平仓初始化
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public UnwindData InitLongShortUnwind(int tradeId, SwapEventTypeEnum eventTypeEnum)
|
||
{
|
||
var td = DbContext.trade.Find(tradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && x.IsInitial && !x.Invalid);
|
||
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||
var dealDate = valuedateBLL.ValueDate <= td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
|
||
//CheckLastEod(dealDate, td.TradeDate.Value, tradeId); //去掉平仓收盘限制
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||
td.trade_extend = tradeExtend;
|
||
var preDealDate = GetPreDealDate(tradeId, dealDate, eventTyps);
|
||
double stockEqvNotional = td.StockEqvNotional;//剩余名义本金
|
||
var hasProcess = HasTradeProcess();
|
||
swap_flow_event floatEvent = new swap_flow_event();
|
||
UnwindData unwindData = new UnwindData();
|
||
if (((valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess)) && (td.TradeStatus == ConsTrade.平仓待复核 || td.TradeStatus == ConsTrade.互换待复核))
|
||
{
|
||
var swapEvent = GetSwapEvent(tradeId, (int)eventTypeEnum);
|
||
if (swapEvent == null)
|
||
{
|
||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||
}
|
||
unwindData = swapEvent.unwindData;
|
||
}
|
||
else
|
||
{
|
||
unwindData.StartDate = td.TradeDate.Value;
|
||
if (preDealDate.HasValue)
|
||
{
|
||
unwindData.StartDate = preDealDate.Value;
|
||
}
|
||
unwindData.ValueDate = dealDate;
|
||
unwindData.UnwindDate = dealDate;
|
||
unwindData.PayDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(td.trade_extend.ExtendObj.SettlementRules));
|
||
unwindData.SwapTradeId = tradeId;
|
||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||
unwindData.NotionalQty = positions.Sum(s => s.PosiQuantity);
|
||
unwindData.PosiNotionalValue = Convert.ToDecimal(stockEqvNotional);
|
||
unwindData.PositionQty = 0;//平仓只做了结为0,互换用不上
|
||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
if (eventTypeEnum == SwapEventTypeEnum.平仓)
|
||
{
|
||
unwindData.FlowEvents = GetUnwindInterests(dealDate, unwindData.UnwindDate.Value, tradeId, 1, (int)SwapEventTypeEnum.平仓);
|
||
}
|
||
}
|
||
return unwindData;
|
||
}
|
||
/// <summary>
|
||
/// 平仓初始化
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public UnwindData InitIncome(int tradeId)
|
||
{
|
||
var checkEventTypes = new List<int>() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||
var td = DbContext.trade.Find(tradeId);
|
||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
|
||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode);
|
||
List<int> eventTypes = new List<int>() { (int)SwapFlowEventTypeEnum.互换, (int)SwapFlowEventTypeEnum.自动互换 };
|
||
var maxIncomeValueDate = GetMaxIncomeValueDate(td);
|
||
var dealDate = valuedateBLL.ValueDate.Date > maxIncomeValueDate.Date ? maxIncomeValueDate : valuedateBLL.ValueDate;
|
||
// 收益结算不检查收盘限制
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||
td.trade_extend = tradeExtend;
|
||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||
//var preSettleDate = CheckLastEod(dealDate, td.StartDate.Value, tradeId);//上一交易日期
|
||
var preDealDate = GetPreDealDate(tradeId, dealDate, eventTypes);
|
||
var hasProcess = HasTradeProcess();
|
||
swap_flow_event floatEvent = new swap_flow_event();
|
||
UnwindData unwindData = new UnwindData();
|
||
if (((valuedateBLL.SystemDate.CloseReCheck == 1) || (valuedateBLL.SystemDate.CloseReApprove == 1 && hasProcess)) && td.TradeStatus == ConsTrade.互换待复核)
|
||
{
|
||
var swapEvent = GetSwapEvent(tradeId, (int)SwapEventTypeEnum.互换);
|
||
if (swapEvent == null)
|
||
{
|
||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||
}
|
||
unwindData = swapEvent.unwindData;
|
||
}
|
||
else
|
||
{
|
||
unwindData.StartDate = td.TradeDate.Value;
|
||
if (preDealDate.HasValue)
|
||
{
|
||
unwindData.StartDate = preDealDate.Value;
|
||
}
|
||
unwindData.TradeStartDate = td.StartDate;
|
||
unwindData.ValueDate = dealDate;
|
||
unwindData.UnwindDate = dealDate;
|
||
floatEvent.UnwindDate = unwindData.UnwindDate;
|
||
floatEvent.EventDate = dealDate;
|
||
unwindData.PayDate = valuedateBLL.ValueDate;
|
||
floatEvent.PayDate = unwindData.PayDate;
|
||
floatEvent.SwapTradeId = tradeId;
|
||
floatEvent.SwapTradeNo = td.TradeNumber;
|
||
floatEvent.EventType = (int)SwapFlowEventTypeEnum.互换;
|
||
floatEvent.EventReason = "交易";
|
||
unwindData.SwapTradeId = tradeId;
|
||
unwindData.StructureType = td.StructureType;
|
||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||
unwindData.NotionalQty = positions.Where(x => x.IsInitial).Sum(s => s.PosiQuantity);
|
||
unwindData.PosiNotionalValue = Convert.ToDecimal(td.StockEqvNotional);
|
||
unwindData.PositionQty = position != null ? position.PosiQuantity : Convert.ToDecimal(td.TradeAmount);
|
||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
unwindData.ClosePercent = unwindData.PosiNotionalValue / unwindData.NotionalValue;
|
||
unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||
if (position != null)
|
||
{
|
||
floatEvent.PositionId = position.PositionId;
|
||
floatEvent.DividendIn = 0;
|
||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||
floatEvent.CloseFee = 0;
|
||
floatEvent.MarkClosePnl = 0;
|
||
floatEvent.PayDirection = position.PosiDirection;
|
||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||
floatEvent.PosiNetPrice = position.PosiNetPrice;
|
||
// 注意:TradingAmountNetAvg 字段名为"成交净价(期末语义)",但收益结算/平仓初始化时装入的是期初净价(PosiNetNoFeePrice),前端展示期初净价时取此字段
|
||
floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice;
|
||
floatEvent.TradingAmountNetFeeAvg = position.PosiNetFeePrice;
|
||
floatEvent.PositionType = position.PositionType;
|
||
floatEvent.Quantity = position.PosiQuantity;
|
||
floatEvent.PositionQty = 0;
|
||
floatEvent.ContractSize = position.ContractSize;
|
||
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
|
||
floatEvent.ClientId = td.ClientId;
|
||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
}
|
||
unwindData.FlowEvents.Add(floatEvent);
|
||
}
|
||
unwindData.MaxIncomeValueDate = maxIncomeValueDate;
|
||
return unwindData;
|
||
}
|
||
/// <summary>
|
||
/// 获取平仓利息端信息
|
||
/// </summary>
|
||
/// <param name="valueDate">平仓日期</param>
|
||
/// <param name="tradeId">交易id</param>
|
||
/// <param name="closePercent">平仓比例</param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public List<swap_flow_event> GetUnwindInterests(DateTime valueDate, DateTime unwindDate, int tradeId, decimal closePercent, int eventType)
|
||
{
|
||
List<swap_flow_event> interests = new List<swap_flow_event>();
|
||
if (closePercent > 1)
|
||
{
|
||
closePercent = 1;//防篡改
|
||
}
|
||
else if (closePercent < 0)
|
||
{
|
||
closePercent = 0;
|
||
}
|
||
var td = DbContext.trade.Find(tradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
var allpositions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid).ToList();
|
||
var origPositions = allpositions.Where(x => x.IsInitial).ToList();
|
||
var realPostitions = allpositions.Where(x => !x.IsInitial).ToList();
|
||
// 根因修复(多次部分平仓预付金返还错误):见 ResolveInterestLegPositions 注释。
|
||
// 迭代源仍用 origPositions(保留 orig.id → eod_swap_position.PositionId 的日终匹配),
|
||
// 仅对预付金腿以实时腿的剩余本金克隆覆盖,故此处不改任何日终匹配行为。
|
||
var positions = ResolveInterestLegPositions(origPositions, realPostitions);
|
||
var fpositions = origPositions.Where(x => x.PosiDirection > 0).ToList();
|
||
var longPositions = fpositions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).ToList();
|
||
var shortPositions = fpositions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).ToList();
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
|
||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||
var lastEod = DbContext.eod_swap.Where(x => x.ValueDate < unwindDate && x.SwapTradeId == tradeId).OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||
var _preSetteDate = lastEod == null ? unwindDate.AddDays(-1) : lastEod.ValueDate;
|
||
List<eod_swap_position> lastEodPositions = new SwapEodPositionService(this).GetPreEodPositions(tradeId, _preSetteDate);//上一交易数据
|
||
var posiLongNotionalValue = longPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||
var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue);
|
||
var posiNotionalValue = stockEqvNotional * closePercent;//剩余名义本金
|
||
var orginPv = lastEod != null ? lastEod.NotionalValue : stockEqvNotional;
|
||
var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice;
|
||
var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
|
||
bool tdClose = closeList.Count > 0;
|
||
interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false,false, closeList);
|
||
return interests;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析利息腿(PosiDirection==0)持仓,供 GetUnwindInterests 使用。抽为纯函数以便无库单测。
|
||
/// <para>根因(多次部分平仓预付金返还错误):预付金腿(初始/追加)的"当前剩余本金"存于实时持仓
|
||
/// realPositions.InterestPrincipalFix,每次平仓由 UpdateInitalPosition 递减;而原始腿
|
||
/// origPositions(IsInitial=1)的 InterestPrincipalFix 恒为初始值。GetInterests 算
|
||
/// closePrincipal = Fix × closePercent 与预付金计息基数 orginPv(InitSwapDealInterest) 时都读
|
||
/// position.InterestPrincipalFix,若沿用原始腿,会在多次部分平仓后仍返还/计算初始本金(如始终 99000)。</para>
|
||
/// <para>修复:迭代源仍用 origPositions(保留 orig.id → eod_swap_position.PositionId 的日终匹配,
|
||
/// 全库实测 eod 均按 orig.id 归档;若换 realPositions 会破坏 preEod 匹配导致利息重算错误),仅对预付金腿
|
||
/// Clone 覆盖其本金值为实时腿的剩余本金。real 与 orig 通过 real.PositionId == orig.id 精确 1:1 关联。
|
||
/// 首次平仓时 orig==real 行为不变;仅在发生过部分平仓后 real≠orig 时用实时腿本金纠正。</para>
|
||
/// </summary>
|
||
/// <param name="origPositions">原始腿(IsInitial=1)全集</param>
|
||
/// <param name="realPositions">实时腿(IsInitial=0)全集,其 PositionId 指向对应 orig 的 id</param>
|
||
/// <returns>利息腿(PosiDirection==0)列表:预付金腿本金已对齐实时剩余本金,其余保持原始腿</returns>
|
||
public static List<swap_position> ResolveInterestLegPositions(List<swap_position> origPositions, List<swap_position> realPositions)
|
||
{
|
||
realPositions ??= new List<swap_position>();
|
||
return origPositions.Where(x => x.PosiDirection == 0).Select(p =>
|
||
{
|
||
if (p.InterestMode == (int)InterestModeEnum.初始预付金 || p.InterestMode == (int)InterestModeEnum.追加预付金)
|
||
{
|
||
var realLeg = realPositions.FirstOrDefault(r => r.PositionId == p.id);
|
||
if (realLeg != null && realLeg.InterestPrincipalFix != p.InterestPrincipalFix)
|
||
{
|
||
var clone = p.Clone();
|
||
clone.InterestPrincipalFix = realLeg.InterestPrincipalFix;
|
||
return clone;
|
||
}
|
||
}
|
||
return p;
|
||
}).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取利息腿"已通过历史互换结出的累计利息"(用于复利重算时扣除,类比分红的 CalcConsumedDividend)。
|
||
/// 数据源为事件级 swap_flow_event.InterestAmount(互换/自动互换 完成态事件,互换当时即落库,不依赖日终归档)。
|
||
/// </summary>
|
||
/// <param name="tradeId">交易id</param>
|
||
/// <param name="positionId">利息腿id</param>
|
||
/// <param name="beforeDate">结算日(不含,仅汇总此日之前的历史已结利息;当日事件由 closeList 去重逻辑单独处理)</param>
|
||
/// <returns>历史已结利息累计金额(绝对值)</returns>
|
||
public virtual decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||
{
|
||
List<int> swapEventTypes = new List<int>() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||
var consumed = DbContext.swap_flow_event
|
||
.Where(x => x.SwapTradeId == tradeId && x.PositionId == positionId
|
||
&& swapEventTypes.Contains(x.EventType)
|
||
&& x.DataState == (int)SwapFlowDateStateEnum.完成
|
||
&& x.EventDate < beforeDate)
|
||
.Sum(s => (decimal?)s.InterestAmount) ?? 0m;
|
||
return consumed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算利息腿计息详细
|
||
/// </summary>
|
||
/// <param name="td">交易</param>
|
||
/// <param name="tradeExtend">交易扩展数据</param>
|
||
/// <param name="valueDate">操作日期</param>
|
||
/// <param name="eodPositions">上一日终持仓</param>
|
||
/// <param name="positions">期初利率端</param>
|
||
/// <param name="posiNotionalValue">持仓名义本金</param>
|
||
/// <param name="posiLongNotionalValue">多头持仓名义本金</param>
|
||
/// <param name="posiShortNotionalValue">空头持仓名义本金</param>
|
||
/// <param name="closePosiNotionalValue">平仓名义本金</param>
|
||
/// <param name="closePrecent"></param>
|
||
/// <param name="eventType"></param>
|
||
/// <param name="tdClose"></param>
|
||
/// <param name="add"></param>
|
||
/// <returns></returns>
|
||
public List<swap_flow_event> GetInterests(
|
||
trade td,
|
||
trade_extend tradeExtend,
|
||
DateTime valueDate,
|
||
DateTime unwindDate,
|
||
List<eod_swap_position> eodPositions,
|
||
List<swap_position> positions,
|
||
decimal posiNotionalValue,
|
||
decimal posiLongNotionalValue,
|
||
decimal posiShortNotionalValue,
|
||
decimal closePosiNotionalValue,
|
||
decimal closePrecent,
|
||
int eventType,
|
||
bool tdClose,
|
||
bool needPrice,
|
||
decimal grossPrice,
|
||
decimal orginPv,
|
||
bool add = false,
|
||
bool settment = true,
|
||
bool newCalcLast= false,
|
||
List<swap_flow_event> closeList = null)
|
||
{
|
||
List<swap_flow_event> interests = new List<swap_flow_event>();
|
||
var annualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
bool calcFirst = tradeExtend?.ExtendObj.InterestCalcMode?.StartsWith("1") ?? true;
|
||
bool calcLast = tradeExtend?.ExtendObj.InterestCalcMode.EndsWith("1") ?? true;
|
||
foreach (var position in positions)
|
||
{
|
||
// 初始化持仓信息
|
||
var preEodPosition = eodPositions.FirstOrDefault(x => x.PositionId == position.id) ?? new eod_swap_position();
|
||
var positionClone = position.Clone();
|
||
DateTime? preDealDate = preEodPosition.id != 0 ? preEodPosition.ValueDate : null;
|
||
|
||
// 计算计息区间
|
||
int interestPeriod = position.interest_rest_days ?? 1;
|
||
bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, out DateTime startDate, out DateTime endDate);
|
||
|
||
// 计算名义本金
|
||
var (closePrincipal, posiPrincipal, newClosePercent) = CalcNotionalByMode(position, closePrecent, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue);
|
||
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.追加预付金 || (InterestModeEnum)position.InterestMode == InterestModeEnum.初始预付金)
|
||
{
|
||
positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum.收取 ? (int)SwapDirectionEnum.支付 : (int)SwapDirectionEnum.收取;
|
||
}
|
||
|
||
// 获取利率
|
||
decimal rate = GetFixedRate(position, unwindDate);
|
||
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
|
||
|
||
// 根据场景计算利息
|
||
if (settment)
|
||
{
|
||
// 收盘归档场景,使用 CalcEodInterest
|
||
interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add));
|
||
}
|
||
else
|
||
{
|
||
// 盘中互换场景,使用 CalcUnwindInterest
|
||
// 取历史已结利息(事件级,互换当时落库),供复利重算扣除(仅复利需要;单利基于日终快照自带状态)
|
||
var consumedInterest = position.InterestType == (int)InterestTypeEnum.复利
|
||
? GetConsumedInterest(td.id, position.id, endDate)
|
||
: 0m;
|
||
interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast||newCalcLast, consumedInterest));
|
||
}
|
||
}
|
||
//当日有平仓或互换记录时,避免重复结算
|
||
if (closeList != null && closeList.Count > 0)
|
||
{
|
||
foreach (var item in interests)
|
||
{
|
||
var closeEvent = closeList.Where(x => x.PositionId == item.PositionId);
|
||
if (eventType == (int)SwapEventTypeEnum.互换 || eventType == (int)SwapEventTypeEnum.自动互换)
|
||
{
|
||
// 互换:该仓位当天有同类型的完成事件,直接归0
|
||
var swapCloseEvent = closeEvent.Where(x => x.EventType == eventType);
|
||
if (swapCloseEvent.Any())
|
||
{
|
||
item.InterestAmount = 0;
|
||
item.TdInterestAmount = 0;
|
||
item.InterestClosePnL = 0;
|
||
}
|
||
}
|
||
else if (!calcLast && !newCalcLast)
|
||
{
|
||
// 平仓不算尾:扣除已结算的利息(算尾时利息已包含关闭日,无重叠)
|
||
var closePnl = closeEvent.Sum(s => s.InterestClosePnL);
|
||
var closeAmount = closeEvent.Sum(s => s.InterestAmount);
|
||
var closeTdAmount = closeEvent.Sum(s => s.TdInterestAmount);
|
||
item.InterestAmount = item.InterestAmount - closeAmount;
|
||
item.TdInterestAmount = item.TdInterestAmount - closeTdAmount;
|
||
item.InterestClosePnL = item.InterestClosePnL - closePnl;
|
||
}
|
||
}
|
||
}
|
||
return interests;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据计息模式计算名义本金
|
||
/// </summary>
|
||
private (decimal close, decimal posi, decimal closePct) CalcNotionalByMode(swap_position position, decimal closePercent, decimal posiNotional, decimal posiLong, decimal posiShort)
|
||
{
|
||
decimal closePrincipal = posiNotional;
|
||
decimal posiPrincipal = posiNotional;
|
||
decimal newClosePercent = closePercent;
|
||
|
||
switch ((InterestModeEnum)position.InterestMode)
|
||
{
|
||
case InterestModeEnum.固定值:
|
||
closePrincipal = posiPrincipal = position.InterestPrincipalFix;
|
||
newClosePercent = 1m;
|
||
break;
|
||
case InterestModeEnum.多头存续名义本金:
|
||
closePrincipal = posiLong * closePercent;
|
||
posiPrincipal = posiLong;
|
||
break;
|
||
case InterestModeEnum.空头存续名义本金:
|
||
closePrincipal = posiShort * closePercent;
|
||
posiPrincipal = posiShort;
|
||
break;
|
||
case InterestModeEnum.标的期初全价:
|
||
closePrincipal = posiNotional * closePercent;
|
||
break;
|
||
case InterestModeEnum.追加预付金:
|
||
case InterestModeEnum.初始预付金:
|
||
closePrincipal = position.InterestPrincipalFix * closePercent;
|
||
posiPrincipal = position.InterestPrincipalFix;
|
||
break;
|
||
}
|
||
return (closePrincipal, posiPrincipal, newClosePercent);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 平仓比例口径转换(解决"显示占期初 / 计算占剩余"双语义问题)。
|
||
/// 前端与事件列表展示用"占期初(original)"语义(A);后端 CalcNotionalByMode / 费用递减 /
|
||
/// 全平判定均按"占剩余(remaining)"语义(B)消费。
|
||
/// A → B:B = A × 期初名义本金(NotionalValue) / 剩余名义本金(PosiNotionalValue),并 cap 到 1。
|
||
/// B → A:A = B × 剩余名义本金 / 期初名义本金。
|
||
/// 分母为 0(无持仓等异常场景)时原样返回,避免除零。
|
||
/// </summary>
|
||
public static decimal ToRemainingClosePercent(decimal originalClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||
{
|
||
if (posiNotionalValue <= 0) return originalClosePercent;
|
||
var remaining = originalClosePercent * notionalValue / posiNotionalValue;
|
||
return remaining > 1 ? 1 : remaining;
|
||
}
|
||
|
||
/// <summary>
|
||
/// B(占剩余) → A(占期初),用于落库 / 事件列表展示还原。见 ToRemainingClosePercent。
|
||
/// </summary>
|
||
public static decimal ToOriginalClosePercent(decimal remainingClosePercent, decimal notionalValue, decimal posiNotionalValue)
|
||
{
|
||
if (notionalValue <= 0) return remainingClosePercent;
|
||
return remainingClosePercent * posiNotionalValue / notionalValue;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算 InitUnwind 默认占期初(A)平仓比例 = "平掉剩余全部持仓"对应的占期初比例。
|
||
/// 即:ClosePercent(A) = PosiNotionalValue / NotionalValue。
|
||
/// 未平仓时 PosiNotionalValue==NotionalValue → 1(平100%);
|
||
/// 部分平仓后自动变为剩余比例(如已平 30% 则默认 0.7)。
|
||
/// 与互换/提前终止 InitIncome 保持一致。抽出为纯函数以支持无库单测。
|
||
/// </summary>
|
||
public static decimal CalcDefaultInitClosePercent(decimal notionalValue, decimal posiNotionalValue)
|
||
{
|
||
return notionalValue > 0 ? posiNotionalValue / notionalValue : 1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取固定利率
|
||
/// </summary>
|
||
private decimal GetFixedRate(swap_position position, DateTime startDate)
|
||
{
|
||
var swapIntervalToday = position.SwapIntervalList?.Where(x => x.Date <= startDate).OrderByDescending(o => o.Date).FirstOrDefault();
|
||
if (swapIntervalToday != null) return swapIntervalToday.Rate;
|
||
var nextInterval = position.SwapIntervalList?.Where(x => x.Date > startDate).OrderBy(o => o.Date).FirstOrDefault();
|
||
return nextInterval?.Rate ?? position.InterestRateDefault;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取浮动利率
|
||
/// </summary>
|
||
private decimal GetFloatRate(swap_position position, eod_swap_position preEod, DateTime startDate, DateTime endDate, int period, bool swap, swap_position positionClone)
|
||
{
|
||
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return position.FloatRate;
|
||
|
||
int days = (endDate - startDate).Days;
|
||
DateTime rateDate = days % period == 0
|
||
? QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0))
|
||
: QdpCalendarHelper.GetNonHolidayDefore(startDate.AddDays(position.interest_rule ?? 0));
|
||
|
||
if (preEod.id != 0 && days % period != 0)
|
||
{
|
||
position.FloatRate = positionClone.FloatRate = preEod.FloatRate;
|
||
return preEod.FloatRate;
|
||
}
|
||
|
||
if (TryGetFloatRate(rateDate, position.FloatRateUnderlyingCode, out double rate))
|
||
{
|
||
position.FloatRate = positionClone.FloatRate = Convert.ToDecimal(rate);
|
||
return position.FloatRate;
|
||
}
|
||
if (!swap) throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
|
||
return 0m;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算收盘利息(EOD)
|
||
/// </summary>
|
||
private swap_flow_event CalcEodInterest(trade td, DateTime valueDate, swap_position position, decimal rate, decimal floatRate, decimal closePrincipal, decimal posiPrincipal, int annualDays, bool calcFirst, bool calcLast, eod_swap_position preEod, int eventType, bool add)
|
||
{
|
||
// 判断当日是否计息:首日不算头或到期日不算尾则不计息
|
||
bool calcToday = true;
|
||
if (calcFirst == false && valueDate == td.StartDate.Value) calcToday = false; // 首日不算头
|
||
if (calcLast == false && valueDate == td.ExerciseDate.Value) calcToday = false; // 到期日不算尾
|
||
if (valueDate < position.PosiStartDate)
|
||
{
|
||
calcToday = false;
|
||
}
|
||
// 初始化EOD持仓信息
|
||
if (preEod.id == 0)
|
||
{
|
||
preEod.FloatRate = floatRate;
|
||
preEod.TdInterestPrincipal = posiPrincipal;
|
||
preEod.PosiNotionalValue = posiPrincipal;
|
||
}
|
||
|
||
// 构建利息事件
|
||
var interest = new swap_flow_event
|
||
{
|
||
SwapTradeId = td.id,
|
||
SwapTradeNo = td.TradeNumber,
|
||
EventType = eventType,
|
||
EventReason = "交易",
|
||
EventDate = valueDate,
|
||
PositionId = position.id,
|
||
InterestDirection = position.InterestDirection,
|
||
InterestRate = rate,
|
||
InterestPrincipal = closePrincipal,
|
||
InterestSwapInterval = position.InterestSwapInterval,
|
||
InterestMode = position.InterestMode,
|
||
FloatRate = floatRate,
|
||
DataState = (int)SwapFlowDateStateEnum.完成,
|
||
ClientId = td.ClientId,
|
||
UnwindDate = valueDate
|
||
};
|
||
// 收盘场景使用 preEod.FloatRate(历史浮动利率),与 InitSwapDealInterest 收盘场景保持一致
|
||
decimal eodFloatRate = preEod.id != 0 ? preEod.FloatRate : floatRate;
|
||
decimal interestAmount = 0;
|
||
decimal tdInterestAmount = 0;
|
||
if (calcToday)
|
||
{
|
||
if (position.InterestType == (int)InterestTypeEnum.复利)
|
||
{
|
||
// 复利计算
|
||
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
|
||
}
|
||
else
|
||
{
|
||
// 单利计算
|
||
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
|
||
}
|
||
|
||
}
|
||
// 四舍五入并赋值
|
||
interest.InterestAmount = Math.Round(interestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
interest.TdInterestAmount = Math.Round(tdInterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
// 计算InterestClosePnL(方向:收取=1为正,支付=-1为负)
|
||
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
|
||
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
|
||
|
||
if (add) UpdateDbOption(interest);
|
||
return interest;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算盘中利息(平仓/互换)
|
||
/// </summary>
|
||
private swap_flow_event CalcUnwindInterest(trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate, decimal floatRate, decimal posiPrincipal, decimal closePrincipal, decimal closePercent, int annualDays, eod_swap_position preEod, int eventType, bool add, bool swap, decimal orginPv, bool calcFirst, bool calcLast, decimal consumedInterest = 0m)
|
||
{
|
||
if (preEod.id == 0)
|
||
{
|
||
preEod.FloatRate = floatRate;
|
||
preEod.TdInterestPrincipal = posiPrincipal;
|
||
preEod.PosiNotionalValue = posiPrincipal;
|
||
preEod.ValueDate = td.StartDate.Value;
|
||
if (calcFirst)
|
||
{
|
||
preEod.ValueDate= preEod.ValueDate.AddDays(-1);
|
||
}
|
||
}
|
||
|
||
return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, closePrincipal, closePercent, annualDays, eventType, preEod, false, orginPv, calcFirst, calcLast, consumedInterest);
|
||
}
|
||
/// <summary>
|
||
/// 初始化利息腿信息
|
||
/// </summary>
|
||
/// <param name="tradeId">交易编码</param>
|
||
/// <param name="valueDate">计息开始日期</param>
|
||
/// <param name="endDate">计息结束日期</param>
|
||
/// <param name="rate">计息年化利率</param>
|
||
/// <param name="position">利息腿</param>
|
||
/// <param name="add">是否新增</param>
|
||
/// <param name="swap">是否已互换</param>
|
||
/// <param name="preEodPosition">上一日终归档</param>
|
||
/// <param name="posiNotionalValue">当日适用名义本金</param>
|
||
/// <param name="closePosiNotionalValue">当日平仓名义本金</param>
|
||
/// <param name="annualDays">年化天数</param>
|
||
/// <returns></returns>
|
||
private swap_flow_event InitSwapDealInterest(trade td,
|
||
DateTime valueDate,
|
||
DateTime endDate,
|
||
decimal rate,
|
||
swap_position position,
|
||
bool add,
|
||
bool swap,
|
||
decimal posiNotionalValue,
|
||
decimal closePosiNotionalValue,
|
||
decimal closePrecent,
|
||
int annualDays,
|
||
int eventType,
|
||
eod_swap_position preEodPosition,
|
||
bool needPrice,
|
||
decimal orginPv,
|
||
bool calcFirst,
|
||
bool calcLast,
|
||
decimal consumedInterest = 0m
|
||
)
|
||
{
|
||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||
swap_flow_event interest = new swap_flow_event();
|
||
interest.SwapTradeId = td.id;
|
||
interest.SwapTradeNo = td.TradeNumber;
|
||
interest.EventType = eventType;
|
||
interest.EventReason = "交易";
|
||
interest.EventDate = valueDate;
|
||
interest.PositionId = position.id;
|
||
interest.InterestDirection = position.InterestDirection;
|
||
interest.InterestRate = rate;
|
||
interest.InterestPrincipal = closePosiNotionalValue;
|
||
interest.InterestSwapInterval = position.InterestSwapInterval;
|
||
interest.InterestMode = position.InterestMode;
|
||
interest.FloatRate = position.FloatRate;
|
||
interest.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
interest.ClientId = td.ClientId;
|
||
interest.UnwindDate = endDate;
|
||
|
||
// 根因修复:预付金(保证金)腿的计息基数维度应为"保证金本金"自身,而非整笔交易的名义本金(orginPv)。
|
||
// 否则公式 dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv
|
||
// 会把交易名义本金(千万~亿级)当减项扣掉,使"应返还本金"(InterestPrincipal)与计息基数变成巨负值。
|
||
// 此处将预付金腿的 orginPv 对齐为其自身保证金(InterestPrincipalFix),
|
||
// 与日终路径(SwapEodPositionService 对预付金腿 orginPv=InterestPrincipalFix)保持一致。
|
||
// 仅作用于初始预付金(5)/追加预付金(6);其它计息模式(含债券本金腿 标的期初全价=9)仍用交易名义本金,不受影响。
|
||
if (position.InterestMode == (int)InterestModeEnum.初始预付金
|
||
|| position.InterestMode == (int)InterestModeEnum.追加预付金)
|
||
{
|
||
orginPv = position.InterestPrincipalFix;
|
||
}
|
||
|
||
if (swap)
|
||
{
|
||
interest.InterestAmount = 0;
|
||
interest.TdInterestAmount = 0;
|
||
interest.InterestAmount = 0;
|
||
interest.InterestClosePnL = 0;
|
||
}
|
||
else
|
||
{
|
||
decimal InterestAmount = 0;
|
||
decimal TdInterestAmount = 0;
|
||
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
|
||
var floateRate = preEodPosition.FloatRate;
|
||
if (position.InterestType == (int)InterestTypeEnum.复利)
|
||
{
|
||
CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest);
|
||
}
|
||
else
|
||
{
|
||
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
|
||
}
|
||
|
||
interest.InterestAmount = Math.Round(InterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
interest.TdInterestAmount = Math.Round(TdInterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
|
||
}
|
||
if (add)
|
||
{
|
||
UpdateDbOption(interest);
|
||
}
|
||
return interest;
|
||
}
|
||
/// <summary>
|
||
/// 计算复利 盘中
|
||
/// </summary>
|
||
/// <param name="lastSwapDate">上一互换日</param>
|
||
/// <param name="endDate">结算日期</param>
|
||
/// <param name="floatUnderylingCode">浮动标的</param>
|
||
/// <param name="principal">计息基数</param>
|
||
/// <param name="interestRate">固定利率</param>
|
||
/// <param name="isAnnualized">是否年化</param>
|
||
/// <param name="annualDays">年化天数</param>
|
||
/// <returns></returns>
|
||
public void CalcDailyCompoundInterest( DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||
{
|
||
var startDate = position.PosiStartDate;
|
||
decimal interestProfitSum = 0;
|
||
decimal TdInterestPrincipal = 0;
|
||
decimal interest = interestProfitSum ;
|
||
decimal tdinterest = interestProfitSum ;
|
||
int interestPeriod = position.interest_rest_days ?? 1;
|
||
// 复利:只能用要平仓的名义本金从头开始算
|
||
decimal dynomicPrincipal = principal;
|
||
decimal tdDynomicPrincipal = dynomicPrincipal;
|
||
var calcDays = (endDate - startDate).Days;
|
||
double floatRate = Convert.ToDouble(floateRate);
|
||
for (int i = 0; i <= calcDays; i++)
|
||
{
|
||
var accrueDate = startDate.AddDays(i);
|
||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾
|
||
if (accrueDate >= startDate)
|
||
{
|
||
if (i % interestPeriod == 0)
|
||
{
|
||
// 复利时:利息并入本金
|
||
dynomicPrincipal = principal + interest;
|
||
tdDynomicPrincipal = principal + interest;
|
||
// 获取新的浮动利率
|
||
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||
{
|
||
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
|
||
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
|
||
{
|
||
if (floatRate1 != 0) floatRate = floatRate1;
|
||
}
|
||
else
|
||
{
|
||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
|
||
}
|
||
}
|
||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||
TdInterestPrincipal = tdDynomicPrincipal;
|
||
}
|
||
else
|
||
{
|
||
// 复利非重置日:利息不并入本金,不用closePercent缩放(principal已反映平仓比例)
|
||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||
TdInterestPrincipal = tdDynomicPrincipal;
|
||
}
|
||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
if (position.IsAnnualized)
|
||
{
|
||
interest1 /= annualDays;
|
||
tdinterest1 /= annualDays;
|
||
}
|
||
interest += interest1;
|
||
tdinterest += tdinterest1;
|
||
}
|
||
}
|
||
// 复利从头重放得到的是"假设从未结出"的整段总利息,需扣除历史已通过互换结出的利息,
|
||
// 否则已结部分会重复计息(类比分红 PosiDividendSum = totalToDate − RealizedDividend)。
|
||
// consumedInterest 为绝对值口径(swap_flow_event.InterestAmount 之和),与 interest 口径一致。
|
||
interest -= consumedInterest;
|
||
tdinterest -= consumedInterest;
|
||
InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率)
|
||
/// </summary>
|
||
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||
{
|
||
var startDate = position.PosiStartDate;
|
||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||
var TdInterestPrincipal = preEodPosition.TdInterestPrincipal;
|
||
decimal interest = interestProfitSum * closePercent;
|
||
decimal tdinterest = interestProfitSum * closePercent;
|
||
int interestPeriod = position.interest_rest_days ?? 1;
|
||
// 单利:可用上一日计息基数
|
||
decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||
decimal tdDynomicPrincipal = dynomicPrincipal;
|
||
var calcDays = (endDate - startDate).Days;
|
||
double floatRate = Convert.ToDouble(floateRate);
|
||
for (int i = 0; i <= calcDays; i++)
|
||
{
|
||
var accrueDate = startDate.AddDays(i);
|
||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾
|
||
if (accrueDate > preEodPosition.ValueDate)
|
||
{
|
||
// 重置日重新获取该段浮动利率;非重置日沿用上一段利率。
|
||
// 两分支唯一差异即"是否重取利率",本金口径(只缩放一次)完全一致,
|
||
// 合并后消除复制粘贴导致的 closePercent^N 类 bug(原非重置日分支多了一行
|
||
// tdDynomicPrincipal = flowEvent.InterestPrincipal 使本金累积乘 closePercent^N)。
|
||
if (i % interestPeriod == 0 && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||
{
|
||
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
|
||
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
|
||
{
|
||
if (floatRate1 != 0) floatRate = floatRate1;
|
||
}
|
||
else
|
||
{
|
||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
|
||
}
|
||
}
|
||
|
||
// 显示本金 = 计息基数 × closePercent(只缩放一次,与日终 ByEod 口径一致);
|
||
// 计息基数(tdDynomicPrincipal)逐日恒定、不缩放(单利特征)。
|
||
flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
|
||
TdInterestPrincipal = tdDynomicPrincipal;
|
||
|
||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
if (position.IsAnnualized)
|
||
{
|
||
interest1 /= annualDays;
|
||
tdinterest1 /= annualDays;
|
||
}
|
||
interest += interest1;
|
||
tdinterest += tdinterest1;
|
||
}
|
||
}
|
||
InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算复利 收盘
|
||
/// </summary>
|
||
/// <param name="lastSwapDate">上一互换日</param>
|
||
/// <param name="endDate">结算日期</param>
|
||
/// <param name="tradeDate">开仓日</param>
|
||
/// <param name="floatUnderylingCode">浮动标的</param>
|
||
/// <param name="principal">计息基数</param>
|
||
/// <param name="interestRate">固定利率</param>
|
||
/// <param name="isAnnualized">是否年化</param>
|
||
/// <param name="annualDays">年化天数</param>
|
||
/// <returns></returns>
|
||
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||
{
|
||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||
decimal interest = interestProfitSum * closePercent;
|
||
decimal tdinterest = interestProfitSum * closePercent;
|
||
int interestPeriod = position.interest_rest_days ?? 1;
|
||
decimal tdDynomicPrincipal = posiPrincipal;
|
||
double floatRate = Convert.ToDouble(floateRate);
|
||
var days = (endDate - tradeDate).Days;
|
||
if (days % interestPeriod == 0)
|
||
{
|
||
tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum;
|
||
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||
{
|
||
// 获取合适的 rateDate
|
||
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0)); // 获取前一工作日;
|
||
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
|
||
{
|
||
if (floatRate1 != 0)
|
||
{
|
||
floatRate = floatRate1;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
|
||
}
|
||
|
||
}
|
||
flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
|
||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
var tdinterest1 = tdDynomicPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
if (position.IsAnnualized)
|
||
{
|
||
interest1 /= annualDays;
|
||
tdinterest1 /= annualDays;
|
||
}
|
||
interest += interest1;
|
||
tdinterest = tdinterest1;
|
||
}
|
||
else
|
||
{
|
||
flowEvent.InterestPrincipal = (preEodPosition.TdInterestPrincipal + tdDynomicPrincipal - orginPv) * closePercent;
|
||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
var tdinterest1 = (preEodPosition.TdInterestPrincipal + tdDynomicPrincipal - orginPv) * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
if (position.IsAnnualized)
|
||
{
|
||
interest1 /= annualDays;
|
||
tdinterest1 /= annualDays;
|
||
}
|
||
interest += interest1;
|
||
tdinterest = tdinterest1;
|
||
}
|
||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||
InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率)
|
||
/// </summary>
|
||
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||
{
|
||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||
int interestPeriod = position.interest_rest_days ?? 1;
|
||
double floatRate = Convert.ToDouble(floateRate);
|
||
var calcDays = (endDate - tradeDate).Days;
|
||
// 修复:首次操作时(preEodPosition.id == 0),TdInterestPrincipal 需要正确初始化
|
||
if (preEodPosition.id == 0)
|
||
{
|
||
preEodPosition.TdInterestPrincipal = posiPrincipal;
|
||
}
|
||
|
||
// 检查是否到达重置周期
|
||
if (calcDays % interestPeriod == 0)
|
||
{
|
||
// 获取新的浮动利率
|
||
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||
{
|
||
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(endDate.AddDays(position.interest_rule ?? 0));
|
||
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double newFloatRate))
|
||
{
|
||
if (newFloatRate != 0)
|
||
{
|
||
floatRate = newFloatRate;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
|
||
}
|
||
}
|
||
}
|
||
|
||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||
var baseTdInterestPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||
var baseInterestPrincipal = baseTdInterestPrincipal * closePercent;
|
||
|
||
// 修复:正确计算本次利息(基于实际持仓本金)
|
||
decimal interest = baseInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
decimal tdinterest = baseTdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||
if (position.IsAnnualized)
|
||
{
|
||
interest /= annualDays;
|
||
tdinterest /= annualDays;
|
||
}
|
||
|
||
InterestAmount = Math.Round(interestProfitSum+interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单标的平仓
|
||
/// </summary>
|
||
/// <param name="unwindData"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void SwapUnwind(UnwindData unwindData)
|
||
{
|
||
var td = FindTrade(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
|
||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||
bool cofirm = false;
|
||
ExecuteInTransaction(() =>
|
||
{
|
||
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||
if (unwindData.SwapMarginAmount != 0)
|
||
{
|
||
AddClientCash(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||
}
|
||
//if (unwindData.SwapMarginRebatePnl != 0)
|
||
//{
|
||
// AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||
//}
|
||
DealFloatPosition(unwindData);
|
||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||
if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓 || unwindData.ClosePercent == 1)
|
||
{
|
||
td.TradeStatus = "已平仓";
|
||
CallSaveSwapTradeClientCash(td, unwindData.ValueDate);
|
||
}
|
||
else
|
||
{
|
||
td.HasPartialUnWind = 1;
|
||
}
|
||
td.UnWindDate = unwindData.UnwindDate;
|
||
td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue);
|
||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||
SaveAllChanges();
|
||
cofirm = true;
|
||
});
|
||
if (cofirm)
|
||
{
|
||
TriggerRealtimeSwapPosition();
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 自动全平仓
|
||
/// </summary>
|
||
/// <param name="td"></param>
|
||
/// <param name="unwindPrice"></param>
|
||
/// <param name="unwindPriceFee"></param>
|
||
public void AuotoSwapUnwind(int tradeid, decimal unwindPrice, decimal unwindPriceFee, decimal unwindNetFee, decimal unwindNet, DateTime valueDate, decimal unwindQty, decimal closeFee)
|
||
{
|
||
unwindPriceFee = decimal.Parse(unwindPriceFee.ToString("F10"));
|
||
var td = DbContext.trade.Find(tradeid);
|
||
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid);
|
||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换 };
|
||
var dealDate = valueDate;
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||
td.trade_extend = tradeExtend;
|
||
var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault();
|
||
var preDealDate = GetPreDealDate(td.id, dealDate, eventTypes);
|
||
swap_flow_event floatEvent = new swap_flow_event();
|
||
UnwindData unwindData = new UnwindData();
|
||
unwindData.CloseType = 2;
|
||
unwindData.StartDate = td.TradeDate.Value;
|
||
if (preDealDate.HasValue)
|
||
{
|
||
unwindData.StartDate = preDealDate.Value;
|
||
}
|
||
unwindData.ValueDate = dealDate;
|
||
unwindData.UnwindDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(1));
|
||
floatEvent.EventDate = dealDate;
|
||
floatEvent.UnwindDate = unwindData.UnwindDate;
|
||
floatEvent.PayDate = QdpCalendarHelper.GetNonHoliday(unwindData.UnwindDate.Value.AddDays(td.trade_extend.ExtendObj.SettlementRules));
|
||
unwindData.PayDate = floatEvent.PayDate;
|
||
floatEvent.SwapTradeId = td.id;
|
||
floatEvent.SwapTradeNo = td.TradeNumber;
|
||
unwindData.SwapTradeId = td.id;
|
||
unwindData.StructureType = td.StructureType;
|
||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||
unwindData.NotionalQty = positions.Where(x => x.IsInitial).Sum(s => s.PosiQuantity);
|
||
unwindData.PosiNotionalValue = position != null ? position.PosiNotionalValue : Convert.ToDecimal(td.StockEqvNotional);
|
||
unwindData.PositionQty = Convert.ToDecimal(td.TradeAmount);
|
||
var unwindPercent = unwindData.PositionQty == 0 ? 0 : unwindQty / unwindData.PositionQty;
|
||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
unwindData.CloseMethod = unwindQty == unwindData.PositionQty ? (int)CloseMethodEnum.全部平仓 : (int)CloseMethodEnum.部分平仓;
|
||
unwindData.ClosePercent = unwindData.PositionQty == 0 ? 0 : unwindPercent;
|
||
unwindData.CloseNotionalValue = position == null ? 0 : unwindQty * position.PosiGrossPrice * position.ContractSize;
|
||
unwindData.CloseNotionalValue = unwindPercent >= 1 ? unwindData.PosiNotionalValue : Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
unwindData.CloseQty = unwindQty;
|
||
if (position != null)
|
||
{
|
||
decimal floatRatio = position.PosiDirection == 1 ? 1m : -1m;
|
||
decimal longRatio = position.PositionType == 1 ? 1m : -1m;
|
||
floatEvent.PositionId = position.PositionId;
|
||
floatEvent.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
floatEvent.EventReason = "交易";
|
||
floatEvent.DividendIn = 0;
|
||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||
floatEvent.CloseFee = 0;
|
||
floatEvent.BeforeCloseFee = position.PosiTradingFee + position.PosiTradingFeePending;
|
||
floatEvent.PayDirection = position.PosiDirection;
|
||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||
floatEvent.PosiNetPrice = position.PosiNetPrice;
|
||
floatEvent.PositionType = position.PositionType;
|
||
floatEvent.Quantity = unwindData.CloseQty;
|
||
floatEvent.PositionQty = unwindData.PositionQty - unwindData.CloseQty;
|
||
floatEvent.ContractSize = position.ContractSize;
|
||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
floatEvent.InterestMode = position.InterestMode;
|
||
floatEvent.TradingAmountAvg = unwindPrice;
|
||
floatEvent.TradingAmountFeeAvg = unwindPriceFee;
|
||
floatEvent.TradingAmountNetFeeAvg = unwindNetFee;
|
||
floatEvent.TradingAmountNetAvg = unwindNet;
|
||
floatEvent.TradingFeePending = position.PosiTradingFeePending * unwindData.ClosePercent;
|
||
floatEvent.TradingFeePending = Math.Round(floatEvent.TradingFeePending, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
floatEvent.TradingFee = closeFee;
|
||
floatEvent.MarkClosePnl = (unwindPrice - position.PosiGrossPrice) * unwindQty * floatRatio * longRatio;
|
||
//MarkClosePnl 纯盯市不要计算交易费用和分红
|
||
floatEvent.MarkClosePnl = Math.Round(floatEvent.MarkClosePnl, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
floatEvent.TradingAmount = unwindPrice * floatEvent.Quantity * floatEvent.ContractSize;
|
||
floatEvent.TradingAmount = Math.Round(floatEvent.TradingAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
floatEvent.OptLog = "流水自动";
|
||
floatEvent.ClientId = td.ClientId;
|
||
floatEvent.SetOpt(UserInfo);
|
||
EnrichDividendIn(floatEvent, unwindQty, td);
|
||
}
|
||
unwindData.FlowEvents.Add(floatEvent);
|
||
var interestPositions = GetUnwindInterests(unwindData.ValueDate, unwindData.UnwindDate.Value, td.id, unwindPercent, (int)SwapEventTypeEnum.平仓);
|
||
interestPositions.ForEach(item =>
|
||
{
|
||
item.OptLog = "流水自动";
|
||
});
|
||
foreach (var item in interestPositions)
|
||
{
|
||
item.TdInterestAmount = Math.Round(item.TdInterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
item.InterestAmount = Math.Round(item.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
item.InterestClosePnL = Math.Round(item.InterestClosePnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
}
|
||
unwindData.FlowEvents.AddRange(interestPositions);
|
||
CalcCloseAmount(unwindData);
|
||
DealUnwind(unwindData, td);
|
||
}
|
||
|
||
private void EnrichDividendIn(swap_flow_event flowEvent, decimal unwindQty, trade td)
|
||
{
|
||
if (flowEvent.UnwindDate == null)
|
||
{
|
||
throw new ArgumentNullException("平仓日期缺失");
|
||
}
|
||
var date = flowEvent.EventDate;
|
||
BondPaymentService servie = new BondPaymentService(UserInfo);
|
||
var payments = servie.GetBondPayments(flowEvent.UnderlyingCode, td.StartDate.Value, date);
|
||
|
||
int shortRatio = flowEvent.PositionType == (int)PositionTypeFlag.Long ? 1 : -1;
|
||
int directionRatio = flowEvent.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
|
||
// + 付息日>上日日终且小于等于平仓日期的分红数据
|
||
var dividendIn = servie.CalcPayment(payments, unwindQty, shortRatio, directionRatio);
|
||
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode);
|
||
decimal tax = um.ValueAddedTax ?? 0;
|
||
dividendIn = dividendIn / (1 + tax) * (1 - tax);
|
||
|
||
flowEvent.DividendIn = Math.Round(dividendIn, 2, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
public decimal GetUnderlyingTax(string code)
|
||
{
|
||
var data = DataCacheProvider.GetUnderlyingDataSource().GetData(code);
|
||
if (data == null)
|
||
{
|
||
return 0;
|
||
}
|
||
return data.ValueAddedTax ?? 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 衡泰新增平仓事件
|
||
/// </summary>
|
||
/// <param name="td"></param>
|
||
/// <param name="valueDate"></param>
|
||
/// <param name="markClosePnl"></param>
|
||
/// <param name="unwindQty"></param>
|
||
/// <param name="allClose"></param>
|
||
public void AutoSwapUnwindFromConsumer(trade td, DateTime valueDate, DateTime payDate, decimal markClosePnl, decimal tradeinfFee, decimal interestAmount, decimal fee, decimal unwindQty, bool allClose)
|
||
{
|
||
List<int> eventTypes = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换 };
|
||
var dealDate = valueDate;
|
||
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||
td.trade_extend = tradeExtend;
|
||
var position = DbContext.swap_position.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && x.IsInitial && !x.Invalid).FirstOrDefault();
|
||
var preDealDate = GetPreDealDate(td.id, dealDate, eventTypes);
|
||
swap_flow_event floatEvent = new swap_flow_event();
|
||
UnwindData unwindData = new UnwindData();
|
||
unwindData.CloseType = 2;
|
||
unwindData.StartDate = td.TradeDate.Value;
|
||
if (preDealDate.HasValue)
|
||
{
|
||
unwindData.StartDate = preDealDate.Value;
|
||
}
|
||
unwindData.ValueDate = dealDate;
|
||
floatEvent.EventDate = dealDate;
|
||
unwindData.UnwindDate = QdpCalendarHelper.GetNonHoliday(dealDate.AddDays(1));
|
||
floatEvent.UnwindDate = unwindData.UnwindDate;
|
||
floatEvent.PayDate = payDate;
|
||
unwindData.PayDate = floatEvent.PayDate;
|
||
floatEvent.SwapTradeId = td.id;
|
||
floatEvent.SwapTradeNo = td.TradeNumber;
|
||
unwindData.SwapTradeId = td.id;
|
||
unwindData.StructureType = td.StructureType;
|
||
unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0);
|
||
unwindData.NotionalQty = position.PosiQuantity;
|
||
unwindData.PosiNotionalValue = Convert.ToDecimal(td.StockEqvNotional);
|
||
unwindData.PositionQty = Convert.ToDecimal(td.TradeAmount);
|
||
unwindData.AnnualDays = tradeExtend == null ? 365 : tradeExtend.ExtendObj.AnnualDays;
|
||
unwindData.CloseMethod = allClose ? (int)CloseMethodEnum.全部平仓 : (int)CloseMethodEnum.部分平仓;
|
||
unwindData.ClosePercent = allClose ? 1 : unwindQty / unwindData.NotionalQty;
|
||
unwindData.CloseNotionalValue = allClose ? unwindData.PosiNotionalValue : unwindQty;
|
||
unwindData.CloseQty = allClose ? unwindData.PositionQty : unwindQty;
|
||
if (position != null)
|
||
{
|
||
decimal floatRatio = position.PosiDirection == 1 ? 1m : -1m;
|
||
floatEvent.PositionId = position.id;
|
||
floatEvent.EventType = (int)SwapEventTypeEnum.平仓;
|
||
floatEvent.EventReason = "接口合约终止交易";
|
||
floatEvent.DividendIn = 0;
|
||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||
floatEvent.CloseFee = 0;
|
||
floatEvent.BeforeCloseFee = position.PosiTradingFee + position.PosiTradingFeePending;
|
||
floatEvent.PayDirection = position.PosiDirection;
|
||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||
floatEvent.PosiNetPrice = position.PosiNetPrice;
|
||
floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice;
|
||
floatEvent.TradingFeePending = position.PosiTradingFeePending * unwindData.ClosePercent;
|
||
floatEvent.TradingFee = tradeinfFee - floatEvent.TradingFeePending;
|
||
floatEvent.MarkClosePnl = markClosePnl;
|
||
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
|
||
floatEvent.PositionType = position.PositionType;
|
||
floatEvent.Quantity = position.PosiQuantity;
|
||
floatEvent.PositionQty = 0;
|
||
floatEvent.ContractSize = position.ContractSize;
|
||
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
floatEvent.InterestMode = position.InterestMode;
|
||
floatEvent.TradingAmount = unwindData.CloseQty;
|
||
floatEvent.ClientId = td.ClientId;
|
||
floatEvent.OptLog = "衡泰同步";
|
||
floatEvent.SetOpt(UserInfo);
|
||
}
|
||
unwindData.FlowEvents.Add(floatEvent);
|
||
var interestPositions = GetUnwindInterestsByHT(unwindData, td, interestAmount, fee);
|
||
unwindData.FlowEvents.AddRange(interestPositions);
|
||
CalcCloseAmount(unwindData);
|
||
DealUnwind(unwindData, td, "合约终止接口回执");
|
||
}
|
||
private List<swap_flow_event> GetUnwindInterestsByHT(UnwindData unwindData, trade td, decimal interestAmount, decimal fee)
|
||
{
|
||
List<swap_flow_event> interests = new List<swap_flow_event>();
|
||
var allpositions = DbContext.swap_position.Where(x => x.SwapTradeId == unwindData.SwapTradeId && !x.Invalid && x.IsInitial && x.PosiDirection > 0).ToList();
|
||
var position = allpositions.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode)).FirstOrDefault();
|
||
if (position == null)
|
||
{
|
||
return interests;
|
||
}
|
||
var grossPrice = allpositions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0;
|
||
var _closePosiNotionalValue = unwindData.CloseNotionalValue;
|
||
var _posiNotionalValue = unwindData.PosiNotionalValue;
|
||
var newClosePercent = unwindData.ClosePercent;
|
||
foreach (var item in allpositions)
|
||
{
|
||
var positionClone = item.Clone();
|
||
var swapIntervalToday = position.SwapIntervalList.OrderByDescending(o => o.Date).FirstOrDefault();
|
||
if (item.InterestMode == (int)InterestModeEnum.固定值)
|
||
{
|
||
_closePosiNotionalValue = item.InterestPrincipalFix;
|
||
_posiNotionalValue = item.InterestPrincipalFix;
|
||
newClosePercent = 1m;
|
||
}
|
||
else if (item.InterestMode == (int)InterestModeEnum.标的期初全价)
|
||
{
|
||
_closePosiNotionalValue = _posiNotionalValue * grossPrice * newClosePercent;
|
||
_posiNotionalValue = _posiNotionalValue * grossPrice;
|
||
}
|
||
else if (item.InterestMode == (int)InterestModeEnum.追加预付金 || item.InterestMode == (int)InterestModeEnum.初始预付金)
|
||
{
|
||
_closePosiNotionalValue = 0;
|
||
positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum.收取 ? (int)SwapDirectionEnum.支付 : (int)SwapDirectionEnum.收取;
|
||
}
|
||
decimal rate = item.InterestRateDefault;
|
||
if (swapIntervalToday != null)//当日无适用观察日
|
||
{
|
||
rate = swapIntervalToday.Rate;
|
||
}
|
||
swap_flow_event interest = new swap_flow_event();
|
||
interest.SwapTradeId = td.id;
|
||
interest.SwapTradeNo = td.TradeNumber;
|
||
interest.EventType = (int)SwapEventTypeEnum.平仓;
|
||
interest.EventReason = "衡泰同步平仓";
|
||
interest.EventDate = unwindData.ValueDate;
|
||
interest.PositionId = item.id;
|
||
interest.InterestDirection = positionClone.InterestDirection;
|
||
interest.InterestRate = rate;
|
||
interest.InterestPrincipal = _closePosiNotionalValue;
|
||
interest.InterestSwapInterval = item.InterestSwapInterval;
|
||
interest.InterestMode = item.InterestMode;
|
||
interest.FloatRate = item.FloatRate;
|
||
interest.DataState = (int)SwapFlowDateStateEnum.完成;
|
||
interest.ClientId = td.ClientId;
|
||
interest.UnwindDate = unwindData.ValueDate;
|
||
interest.PayDate = unwindData.PayDate;
|
||
if (position != null && item.id == position.id)
|
||
{
|
||
interest.InterestAmount = interestAmount;
|
||
interest.TdInterestAmount = interestAmount;
|
||
interest.InterestClosePnL = interestAmount;
|
||
interest.InterestFee = fee;
|
||
}
|
||
UpdateDbOption(interest);
|
||
interests.Add(interest);
|
||
}
|
||
|
||
return interests;
|
||
}
|
||
private void DealUnwind(UnwindData unwindData, trade td, string actionMsg = "系统操作_自动平仓")
|
||
{
|
||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||
if (unwindData.SwapMarginAmount != 0)
|
||
{
|
||
AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||
}
|
||
//if (unwindData.SwapMarginRebatePnl != 0)
|
||
//{
|
||
// AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||
//}
|
||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, actionMsg);
|
||
if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||
{
|
||
td.TradeStatus = "已平仓";
|
||
td.trade_extend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id);
|
||
if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee)
|
||
{
|
||
new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, unwindData.ValueDate, 0);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
td.HasPartialUnWind = 1;
|
||
}
|
||
td.UnWindDate = unwindData.UnwindDate;
|
||
td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue);
|
||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||
td.Notional = td.TradeAmount;
|
||
td.OptDate = DateTime.Now;
|
||
td.OptId = UserId;
|
||
td.OptName = UserName;
|
||
DbContext.SaveChanges();
|
||
}
|
||
/// <summary>
|
||
/// 计算平仓总额
|
||
/// </summary>
|
||
/// <param name="unwindData"></param>
|
||
private void CalcCloseAmount(UnwindData unwindData)
|
||
{
|
||
var floatPosition = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||
var interestList = unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode));
|
||
decimal floatRatio = floatPosition.PayDirection == 1 ? 1m : -1m;
|
||
var pnl = floatPosition.FloatPnlSum;
|
||
unwindData.SwapCloseAmount = pnl;
|
||
unwindData.SwapRealizedPnL = pnl;
|
||
unwindData.SwapMarginRebatePnl = 0;
|
||
unwindData.SwapMarginAmount = 0;
|
||
if (interestList != null)
|
||
{
|
||
interestList.ForEach(x =>
|
||
{
|
||
if (x.InterestMode == (int)InterestModeEnum.追加预付金 || x.InterestMode == (int)InterestModeEnum.初始预付金)
|
||
{
|
||
decimal interestRatio = x.InterestDirection == 1 ? -1m : 1m;
|
||
unwindData.SwapMarginRebatePnl += x.InterestClosePnL;
|
||
unwindData.SwapMarginAmount += x.InterestPrincipal * interestRatio;
|
||
}
|
||
unwindData.SwapRealizedPnL += x.InterestClosePnL;
|
||
unwindData.SwapCloseAmount += x.InterestClosePnL;
|
||
|
||
});
|
||
}
|
||
unwindData.SwapCloseAmount = Math.Round(unwindData.SwapCloseAmount, 2, MidpointRounding.AwayFromZero);
|
||
unwindData.SwapRealizedPnL = Math.Round(unwindData.SwapRealizedPnL, 2, MidpointRounding.AwayFromZero);
|
||
}
|
||
/// <summary>
|
||
/// 多空组合平仓
|
||
/// </summary>
|
||
/// <param name="unwindData"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void SwapLongShortUnwind(UnwindData unwindData)
|
||
{
|
||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||
var trans = DbContext.Database.BeginTransaction();
|
||
try
|
||
{
|
||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||
if (unwindData.SwapMarginAmount != 0)
|
||
{
|
||
AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||
}
|
||
if (unwindData.SwapMarginRebatePnl != 0)
|
||
{
|
||
AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||
}
|
||
SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||
td.UnWindDate = unwindData.UnwindDate;
|
||
td.StockEqvNotional = 0;
|
||
td.TradeStatus = "已平仓";
|
||
DbContext.SaveChanges();
|
||
trans.Commit();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
trans.Rollback();
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
trans.Dispose();
|
||
}
|
||
|
||
}
|
||
/// <summary>
|
||
/// 多空组合互换
|
||
/// </summary>
|
||
/// <param name="swap_Deal"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void SwapLongShort(UnwindData unwindData)
|
||
{
|
||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||
var trans = DbContext.Database.BeginTransaction();
|
||
try
|
||
{
|
||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
|
||
SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.互换, clientCashId, "系统操作_互换");
|
||
td.UnWindDate = unwindData.UnwindDate;
|
||
if (td.ExerciseDate <= unwindData.ValueDate)
|
||
{
|
||
td.Notional = 0;
|
||
td.StockEqvNotional = 0;
|
||
td.TradeStatus = "已到期";
|
||
}
|
||
DbContext.SaveChanges();
|
||
trans.Commit();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
trans.Rollback();
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
trans.Dispose();
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 互换
|
||
/// </summary>
|
||
/// <param name="swap_Deal"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void SwapIncome(UnwindData unwindData)
|
||
{
|
||
var td = FindTrade(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
NormalizeIncomeUnwindDate(unwindData);
|
||
ValidateIncomeValueDate(unwindData, td);
|
||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||
ValidateFrontendPnL(unwindData, isIncome: true); // 只读校验告警,不阻断交易
|
||
ExecuteInTransaction(() =>
|
||
{
|
||
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
|
||
if (unwindData.SwapMarginRebatePnl != 0)
|
||
{
|
||
AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||
}
|
||
foreach (var item in unwindData.FlowEvents)
|
||
{
|
||
item.OptLog = "手工操作";
|
||
}
|
||
SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.互换, clientCashId, "系统操作_互换");
|
||
if (td.ExerciseDate <= unwindData.ValueDate)
|
||
{
|
||
td.Notional = 0;
|
||
td.StockEqvNotional = 0;
|
||
td.TradeStatus = "已到期";
|
||
}
|
||
td.UnWindDate = unwindData.UnwindDate;
|
||
SaveAllChanges();
|
||
});
|
||
}
|
||
/// <summary>
|
||
/// 互换/平仓审核通过
|
||
/// </summary>
|
||
/// <param name="td"></param>
|
||
/// <param name="eventType"></param>
|
||
/// <exception cref="Exception"></exception>
|
||
public void ApproveSwapTrade(trade td, int eventType)
|
||
{
|
||
var swapEvent = FindSwapEvent(td.id, eventType);
|
||
if (swapEvent == null)
|
||
{
|
||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||
}
|
||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||
{
|
||
NormalizeIncomeUnwindDate(swapEvent.unwindData);
|
||
ValidateIncomeValueDate(swapEvent.unwindData, td);
|
||
}
|
||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||
if (swapEvent.unwindData.SwapMarginAmount != 0)
|
||
{
|
||
AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate);
|
||
}
|
||
swapEvent.ClientCashId = clientCashId;
|
||
if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||
{
|
||
td.TradeStatus = "已平仓";
|
||
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
|
||
}
|
||
else
|
||
{
|
||
td.TradeStatus = ConsTrade.确认成交;
|
||
td.HasPartialUnWind = 1;
|
||
}
|
||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||
{
|
||
td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue);
|
||
td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||
}
|
||
|
||
td.Notional = td.TradeAmount;
|
||
UpdateInitalPosition(flowList, swapEvent.unwindData, eventType);
|
||
SaveAllChanges();
|
||
|
||
}
|
||
/// <summary>
|
||
/// 互换/平仓提交审核
|
||
/// </summary>
|
||
/// <param name="unwindData"></param>
|
||
/// <param name="eventType"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void ApplySwapTrade(UnwindData unwindData, int eventType)
|
||
{
|
||
var td = FindTrade(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||
{
|
||
NormalizeIncomeUnwindDate(unwindData);
|
||
ValidateIncomeValueDate(unwindData, td);
|
||
}
|
||
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
|
||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||
ExecuteInTransaction(() =>
|
||
{
|
||
CloseReCheckSetTrade(unwindData.SwapTradeId, eventType == (int)SwapEventTypeEnum.互换, true);
|
||
SaveSwapDeal(unwindData, eventType, 0, action, true);
|
||
SaveAllChanges();
|
||
// 需求①:若触发条件判定无需审批(CloseReCheck_SetTrade 已将 ProcessOrderId 设为审批通过),
|
||
// 在 swap_event 记录创建完成后再执行审批通过流程。
|
||
td = FindTrade(unwindData.SwapTradeId);
|
||
if (td.ProcessOrderId == ProcessTradeLog.审批通过)
|
||
{
|
||
new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
|
||
{
|
||
tradeId = td.id,
|
||
status = "pass",
|
||
comments = "触发条件未满足,自动跳过审批",
|
||
notNeedOperationHistory = false
|
||
});
|
||
}
|
||
});
|
||
}
|
||
private void ValidateIncomeValueDate(UnwindData unwindData, trade td)
|
||
{
|
||
var maxIncomeValueDate = GetMaxIncomeValueDate(td).Date;
|
||
if (unwindData.ValueDate.Date > maxIncomeValueDate)
|
||
{
|
||
throw new ServiceException($"手动互换结算日期不能晚于当前交易结束日期T-1:{maxIncomeValueDate:yyyy-MM-dd}");
|
||
}
|
||
}
|
||
|
||
private void NormalizeIncomeUnwindDate(UnwindData unwindData)
|
||
{
|
||
unwindData.UnwindDate = unwindData.ValueDate;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存平仓/互换事件
|
||
/// </summary>
|
||
/// <param name="swap_Deal"></param>
|
||
/// <param name="eventType"></param>
|
||
private long SaveSwapDealInternal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||
{
|
||
var td = DbContext.trade.Find(unwindData.SwapTradeId);
|
||
if (td == null)
|
||
{
|
||
throw new ServiceException("未找到交易信息");
|
||
}
|
||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||
unwindData.FlowEvents.Clear();
|
||
// 落库展示用"占期初(original)"语义(A);计算链(费用递减/全平判定)用"占剩余(remaining)"语义(B)。
|
||
// 序列化前把 ClosePercent 还原为 A,序列化后立即还原回 B 供后续使用。
|
||
var storedClosePercent = ToOriginalClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||
var incomingClosePercent = unwindData.ClosePercent;
|
||
unwindData.ClosePercent = storedClosePercent;
|
||
string data = JsonConvert.SerializeObject(unwindData);
|
||
unwindData.ClosePercent = incomingClosePercent;
|
||
var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, eventType, data, clientCashId, true, eventResason);//将平仓、互换总额存入事件
|
||
foreach (var item in flowList)
|
||
{
|
||
if (item.PositionType > 0 && eventType == (int)SwapEventTypeEnum.平仓)
|
||
{
|
||
item.Quantity = unwindData.CloseQty;
|
||
item.PositionQty = unwindData.ClosePercent == 1 ? 0 : unwindData.PositionQty - unwindData.CloseQty;
|
||
}
|
||
item.PayDate = unwindData.PayDate;
|
||
item.UnwindDate = unwindData.UnwindDate;
|
||
item.EventDate = unwindData.ValueDate;
|
||
FillSwapFlowEvent(item, unwindData, td, eventType, swapEvent.id);
|
||
DbContext.swap_flow_event.Add(item);
|
||
}
|
||
if (!approve)
|
||
{
|
||
UpdateInitalPosition(flowList, unwindData, eventType);
|
||
}
|
||
DbContext.SaveChanges();
|
||
var savedFlowEvents = DbContext.swap_flow_event.Where(x => x.EventId == swapEvent.id).ToList();
|
||
foreach (var item in savedFlowEvents)
|
||
{
|
||
FillSwapFlowEvent(item, unwindData, td, eventType, swapEvent.id);
|
||
}
|
||
DbContext.SaveChanges();
|
||
return swapEvent.id;
|
||
}
|
||
|
||
private void FillSwapFlowEvent(swap_flow_event item, UnwindData unwindData, trade td, int eventType, long eventId)
|
||
{
|
||
item.EventId = eventId;
|
||
item.SwapTradeId = unwindData.SwapTradeId;
|
||
item.SwapTradeNo = td.TradeNumber;
|
||
item.EventType = eventType;
|
||
item.EventReason = string.IsNullOrWhiteSpace(item.EventReason) ? "交易" : item.EventReason;
|
||
UpdateDbOption(item);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算平仓数据
|
||
/// </summary>
|
||
/// <param name="unwindData"></param>
|
||
private void DealFloatPosition(UnwindData unwindData)
|
||
{
|
||
foreach (var item in unwindData.FlowEvents)
|
||
{
|
||
item.OptLog = "手工操作";
|
||
if (item.PositionType > 0 && item.EventType == (int)SwapEventTypeEnum.平仓)
|
||
{
|
||
decimal shortRatio = item.PositionType == (int)PositionTypeFlag.Long ? -1m : 1m;
|
||
item.TradingAmountFeeAvg = item.TradingAmountAvg + item.TradingFeePending / unwindData.CloseQty * shortRatio;
|
||
item.TradingAmountNetFeeAvg = item.TradingAmountNetAvg + item.TradingFeePending / unwindData.CloseQty * shortRatio;
|
||
item.TradingAmount = item.TradingAmountAvg * unwindData.CloseQty;
|
||
}
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 单标的互换更新实时持仓信息
|
||
/// </summary>
|
||
/// <param name="swap_Deal"></param>
|
||
private void UpdateInitalPosition(List<swap_flow_event> flowList, UnwindData unwindData, int eventType)
|
||
{
|
||
var positions = DbContext.swap_position.Where(x => !x.IsInitial && x.SwapTradeId == unwindData.SwapTradeId && !x.Invalid);
|
||
foreach (var position in positions)
|
||
{
|
||
if (!string.IsNullOrEmpty(position.UnderlyingCode))
|
||
{
|
||
// 收益结算(互换)不改变持仓数量和名义本金,只更新费用
|
||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||
{
|
||
position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent;
|
||
position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent;
|
||
}
|
||
else
|
||
{
|
||
// 平仓时才扣减持仓
|
||
position.PosiQuantity -= unwindData.CloseQty;
|
||
position.PosiNotionalValue -= unwindData.CloseNotionalValue;
|
||
position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent;
|
||
position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var interest = flowList.FirstOrDefault(x => x.PositionId == position.PositionId);
|
||
if (interest != null)
|
||
{
|
||
position.InterestAmount += interest.InterestAmount;
|
||
position.InterestFeePending += interest.InterestFee;
|
||
if ((interest.InterestMode == (int)InterestModeEnum.追加预付金 || interest.InterestMode == (int)InterestModeEnum.初始预付金) && eventType == (int)SwapEventTypeEnum.平仓)
|
||
{
|
||
position.InterestPrincipalFix -= interest.InterestPrincipal;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|