529 lines
21 KiB
C#
529 lines
21 KiB
C#
using YLErp.Abstract.DataProviders;
|
|
using YLErp.BLL.Eod;
|
|
using YLErp.BLL.Hedge;
|
|
using YLErp.DBModels.Helpers;
|
|
using YLErp.Modules.CalculationModule.Abstract;
|
|
using YLErp.Modules.VolatilityModule;
|
|
|
|
namespace YLErp.Modules.CalculationModule
|
|
{
|
|
/// <summary>
|
|
/// 对冲交易盈亏计算
|
|
/// </summary>
|
|
public class HedgePnlCalc
|
|
{
|
|
readonly IHedgePnlCalcContext _context;
|
|
readonly IUnderlyingDataProvider _unDataProvider;
|
|
readonly IPriceProvider _unPriceProvider;
|
|
readonly IPriceProvider _exchangeOptionPriceProvider;
|
|
readonly IPriceProvider _unSettlePriceProvider;
|
|
|
|
public HedgePnlCalc(IHedgePnlCalcContext context)
|
|
{
|
|
_context = CalcCheckHelper.CheckHedgePnlCalcContext(context);
|
|
_unDataProvider = context.UnderlyingDataProvider;
|
|
_unPriceProvider = context.UnderlyingPriceProvider;
|
|
_unSettlePriceProvider = context.UnderlyingSettlePriceProvider;
|
|
_exchangeOptionPriceProvider = context.ExchangeOptionPriceProvider;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算对冲交易Pnl信息
|
|
/// </summary>
|
|
/// <param name="newHedgeTrades">当日对冲交易数据</param>
|
|
/// <param name="eodPositions">上日持仓数据</param>
|
|
public IEnumerable<HedgePnl> Calculate(IEnumerable<ExchangeTrade> newHedgeTrades, IEnumerable<EodTradePosition> eodPositions)
|
|
{
|
|
if (newHedgeTrades == null && eodPositions == null)
|
|
{
|
|
return Enumerable.Empty<HedgePnl>();
|
|
}
|
|
|
|
var pnlResults = new List<HedgePnl>();
|
|
|
|
//-----------------------------------------
|
|
// 处理昨日持仓盈亏
|
|
//-----------------------------------------
|
|
|
|
if (eodPositions != null)
|
|
{
|
|
foreach (var eodPosition in eodPositions)
|
|
{
|
|
ExchangeListOption exchangeOption = null;
|
|
if (eodPosition.TradeType == "场内期权")
|
|
{
|
|
exchangeOption = _unDataProvider.GetExchange_List_Option(eodPosition.ExchangeOptionCode);
|
|
//剔除已到期场内期权持仓
|
|
if (exchangeOption == null || exchangeOption.MaturityDate < _context.ValueDate)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (!(ConsTrade.TradeTypesForHedge.Contains(eodPosition.TradeType)||ConsTrade.BondTypeList.Contains(eodPosition.TradeType)))
|
|
{
|
|
continue;
|
|
}
|
|
var eodPnl = ProcessEodPositionHegePnl(eodPosition, exchangeOption);
|
|
pnlResults.Add(eodPnl);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------
|
|
// 处理当日对冲交易盈亏
|
|
//-----------------------------------------
|
|
|
|
if (newHedgeTrades != null && newHedgeTrades.Any())
|
|
{
|
|
//对冲交易手续费计算
|
|
var tradeCommissionDict = _context.CommissionCalc.GetTradeCommission(newHedgeTrades);
|
|
foreach (var newTrade in newHedgeTrades)
|
|
{
|
|
//手续费
|
|
var commission = tradeCommissionDict.GetTradeCommission(newTrade.id);
|
|
//标的价格
|
|
_unPriceProvider.TryGetPrice(newTrade.UnderlyingCode, out var underlyingPrice);
|
|
//计算
|
|
ProcessNewTradeHegePnlV2(pnlResults, newTrade, commission, underlyingPrice);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------
|
|
// 场内期权对冲盈利
|
|
//-----------------------------------------
|
|
var tempTradeId = 0;
|
|
foreach (var pnl in pnlResults)
|
|
{
|
|
var underlying = _unDataProvider.GetUnderlying(pnl.UnderlyingCode, out var contractSize);
|
|
if (underlying == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (pnl.TradeType == "场内期权")
|
|
{
|
|
tempTradeId--;
|
|
ProcessExchangeOptionPnl(pnl, underlying, tempTradeId);
|
|
}
|
|
else
|
|
{
|
|
pnl.Delta = pnl.Notional;
|
|
pnl.DeltaCash = pnl.Pv;
|
|
pnl.DeltaInLots = pnl.Delta / contractSize;
|
|
pnl.Lots = pnl.Notional / contractSize;
|
|
}
|
|
}
|
|
|
|
return pnlResults;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 昨日持仓对冲盈利
|
|
/// </summary>
|
|
private HedgePnl ProcessEodPositionHegePnl(EodTradePosition eodPosition, ExchangeListOption exchangeOption)
|
|
{
|
|
double lastPv = 0, pv = 0, dailyPnl = 0, realizedPnL = 0, cost = 0;
|
|
|
|
_unPriceProvider.TryGetPrice(eodPosition.UnderlyingCode, out var SettlePrice);
|
|
|
|
var notional = eodPosition.Amount;
|
|
|
|
if (Math.Abs(notional) > 0)
|
|
{
|
|
lastPv = eodPosition.Pv;
|
|
pv = SettlePrice * eodPosition.Amount;
|
|
dailyPnl = pv - lastPv;
|
|
realizedPnL = eodPosition.ClosedPnL;
|
|
cost = eodPosition.Cost;
|
|
}
|
|
else
|
|
{
|
|
notional = 0;
|
|
}
|
|
|
|
var uniqueCode = GetHedgeUniqueCode(eodPosition.BookId, eodPosition.TradeType, eodPosition.PositionType, eodPosition.UnderlyingCode, eodPosition.ExchangeOptionCode);
|
|
var lastTotalPnl = eodPosition.TotalPnL;
|
|
|
|
var eodPnl = new HedgePnl
|
|
{
|
|
BookId = eodPosition.BookId,
|
|
ValueDate = _context.ValueDate,
|
|
TradeType = eodPosition.TradeType,
|
|
PositionType = eodPosition.PositionType,
|
|
CallPut = TradeHelper.GetCallPut(exchangeOption?.OptionType),
|
|
BuySell = eodPosition.BuySell,
|
|
UnderlyingId = eodPosition.UnderlyingId,
|
|
UnderlyingCode = eodPosition.UnderlyingCode,
|
|
HedgeUniqueCode = uniqueCode,
|
|
Notional = notional,
|
|
LastPv = lastPv,
|
|
Pv = pv,
|
|
DailyPnL = dailyPnl,
|
|
RealizedPnL = realizedPnL,
|
|
TotalPnl = lastTotalPnl + dailyPnl,
|
|
LastTotalPnl = lastTotalPnl,
|
|
Cost = cost,
|
|
Commission = eodPosition.Commission,
|
|
Strike = exchangeOption?.Strike ?? 0,
|
|
ExchangeOptionCode = eodPosition.ExchangeOptionCode,
|
|
SettlePrice = SettlePrice,
|
|
PositionPnl = eodPosition.PositionPnL,
|
|
ExerciseDate = exchangeOption?.MaturityDate
|
|
};
|
|
if (Math.Abs(eodPnl.Strike) < 1e-7 && exchangeOption != null)
|
|
{
|
|
eodPnl.Strike = exchangeOption.Strike;
|
|
eodPnl.BuySell = "long".Equals(eodPosition.PositionType) ? "买入" : "卖出";
|
|
}
|
|
return eodPnl;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当日交易对冲盈利V2
|
|
/// </summary>
|
|
private HedgePnl ProcessNewTradeHegePnlV2(List<HedgePnl> pnlResults, ExchangeTrade newTrade, double newTradeCommission, double underlyingPrice)
|
|
{
|
|
if (newTrade.Notional < 0)
|
|
{
|
|
throw new ServiceFaultException($"[场内交易数据,id:{newTrade.id},成交份额:{newTrade.Notional}]不允许成交份额小于0的场内交易数据存在!");
|
|
}
|
|
|
|
//持仓类型 多头 空头分割
|
|
var positionType = GetHedgeLongShort(newTrade.TradeType, newTrade.TradeSide);
|
|
//对冲唯一编码(簿记账户ID_结构类型_持仓类型_合约代码)
|
|
var uniqueCode = GetHedgeUniqueCode(newTrade.AssetBookId, newTrade.TradeType, positionType, newTrade.UnderlyingCode, newTrade.OptionCode);
|
|
//获取是否存在对应uniqueCode的对冲信息
|
|
var eodPnl = pnlResults.FirstOrDefault(t => t.HedgeUniqueCode == uniqueCode);
|
|
|
|
//新的pnl(如果eodPnl存在则使用eodPnl)
|
|
var newPnl = eodPnl;
|
|
if (eodPnl == null)
|
|
{
|
|
newPnl = new HedgePnl
|
|
{
|
|
BookId = newTrade.AssetBookId,
|
|
ValueDate = _context.ValueDate,
|
|
TradeType = newTrade.TradeType,
|
|
PositionType = positionType,
|
|
CallPut = TradeHelper.GetCallPut(newTrade.OptionType),
|
|
BuySell = positionType == "long" ? "买入" : "卖出",
|
|
UnderlyingId = newTrade.UnderlyingId,
|
|
UnderlyingCode = newTrade.UnderlyingCode,
|
|
HedgeUniqueCode = uniqueCode,
|
|
Strike = newTrade.OptionStrike ?? 0,
|
|
ExchangeOptionCode = newTrade.OptionCode,
|
|
SettlePrice = underlyingPrice,
|
|
ExerciseDate = newTrade.MaturityDate
|
|
};
|
|
|
|
pnlResults.Add(newPnl);
|
|
|
|
if (newTrade.TradeType == "场内期权")
|
|
{
|
|
//场内期权的buysell不影响qdp计算
|
|
newPnl.BuySell = newTrade.TradeSide;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
newPnl.LastPv = eodPnl.LastPv; //昨市值
|
|
}
|
|
|
|
//持仓符号和新交易符号
|
|
var posSign = newPnl.Notional < 0 ? -1 : 1;
|
|
var newSign = EodOperationBase.GetSign(newTrade.TradeSide);
|
|
|
|
double newNotional = newTrade.Notional, openNotional = 0d;
|
|
|
|
//平仓处理
|
|
if (newSign != posSign)
|
|
{
|
|
var closeNotional = Math.Abs(newPnl.Notional);
|
|
|
|
if (newNotional > closeNotional)
|
|
{
|
|
//平仓超出部分需要变成开仓
|
|
openNotional = newNotional - closeNotional;
|
|
}
|
|
else
|
|
{
|
|
closeNotional = newNotional;
|
|
}
|
|
|
|
if (closeNotional > 0)
|
|
{
|
|
//开仓金额(带符号)
|
|
var openAmount = closeNotional * posSign * newPnl.Cost / newPnl.Notional;
|
|
|
|
//平仓金额(带符号)
|
|
var closeAmount = closeNotional * newSign * newTrade.TradeSinglePrice;
|
|
|
|
//平仓盈亏
|
|
var closeProfit = -(openAmount + closeAmount);
|
|
|
|
//已实现盈亏
|
|
newPnl.RealizedPnL += closeProfit;
|
|
|
|
//减去持仓成本
|
|
newPnl.Cost -= newTrade.TradeAmount* newTrade.TradeSinglePrice;
|
|
|
|
//减去持仓份额
|
|
newPnl.Notional -= closeNotional * posSign;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
openNotional = newNotional;
|
|
}
|
|
|
|
//开仓处理
|
|
if (openNotional > 0)
|
|
{
|
|
//转换为带符号的值
|
|
openNotional *= newSign;
|
|
|
|
//累加开仓份额
|
|
newPnl.Notional += openNotional;
|
|
|
|
//累加开仓成本
|
|
newPnl.Cost += openNotional * newTrade.TradeSinglePrice;
|
|
}
|
|
|
|
var newNotional_s = newNotional * newSign;
|
|
var newCost = (newTrade.TradeSinglePrice * newNotional_s) + newTradeCommission;
|
|
var newPv = newNotional_s * underlyingPrice;
|
|
var newDailyPnl = newPv - newCost;
|
|
|
|
newPnl.Cost += newTradeCommission; //成本加上手续费
|
|
newPnl.Commission += newTradeCommission; //累加总手续费
|
|
newPnl.DailyPnL += newDailyPnl; //当日盈亏
|
|
newPnl.TotalPnl += newDailyPnl; //总盈亏
|
|
newPnl.Pv += newPv; //总市值
|
|
newPnl.TdCost += newCost; //当日成本--主要用于下方的场内期权盈亏计算
|
|
|
|
return newPnl;
|
|
}
|
|
|
|
#region----场内期权对冲盈利----
|
|
|
|
private void ProcessExchangeOptionPnl(HedgePnl pnl, underlying_manager underlying, int tempTradeId)
|
|
{
|
|
TradeValueResult optionResult = null;
|
|
|
|
//场内期权合约信息
|
|
var exchangeOption = (_unDataProvider.GetExchange_List_Option(pnl.ExchangeOptionCode)?.Clone())
|
|
?? throw new HedgePnlCalcException($"场内期权合约[{pnl.ExchangeOptionCode}]在场内期权合约信息表中不存在!");
|
|
|
|
if (exchangeOption.MaturityDate < _context.ValueDate)
|
|
{
|
|
throw new HedgePnlCalcException($"场内期权合约'{pnl.ExchangeOptionCode}'已过期,合约到期日:{exchangeOption.MaturityDate:yyyy-MM-dd},估值日期:{_context.ValueDate:yyyy-MM-dd}");
|
|
}
|
|
|
|
if (exchangeOption.UnderlyingCode.StartsWith("IO"))
|
|
{
|
|
exchangeOption.UnderlyingCode = "000300.SH";
|
|
}
|
|
|
|
//pnl callput
|
|
pnl.CallPut = TradeHelper.GetCallPut(exchangeOption.OptionType);
|
|
|
|
//场内期权 合约乘数
|
|
var contractSize = underlying.ContractSize;
|
|
if (exchangeOption.ContractSize > 1e-6)
|
|
{
|
|
contractSize = exchangeOption.ContractSize;
|
|
}
|
|
|
|
//pnl持仓手数
|
|
pnl.Lots = pnl.Notional / contractSize;
|
|
|
|
//更新场内期权市场价格
|
|
pnl.ExOptionPrice = _exchangeOptionPriceProvider.TryGetPrice(pnl.ExchangeOptionCode, out var price) ? price : null;
|
|
|
|
//是否有持仓
|
|
var hasPosition = Math.Abs(pnl.Notional) > 1e-10;
|
|
|
|
if (hasPosition)
|
|
{
|
|
//使用曲面波动率或者隐含波动率计算期权风险
|
|
optionResult = InnerCalcExchangeOptionRisks(tempTradeId, pnl, underlying, exchangeOption);
|
|
}
|
|
|
|
if (optionResult != null && optionResult.Succeeded)
|
|
{
|
|
pnl.Vol = optionResult.Vol;
|
|
pnl.Delta = optionResult.Delta;
|
|
pnl.DeltaT1 = optionResult.DeltaT1;
|
|
pnl.SA_Delta = optionResult.SA_Delta;
|
|
pnl.Gamma = optionResult.Gamma;
|
|
pnl.Vega = optionResult.Vega;
|
|
pnl.DeltaCash = optionResult.DeltaCash;
|
|
pnl.GammaCash = optionResult.GammaCash;
|
|
pnl.DeltaInLots = optionResult.Delta / contractSize;
|
|
pnl.GammaInLots = optionResult.Gamma / contractSize;
|
|
pnl.Theta = optionResult.Theta;
|
|
pnl.Rho = optionResult.Rho * 100;
|
|
pnl.DdeltaDt = optionResult.DDeltaDt;
|
|
pnl.DdeltaDvol = optionResult.DDeltaDVol;
|
|
pnl.DvegaDt = optionResult.DVegaDt;
|
|
pnl.DvegaDvol = optionResult.DVegaDVol;
|
|
pnl.Pv = optionResult.Pv;
|
|
pnl.TimeValue = optionResult.TimeValue;
|
|
}
|
|
else
|
|
{
|
|
pnl.Pv = 0;
|
|
}
|
|
|
|
var flag = _context.ExchangeOptionPriceUseFlag;
|
|
|
|
if (PS.Config.Is光大光子)
|
|
{
|
|
flag = flag == ExchangeOptionPriceUseFlag.TrialCalclMode
|
|
? ExchangeOptionPriceUseFlag.SetExOptionPrice : ExchangeOptionPriceUseFlag.CalcPv;
|
|
}
|
|
|
|
if (flag == ExchangeOptionPriceUseFlag.CalcPv && hasPosition)
|
|
{
|
|
pnl.Pv = (pnl.ExOptionPrice * pnl.Notional) ?? 0;
|
|
var intrinsicValue = ConsGlobal.CallPut.IsCall(pnl.CallPut) ? pnl.SettlePrice - pnl.Strike : pnl.Strike - pnl.SettlePrice;
|
|
pnl.TimeValue = pnl.Pv - Math.Max(0, intrinsicValue) * pnl.Notional;
|
|
}
|
|
|
|
pnl.DailyPnL = pnl.Pv - pnl.LastPv - pnl.TdCost;
|
|
pnl.TotalPnl = pnl.LastTotalPnl + pnl.DailyPnL;
|
|
}
|
|
|
|
//场内期权估值计算,pnl在此方法中会对CallPut、Lots、ExOptionPrice赋值处理
|
|
private TradeValueResult InnerCalcExchangeOptionRisks(int tempTradeId, HedgePnl pnl, underlying_manager underlying, ExchangeListOption exchangeOption)
|
|
{
|
|
var tempTrade = new trade
|
|
{
|
|
TradeType = pnl.TradeType,
|
|
UnderlyingCode = pnl.UnderlyingCode,
|
|
UnderlyingId = pnl.UnderlyingId,
|
|
TradeDate = _context.ValueDate,
|
|
BuySell = pnl.BuySell,
|
|
StartDate = _context.ValueDate,
|
|
ExerciseDate = exchangeOption.MaturityDate,
|
|
MaturityDate = underlying.MaturityDate,
|
|
TradePrice = Math.Abs(pnl.Cost),
|
|
TradeStatus = "确认成交",
|
|
ExerciseMode = exchangeOption.ExerciseMode,
|
|
OptionType = exchangeOption.OptionType,
|
|
Strike = pnl.Strike,
|
|
Notional = pnl.Notional,
|
|
UnderlyingInstrumentType = underlying.UnderlyingInstrumentType,
|
|
ExchangeOptionCode = pnl.ExchangeOptionCode,
|
|
AssetId = pnl.BookId,
|
|
id = tempTradeId,
|
|
UnderlyingAssetClass = underlying.UnderlyingType,
|
|
|
|
//用于反算隐含波动率
|
|
StructureType = "场内期权",
|
|
TradeSinglePrice = pnl.ExOptionPrice
|
|
};
|
|
|
|
if (PS.Config.ErpElement.ExchangeOptionVolType == Configuration.Enums.ExchangeOptionVolType.ImpliedVol)
|
|
{
|
|
_unSettlePriceProvider.TryGetPrice(pnl.UnderlyingCode, out var price);
|
|
tempTrade.Vol = VolatilityHelper.GetImpliedVol(_context.ValueDate, tempTrade, null, price.Normalize(), _context.IsEodCalc);
|
|
}
|
|
|
|
var optionCalcContext = _context.CreateOptionCalculateContext();
|
|
|
|
var optionResult = TradeRiskCalcUtil.CalcTradeRisk(tempTrade, optionCalcContext, out _);
|
|
|
|
if (optionResult != null)
|
|
{
|
|
if (optionResult.FailReason == TradeValueFailReason.missingVol)
|
|
{
|
|
if (PS.Config.Is光大光子 && _context.CalcScenario == Enums.CalcScenarioEnum.EodSettlement && _context.VolType == "对冲")
|
|
{
|
|
var errmsg = $"[光子对冲收盘 {_context.ValueDate:yyyy-MM-dd}]场内期权'{pnl.ExchangeOptionCode}' 找不到波动率!";
|
|
throw new HedgePnlCalcException(errmsg);
|
|
}
|
|
}
|
|
|
|
if (!optionResult.Succeeded)
|
|
{
|
|
var errmsg = $"{_context.ValueDate:yyyy-MM-dd},场内期权'{pnl.ExchangeOptionCode}' 计算失败,{optionResult.ErrorMessage},fail reason:{optionResult.FailReason}";
|
|
|
|
if (_context.CalcScenario == Enums.CalcScenarioEnum.EodSettlement)
|
|
{
|
|
LogFactory.GetLogger(nameof(HedgePnlCalc)).Error(errmsg);
|
|
}
|
|
else
|
|
{
|
|
//其他场景可能会产生大量重复日志,为了避免这种情况使用debug方式输出
|
|
LogFactory.GetLogger(nameof(HedgePnlCalc)).Debug(errmsg);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var errmsg = $"{_context.ValueDate:yyyy-MM-dd},场内期权'{pnl.ExchangeOptionCode}' 获取不到计算结果";
|
|
|
|
if (_context.CalcScenario == Enums.CalcScenarioEnum.EodSettlement)
|
|
{
|
|
LogFactory.GetLogger(nameof(HedgePnlCalc)).Error(errmsg);
|
|
}
|
|
else
|
|
{
|
|
LogFactory.GetLogger(nameof(HedgePnlCalc)).Debug(errmsg);
|
|
}
|
|
}
|
|
|
|
return optionResult;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 根据对冲账号 簿记账户I 结构类型 买卖方向 标的代码 场内期权代码 编制对冲唯一编码
|
|
/// </summary>
|
|
public static string GetHedgeUniqueCode(int BookId, string TradeType, string LongShort, string UnderlyingCode, string ExchangeOptionCode = null)
|
|
{
|
|
List<string> tradeTypes=new List<string>() { "利率债", "信用债", "其它债券" };
|
|
if (tradeTypes.Contains(TradeType))
|
|
{
|
|
return $"{BookId}_{TradeType}_{UnderlyingCode}".ToUpperInvariant();
|
|
}
|
|
return $"{BookId}_{TradeType}_{LongShort}_{("场内期权".Equals(TradeType) ? ExchangeOptionCode : UnderlyingCode)}".ToUpperInvariant();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 对冲交易 根据结构类型 买卖方向 看涨看跌 获取持仓long short
|
|
/// </summary>
|
|
public static string GetHedgeLongShort(string TradeType, string BuySell)
|
|
{
|
|
switch (TradeType)
|
|
{
|
|
case "商品期货":
|
|
case "商品现货":
|
|
case "场内期权":
|
|
case "利率债":
|
|
case "信用债":
|
|
case "其它债券":
|
|
return BuySell.Contains("多头") ? "long" : "short";
|
|
case "股票":
|
|
default: return "long";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 对冲交易计算异常
|
|
/// </summary>
|
|
public class HedgePnlCalcException : Exception
|
|
{
|
|
public HedgePnlCalcException(string message) : base(message)
|
|
{
|
|
|
|
}
|
|
|
|
public HedgePnlCalcException(string message, Exception innerException) : base(message, innerException)
|
|
{
|
|
}
|
|
}
|
|
}
|