using YLErp.Commons; using YLErp.Models; using YLErp.Modules.DataCacheModule; using YLErp.Office.ExcelModule; using YLErp.QdpModule.Constants; namespace YLErp.Modules.VolatilityModule { /// /// 波动率导入读取服务 /// public class VolatilityImportReadService : YLBaseService { VolatilityImportReadModel _model; //三种空格\u0020:32--半角空格(英文符号),\u3000:全角空格(中文符号),\u00a0:160--不间断空格(office常用) readonly static char[] TrimChars; readonly static char[] TrimStrikeChars; static VolatilityImportReadService() { TrimChars = new[] { '"', '\t', ' ', '\u00a0', '\u3000' }; TrimStrikeChars = new[] { '"', '\t', ' ', '\u00a0', '\u3000', '%' }; } public VolatilityImportReadService(OptUserInfo userInfo) : base(userInfo) { } class SheetHandleModel { public string sheetName; public string volType; public string volSurfaceMode; public double reviewDownLimit; public double reviewUpLimit; public string userGroup; public SheetHandleModel(string sheetName, string volType, VolatilityImportReadModel baseModel) { this.sheetName = sheetName; this.volType = volType; volSurfaceMode = baseModel.volSurfaceMode; reviewDownLimit = baseModel.ReviewDownLimit; reviewUpLimit = baseModel.ReviewUpLimit; userGroup = baseModel.UserGroup; } } public List ImportFile(Stream file, VolatilityImportReadModel model) { _model = model ?? new VolatilityImportReadModel(); var reader = new NpoiExcelReader(file); if (model.volType == "BidAsk") { return ImportBidAskVols(reader, model); } if (model.volType == "All") { return ImportAllVols(model, reader); } if (model.volType != "BidAsk" && reader.NumberOfSheets() == 2) { throw new ServiceException($"文件选择有误!当前文件仅支持BidAsk波动率类型导入,请重新选择!"); } else { if (!reader.TrySetSheet(0, out var sheet) || sheet.LastRowNum < 0) { throw new ServiceException("导入数据空,请重新导入!"); } var result = SheetHandle(reader, new SheetHandleModel(sheet.SheetName, model.volType, model), false); if (!result.Any()) { throw new ServiceException($"导入名为{sheet.SheetName}的Sheet中数据为空,请重新导入!"); } var codes = result.GroupBy(x => $"[{x.VolType}]{x.ContractCode}").Select(g => (new { name = g.Key, count = g.Count() })) .Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException($"导入名为{sheet.SheetName}的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } return result; } } private List SheetHandle(NpoiExcelReader reader, SheetHandleModel model, bool needSetSheet = true) { if (model is null) { throw new ArgumentNullException(nameof(model)); } ReadProcessBase proc = null; if (needSetSheet && (!reader.TrySetSheet(model.sheetName, out var lastRowNum) || lastRowNum < 0)) { throw new ServiceException($"【Sheet: {model.sheetName} 】导入数据空,请重新导入!"); } try { for (var i = 0; i <= 100000; i++) { if (reader.ReadRow(out var rowNum, out var lineValues)) { if (i == 0) { var isMultiMode = false; //0voltype,1ms,2expire var volinfo = lineValues[0].Split('_').ToList(); if (volinfo.Count > 1) { isMultiMode = volinfo[1].IndexOf("Moneyness") >= 0 || volinfo[1].IndexOf("Strike") >= 0; } if (isMultiMode && volinfo.Count < 3) { throw new ServiceException("请输入格式如:bid_Moneyless_1M"); } //判断第一行第一列为MoneynessVol/StrikeVol_期限 时为申万导入相对行权价简易模式 proc = isMultiMode ? (ReadProcessBase)new ShenWanProcess(this, model.reviewDownLimit, model.reviewUpLimit, model.userGroup) : new StandardProcess(this, model.volType, model.volSurfaceMode, model.reviewDownLimit, model.reviewUpLimit, model.userGroup); } proc.ProcessLine(lineValues, rowNum + 1); } } } catch (Exception e) { throw new Exception($"【Sheet:{model.sheetName}】导入出错。{e.Message}"); } proc.ProcessEnd(); return proc.GetResults(); } //导入bidask类型的波动率 private List ImportBidAskVols(NpoiExcelReader reader, VolatilityImportReadModel model) { if (!reader.TryGetSheet("BID", out var sheet) || sheet.LastRowNum < 0) { throw new ServiceException("导入名为BID的Sheet中数据为空,请重新导入!"); } if (!reader.TryGetSheet("ASK", out sheet) || sheet.LastRowNum < 0) { throw new ServiceException("导入名为ASK的Sheet中数据为空,请重新导入!"); } var bidList = SheetHandle(reader, new SheetHandleModel("BID", "报价Bid", model)); if (bidList.Any()) { var codes = bidList.GroupBy(x => x.ContractCode).Select(g => (new { name = g.Key, count = g.Count() })).Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException("导入名为BID的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } var askList = SheetHandle(reader, new SheetHandleModel("ASK", "报价Ask", model)); if (askList.Any()) { codes = askList.GroupBy(x => x.ContractCode).Select(g => (new { name = g.Key, count = g.Count() })).Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException("导入名为ASK的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } #region 检查数据匹配并顺便计算mid var midList = new List(); foreach (var itema in askList) { try { var itemb = bidList.Single(x => x.ContractCode == itema.ContractCode); if (itema.VolTable.Count != itemb.VolTable.Count) { throw new Exception(); } var midVolTable = new List(); foreach (var avol in itema.VolTable) { var bvol = itemb.VolTable.Single(x => x.Expire == avol.Expire && x.Strike == avol.Strike); // mid曲面的 strike 和 期限一致, 波动率值为 (bid+ask)/2. midVolTable.Add(new SingleVol() { Expire = avol.Expire, Strike = avol.Strike, Vol = (avol.Vol + bvol.Vol) / 2 }); } var mid = ConstructVolSurfaceWithDeviationCheck(itema.UnderlyingId ?? 0, itema.ContractCode , itema.QuotationDate, "", itema.VolSurfaceMode, midVolTable); mid.VolType = "交易";//计算Deviation? mid.ReviewDownLimit = _model.ReviewDownLimit; mid.ReviewUpLimit = _model.ReviewUpLimit; mid.UserGroup = _model.UserGroup; midList.Add(mid); } catch { throw new ServiceException($"匹配合约{itema.ContractCode}出错,请检查Bid/Ask合约-期限-执行价是否匹配!"); } } #endregion bidList.AddRange(askList); bidList.AddRange(midList); return bidList; } throw new ServiceException("导入ASK的Sheet数据空,请重新导入!"); } throw new ServiceException("导入BID的Sheet数据空,请重新导入!"); } //全量导入 private List ImportAllVols(VolatilityImportReadModel model, NpoiExcelReader reader) { var volSurfaceMode = model.volSurfaceMode; if (!reader.TryGetSheet("BID", out var sheetBid) && sheetBid.LastRowNum < 0) { throw new ServiceException("导入名为BID的Sheet中数据为空,请重新导入!"); } if (!reader.TryGetSheet("ASK", out var sheetAsk) && sheetAsk.LastRowNum < 0) { throw new ServiceException("导入名为ASK的Sheet中数据为空,请重新导入!"); } if (!reader.TryGetSheet("MID", out var sheetMid) && sheetAsk.LastRowNum < 0) { throw new ServiceException("导入名为MID的Sheet中数据为空,请重新导入!"); } var bidList = SheetHandle(reader, new SheetHandleModel("BID", "报价Bid", model)); if (!bidList.Any()) { throw new ServiceException("导入名为BID的Sheet中数据为空,请重新导入!"); } var codes = bidList.GroupBy(x => x.ContractCode).Select(g => (new { name = g.Key, count = g.Count() })).Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException("导入名为BID的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } var askList = SheetHandle(reader, new SheetHandleModel("ASK", "报价Ask", model)); if (!askList.Any()) { throw new ServiceException("导入名为ASK的Sheet中数据为空,请重新导入!"); } codes = askList.GroupBy(x => x.ContractCode).Select(g => (new { name = g.Key, count = g.Count() })).Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException("导入名为ASK的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } var midList = SheetHandle(reader, new SheetHandleModel("MID", "交易", model)); if (!midList.Any()) { throw new ServiceException("导入名为MID的Sheet中数据为空,请重新导入!"); } codes = midList.GroupBy(x => x.ContractCode).Select(g => (new { name = g.Key, count = g.Count() })).Where(x => x.count > 1).Select(O => O.name); if (codes.Any()) { throw new ServiceException("导入名为MID的Sheet中数据有重复合约,请重新导入!\r\n重复合约:\r\n" + string.Join("\r\n", codes)); } #region 检查数据匹配并顺便计算mid foreach (var itemm in midList) { try { var itemb = bidList.Single(x => x.ContractCode == itemm.ContractCode); if (itemm.VolTable.Count != itemb.VolTable.Count) { throw new Exception(); } var itema = askList.Single(x => x.ContractCode == itemm.ContractCode); if (itemm.VolTable.Count != itema.VolTable.Count) { throw new Exception(); } } catch { throw new ServiceException($"匹配合约{itemm.ContractCode}出错,请检查Bid/Ask/Mid合约-期限-执行价是否匹配!"); } } #endregion bidList.AddRange(askList); bidList.AddRange(midList); return bidList; } public DateTime quotationDate { get { return _model.quotationDate; } } /// /// 构造波动率曲面,并检查是否需要从交易波动率调整出Bid和Ask /// private volatility ConstructVolSurfaceWithDeviationCheck(int underlyingId, string contractCode, DateTime quotationDate, string volType, string volSurfaceMode, List singleVolList) { var newVol = new volatility { UnderlyingId = underlyingId, ContractCode = contractCode, OptId = UserId, OptName = UserName, QuotationDate = quotationDate, VolType = volType, VolSurfaceMode = volSurfaceMode }; newVol.Data = singleVolList.ToJson(); newVol.InterpolationMethod = ConsVolMethod.Default; if (newVol.VolType == "交易") { newVol.Ask_Deviation = _model.Ask_Deviation; newVol.Bid_Deviation = _model.Bid_Deviation; } return newVol; } /// /// 申万excel波动率类型匹配,目前只有ask,bid和交易 /// public static string GetVolTypeViaPre(string pre) { switch (pre) { case "ask": return ConsVolInfos.报价Ask; case "bid": return ConsVolInfos.报价Bid; case "trade": return "交易"; } return "交易"; } public static string GetVolModeViaPre(string mode) { if (mode.IndexOf("Moneyness") >= 0) { return "MoneynessVol"; } if (mode.IndexOf("Strike") >= 0) { return "StrikeVol"; } return "MoneynessVol"; } abstract class ReadProcessBase { protected readonly List _volList = new List(); public abstract void ProcessLine(string[] lineValues, int lineNumber); public abstract void ProcessEnd(); public List GetResults() { return _volList; } } class StandardProcess : ReadProcessBase { string contractCode = null; underlying_manager _underlying; List strikeList = null; readonly List singleVolList = new List(); bool nextFlag = true; readonly string _volType, _volSurfaceMode, _userGroup; readonly double _reviewDownLimit, _reviewUpLimit; readonly VolatilityImportReadService _service; public StandardProcess(VolatilityImportReadService service, string volType, string volSurfaceMode, double reviewDownLimit, double reviewUpLimit, string userGroup) { _service = service; _volType = volType; _volSurfaceMode = volSurfaceMode; _reviewDownLimit = reviewDownLimit; _reviewUpLimit = reviewUpLimit; _userGroup = userGroup; } public override void ProcessLine(string[] lineValues, int lineNumber) { if (!lineValues.Any(O => !string.IsNullOrWhiteSpace(O))) { nextFlag = true; if (_underlying == null) { throw new ServiceException($"第{lineNumber}行处理失败:不能确认标的"); } if (singleVolList.Count > 0) { var newVol = _service.ConstructVolSurfaceWithDeviationCheck( _underlying.id, contractCode, _service.quotationDate, _volType, _volSurfaceMode, singleVolList); newVol.ReviewDownLimit = _reviewDownLimit; newVol.ReviewUpLimit = _reviewUpLimit; newVol.UserGroup = _userGroup; _volList.Add(newVol); singleVolList.Clear(); } return; } if (nextFlag) { contractCode = lineValues[0].Trim(TrimChars); _underlying = DataCacheManager.GetUnderlyingDataSource().GetData(contractCode); if (_underlying == null) { throw new ServiceException($"第{lineNumber}行{contractCode}不存在\r\n"); } nextFlag = false; strikeList = new List(); foreach (var x in lineValues.Skip(1)) { if (x.Contains("%")) { var value = double.Parse(x.Trim(TrimStrikeChars)); if (value <= 0) { throw new ServiceException($"导入文件中存在非正数行权价! 请检查第{lineNumber}行"); } if (strikeList.Any(s => s == value / 100)) { throw new ServiceException("行权价重复:" + x); } strikeList.Add(value / 100); } else { strikeList.Add(double.Parse(x.Trim(TrimChars))); } } } else { var expire = lineValues[0].Trim(TrimChars).ToUpper(); //检查导入文件中到期日格式是否正确 if (!new System.Text.RegularExpressions.Regex(@"^\d+(W|Y|M|D)$").IsMatch(expire)) { throw new ServiceException($"导入文件中到期日格式错误! 请检查第{lineNumber}行"); } if (lineValues.Length < (strikeList.Count + 1)) { throw new ServiceException($"请检查导入文件内容,内容有误! 请检查第{lineNumber}行"); } double vol = 0; var oneRowVol = new List(); for (var x = 1; x < lineValues.Length; x++) { if (double.TryParse(lineValues[x].Trim(TrimChars), out var value)) { if (PS.Config.ErpElement.VolMoreAccurate) { vol = OtcFormatHelper.FormatValue(value, 6); } else { vol = OtcFormatHelper.FormatValue(value, 4); } } else { throw new ServiceException($"非法的波动率数值! 请检查第{lineNumber}行"); } var singleVol = new SingleVol { Strike = strikeList[x - 1], Expire = expire, Vol = vol }; oneRowVol.Add(singleVol); } if (singleVolList.Any(s => s.Expire == expire)) { throw new ServiceException($"重复的到期日{expire}"); } singleVolList.AddRange(oneRowVol); } } public override void ProcessEnd() { //当文件最后面有空行时,文件中的最后一个波动率曲面已经在上面的循环中保存了 //当文件最后面没有空行时,需要在这里保存最后一个波动率曲面 if (singleVolList.Count > 0) { if (_underlying == null) { throw new ServiceException($"尾行处理失败:不能确认标的"); } var newVol = _service.ConstructVolSurfaceWithDeviationCheck( _underlying.id, contractCode, _service.quotationDate, _volType, _volSurfaceMode, singleVolList); newVol.ReviewDownLimit = _reviewDownLimit; newVol.ReviewUpLimit = _reviewUpLimit; newVol.UserGroup = _userGroup; _volList.Add(newVol); } } } class ShenWanProcess : ReadProcessBase { readonly VolatilityImportReadService _service; readonly List _strikes; private string _simpleExpire; private string _volType; private string _volSurfaceMode; private readonly string _userGroup; readonly double _reviewDownLimit, _reviewUpLimit; public ShenWanProcess(VolatilityImportReadService service, double reviewDownLimit, double reviewUpLimit, string userGroup) { _service = service; _reviewDownLimit = reviewDownLimit; _reviewUpLimit = reviewUpLimit; _userGroup = userGroup; _strikes = new List(); } public override void ProcessLine(string[] lineValues, int lineNumber) { //0voltype,1ms,2expire if (lineValues.Length == 0) { return; } var volheadinfo = lineValues[0].Split('_').ToList(); if (volheadinfo.Count == 3) { _strikes.Clear(); _simpleExpire = volheadinfo[2]; _volType = GetVolTypeViaPre(volheadinfo[0]); _volSurfaceMode = GetVolModeViaPre(volheadinfo[1]); //当前Strike信息 foreach (var s in lineValues.Skip(1).ToList()) { var isPercent = s.IndexOf("%") >= 0; var strike = Convert.ToDouble(s.Trim(TrimStrikeChars)); if (isPercent) { strike = strike / 100; } if (_strikes.Any(sk => sk == strike)) { throw new ServiceException($"{strike}行权价重复"); } _strikes.Add(strike); } } else if (lineValues.Length > 0 && !string.IsNullOrWhiteSpace(lineValues[0])) { //有数据 var voldata = lineValues.Skip(1).ToList(); var contractCode = lineValues[0].Split('.')[0]; var underlying = DataCacheManager.GetUnderlyingDataSource().GetData(contractCode); if (underlying == null) { throw new ServiceException($"标的代码[{contractCode}]在系统中不存在!"); } double vol = 0; var singleVolList = new List(); for (var i = 0; i < voldata.Count; i++) { if (double.TryParse(voldata[i].Trim(TrimChars), out var value)) { if (PS.Config.ErpElement.VolMoreAccurate) { vol = OtcFormatHelper.FormatValue(value, 6); } else { vol = OtcFormatHelper.FormatValue(value, 4); } } else { throw new ServiceException($"非法的波动率数值! 请检查第{lineNumber}行"); } var singleVol = new SingleVol { Strike = _strikes[i], Expire = _simpleExpire, Vol = vol }; singleVolList.Add(singleVol); } var newVol = _service.ConstructVolSurfaceWithDeviationCheck( underlying.id, contractCode, _service.quotationDate, _volType, _volSurfaceMode, singleVolList); newVol.ReviewDownLimit = _reviewDownLimit; newVol.ReviewUpLimit = _reviewUpLimit; newVol.UserGroup = _userGroup; _volList.Add(newVol); } } public override void ProcessEnd() { //申万模式特殊处理,交易不要放在第一个,否则导入时会以为是交易波动率,按照之前api规则替换ask,bid var vgroup = _volList.GroupBy(v => v.ContractCode).ToArray(); //检查 是否存在相同标的,不同波动率模式的数据,存在则不让导入提示错误 var invalidGroup = vgroup.FirstOrDefault(n => n.GroupBy(v => v.VolSurfaceMode).Count() > 1); if (invalidGroup != null) { throw new ServiceException($"导入失败,{invalidGroup.Key}有多种模式波动率,请只输入一种!"); } _volList.Clear(); foreach (var single in vgroup) { var singlevols = single.ToList(); var singlevoltypes = singlevols.GroupBy(s => s.VolType).ToArray(); foreach (var singlevoltype in singlevoltypes) { var first = singlevoltype.First(); var importvol = new volatility { Ask_Deviation = 0, Bid_Deviation = 0, VolSurfaceMode = first.VolSurfaceMode, UnderlyingId = first.UnderlyingId, ContractCode = first.ContractCode, VolType = singlevoltype.Key, QuotationDate = _service.quotationDate, OptDate = DateTime.Now, OptId = _service.UserId, OptName = _service.UserName, ReviewDownLimit = _reviewDownLimit, ReviewUpLimit = _reviewUpLimit, UserGroup = _userGroup }; var ssv = new List(); foreach (var sv in singlevoltype) { ssv.AddRange(sv.VolTable); } importvol.Data = ssv.ToJson(); _volList.Add(importvol); } } } } } public class VolatilityImportReadModel { public DateTime quotationDate { get; set; } public string volType { get; set; } public string volSurfaceMode { get; set; } public double Bid_Deviation { get; set; } public double Ask_Deviation { get; set; } /// /// 审核波动率下限 /// public double ReviewDownLimit { get; set; } /// /// 审核波动率上限 /// public double ReviewUpLimit { get; set; } /// /// 用户组 /// public string UserGroup { get; set; } } }