Files
zszq-trs/YLErpDAL/Modules/SwapModule/SwapDealService.cs
T
张名锐 aa3548e77f feat(bond): 支持债券与股票基金公司行为现金流计算的差异化处理 - init2
- 修改 CalcPayment 方法添加 useBondPriceScale 参数区分债券和股票/基金的金额计算口径
- 债券利息按每100元面值票息通过BondPriceConverter转为入库金额,股票基金分红直接计算
- 在BondPaymentService中添加详细的参数说明文档注释
- 更新SwapDealService中分红计算逻辑,根据标的类型自动选择合适的金额转换方式
- 新增CorporateActionEventLifecycleTest单元测试验证公司行为事件生命周期管理
- 添加SplitCorporateActionTddTest测试验证拆合股功能
- 优化FundCorporateActionRollbackAndUnwindTest扩展到股票类型测试
- 更新前端OperationHistory页面表格列宽和显示格式支持更长的说明信息
2026-08-19 17:48:34 +08:00

2659 lines
148 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 MoreLinq.Extensions;
using Newtonsoft.Json;
using YLErp.BLL;
using YLErp.BLL.Eod;
using YLErp.DBModels.Enums;
using YLErp.Core.Interest;
using YLErp.Derivatives.Interest;
using YLErp.Helpers;
using YLErp.Modules.DataProviderModule;
using YLErp.Modules.EodModule;
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Modules.SwapModule.FundingLegs;
using YLErp.Modules.SwapModule.Margin;
using YLErp.Modules.SwapModule.ReturnLegs;
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);
}
private IIndexFixer _indexFixer;
/// <summary>FR007 取价器,委托 TryGetFloatRate(保留 virtual 接缝供测试 stub)。</summary>
protected virtual IIndexFixer IndexFixer
=> _indexFixer ??= new SwapDealIndexFixer((d, c) =>
TryGetFloatRate(d, c, out double r) ? (true, r) : (false, 0d));
#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)
{
NormalizeNotionalValues(unwindData);
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
}
private static void NormalizeNotionalValues(UnwindData unwindData)
{
unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
private static bool NormalizeFullCloseRequest(UnwindData unwindData)
{
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
&& unwindData.ClosePercent < 1
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
{
return false;
}
var closeQty = unwindData.CloseQty;
var closeNotionalValue = unwindData.CloseNotionalValue;
unwindData.ClosePercent = 1;
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
}
private static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
{
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
var input = new UnwindInput
{
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
PosiGrossPrice = floatLeg.PosiGrossPrice,
TradingAmountAvg = floatLeg.TradingAmountAvg,
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 = MarginModes.Contains(leg.InterestMode)
? input.MarginLegs
: input.InterestLegs;
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
}
var result = FrontendCalcReference.CalcUnwind(input);
floatLeg.MarkClosePnl = result.MarkClosePnl;
unwindData.SwapCloseAmount = result.SwapCloseAmount;
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
}
private static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
{
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
}
// 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 SwapInterest.FundingLegPrecision,消除重复定义。
private const int InterestCalculationPrecision = SwapInterest.FundingLegPrecision;
/// <summary>
/// 手工平仓、手工互换及收益结算的利息事件按金额两位落库。
/// 自动平仓保留原有计算与落库口径,不适用本阶段的手工结算规则。
/// </summary>
private static bool NormalizeSettledInterestAmounts(IEnumerable<swap_flow_event> flowEvents, int eventType, string eventReason)
{
if ((eventType != (int)SwapEventTypeEnum.平仓 && eventType != (int)SwapEventTypeEnum.互换)
|| eventReason == "系统操作_自动平仓")
{
return false;
}
foreach (var flowEvent in flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
{
// 只处理利息腿;浮动腿损益在日终快照入口统一按两位落库。
flowEvent.InterestPrincipal = Math.Round(flowEvent.InterestPrincipal, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
flowEvent.InterestAmount = Math.Round(flowEvent.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
flowEvent.TdInterestAmount = Math.Round(flowEvent.TdInterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
flowEvent.InterestClosePnL = Math.Round(flowEvent.InterestClosePnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
flowEvent.InterestFee = Math.Round(flowEvent.InterestFee, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
return true;
}
// 客户现金在 SaveSwapDeal 之前创建,手工结算必须先收敛流水并重算汇总金额。
private void NormalizeManualSettlementAmounts(UnwindData unwindData, int eventType, string eventReason)
{
if (!NormalizeSettledInterestAmounts(unwindData.FlowEvents, eventType, eventReason))
{
return;
}
if (unwindData.FlowEvents.Any(x => !string.IsNullOrEmpty(x.UnderlyingCode)))
{
CalcCloseAmount(unwindData);
if (eventType == (int)SwapEventTypeEnum.互换)
{
unwindData.SwapMarginAmount = 0;
}
return;
}
unwindData.SwapCloseAmount = Math.Round(unwindData.SwapCloseAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
unwindData.SwapRealizedPnL = Math.Round(unwindData.SwapRealizedPnL, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
/// <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);
}
/// <summary>查询交易当前有效的初始腿和实时腿。测试可返回内存快照,避免初始化测试触库。</summary>
protected virtual List<swap_position> FindActiveSwapPositions(int tradeId)
{
return DbContext.swap_position
.Where(x => x.SwapTradeId == tradeId && !x.Invalid)
.ToList();
}
/// <summary>
/// 找到平仓数据对应的实时浮动腿。正式路径以 PositionId 绑定,缺失时才按标的代码兜底;
/// 这样后台不会把前端传入的价格当成权威基线。测试可 override 为内存持仓。
/// </summary>
protected virtual swap_position FindRealtimeFloatPosition(UnwindData unwindData)
{
if (unwindData == null)
{
return null;
}
var floatEvent = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
var query = DbContext.swap_position
.Where(x => x.SwapTradeId == unwindData.SwapTradeId && !x.IsInitial && !x.Invalid
&& !string.IsNullOrEmpty(x.UnderlyingCode));
if (floatEvent?.PositionId > 0)
{
var byPositionId = query.FirstOrDefault(x => x.PositionId == floatEvent.PositionId);
if (byPositionId != null)
{
return byPositionId;
}
}
if (!string.IsNullOrEmpty(floatEvent?.UnderlyingCode))
{
var byCode = query.FirstOrDefault(x => x.UnderlyingCode == floatEvent.UnderlyingCode);
if (byCode != null)
{
return byCode;
}
}
return query.FirstOrDefault();
}
/// <summary>查询 valueDate 当日已经生效的最近有效 Fund EOD。</summary>
protected virtual eod_swap_position FindLatestFundEodPosition(
int tradeId,
long positionId,
DateTime valueDate)
{
return new SwapEodPositionService(this)
.GetLatestValidEodPosition(tradeId, positionId, valueDate);
}
/// <summary>
/// 查询 valueDate 当天真正生效的 Fund 公司行为。
/// ExDividendDate 只是登记日,盘中基线不能按登记日提前切换;只有
/// EffectiveDate == valueDate 时才把上一 EOD 的 Q/P 转成当日 BOD 的除权后 Q/P。
/// </summary>
protected virtual ex_dividend_info FindFundCorporateAction(
string underlyingCode,
DateTime valueDate)
{
return DbContext.ex_dividend_info.FirstOrDefault(x => x.ValidStatus
&& x.UnderlyingCode == underlyingCode
&& x.EffectiveDate.HasValue
&& x.EffectiveDate.Value == valueDate.Date);
}
/// <summary>
/// 公司行为系数仍使用登记日收盘价,而不是生效日盘中/收盘价。
/// 测试可用 EOD 快照价格作为回退值;生产从登记日行情表取真实收盘价。
/// </summary>
protected virtual decimal GetFundCorporateActionClosePrice(
ex_dividend_info dividendInfo,
decimal fallbackPrice)
{
if (!dividendInfo.ExDividendDate.HasValue)
{
return fallbackPrice;
}
var closePrice = new EodPriceProvider(dividendInfo.ExDividendDate.Value)
.GetPrice(dividendInfo.UnderlyingCode, SettlementTypeEnum.ClosePrice);
return Convert.ToDecimal(closePrice);
}
/// <summary>读取 Fund 现金分红税率;单元测试可固定为 0,避免依赖系统日期配置。</summary>
protected virtual decimal GetFundDividendTaxRate()
=> new DividendService(this).GetDividendTaxRateDecimal();
/// <summary>
/// 判断最新 EOD 之后是否已有同一浮动腿的完成流水。若有,说明当日实时持仓已发生部分平仓/互换,
/// 不能再把较早 EOD 的数量覆盖回来,否则会抹掉当日成交结果。
/// </summary>
protected virtual bool HasCompletedFlowAfterFundEod(
int tradeId,
long positionId,
DateTime eodDate,
DateTime valueDate)
{
var asOfDate = valueDate.Date;
return DbContext.swap_flow_event.Any(x => x.SwapTradeId == tradeId
&& x.PositionId == positionId
&& x.DataState == (int)SwapFlowDateStateEnum.完成
&& x.EventDate > eodDate
&& x.EventDate <= asOfDate);
}
/// <summary>
/// 恢复实时 Fund 浮动腿到截至指定日有效的 EOD 基线。
/// 这是唯一允许把 EOD 公司行为结果带入盘中平仓的入口:10 送 10 后 EOD 是 2000 份/50
/// 时,下一日直接使用 2000/50,不再把前端可能传入的 1000/100 或已除权价格重复套系数。
/// 若最新 EOD 后存在完成流水则保持实时腿原值,避免覆盖当日部分平仓;非 Fund、无 EOD
/// 和固定/利息腿均返回 false,沿用原逻辑。
/// </summary>
protected virtual bool TryRestoreEffectiveFundPosition(
swap_position position,
DateTime valueDate)
{
// 只对收取方向的 Fund 浮动腿恢复 EOD;固定腿、利息腿和支付方向不应被公司行为改写。
// 无历史 EOD 或最新 EOD 后已有完成流水时返回 false,由调用方保持实时持仓原值,
// 不伪造一份快照,也不把较早的 2000 份/50 覆盖掉当日已经部分平仓后的实时数量。
if (position == null
|| position.PosiDirection <= 0
|| position.UnderlyingInstrumentType != ConsGlobal.InstrumentType.Fund)
{
return false;
}
var eodPosition = FindLatestFundEodPosition(
position.SwapTradeId,
position.PositionId,
valueDate);
if (eodPosition == null
|| HasCompletedFlowAfterFundEod(
position.SwapTradeId,
position.PositionId,
eodPosition.ValueDate,
valueDate))
{
return false;
}
if (!SwapEodPositionService.RestoreFundPositionFromEod(position, eodPosition))
{
return false;
}
// 最近 EOD 已经处于生效日或更晚时,说明该快照本身已经是除权后基线,
// 不能再次套系数。只有“最近 EOD < EffectiveDate <= valueDate”时,
// 才在盘中恢复后补一次公司行为。
var corporateAction = FindFundCorporateAction(
position.UnderlyingCode,
valueDate);
if (corporateAction?.EffectiveDate > eodPosition.ValueDate.Date
&& corporateAction.EffectiveDate.Value.Date <= valueDate.Date)
{
var closePrice = GetFundCorporateActionClosePrice(
corporateAction,
position.PosiGrossPrice);
if (closePrice <= 0)
{
throw new ServiceException(
$"Fund 标的【{position.UnderlyingCode}】登记日【{corporateAction.ExDividendDate:yyyy-MM-dd}】缺少有效收盘价,无法执行除权");
}
var dividendTaxRate = GetFundDividendTaxRate();
SwapEodPositionService.ApplyFundCorporateActionToPosition(
position,
corporateAction,
closePrice,
dividendTaxRate);
}
return true;
}
/// <summary>
/// 在直接提交前复核前端平仓数据。基线恢复成功时同步浮动流水价格、有效数量和名义本金,
/// 并拒绝 CloseQty 超过有效 EOD 数量;全平请求则把数量规范为当前有效全部持仓。
/// </summary>
protected virtual bool TryRestoreEffectiveFundPosition(
UnwindData unwindData,
DateTime valueDate)
{
var position = FindRealtimeFloatPosition(unwindData);
if (!TryRestoreEffectiveFundPosition(position, valueDate))
{
return false;
}
var floatEvent = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
var effectiveQty = position.PosiQuantity;
var requestedQty = unwindData.CloseQty;
var fullClose = unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓
|| unwindData.ClosePercent >= 1m;
// CloseQty 是部分平仓请求的数量口径;全平请求忽略前端缓存的旧数量,统一取 EOD 有效数量。
// 例如 10 送 10 后 EOD 为 2000 份/50,前端仍传 1000 份时,全平必须落成 2000 份,
// 否则会遗留 1000 份;现金派现后若 EOD 名义本金为 99000,平一半应按 49500 扣减。
// 若交易级余额仍沿用旧值 100000,再扣有效平仓额 49500,就会错误留下 50500。
if (requestedQty < 0m || (!fullClose && requestedQty > effectiveQty))
{
throw new ServiceException(
$"Fund 浮动腿平仓数量 {requestedQty} 超过截至 {valueDate:yyyy-MM-dd} 有效持仓 {effectiveQty}");
}
var closeQty = fullClose ? effectiveQty : requestedQty;
var closeNotional = fullClose
? position.PosiNotionalValue
: Math.Round(
closeQty * position.PosiGrossPrice * position.ContractSize,
ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
unwindData.PositionQty = effectiveQty;
unwindData.PosiNotionalValue = position.PosiNotionalValue;
unwindData.CloseQty = closeQty;
unwindData.CloseNotionalValue = closeNotional;
if (!fullClose)
{
unwindData.ClosePercent = unwindData.NotionalValue > 0m
? closeNotional / unwindData.NotionalValue
: (effectiveQty == 0m ? 0m : closeQty / effectiveQty);
}
if (floatEvent != null)
{
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
floatEvent.PosiNetPrice = position.PosiNetPrice;
floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice;
floatEvent.TradingAmountNetFeeAvg = position.PosiNetFeePrice;
floatEvent.Quantity = closeQty;
floatEvent.PositionQty = effectiveQty - closeQty;
floatEvent.ContractSize = position.ContractSize;
// EOD 恢复会改变入场基准和有效平仓数量;按当前平仓价重算前端派生盈亏。
// FloatPnlSum 是只读属性,由 MarkClosePnl、费用和分红自动派生,不能直接写入。
RecalculateNormalizedUnwindAmounts(unwindData);
}
return true;
}
#endregion
public SwapDealService(OptUserInfo optUser) : base(optUser)
{
}
public SwapDealService(YLBaseService baseService) : base(baseService)
{
}
#region 前端盈亏只读校验(不阻断交易)
/// <summary>
/// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对,
/// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。
///
/// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底,
/// 为将来公式统一积累"前后端差异"数据。
/// 核心比对逻辑已抽到 SwapFrontendPnlValidator.BuildFrontendValidationDiffs 纯函数,便于单测覆盖。
/// </summary>
/// <param name="unwindData">前端算好传入的结算数据</param>
/// <param name="isIncome">true=结息页(income公式)false=平仓页(unwind公式)</param>
private void ValidateFrontendPnL(UnwindData unwindData, bool isIncome)
{
try
{
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome);
if (diffs == null) return;
// 取浮动腿用于日志上下文(与原实现一致)
var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
foreach (var d in diffs)
{
Logger.Error($"[互换盈亏校验分歧] tradeId={unwindData.SwapTradeId} field={d.Field} frontend={d.FrontendValue} backend={d.BackendValue} diff={d.Delta} " +
$"floatLeg=[gross={floatLeg?.PosiGrossPrice} avg={floatLeg?.TradingAmountAvg} qty={floatLeg?.Quantity} payDir={floatLeg?.PayDirection} posType={floatLeg?.PositionType}]");
}
}
catch (Exception ex)
{
// 校验自身错误绝不阻断交易
Logger.Error($"[互换盈亏校验异常] tradeId={unwindData.SwapTradeId} isIncome={isIncome}", ex);
}
}
#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 = FindActiveSwapPositions(tradeId);
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();
// Fund 的盘中平仓基线来自最近有效 EOD;10 送 10 后应直接使用 2000 份/50
// 不能继续读取实时表中的 1000 份/100 再让前端重复套用除权系数。
var restoredFundBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
// 恢复失败表示非 Fund、无历史 EOD,或 EOD 后已有完成流水;此时保留当前实时值,
// 继续原有盘中流程,避免用不完整快照制造数量/价格。
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);
// 现金分红会调整 EOD 期初价但不改数量,因此持仓名义本金可能从 100000 变为 99000。
// 只有 Fund EOD 基线恢复成功时才使用该值;其他品种继续沿用 trade 原口径。
unwindData.PosiNotionalValue = restoredFundBaseline
? position.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 = "交易";
// 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源),
// 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。
// 同一 EOD 值取一次喂两栏:
// DividendIn = "浮动端平仓盈亏·分红"(本次动作要落袋的,落库后被前端按需展示)
// DividendPending = "待结算分红收益"(仍挂在账上、未来才结的存量 = PosiDividendSum 全量口径,
// 见 GetPreEodDividendSum 注释的口径论证;切勿改回硬0或分摊,会落库回归)
decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
floatEvent.DividendIn = preEodDividendSum;
floatEvent.DividendPending = preEodDividendSum;
floatEvent.UnderlyingCode = position.UnderlyingCode;
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
floatEvent.CloseFee = 0;
floatEvent.BeforeCloseFee = oriPosition.PosiTradingFeePending;
floatEvent.TradingFee = CalcInitTradingFee(oriPosition, unwindData);
floatEvent.PosiTradingFeeUnit = oriPosition?.PosiTradingFeeUnit ?? 0;
floatEvent.PosiFeeType = oriPosition?.PosiFeeType ?? 0;
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 = CalcInitTradingFeePending(oriPosition, position, unwindData);
floatEvent.DataState = (int)SwapFlowDateStateEnum.完成;
floatEvent.InterestMode = position.InterestMode;
floatEvent.ClientId = td.ClientId;
floatEvent.SetOpt(UserInfo);
}
unwindData.FlowEvents.Add(floatEvent);
}
return unwindData;
}
private static decimal CalcInitTradingFee(swap_position oriPosition, UnwindData unwindData)
{
if (oriPosition == null || unwindData == null)
{
return 0;
}
if (oriPosition.PosiFeeType == 1)
{
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
private static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
{
if (oriPosition == null || unwindData == null || oriPosition.PosiTradingFeeUnit == 0)
{
return position?.PosiTradingFeePending ?? 0;
}
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
if (originalBase <= 0)
{
return position?.PosiTradingFeePending ?? 0;
}
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
/// <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 = FindActiveSwapPositions(tradeId);
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();
// 收益结算与手工平仓共用 Fund 的有效 EOD 基线,避免仍返回除权前价格/数量。
var restoredFundBaseline = TryRestoreEffectiveFundPosition(position, dealDate);
// 若无法恢复(例如当日已有互换/平仓流水),这里故意沿用实时腿,不能把较早 EOD
// 当作当日最终状态;收益结算的其余字段仍按原始实时口径组装。
//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 = restoredFundBaseline
? position.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;
// 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源),
// 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。
floatEvent.DividendIn = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
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 = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, 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 (MarginModes.Contains(p.InterestMode))
{
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>
/// 计算预付金腿当前真实持仓 (当前持仓+未来持仓)
/// </summary>
/// <param name="origPositions"></param>
/// <param name="realPositions"></param>
/// <param name="completedFlowEvents"></param>
/// <param name="settleDate"></param>
/// <returns></returns>
public static List<swap_position> ResolveInterestLegPositionsAsOf(
List<swap_position> origPositions, List<swap_position> realPositions,
IEnumerable<swap_flow_event> completedFlowEvents, DateTime settleDate)
{
realPositions ??= new List<swap_position>();
var futureFlows = (completedFlowEvents ?? Enumerable.Empty<swap_flow_event>())
.Where(x => x.EventType == (int)SwapEventTypeEnum.平仓 && x.EventDate > settleDate)
.ToList();
var originalNotional = origPositions.Where(x => x.PosiDirection > 0)
.Sum(x => x.PosiNotionalValue);
var futureCloseNotional = futureFlows.Where(x => x.PositionType > 0)
.Sum(x => x.TradingAmount);
var hasNotionalFlows = futureCloseNotional > 0 && originalNotional > 0;
var futureClosePrincipal = futureFlows
.Where(x => MarginModes.Contains(x.InterestMode))
.GroupBy(x => x.PositionId)
.ToDictionary(x => x.Key, x => x.Sum(v => v.InterestPrincipal));
return origPositions.Where(x => x.PosiDirection == 0).Select(p =>
{
if (MarginModes.Contains(p.InterestMode))
{
var realLeg = realPositions.FirstOrDefault(r => r.PositionId == p.id);
if (realLeg != null)
{
var futurePrincipal = hasNotionalFlows
? p.InterestPrincipalFix * futureCloseNotional / originalNotional
: futureClosePrincipal.TryGetValue(p.id, out var flowPrincipal) ? flowPrincipal : 0m;
var asOfPrincipal = realLeg.InterestPrincipalFix + futurePrincipal;
asOfPrincipal = Math.Min(p.InterestPrincipalFix, Math.Max(0m, asOfPrincipal));
if (asOfPrincipal != p.InterestPrincipalFix)
{
var clone = p.Clone();
clone.InterestPrincipalFix = asOfPrincipal;
return clone;
}
}
}
return p;
}).ToList();
}
public static decimal ResolveUnwindPreviousNotional(
eod_swap lastEod,
IEnumerable<eod_swap_position> lastEodPositions,
decimal currentNotional)
{
var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList();
decimal previousNotional;
if (floatingPositions?.Count > 0)
{
previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue);
}
else
{
previousNotional = lastEod == null
? currentNotional
: Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort);
}
return previousNotional == 0m && currentNotional != 0m
? currentNotional
: previousNotional;
}
/// <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;
// true 跳过 不计利息; false 正常利息
bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, out DateTime startDate, out DateTime endDate);
// 计算名义本金
decimal closePrincipal;
decimal posiPrincipal;
decimal newClosePercent = closePrecent;
var mode = (InterestModeEnum)position.InterestMode;
if (MarginModes.Contains(position.InterestMode))
{
// 保证金腿: 计息基数 = InterestPrincipalFix(保证金余额)
closePrincipal = position.InterestPrincipalFix * closePrecent;
posiPrincipal = position.InterestPrincipalFix;
}
else
{
// 融资腿(1/2/9): 走策略工厂
var r = FundingLegStrategyFactory.Get(mode)
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue, closePrecent);
closePrincipal = r.ClosePrincipal;
posiPrincipal = r.PosiPrincipal;
newClosePercent = r.ClosePercent;
}
// 根因位置:SwapEodPositionService.SaveAutoEodWithCloseInterestPosition 在平仓后收盘时传入
// “收盘后剩余本金 + closePercent=1”,与盘中“平仓前本金 + 实际关闭比例”不是同一语义。
// GetInterests 同时被盘中试算和 EOD 平仓后收盘调用:后者传入的
// posiNotionalValue 是收盘后的剩余本金,closePosiNotionalValue 才是本次实际平掉的本金。
// 例如平仓前 100、平掉 30、收盘后剩余 70 时,EOD 传入 posi=70、close=30、closePercent=1。
// 模式2(合约名义本金规模)的本次结息本金必须始终是实际平仓额,因此无条件覆盖,
// 否则会错误地用剩余 70 结算本次平掉的 30。模式9(标的期初全价)的部分平仓
// 仍保留既有的剩余/复利动态本金承接逻辑;仅最终全平时 posi=0,才覆盖以避免结息本金为 0。
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.合约名义本金规模
|| ((InterestModeEnum)position.InterestMode == InterestModeEnum.标的期初全价
&& posiNotionalValue == 0m))
{
closePrincipal = closePosiNotionalValue;
}
if (MarginModes.Contains(position.InterestMode))
{
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
}
// 获取利率
decimal rate = Math.Round(GetFixedRate(position, unwindDate), InterestCalculationPrecision, MidpointRounding.AwayFromZero); // 做精度调整 原数据有精度误差
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>
/// 平仓比例口径转换(解决"显示占期初 / 计算占剩余"双语义问题)。
/// 前端与事件列表展示用"占期初(original)"语义(A);后端计息基数计算 / 费用递减 /
/// 全平判定均按"占剩余(remaining)"语义(B)消费。
/// A → BB = 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>
/// 读取"上一收盘日"浮动腿的待实现分红(eod_swap_position.PosiDividendSum),
/// 用于平仓/互换预览页展示"浮动端平仓盈亏·分红(DividendIn)" 与 "待结算分红收益(DividendPending)"。
/// <para>方案C:替代前端 totalInterest × 期初持仓 的重算——后者会把登记日前已平仓、
/// 不享有该笔分红的部分重复计入(GLMS-20260105-0004 误显 -36,160)。
/// EOD 的 PosiDividendSum 已按"实际持仓递推 + 当日实现扣除"算出待实现分红,
/// 是单一可信源。</para>
/// <para>复用 GetUnwindInterests(cs:624-626) 的"上一 EOD 日期"推导:取 eod_swap 中
/// ValueDate &lt; dealDate 的最大日期,无则 dealDate.AddDays(-1);再经
/// SwapEodPositionService.GetPreEodPositions 取该日持仓,匹配 PositionId。</para>
/// <para>抽为 protected virtual:与 GetMaxIncomeValueDate 一致,便于测试替身覆写、
/// 也兼容无 EOD 的边界(返回 0,与历史 DividendIn=0 行为一致)。</para>
/// </summary>
/// <returns>上一收盘日该浮动腿的待实现分红;无 EOD 记录返回 0</returns>
/// <remarks>
/// 【口径论证·勿改】为什么 DividendPending 也用本方法的全量值(非分摊、非硬0):
/// <para>1. 字段语义直接对应:EOD PosiDividendSum 的 DisplayName="浮动端平仓盈亏·分红未实现"
/// (EodSwapPosition.cs:186),递推式 PosiDividendSum=前日+当日新计-当日实现
/// (SwapEodPositionService.cs:1825),即"扣过当日实现后、还挂在账上未来才结的存量"。
/// 前端列"待结算分红收益"(SwapflowList.js:561) 字面就是同一回事 → 直接取 PosiDividendSum。</para>
/// <para>2. 是"存量"非"流量"DividendPending 描述的是"账上还欠多少"(与本次平仓比例无关的总额),
/// 而 DividendIn 才是"本次动作落袋多少"。两者口径本就不同,各自正确。若把 DividendPending 改成
/// 按本次平仓比例分摊,会把"存量"误当"流量",与列名"待结算"矛盾。</para>
/// <para>3. 历史教训:方案C 初版曾把前端 DividendPending 硬编码 0commit e3c473ba),因测试交易
/// PosiDividendSum 恰好=03/2 已全额互换)而测试通过、掩盖问题。但对 PosiDividendSum≠0 的部分
/// 平仓交易,硬0 会落库(SwapFlowEventService.cs:588 冲账取负写入 swap_flow_event.DividendPending
/// 并在事件列表"待结算分红收益"列显示错误的 0 —— 这是确定的回归。故本方法返回值同时喂两栏,
/// 前端不得再覆盖。例外:互换页 DividendPending 保持 0(互换语义=全量结清,结清后待结算归0)。</para>
/// </remarks>
protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
{
var lastEod = DbContext.eod_swap
.Where(x => x.ValueDate < dealDate && x.SwapTradeId == tradeId)
.OrderByDescending(o => o.ValueDate).FirstOrDefault();
var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate;
var preEod = new SwapEodPositionService(this)
.GetPreEodPositions(tradeId, preEodDate)
.FirstOrDefault(x => x.PositionId == positionId);
return preEod == null ? 0m : preEod.PosiDividendSum;
}
/// <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 = IndexFixerBase.GetFixingDate(
days % period == 0 ? endDate : startDate, position.interest_rule);
if (preEod.id != 0 && days % period != 0)
{
position.FloatRate = positionClone.FloatRate = preEod.FloatRate;
return preEod.FloatRate;
}
if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate))
{
position.FloatRate = positionClone.FloatRate = 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, ref interestAmount, ref tdInterestAmount);
}
else
{
// 单利计算
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
}
// 四舍五入并赋值
interest.InterestAmount = Math.Round(interestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.TdInterestAmount = Math.Round(tdInterestAmount, InterestCalculationPrecision, 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>
/// 保证金腿的 orginPv 维度重映射。
///
/// 保证金腿被迫走融资腿的差分公式(dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv)
/// 但 orginPv 对融资腿是"交易名义本金(千万~亿级)",对保证金腿必须是"保证金本金"——
/// 否则维度不匹配会算出巨负值。本方法把保证金场景的 orginPv 对齐到"上一日保证金本金"。
///
/// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。
/// </summary>
/// <summary>
/// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。
/// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。
/// </summary>
private void RecordMarginCashFlow(trade td, UnwindData unwindData)
=> RecordMarginCashFlow(td, unwindData.ValueDate,
unwindData.SwapMarginAmount, unwindData.SwapMarginRebatePnl,
AddClientCashInCashOut);
/// <summary>
/// 写入保证金资金记录的通用重载,接受资金写入委托。
/// AddClientCash(virtual,测试可stub) 和 AddClientCashInCashOut(非virtual,直接写库)
/// 都可通过此重载统一。
/// </summary>
private void RecordMarginCashFlow(trade td, DateTime valueDate,
decimal marginAmount, decimal marginRebate,
Func<trade, double, string, DateTime, int> writeCash)
{
if (marginAmount != 0)
writeCash(td, Convert.ToDouble(marginAmount), ClientCashInCashOut.系统操作_应付预付金, valueDate);
if (marginRebate != 0)
writeCash(td, Convert.ToDouble(-marginRebate), ClientCashInCashOut.系统操作_预付金返息, valueDate);
}
/// <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 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值
if (MarginModes.Contains(position.InterestMode))
{
orginPv = MarginCalc.PreviousBalance(preEodPosition, 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.复利)
{
var daysFromStart = (endDate - position.PosiStartDate).Days;
var daysFromPreEod = preEodPosition.id != 0
? (endDate - preEodPosition.ValueDate).Days
: 0;
// 不算尾 + 当日即新周期首日 + 未到重置日 ==> 说明这一天应归入下一个计息周期 当天无需单独计息
if (!calcLast && daysFromPreEod == 1 && daysFromStart % (position.interest_rest_days ?? 1) != 0)
{
interest.InterestPrincipal = preEodPosition.TdInterestPrincipal * closePrecent; // 计息基数
interest.FloatRate = preEodPosition.FloatRate;
InterestAmount = preEodPosition.InterestIncomeSum * closePrecent; // 利息金额 = 待实现 * 平仓比例
TdInterestAmount = preEodPosition.InterestIncomeSum; // 当日新增利息
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.TdInterestAmount = Math.Round(TdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.InterestClosePnL = interest.InterestAmount * interestRatio; // 利息端平仓盈亏 = 利息金额 * 方向
return interest;
}
// remainingPercent 只用于把上一日待实现分配给本次计算对应的本金。
// 按照利息腿的实际 计息基数 重新计算一个历史待实现利息的 平仓比例。不替代全局的平仓比例
// 部分平仓计算关闭 30% 时取 30%;最终全平剩余仓位时取 100%。
var remainingPercent = preEodPosition.TdInterestPrincipal > 0m
? closePosiNotionalValue / preEodPosition.TdInterestPrincipal
: 1m;
remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent));
// resetCarryInterest 是重置日并入复利本金的历史待实现,不是当天新增利息。
// 把上日尚未实现的的利息 按本次平掉的这部分计息基数分给本次平仓 并在重置日并入计息基数
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
consumedInterest, resetCarryInterest);
if (preEodPosition.id != 0 && closePrecent == 1m)
{
// 最终全平只重放上一日终之后的新增利息;历史部分平仓的两位结算尾差已在日终待实现中。
// InterestAmount 是本次最终应结金额;TdInterestAmount 是不按关闭比例缩放的参考累计值。
// 二者在全平时都以上一日 InterestIncomeSum 为起点,保证之前攒下的尾差最后一次带走。
var interestAtEnd = new swap_flow_event { InterestRate = rate };
decimal amountAtEnd = 0m;
decimal tdAmountAtEnd = 0m;
// InitInterestDate 在最终日不算尾时会先把 endDate 回拨一天;
// 历史差分的 amountAtEnd 需补回该日,但计算器仍使用交易 calcLast,
// 并将重放日期限制在合约到期日,避免提前全平或超期重复计息。
// 如果算尾 重放日 = 正常到期日
// 不算尾 且未超过到期日 重放日 = endDate+1 (补齐不算尾那天漏计的利息)
// 加1天超过到期日 截断到到期日
var replayEndDate = endDate;
if (!calcLast && endDate < valueDate)
{
replayEndDate = endDate.AddDays(1);
if (replayEndDate > td.ExerciseDate.Value)
{
replayEndDate = td.ExerciseDate.Value;
}
}
// 计算截至本次平仓日的累计利息 amountAtEnd
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
interestAtEnd, annualDays, needPrice, floateRate, closePrecent,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
decimal tdAmountAtPreviousEod = 0m;
// 最终日重放仍遵守交易的 calcLast;上一日终是历史截点而非合约尾日,
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
// 计算截至上一日终累积的利息 amountAtPreviousEod
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent,
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
// 例如 00045/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
// 上一日终已保存的待实现利息 + 截至平仓日累计利息 - 截至上一日终累计利息
// 这样只带走“上一日终以后新增的利息”,同时保留历史部分平仓时因两位金额结算留下的尾差,最终全平一次性结清。
InterestAmount = preEodPosition.InterestIncomeSum + amountAtEnd - amountAtPreviousEod;
TdInterestAmount = preEodPosition.InterestIncomeSum + tdAmountAtEnd - tdAmountAtPreviousEod;
}
}
else
{
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
}
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.TdInterestAmount = Math.Round(TdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
}
if (add)
{
UpdateDbOption(interest);
}
return interest;
}
/// <summary>
/// 按 interest_rule 取 FR007 定盘价。无浮动标的时返回 fallback;取不到抛异常。
/// EOD 单日取率 + BuildSegmentRates 多日取率共用此方法,FR007 定盘逻辑收口到一处。
/// </summary>
private decimal ResolveFloatRate(swap_position position, DateTime date, decimal fallback)
{
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback;
var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
return fixing != 0m ? fixing : fallback;
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
/// <summary>构造利率值对象:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。</summary>
private static FundingLegRate BuildLegRate(swap_position position, decimal spread, decimal effectiveFloat)
=> string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
? FundingLegRate.Fixed(spread)
: FundingLegRate.Floating(spread, effectiveFloat);
/// <summary>构造 EOD 计息政策(算头算尾,重置周期取 position.interest_rest_days)。</summary>
private static AccrualPolicy BuildEodPolicy(swap_position position, int annualDays, bool isCompound)
=> new AccrualPolicy(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized);
/// <summary>
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
/// </summary>
private (List<(DateTime StartDate, decimal Rate)> Segments, decimal LastFloat) BuildSegmentRates(
DateTime startDate, DateTime endDate, int interestPeriod,
swap_position position, decimal spread, decimal initialFloat,
DateTime? fetchAfterDate)
{
var rates = new List<(DateTime, decimal)>();
var calcDays = (endDate - startDate).Days;
decimal currentFloat = initialFloat;
for (int i = 0; i <= calcDays; i += interestPeriod)
{
var resetDate = startDate.AddDays(i);
if (fetchAfterDate == null || resetDate > fetchAfterDate.Value)
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
rates.Add((resetDate, spread + currentFloat));
}
return (rates, currentFloat);
}
/// <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, bool calcFirst, bool calcLast,
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
{
var startDate = position.PosiStartDate;
int interestPeriod = position.interest_rest_days ?? 1;
// 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007fetchAfterDate=null)。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: null);
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
var result = CompoundInterestAccrual.AccruePeriod(
notional: principal,
segmentRates: segmentRates,
startDate: startDate,
endDate: endDate,
boundary: AccrualBoundary.Of(calcFirst, calcLast),
annualDays: annualDays,
isAnnualized: position.IsAnnualized,
resetCarryInterest: resetCarryInterest,
realizedInterest: consumedInterest,
unwindFraction: closePercent,
finalBasis: out var finalBasis,
trace: interestTrace);
SwapCalcTrace.Write(interestTrace);
// flowEvent 副作用:FloatRate=末段浮动利率;InterestPrincipal=复利终期本金(最后一次并本金后的基数)。
flowEvent.FloatRate = currentFloat;
flowEvent.InterestPrincipal = finalBasis;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
}
/// <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;
int interestPeriod = position.interest_rest_days ?? 1;
// orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金;保证金腿=上一日终保证金余额)。
// 单利差分:accrualBasis 全程恒定 = 昨日终滚动基数 + 当日名义本金 - 参考本金。
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
// 分段取率:仅 ValueDate 之后的重置日才取 FR007fetchAfterDate=ValueDate)。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: preEodPosition.ValueDate);
// 纯函数计息:Accrued=缩放累计(InterestAmount)AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
notional: accrualBasis,
unwindFraction: closePercent,
segmentRates: segmentRates,
startDate: startDate,
endDate: endDate,
priorValueDate: preEodPosition.ValueDate,
boundary: AccrualBoundary.Of(calcFirst, calcLast),
annualDays: annualDays,
isAnnualized: position.IsAnnualized,
trace: interestTrace);
SwapCalcTrace.Write(interestTrace);
// flowEvent 副作用(下游 EOD 用 InterestPrincipal 播种次日 TdInterestPrincipal
flowEvent.InterestPrincipal = accrualBasis * closePercent;
flowEvent.FloatRate = currentFloat;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
}
/// <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, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
int interestPeriod = position.interest_rest_days ?? 1;
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
// 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
// remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。
var remainingFraction = posiPrincipal > 0m
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
: 1m;
// 纯数学下沉至 CompoundInterestAccrual.AccrueEodDDD 命名 + 末位生产精度 12 舍入)。
var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: true);
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
var interestTrace = new AccrualTrace();
var result = CompoundInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
unwindFraction: closePercent,
rate: legRate,
policy: accrualPolicy,
isResetDay: isResetDay,
remainingFraction: remainingFraction,
eodDate: endDate,
trace: interestTrace);
SwapCalcTrace.Write(interestTrace);
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
preEodPosition.TdInterestPrincipal) * closePercent;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
}
/// <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, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
if (preEodPosition.id == 0)
{
preEodPosition.TdInterestPrincipal = posiPrincipal;
}
// 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。
int interestPeriod = position.interest_rest_days ?? 1;
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
// 纯数学下沉至 SimpleInterestAccrual(末位生产精度 12 舍入)。
var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: false);
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
var interestTrace = new AccrualTrace();
var result = SimpleInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
unwindFraction: closePercent,
rate: legRate,
policy: accrualPolicy,
eodDate: endDate,
trace: interestTrace);
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
SwapCalcTrace.Write(interestTrace);
}
/// <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("未找到交易信息");
}
NormalizeEventUnwindDate(unwindData);
// 提交时再次从有效 EOD/实时腿复核基线,不能只相信前端缓存的数量和价格。
var restoredFundBaseline = TryRestoreEffectiveFundPosition(unwindData, unwindData.ValueDate);
// 这是直接提交路径的最后一道复核。若返回 false(非 Fund、无快照、或 EOD 后已有完成流水),
// 不改写前端数据,沿用当日实时持仓;审批冻结事件和自动平仓入口不经过此复核,见下方说明。
if (restoredFundBaseline)
{
// 正式提交必须让交易级余额与同一 Fund EOD 基线一致,再执行原有扣减。
// 例:派现后有效名义本金为 99000,平掉一半 49500 后应剩 49500
// 若仍从 trade 旧值 100000 扣减,会错误留下 50500。
td.StockEqvNotional = Convert.ToDouble(unwindData.PosiNotionalValue);
td.TradeAmount = Convert.ToDouble(unwindData.PositionQty);
}
NormalizeNotionalValues(unwindData);
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
if (NormalizeFullCloseRequest(unwindData))
{
RecalculateNormalizedUnwindAmounts(unwindData);
}
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
bool cofirm = false;
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCash);
DealFloatPosition(unwindData);
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(unwindData.CloseQty);
var isFullClose = IsFullCloseAfterDeduction(unwindData, remainingStockEqvNotional, remainingTradeAmount);
if (isFullClose)
{
td.TradeStatus = "已平仓";
td.StockEqvNotional = 0;
td.TradeAmount = 0;
CallSaveSwapTradeClientCash(td, unwindData.ValueDate);
}
else
{
td.HasPartialUnWind = 1;
td.StockEqvNotional = remainingStockEqvNotional;
td.TradeAmount = remainingTradeAmount;
}
td.Notional = td.TradeAmount;
td.UnWindDate = unwindData.UnwindDate;
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();
// 自动平仓由系统流水直接生成,当前入口沿用实时持仓和传入平仓数量,未重新读取 Fund EOD。
// 因此它不具备手工 SwapUnwind 的 EOD 复核保护,生产上需确保自动流水已在正确的 EOD 基线之后生成。
var storagePriceRound = ConsGlobal.InstrumentType.IsBond(position?.UnderlyingInstrumentType)
? ConsGlobal.PriceRound
: ConsGlobal.SwapDeliveryPriceRound;
unwindPrice = Math.Round(unwindPrice, storagePriceRound, MidpointRounding.AwayFromZero);
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 = DirectionRatio.LongShort(flowEvent.PositionType);
int directionRatio = DirectionRatio.ReceivePay(flowEvent.PayDirection);
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent.UnderlyingCode);
// 债券付息按每百元票息存储,继续走 BondPriceConverterStock/Fund 的公司行为
// 现金分红按每 10 份金额存储,实际现金就是 payment_interest * qty,不能 /100。
// 标的资料缺失时保持旧债券口径,避免未知标的的历史平仓金额被放大。
var useBondPriceScale = um == null
|| !SwapEodPositionService.IsCorporateActionInstrument(um.UnderlyingInstrumentType);
// + 付息日>上日日终且小于等于平仓日期的分红数据
var dividendIn = servie.CalcPayment(
payments,
unwindQty,
shortRatio,
directionRatio,
useBondPriceScale);
decimal tax = um?.ValueAddedTax ?? 0;
dividendIn = DividendCalc.AfterTaxRaw(dividendIn, 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 (MarginModes.Contains(item.InterestMode))
{
_closePosiNotionalValue = 0;
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
}
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);
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut);
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 = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
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)
{
MarginCalc.AccumulateSettlement(interestList.ToList(), unwindData);
interestList.ForEach(x =>
{
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("未找到交易信息");
}
NormalizeEventUnwindDate(unwindData);
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
var trans = DbContext.Database.BeginTransaction();
try
{
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData);
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("未找到交易信息");
}
NormalizeEventUnwindDate(unwindData);
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.互换, "系统操作_互换");
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("未找到交易信息");
}
NormalizeEventUnwindDate(unwindData);
// 正常页面先由 InitIncome 读取最近有效 Fund EOD;本提交方法本身不再重读快照,
// 直接使用调用方传入的数据。若数据来自待复核事件,则它是申请时冻结的快照,日期之后的除权
// 不会在这里回写,属于审批链路的残余风险。
ValidateIncomeValueDate(unwindData, td);
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.互换, "系统操作_互换");
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
ValidateFrontendPnL(unwindData, isIncome: true); // 只读校验告警,不阻断交易
ExecuteInTransaction(() =>
{
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
RecordMarginCashFlow(td, unwindData.ValueDate, 0m, unwindData.SwapMarginRebatePnl, AddClientCash);
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("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
}
// 审批通过消费申请时序列化的 unwindData/流水,不重新按当前 Fund EOD 重建数量和价格。
// 这是为了保持待复核事件可重放的一致性,但也意味着申请后发生除权时仍可能带入冻结的旧基线;
// 直接提交路径的 EOD 复核不覆盖此审批路径。
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
NormalizeEventUnwindDate(swapEvent.unwindData);
NormalizeNotionalValues(swapEvent.unwindData);
// Stored events keep display ratio A; approval calculations consume remaining ratio B.
swapEvent.unwindData.ClosePercent = ToRemainingClosePercent(
swapEvent.unwindData.ClosePercent,
swapEvent.unwindData.NotionalValue,
swapEvent.unwindData.PosiNotionalValue);
var flowList = FindFlowEventsByEventId(swapEvent.id);
foreach (var item in flowList)
{
item.EventDate = swapEvent.unwindData.ValueDate;
item.UnwindDate = swapEvent.unwindData.UnwindDate;
}
swapEvent.unwindData.FlowEvents = flowList;
if (eventType == (int)SwapEventTypeEnum.平仓)
{
if (NormalizeFullCloseRequest(swapEvent.unwindData))
{
RecalculateNormalizedUnwindAmounts(swapEvent.unwindData);
}
}
if (eventType == (int)SwapEventTypeEnum.互换)
{
ValidateIncomeValueDate(swapEvent.unwindData, td);
}
if (eventType == (int)SwapEventTypeEnum.平仓)
{
foreach (var item in flowList.Where(x => x.PositionType > 0))
{
item.Quantity = swapEvent.unwindData.CloseQty;
item.PositionQty = swapEvent.unwindData.ClosePercent == 1
? 0
: swapEvent.unwindData.PositionQty - swapEvent.unwindData.CloseQty;
}
}
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
if (eventType == (int)SwapEventTypeEnum.平仓)
{
RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash);
}
swapEvent.ClientCashId = clientCashId;
td.UnWindDate = swapEvent.unwindData.UnwindDate;
if (eventType != (int)SwapEventTypeEnum.互换)
{
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(swapEvent.unwindData.CloseQty);
var isFullClose = IsFullCloseAfterDeduction(swapEvent.unwindData, remainingStockEqvNotional, remainingTradeAmount);
if (isFullClose)
{
td.TradeStatus = "已平仓";
td.StockEqvNotional = 0;
td.TradeAmount = 0;
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
}
else
{
td.TradeStatus = ConsTrade.确认成交;
td.HasPartialUnWind = 1;
td.StockEqvNotional = remainingStockEqvNotional;
td.TradeAmount = remainingTradeAmount;
}
}
else if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
{
td.TradeStatus = "已平仓";
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
}
else
{
td.TradeStatus = ConsTrade.确认成交;
td.HasPartialUnWind = 1;
}
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("未找到交易信息");
}
NormalizeEventUnwindDate(unwindData);
// 进入审批申请时保存的是前端冻结的事件数据;当前路径不执行直接 SwapUnwind 的 Fund EOD 复核。
// 因而申请发生在除权前、审批发生在除权后的场景,冻结数据仍是旧基线,需重新发起申请才能刷新。
if (eventType == (int)SwapEventTypeEnum.互换)
{
ValidateIncomeValueDate(unwindData, td);
}
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
NormalizeManualSettlementAmounts(unwindData, eventType, eventType == (int)SwapEventTypeEnum.互换 ? "系统操作_互换" : "系统操作_平仓");
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
// 与 SwapUnwind(L1270) 保持一致——缺少此转换会导致 SaveSwapDealInternal 的 B→A 还原出错
// (例如第二次部分平仓 50%(A) → 错误还原为 0.325 而非 0.50)。
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
if (eventType == (int)SwapEventTypeEnum.平仓)
{
if (NormalizeFullCloseRequest(unwindData))
{
RecalculateNormalizedUnwindAmounts(unwindData);
}
}
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 static void NormalizeEventUnwindDate(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);
NormalizeSettledInterestAmounts(flowList, eventType, eventResason);
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
{
// 平仓时才扣减持仓
var remainingPositionQty = position.PosiQuantity - unwindData.CloseQty;
var remainingPositionNotional = Math.Round(
position.PosiNotionalValue - unwindData.CloseNotionalValue,
ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
position.PosiQuantity = unwindData.ClosePercent == 1
? 0
: remainingPositionQty;
position.PosiNotionalValue = unwindData.ClosePercent == 1
? 0
: remainingPositionNotional;
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 (MarginModes.Contains(interest.InterestMode) && eventType == (int)SwapEventTypeEnum.平仓)
{
var remainingInterestPrincipal = Math.Round(
position.InterestPrincipalFix - interest.InterestPrincipal,
ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
position.InterestPrincipalFix = unwindData.ClosePercent == 1
? 0
: remainingInterestPrincipal;
}
}
}
}
}
}
}