Files
zszq-trs/YLErpDAL/Modules/TQuoteModule/TQuoteService.cs
T
2024-05-09 14:06:26 +08:00

545 lines
25 KiB
C#

using YLErp.BLL;
using YLErp.BLL.Calculation;
using YLErp.BLL.Calculation.V2;
using YLErp.BLL.Calculation.V2.Parameter;
using YLErp.Commons;
using YLErp.Model;
using YLErp.Modules.CalculationModule;
using YLErp.Modules.ClientModule;
using YLErp.Modules.VolatilityModule;
using YLErp.QdpModule;
namespace YLErp.Modules.TQuoteModule
{
/// <summary>
///
/// </summary>
public class TQuoteService : YLBaseService
{
public TQuoteService(OptUserInfo optUser) : base(optUser)
{
}
public TQuoteResult TQuoteWithCustomizedVol(TQuoteWithCustomizedVolRequest request)
{
if (request == null || request.Records == null)
{
return new TQuoteResult();
}
var valueDate = CalculatorHelper.RealtimeQuoteValueDate();
var valueDateStr = valueDate.ToString("yyyy-MM-dd");
underlying_manager underlying = null;
Variety variety = null;
using (var db = new YLContext())
{
underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(request.UnderlyingCode);
variety = db.variety.FirstOrDefault(x => x.VarietyCode != null && x.id == underlying.UnderlyingTypeId);
}
if (underlying == null)
{
return new TQuoteResult();
}
var qdpMarketId = Guid.NewGuid().ToString();
var marketProxy = QdpMarketManager.Instance.GetPrebuiltMarketProxy(qdpMarketId);
//使用全局的DiscountCurve以提高计算效率
var discountCurveName = Guid.NewGuid().ToString();
var discountCurve = CalculatorHelper.CreateConstantRiskFreeCurve(discountCurveName, valuedateBLL.RiskFreeRate / 100.0);
marketProxy.AddYieldCurve(discountCurveName, valueDateStr, discountCurve);
var tQuoteResult = new TQuoteResult
{
quotaList = new List<TQuoteRecord>()
};
//根据用户设置的bid/ask天数调整规则来分别调整到期日
//TODO: bidMaturityShift和askMaturityShift应该从某数据库表读取
var bidMaturityShift = 0;
var askMaturityShift = 0;
var otherInfo = "";
var client_param = ClientPricingParamService.GetPricingParam(valueDate.Date, Convert.ToDateTime(request.MaturityDate).Date);
if (client_param != null)
{
askMaturityShift = client_param.ask_tuning_day ?? 0;
bidMaturityShift = client_param.bid_tuning_day ?? 0;
otherInfo = client_param.ToJson();//$"ask到期日偏离{askMaturityShift}天,bid到期日偏离{bidMaturityShift}天";
}
var dayCount = CalculatorHelper.GetTradeDayCount();
var bidMaturityDate = QdpCalendarHelper.ShiftDate(request.MaturityDate, dayCount, bidMaturityShift);
var askMaturityDate = QdpCalendarHelper.ShiftDate(request.MaturityDate, dayCount, askMaturityShift);
var hasNightMarket = (variety?.HasNightMarket) ?? false;
var trade = new trade()
{
TradeType = "香草期权",
TradeDate = valueDate,
MaturityDate = DateTime.Parse(bidMaturityDate),
ExerciseDate = DateTime.Parse(bidMaturityDate),
OptionType = "看涨",
ExerciseMode = "European",
BuySell = "Buy",
Notional = 1,
UnderlyingCode = request.UnderlyingCode,
UnderlyingInstrumentType = underlying.UnderlyingInstrumentType
};
var parameter = new VanillaOptionParameter()
{
ValueDate = valueDate,
DiscountCurveName = discountCurveName,
SpotPrices = new Dictionary<string, double> { { underlying.UnderlyingCode, request.SpotPrice } },
HasNightMarket = hasNightMarket,
PreciseTimeMode = true
};
foreach (var record in request.Records)
{
trade.OptionType = "看涨";
trade.Strike = record.Strike;
parameter.Volatility = record.CallBidVol;
var callBidResult = ValueCalculator.CalculateTradeValue(qdpMarketId, trade, underlying, parameter);
trade.OptionType = "看跌";
trade.Strike = record.Strike;
parameter.Volatility = record.PutBidVol;
var putBidResult = ValueCalculator.CalculateTradeValue(qdpMarketId, trade, underlying, parameter);
trade.OptionType = "看涨";
trade.Strike = record.Strike;
parameter.Volatility = record.CallAskVol;
var callAskResult = ValueCalculator.CalculateTradeValue(qdpMarketId, trade, underlying, parameter);
trade.OptionType = "看跌";
trade.Strike = record.Strike;
parameter.Volatility = record.PutAskVol;
var putAskResult = ValueCalculator.CalculateTradeValue(qdpMarketId, trade, underlying, parameter);
tQuoteResult.quotaList.Add(new TQuoteRecord()
{
Strike = record.Strike,
CallBidPrice = callBidResult.Pv,
CallBidPricePercent = underlying.Price.HasValue ? callBidResult.Pv / underlying.Price.Value : 0.0,
CallBidVol = callBidResult.Vol,
PutBidPrice = putBidResult.Pv,
PutBidPricePercent = underlying.Price.HasValue ? putBidResult.Pv / underlying.Price.Value : 0.0,
PutBidVol = putBidResult.Vol,
CallAskPrice = callAskResult.Pv,
CallAskPricePercent = underlying.Price.HasValue ? callAskResult.Pv / underlying.Price.Value : 0.0,
CallAskVol = callAskResult.Vol,
PutAskPrice = putAskResult.Pv,
PutAskPricePercent = underlying.Price.HasValue ? putAskResult.Pv / underlying.Price.Value : 0.0,
PutAskVol = putAskResult.Vol,
});
}
QdpMarketManager.Instance.RemovePrebuiltMarketProxy(qdpMarketId);
return tQuoteResult;
}
public TQuoteResult Execute(TQuoteRequest quoteRequest, string userGroup = "")
{
try
{
var valueDate = CalculatorHelper.RealtimeQuoteValueDate();
if (quoteRequest.maturityDate < valueDate)
{
return new TQuoteResult() { Info = "到期日小于估值日", StatusCode = -1 };
}
var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(quoteRequest.underlyingCode);
if (underlying == null)
{
return null;
}
var valueDateStr = valueDate.ToString("yyyy-MM-dd");
var actualStrikeInteranl = quoteRequest.strikeInterval ?? 100;
var actualStrikeAccount = quoteRequest.strikeCount ?? 11;
Variety variety = null;
if (underlying != null)
{
variety = DataCacheProvider.GetVarietyDataSource().GetData(underlying.UnderlyingTypeId);
if (!quoteRequest.strikeInterval.HasValue)
{
using (var db = DbContextFactory.GetYLDbContext())
{
var contract = db.underlying_main_contract.FirstOrDefault(x => x.UnderlyingType == underlying.UnderlyingType);
if (contract != null && contract.StrikeInterval.HasValue)
{
actualStrikeInteranl = contract.StrikeInterval.Value;
}
}
}
}
if (quoteRequest.spotPrice.HasValue)
{
underlying.Price = quoteRequest.spotPrice;
}
if (underlying?.Price == null || underlying.Price < 0.0001)
{
return new TQuoteResult() { Info = "价格无效", StatusCode = -1 };
}
underlying.QuotationDate = valueDate;
var hasNightMarket = (variety?.HasNightMarket) ?? false;
var strikes = GetStrikes(underlying.Price ?? 0, actualStrikeInteranl, actualStrikeAccount);
//根据用户设置的bid/ask天数调整规则来分别调整到期日
//TODO: bidMaturityShift和askMaturityShift应该从某数据库表读取
var bidMaturityShift = 0;
var askMaturityShift = 0;
var otherInfo = "";
var client_param = ClientPricingParamService.GetPricingParam(valueDate.Date, quoteRequest.maturityDate);
if (client_param != null)
{
askMaturityShift = client_param.ask_tuning_day ?? 0;
bidMaturityShift = client_param.bid_tuning_day ?? 0;
otherInfo = client_param.ToJson(); // $"ask到期日偏离{askMaturityShift}天,bid到期日偏离{bidMaturityShift}天";
}
var dayCount = CalculatorHelper.GetTradeDayCount();
//查询
var bidMaturityDate = QdpCalendarHelper.ShiftDate(quoteRequest.maturityDate, dayCount, bidMaturityShift).DateTime;
var askMaturityDate = QdpCalendarHelper.ShiftDate(quoteRequest.maturityDate, dayCount, askMaturityShift).DateTime;
var qdpMarketId = Guid.NewGuid().ToString();
var marketProxy = QdpMarketManager.Instance.GetPrebuiltMarketProxy(qdpMarketId);
//使用全局的DiscountCurve以提高计算效率
var discountCurveName = Guid.NewGuid().ToString();
var discountCurve = CalculatorHelper.CreateConstantRiskFreeCurve(discountCurveName, valuedateBLL.RiskFreeRate / 100.0);
marketProxy.AddYieldCurve(discountCurveName, valueDateStr, discountCurve);
trade newTrade(int tradeId, double strike, DateTime exerciseDate, string buySell, string optionType)
{
return new trade
{
id = tradeId,
TradeType = "香草期权",
TradeDate = valueDate,
ExerciseMode = "European",
Notional = 1,
BuySell = buySell,
OptionType = optionType,
Strike = strike,
SpotPrice = underlying.Price,
UnderlyingCode = quoteRequest.underlyingCode,
UnderlyingInstrumentType = underlying.UnderlyingInstrumentType,
MaturityDate = exerciseDate,
ExerciseDate = exerciseDate,
};
}
if (PS.Config.ErpElement.SkewMapVolConstruction)
{
var vol = VolatilityHelper.GetVol(DateTime.Today, "交易", underlying.UnderlyingCode, userGroup);
var parameter = new VanillaOptionParameter()
{
ValueDate = valueDate,
SpotPrices = new Dictionary<string, double> { { underlying.UnderlyingCode, underlying.Price ?? 0.0 } },
DiscountCurveName = discountCurveName,
PreciseTimeMode = true,
HasNightMarket = hasNightMarket
};
var tQuoteResult = new TQuoteResult() { OtherInfo = otherInfo };
tQuoteResult.quotaList = new List<TQuoteRecord>();
foreach (var strike in strikes)
{
var buyCallTrade = newTrade(0, strike, bidMaturityDate, "买入", "看涨");
parameter.Volatility = SkewMapVolHelper.GetInterpolatedVol(
volSurface: vol,
valueDate: valueDate,
underlyingCode: underlying.UnderlyingCode,
exerciseDate: bidMaturityDate,
strikePrice: strike,
isBuy: true,
isCall: true,
spotPrice: underlying.Price ?? 0);
var result = ValueCalculator.CalculateTradeValue(qdpMarketId, buyCallTrade, underlying, parameter);
var resultRecord = new TQuoteRecord()
{
Strike = strike,
CallBidPrice = Math.Abs(result.Pv),
CallBidPricePercent = underlying.Price.HasValue ? Math.Abs(result.Pv) / underlying.Price.Value : 0.0,
CallBidVol = result.Vol,
};
var buyPutTrade = newTrade(0, strike, bidMaturityDate, "买入", "看跌");
parameter.Volatility = SkewMapVolHelper.GetInterpolatedVol(
volSurface: vol,
valueDate: valueDate,
underlyingCode: underlying.UnderlyingCode,
exerciseDate: bidMaturityDate,
strikePrice: strike,
isBuy: true,
isCall: false,
spotPrice: underlying.Price ?? 0);
result = ValueCalculator.CalculateTradeValue(qdpMarketId, buyPutTrade, underlying, parameter);
resultRecord.PutBidPrice = Math.Abs(result.Pv);
resultRecord.PutBidPricePercent = underlying.Price.HasValue ? Math.Abs(result.Pv) / underlying.Price.Value : 0.0;
resultRecord.PutBidVol = result.Vol;
var sellCallTrade = newTrade(0, strike, askMaturityDate, "卖出", "看涨");
parameter.Volatility = SkewMapVolHelper.GetInterpolatedVol(
volSurface: vol,
valueDate: valueDate,
underlyingCode: underlying.UnderlyingCode,
exerciseDate: askMaturityDate,
strikePrice: strike,
isBuy: false,
isCall: true,
spotPrice: underlying.Price ?? 0);
result = ValueCalculator.CalculateTradeValue(qdpMarketId, sellCallTrade, underlying, parameter);
resultRecord.CallAskPrice = Math.Abs(result.Pv);
resultRecord.CallAskPricePercent = underlying.Price.HasValue ? Math.Abs(result.Pv) / underlying.Price.Value : 0.0;
resultRecord.CallAskVol = result.Vol;
var sellPutTrade = newTrade(0, strike, askMaturityDate, "卖出", "看跌");
parameter.Volatility = SkewMapVolHelper.GetInterpolatedVol(
volSurface: vol,
valueDate: valueDate,
underlyingCode: underlying.UnderlyingCode,
exerciseDate: askMaturityDate,
strikePrice: strike,
isBuy: false,
isCall: false,
spotPrice: underlying.Price ?? 0);
result = ValueCalculator.CalculateTradeValue(qdpMarketId, sellPutTrade, underlying, parameter);
resultRecord.PutAskPrice = Math.Abs(result.Pv);
resultRecord.PutAskPricePercent = underlying.Price.HasValue ? Math.Abs(result.Pv) / underlying.Price.Value : 0.0;
resultRecord.PutAskVol = result.Vol;
tQuoteResult.quotaList.Add(resultRecord);
}
tQuoteResult.LatestPrice = underlying.Price.Value;
if (underlying.PrevClosePrice.HasValue && underlying.PrevClosePrice.Value > 0.0)
{
tQuoteResult.Change = OtcFormatHelper.FormatValue(underlying.Price.Value - underlying.PrevClosePrice.Value, 2);
tQuoteResult.ChangePercent = OtcFormatHelper.FormatValue(tQuoteResult.Change / underlying.PrevClosePrice.Value, 4);
}
else
{
tQuoteResult.Change = 0.0;
tQuoteResult.ChangePercent = 0.0;
}
QdpMarketManager.Instance.RemovePrebuiltMarketProxy(qdpMarketId);
return tQuoteResult;
}
else
{
var buyCallTrades = new List<trade>();
var buyPutTrades = new List<trade>();
var sellCallTrades = new List<trade>();
var sellPutTrades = new List<trade>();
var tradeId = 0;
foreach (var strike in strikes)
{
buyCallTrades.Add(newTrade(tradeId, strike, bidMaturityDate, "买入", "看涨"));
buyPutTrades.Add(newTrade(tradeId, strike, bidMaturityDate, "买入", "看跌"));
sellCallTrades.Add(newTrade(tradeId, strike, askMaturityDate, "卖出", "看涨"));
sellPutTrades.Add(newTrade(tradeId, strike, askMaturityDate, "卖出", "看跌"));
++tradeId;
}
var bidVol = VolatilityHelper.GetVol(DateTime.Today, "报价Bid", underlying.UnderlyingCode, userGroup);
var askVol = VolatilityHelper.GetVol(DateTime.Today, "报价Ask", underlying.UnderlyingCode, userGroup);
var parameter = new VanillaOptionParameter()
{
ValueDate = valueDate,
DiscountCurveName = discountCurveName,
SpotPrices = new Dictionary<string, double>() { { underlying.UnderlyingCode, underlying.Price ?? 0 } },
PreciseTimeMode = true,
HasNightMarket = hasNightMarket
};
if (PS.Config.Is润和)
{
parameter.OverrideTTM = TradeCalcHelper.CalculateTTMDays(valueDate, bidMaturityDate, 0, false);
}
var callBidResults = ValueCalculator.CalculateOptionsWithSharedVolSurface(
qdpMarketId,
buyCallTrades,
underlying,
parameter,
bidVol,
bidMaturityDate);
var putBidResults = ValueCalculator.CalculateOptionsWithSharedVolSurface(
qdpMarketId,
buyPutTrades,
underlying,
parameter,
bidVol,
bidMaturityDate);
if (PS.Config.Is润和)
{
parameter.OverrideTTM = TradeCalcHelper.CalculateTTMDays(valueDate, askMaturityDate, 0, false);
}
var callAskResults = ValueCalculator.CalculateOptionsWithSharedVolSurface(
qdpMarketId,
sellCallTrades,
underlying,
parameter,
askVol,
askMaturityDate);
var putAskResults = ValueCalculator.CalculateOptionsWithSharedVolSurface(
qdpMarketId,
sellPutTrades,
underlying,
parameter,
askVol,
askMaturityDate);
var tQuoteResult = new TQuoteResult() { OtherInfo = otherInfo };
tQuoteResult.quotaList = new List<TQuoteRecord>();
for (var i = 0; i < strikes.Length; ++i)
{
tQuoteResult.quotaList.Add(new TQuoteRecord()
{
Strike = strikes[i],
CallAskPrice = Math.Abs(callAskResults[i].Pv),
CallAskPricePercent = underlying.Price.HasValue ? Math.Abs(callAskResults[i].Pv) / underlying.Price.Value : 0.0,
CallAskVol = callAskResults[i].Vol,
CallBidPrice = Math.Abs(callBidResults[i].Pv),
CallBidPricePercent = underlying.Price.HasValue ? Math.Abs(callBidResults[i].Pv) / underlying.Price.Value : 0.0,
CallBidVol = callBidResults[i].Vol,
PutAskPrice = Math.Abs(putAskResults[i].Pv),
PutAskPricePercent = underlying.Price.HasValue ? Math.Abs(putAskResults[i].Pv) / underlying.Price.Value : 0.0,
PutAskVol = putAskResults[i].Vol,
PutBidPrice = Math.Abs(putBidResults[i].Pv),
PutBidPricePercent = underlying.Price.HasValue ? Math.Abs(putBidResults[i].Pv) / underlying.Price.Value : 0.0,
PutBidVol = putBidResults[i].Vol
});
}
tQuoteResult.LatestPrice = underlying.Price.Value;
if (underlying.PrevClosePrice.HasValue && underlying.PrevClosePrice.Value > 0.0)
{
tQuoteResult.Change = OtcFormatHelper.FormatValue(underlying.Price.Value - underlying.PrevClosePrice.Value, 2);
tQuoteResult.ChangePercent = OtcFormatHelper.FormatValue(tQuoteResult.Change / underlying.PrevClosePrice.Value, 4);
}
else
{
tQuoteResult.Change = 0.0;
tQuoteResult.ChangePercent = 0.0;
}
QdpMarketManager.Instance.RemovePrebuiltMarketProxy(qdpMarketId);
return tQuoteResult;
}
}
catch (Exception ex)
{
LogFactory.GetLogger().Error("TQuote", ex);
return new TQuoteResult() { Info = ex.Message, StatusCode = -1 };
}
}
private double[] GetStrikes(double spotPrice, int strikeInterval, int strikeCount = 11, bool isAsc = true)
{
// 强制将行权价个数调整为奇数
if (strikeCount % 2 == 0)
{
strikeCount += 1;
}
var minInterval = strikeInterval >= 10 ? 10 : strikeInterval;
if (strikeCount <= 0)
{
throw new Exception($"非法的行权价个数{strikeCount}");
}
var strikes = new double[strikeCount];
var midIndex = (strikeCount - 1) / 2;
strikes[midIndex] = spotPrice;
var baseStrike = (int)strikes[midIndex];
var priceDigit = (int)strikes[midIndex] % minInterval;
//OTC-4700:
//当报价间隔大于现价的个位数时,才按10位取整。否则,会出现strike序列错误
//例如AU1812现价269.60,报价间隔3。如果不做以下判断,baseStrike会是260
//这样会出现257,269.30,263,266这样错误的序列
if (strikeInterval > priceDigit)
{
baseStrike -= priceDigit;
}
if (isAsc)
{
for (var i = midIndex + 1; i < strikeCount; ++i)
{
strikes[i] = baseStrike + strikeInterval;
baseStrike += strikeInterval;
}
if (strikeInterval > priceDigit)
{
baseStrike = (int)strikes[midIndex] - priceDigit;
}
for (var i = midIndex - 1; i >= 0; --i)
{
strikes[i] = baseStrike - strikeInterval;
baseStrike -= strikeInterval;
}
}
else
{
for (var i = midIndex - 1; i >= 0; --i)
{
strikes[i] = baseStrike + strikeInterval;
baseStrike += strikeInterval;
}
if (strikeInterval > priceDigit)
{
baseStrike = (int)strikes[midIndex] - priceDigit;
}
for (var i = midIndex + 1; i < strikeCount; ++i)
{
strikes[i] = baseStrike - strikeInterval;
baseStrike -= strikeInterval;
}
}
return strikes;
}
}
}