feat: 国联民生收益互换按簿记分类生成交易编号 簿记账户新增“簿记分类”字段,支持代客、融券,新增账户默认代客,历史账户统一初始化为代客 簿记账户分类设为必填,列表、编辑及详情页面同步展示 修改已被交易引用的簿记账户分类时增加二次确认,并提示关联交易数量 国联民生收益互换交易编号不再根据甲乙方判断,改为根据簿记账户分类生成 代客使用数字序号,融券使用字母序号,分别从已有同客户同日编号的最大序号继续递增 保留手工交易编号优先逻辑,标准格式的手工编号参与后续最大序号计算 生成候选编号后检查完整交易编号是否已存在,存在则继续递增 新增簿记分类编号格式、最大序号计算及数字字母序列隔离专项单测
1580 lines
72 KiB
C#
1580 lines
72 KiB
C#
using BaseOUDAL;
|
|
using YLErp.BLL;
|
|
using YLErp.Configuration;
|
|
using YLErp.Configuration.Enums;
|
|
using YLErp.CustomizedBizLogic;
|
|
using YLErp.DBModels.Consts;
|
|
using YLErp.DBModels.Enums;
|
|
using YLErp.DBModels.Helpers;
|
|
using YLErp.Helpers;
|
|
using YLErp.Model;
|
|
using YLErp.Model.Enum;
|
|
using YLErp.Modules.DataCacheModule;
|
|
using YLErp.Modules.DataProviderModule;
|
|
using YLErp.Modules.EodModule;
|
|
using YLErp.Modules.UnderlyingModule;
|
|
using YLErp.QdpModule;
|
|
using static YLErp.DBModels.ConsTrade;
|
|
|
|
namespace YLErp.Modules.TradeModule.OrderModule
|
|
{
|
|
/// <summary>
|
|
/// 交易保存预处理
|
|
/// </summary>
|
|
partial class TradeSavePreHandler : YLBaseService
|
|
{
|
|
AssetUnitDto _assetUnit;
|
|
underlying_manager _underlying;
|
|
|
|
readonly TradeRightChecker _tradeRightChecker;
|
|
|
|
public TradeSavePreHandler(YLBaseService baseService) : base(baseService)
|
|
{
|
|
_tradeRightChecker = new TradeRightChecker();
|
|
}
|
|
|
|
public bool isAddNew { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 交易编号已自动生成
|
|
/// </summary>
|
|
public bool TradeNumberGenerated { get; private set; }
|
|
|
|
public trade Prepare(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(req.TradeType))
|
|
{
|
|
throw new ServiceException("结构类型 必须填写");
|
|
}
|
|
|
|
if (ConsTrade.TradeTypesForHedge.Contains(req.TradeType))
|
|
{
|
|
throw new ServiceException("程序错误,不再支持对冲类型的交易写入");
|
|
}
|
|
|
|
if (req.UnWindDate != null && req.UnWindDate < req.StartDate)
|
|
{
|
|
throw new ServiceException("导入交易的平仓日期早于交易日期");
|
|
}
|
|
|
|
if (req.TradeType != "现金流交易")
|
|
{
|
|
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(req.UnderlyingCode);
|
|
|
|
// 标的代码
|
|
if (um == null)
|
|
{
|
|
if (req.TradeType == "收益互换" && req.trade_swap.SwapType == "多空组合")
|
|
{
|
|
um = DataCacheProvider.GetUnderlyingDataSource().AsQueryable().FirstOrDefault();
|
|
if (um != null)
|
|
{
|
|
req.UnderlyingCode = um.UnderlyingCode;
|
|
req.UnderlyingId = um.id;
|
|
req.UnderlyingAssetClass = um.UnderlyingType;
|
|
req.UnderlyingInstrumentType = um.UnderlyingInstrumentType;
|
|
req.UnderlyingAssetName = um.UnderlyingName;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceException("标的代码 必须在系统中存在");
|
|
}
|
|
}
|
|
|
|
if (um.UnderlyingInstrumentType == "Stock" && !um.IsCombined())
|
|
{
|
|
var limit = req.TradeType == "收益互换" ? LimitRangeEnum.Swap : LimitRangeEnum.Option;
|
|
var limitStatus = new StockBlackWhiteService(UserInfo).GetStockBlackWhiteList(limit, out var Codes);
|
|
if (Codes != null)
|
|
{
|
|
if (limitStatus == Codes.Contains(um.UnderlyingCode))
|
|
{
|
|
if (limitStatus)
|
|
{
|
|
throw new ServiceException("标的代码 填写错误,存在于黑名单中,请求值:" + req.UnderlyingCode);
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceException("标的代码 填写错误,不存在于白名单中,请求值:" + req.UnderlyingCode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (req.TradeType == "收益互换")
|
|
{
|
|
req.InitialMargin = (req.trade_swap.GetMarginRate - req.trade_swap.PayMarginRate) * req.OriginalStockEqvNotional;
|
|
var udm = DataCacheProvider.GetUnderlyingDataSource().GetData(req.UnderlyingCode);
|
|
//价差组合初始履约预付金=Max{腿一组合系数×腿一价格×腿一预付金率×数量×合约乘数,abs(腿二组合系数)×腿二价格×腿二预付金率×数量×合约乘数}
|
|
if (PS.Config.Company == CompanyEnum.华安 && udm.CommodityCode == "组合标的")
|
|
{
|
|
var synthetic = new SyntheticUnderlyingPriceService(this.OptUser).GetPriceModel(req.UnderlyingCode);
|
|
var InitMarginRate = synthetic.SuList.Max(l => DataCacheProvider.GetUnderlyingDataSource().GetData(l.UnderlyingCode).MarginRate);
|
|
var eodprice = new EodPriceProvider(req.TradeDate ?? DateTime.Now).GetPriceProvider(SettlementTypeEnum.ClosePrice);
|
|
if (req.trade_swap.GetMarginRate == InitMarginRate)
|
|
{
|
|
req.InitialMargin = synthetic.SuList.Max(l => Math.Abs(l.Coefficient) * l.ContractSize * eodprice.GetPrice(l.UnderlyingCode) * DataCacheProvider.GetUnderlyingDataSource().GetData(l.UnderlyingCode).MarginRate * req.trade_swap.PayNotional);
|
|
}
|
|
}
|
|
}
|
|
|
|
isAddNew = req.id == 0;
|
|
|
|
if (isAddNew)
|
|
{
|
|
SetDBModelCreator(req);
|
|
req.TradeStatus = ConsTrade.新增待确认;
|
|
req.TradeSource = dataSource.ToString();
|
|
if (string.IsNullOrWhiteSpace(req.TradeNumber) || (PS.Config.Company == CompanyEnum.光大光子 && (req.TradeNumber == "STG" || req.TradeNumber == "STD" || req.TradeNumber == "FWD" || req.TradeNumber == "SPD")))
|
|
{
|
|
TradeNumberGenerated = true;
|
|
if (req.UnderlyingCode != null && req.UnderlyingCode != "")
|
|
{
|
|
PrepareUnderlying(req, false);
|
|
}
|
|
|
|
req.TradeNumber = BizLogicSingleton.Instance.GenerateTradeNumberBeforeConfirm(req, DbContext);
|
|
}
|
|
else if (DbContext.trade.Any(n => n.TradeNumber == req.TradeNumber))
|
|
{
|
|
throw new ServiceException($"交易编号'{req.TradeNumber}'已经存在");
|
|
}
|
|
else if (PS.Config.ErpElement.UpperTradeNumber)
|
|
{
|
|
req.TradeNumber = req.TradeNumber.ToUpperInvariant();
|
|
}
|
|
|
|
if (req.TradeType == "远期")
|
|
{
|
|
//添加拓展字段 是否补充远期价值,Type2:默认值 -1,其他默认值 0
|
|
if (!req.MetaDic.TryGetValue("IsSupplyForwardValue", out var metaDic))
|
|
{
|
|
if (PS.Config.ErpElement.ForwardValueIsSupplyOrPay == YLErp.Configuration.Enums.ForwardValueIsSupplyOrPay.Type2)
|
|
{
|
|
req.MetaDic.Add("IsSupplyForwardValue", "-1");
|
|
}
|
|
else
|
|
{
|
|
req.MetaDic.Add("IsSupplyForwardValue", "0");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(req.TradeNumber))
|
|
{
|
|
if (DbContext.trade.Any(n => n.id != req.id && n.TradeNumber == req.TradeNumber && n.ValidState != "InValid"))
|
|
{
|
|
throw new ServiceException($"交易编号'{req.TradeNumber}'已经存在");
|
|
}
|
|
|
|
if (PS.Config.ErpElement.UpperTradeNumber)
|
|
{
|
|
req.TradeNumber = req.TradeNumber.ToUpperInvariant();
|
|
}
|
|
}
|
|
|
|
if (req.TradeType == "结构化交易")
|
|
{
|
|
if (!isAddNew)
|
|
{
|
|
throw new ServiceException("程序错误,结构化交易仅支持新增录入");
|
|
}
|
|
PrepareStructureTrades(req, dataSource);
|
|
|
|
if (req.IsGroup == 1)
|
|
{
|
|
PrepareOtcTradeCalc(req);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (req.ParentTradeId < 1 && req.TradeType != ConsGlobal.TradeType.Forward
|
|
&& req.TradeType != ConsGlobal.TradeType.Custom)
|
|
{
|
|
req.StructureType = null;
|
|
}
|
|
PrepareSingleTrade(req, dataSource);
|
|
}
|
|
req.PrincipalRateWrite = req.PrincipalRateWrite.IsNormalize() ? req.PrincipalRateWrite : null;
|
|
|
|
return req;
|
|
}
|
|
|
|
//准备结构化交易
|
|
private void PrepareStructureTrades(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
var subTrades = req.SubTrades?.ToList();
|
|
|
|
if (subTrades == null || !subTrades.Any())
|
|
{
|
|
throw new ServiceException("结构化交易缺少子交易");
|
|
}
|
|
|
|
if (subTrades.Count < 2)
|
|
{
|
|
throw new ServiceException("结构化交易需要最少两笔子交易");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(req.StructureType))
|
|
{
|
|
throw new ServiceException("缺少期权结构类型");
|
|
}
|
|
|
|
var option = StructureOption_Code.StructureOptions.FirstOrDefault(o => o.Name == req.StructureType || o.CnName == req.StructureType);
|
|
if (option != null)
|
|
{
|
|
req.StructureType = option.CnName;
|
|
}
|
|
|
|
var index = 1;
|
|
|
|
foreach (var subTrade in subTrades)
|
|
{
|
|
PrepareSingleTrade(subTrade, dataSource);
|
|
if (req.IsGroup == 0)
|
|
{
|
|
subTrade.StructureType = req.StructureType;
|
|
}
|
|
|
|
if (PS.Config.Is海通)
|
|
{
|
|
subTrade.TradeNumber = req.TradeNumber.Substring(0, req.TradeNumber.Length - 2) + index.ToString("D2");
|
|
}
|
|
else
|
|
{
|
|
subTrade.TradeNumber = BizLogicSingleton.Instance.GenerateSubTradeNumberBeforeConfirm(subTrade, req, index, DbContext);
|
|
}
|
|
index++;
|
|
}
|
|
|
|
if (subTrades.Select(n => n.ClientId).Distinct().Count() > 1)
|
|
{
|
|
throw new ServiceException("结构化交易组合必须有相同的交易对手方");
|
|
}
|
|
|
|
if (subTrades.Select(n => n.AssetId).Distinct().Count() > 1)
|
|
{
|
|
throw new ServiceException("结构化交易组合必须有相同的簿记账户");
|
|
}
|
|
}
|
|
|
|
//准备单个交易
|
|
private trade PrepareSingleTrade(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
//准备交易基础信息
|
|
PrepareBaseInfo(req);
|
|
int? VarietyId2 = 0;
|
|
if (req.TradeType == "远期")
|
|
{
|
|
var underlying = DataCacheManager.GetUnderlyingDataSource().GetData(req.BasisUnderlyingCode ?? "");
|
|
VarietyId2 = underlying?.UnderlyingTypeId;
|
|
}
|
|
//检查交易权限,req.VarietyId来自PrepareBaseInfo
|
|
_tradeRightChecker.Check(_assetUnit, req.TraderId, req.ClientId, req.VarietyId ?? 0, VarietyId2 ?? 0, req.TradeType);
|
|
|
|
if (req.TradeType == "收益互换")
|
|
{
|
|
PrepareSwapTrade(req, dataSource);
|
|
}
|
|
else
|
|
{
|
|
PrepareOtcTrade(req, dataSource);
|
|
}
|
|
|
|
if (req.TradeType == "现金流交易")
|
|
{//现金流交易在平仓时的份额即名义本金,现金流没有份额的概念
|
|
req.Notional = req.StockEqvNotional;
|
|
req.OriginalNotional = req.StockEqvNotional;
|
|
req.TradeAmount = req.StockEqvNotional;
|
|
if (req.trade_cashflow.DepositType != CashflowDepositTypeEnum.资金收益)
|
|
{
|
|
req.trade_cashflow.PrepayRatio = 0;
|
|
}
|
|
}
|
|
|
|
req.OptId = UserId;
|
|
req.OptName = UserName;
|
|
req.OptDate = OptDate;
|
|
//设置HasGeneratedConfirmBook为0
|
|
//req.HasGeneratedConfirmBook = 0;
|
|
|
|
if (isAddNew)
|
|
{
|
|
req.ValidState = ConsGlobal.Valid;
|
|
req.CreateDate = DateTime.Now;
|
|
req.TradeSavedVol = PS.Config.Is厦门象屿 ? req.TradeCloseVolatility : req.TradeOpenVolatility;
|
|
if (PS.Config.ErpElement.VolMode == VolModeEnum.VolSurface)
|
|
{
|
|
req.TradeSavedVol = req.Vol;
|
|
}
|
|
}
|
|
|
|
req.UnderlyingCode = req.UnderlyingCode == null ? null : req.UnderlyingCode.ToUpperInvariant();
|
|
|
|
return req;
|
|
}
|
|
|
|
//准备交易标的信息
|
|
public underlying_manager PrepareUnderlying(trade req, bool prepareSynthetic)
|
|
{
|
|
var underlyingCode = req.UnderlyingCode ?? string.Empty;
|
|
|
|
_underlying ??= UnderlyingDataProvider.GetUnderlying(underlyingCode);
|
|
|
|
if (_underlying == null)
|
|
{
|
|
throw new ServiceException("标的信息不存在:" + req.UnderlyingCode);
|
|
}
|
|
|
|
if (_underlying.IsFutures() && _underlying.MaturityDate == null)
|
|
{
|
|
throw new ServiceException("标的到期日不存在:" + req.UnderlyingCode);
|
|
}
|
|
|
|
//组合标的缺少合成项的价格
|
|
if (prepareSynthetic)
|
|
{
|
|
var checkSynthetic = false;
|
|
|
|
if (req.TradeType == "收益互换")
|
|
{
|
|
if (!req.trade_swap.IsGetFloatingProfit)
|
|
{
|
|
var gun = UnderlyingDataProvider.GetUnderlying(req.trade_swap.GetUnderlyingCode);
|
|
checkSynthetic = gun != null && gun.IsSynthetic();
|
|
}
|
|
|
|
if (!req.trade_swap.IsPayFloatingProfit)
|
|
{
|
|
var pun = UnderlyingDataProvider.GetUnderlying(req.trade_swap.PayUnderlyingCode);
|
|
if (pun != null && pun.IsSynthetic() && (!req.MetaDic.TryGetValue("组合标的2", out var str) || string.IsNullOrWhiteSpace(str)))
|
|
{
|
|
throw new ServiceException("组合标的缺少组合价格:" + pun.UnderlyingCode);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
checkSynthetic = _underlying.IsSynthetic();
|
|
}
|
|
|
|
if (checkSynthetic)
|
|
{
|
|
if (!req.MetaDic.TryGetValue("组合标的", out var str) || string.IsNullOrWhiteSpace(str))
|
|
{
|
|
throw new ServiceException("组合标的缺少组合价格:" + req.UnderlyingCode);
|
|
}
|
|
var saveExt = req.SaveExt;
|
|
if (saveExt == null || !saveExt.MaxSpotPrice.HasValue || saveExt.MaxSpotPrice.Value < 1e-6)
|
|
{
|
|
var sulist = JsonHelper.Deserialize<SyntheticPriceModel>(str)?.SuList;
|
|
var maxSpotPrice = sulist == null ? 0 : sulist.Max(n => Math.Abs(n.Coefficient * n.Price));
|
|
if (maxSpotPrice < 1e-6)
|
|
{
|
|
throw new ServiceException("[组合标的]标的价格 填写错误");
|
|
}
|
|
if (saveExt == null)
|
|
{
|
|
req.SaveExt = new TradeSaveExt();
|
|
}
|
|
req.SaveExt.MaxSpotPrice = maxSpotPrice;
|
|
}
|
|
}
|
|
}
|
|
|
|
req.UnderlyingId = _underlying.id;
|
|
req.UnderlyingCode = _underlying.UnderlyingCode;
|
|
req.VarietyId = _underlying.UnderlyingTypeId;
|
|
req.UnderlyingAssetClass = _underlying.UnderlyingType;
|
|
req.UnderlyingAssetName = _underlying.UnderlyingName;
|
|
req.MaturityDate = _underlying.IsFutures() ? _underlying.MaturityDate : null;
|
|
req.UnderlyingInstrumentType = _underlying.GetMainType();
|
|
req.CountRatio = _underlying.CountRatio;
|
|
|
|
//权益类标的关联的交易如果没有设置分红率则取标的上的分红率,如果标的分红率为空则取0
|
|
if (!req.DividendRate.HasValue && _underlying.CalcTypeIsStock())
|
|
{
|
|
req.DividendRate = _underlying.DividendRate ?? 0;
|
|
}
|
|
|
|
return _underlying;
|
|
}
|
|
|
|
//准备交易基础信息(标的、簿记、交易日期)
|
|
private void PrepareBaseInfo(trade req)
|
|
{
|
|
if (!req.TradeDate.HasValue)
|
|
{
|
|
throw new ServiceException("交易日期 必须填写");
|
|
}
|
|
|
|
if (req.TradeDate.Value.Year < 2000)
|
|
{
|
|
throw new ServiceException("交易日期 填写错误:" + req.TradeDate.Value.ToString("yyyy-MM-dd"));
|
|
}
|
|
|
|
//if (req.TradeDate > valuedateBLL.ValueDate)
|
|
//{
|
|
// throw new ServiceException("交易日期不能是未来日期:" + req.TradeDate.Value.ToString("yyyy-MM-dd"));
|
|
//}
|
|
|
|
if (QdpCalendarHelper.IsHoliday(req.TradeDate.Value) && req.TradeType != "现金流交易")
|
|
{
|
|
throw new ServiceException("交易日期不能是假日:" + req.TradeDate.Value.ToString("yyyy-MM-dd"));
|
|
}
|
|
|
|
//如果起始日没有值,默认赋值交易日
|
|
if (!req.StartDate.HasValue)
|
|
{
|
|
req.StartDate = req.TradeDate;
|
|
}
|
|
|
|
//--------------------------------------
|
|
// 标的信息
|
|
//--------------------------------------
|
|
if (req.TradeType != "现金流交易")
|
|
{
|
|
PrepareUnderlying(req, true);
|
|
}
|
|
|
|
//--------------------------------------
|
|
// 簿记帐户
|
|
//--------------------------------------
|
|
if (req.AssetId < 1)
|
|
{
|
|
throw new ServiceException("请选择簿记账户");
|
|
}
|
|
|
|
if (_assetUnit == null)
|
|
{
|
|
_assetUnit = DbContext.assetunit.Where(n => n.id == req.AssetId)
|
|
.Select(n => new AssetUnitDto { id = n.id, Name = n.Name, BookCategory = n.BookCategory, TraderIds = n.TraderIds, UserGroup = n.UserGroup }).FirstOrDefault();
|
|
if (_assetUnit == null)
|
|
{
|
|
throw new ServiceException("簿记账户不存在");
|
|
}
|
|
}
|
|
else if (_assetUnit.id != req.AssetId)
|
|
{
|
|
throw new ServiceException("程序错误,多个簿记账户并存");
|
|
}
|
|
|
|
req.AssetBookName = _assetUnit.Name;
|
|
|
|
//--------------------------------------
|
|
// 交易员信息(如果没有交易员,查找簿记账户中的交易员)
|
|
//--------------------------------------
|
|
if (string.IsNullOrWhiteSpace(req.TraderName))
|
|
{
|
|
//有交易员id,通过id查名字
|
|
if (req.TraderId > 0)
|
|
{
|
|
req.TraderName = UserBLL.GetNameById(req.TraderId);
|
|
}
|
|
else
|
|
{
|
|
//当前登录用户作为交易员
|
|
req.TraderId = UserId;
|
|
req.TraderName = UserName;
|
|
}
|
|
}
|
|
if (req.TradeType != "现金流交易")
|
|
{
|
|
if (req.TradeType != "远期")
|
|
{
|
|
req.Lots = req.Notional / _underlying.ContractSize;
|
|
}
|
|
else if (req.Lots == null || req.Lots < 1e-6)
|
|
{
|
|
throw new ServiceException("成交手数 必须填写并大于0");
|
|
}
|
|
}
|
|
}
|
|
|
|
//准备衍生品交易基础信息
|
|
private void PrepareOtcTrade(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
var isForward = req.TradeType == "远期";
|
|
var isCustomTrad = req.TradeType == "自定义交易";
|
|
|
|
|
|
var flag = false;
|
|
|
|
var codes = new List<string>
|
|
{
|
|
req.UnderlyingCode
|
|
};
|
|
if (isForward)
|
|
{
|
|
codes.Add(req.BasisUnderlyingCode);
|
|
}
|
|
|
|
//判断标的是否为现货
|
|
flag = UndelyingHelper.IsCodesExistsCommoditySpot(codes, (um) =>
|
|
{
|
|
return um.UnderlyingInstrumentType == "CommoditySpot" ? true : false;
|
|
});
|
|
|
|
|
|
|
|
//--------------------------------------
|
|
// 日期信息
|
|
//--------------------------------------
|
|
|
|
if (!req.ExerciseDate.HasValue)
|
|
{
|
|
throw new ServiceException("到期日期 必须填写");
|
|
}
|
|
|
|
if (req.TradeDate.Value > req.ExerciseDate.Value)
|
|
{
|
|
throw new ServiceException("交易日应该在到期日之前");
|
|
}
|
|
|
|
if (req.MaturityDate != null && req.ExerciseDate.Value > req.MaturityDate.Value && req.TradeType != "累计期权")
|
|
{
|
|
throw new ServiceException("到期日期 必须小于等于 标的到期日");
|
|
}
|
|
|
|
if (flag && PS.Config.Is浙期 /*&& isForward*/)
|
|
{
|
|
|
|
if (!req.MetaDic.TryGetValue("ActualExerciseDate", out var ActualExerciseDateStr) && string.IsNullOrEmpty(ActualExerciseDateStr))
|
|
{
|
|
throw new ServiceException("实际到期日期 必须填写");
|
|
}
|
|
if (!DateTime.TryParse(ActualExerciseDateStr, out var ActualExerciseDate))
|
|
{
|
|
throw new ServiceException("实际到期日期 必须是日期格式");
|
|
}
|
|
|
|
if (req.TradeDate.Value > ActualExerciseDate)
|
|
{
|
|
throw new ServiceException("交易日应该在实际到期日之前");
|
|
}
|
|
|
|
if (req.MaturityDate != null && ActualExerciseDate > req.MaturityDate.Value)
|
|
{
|
|
throw new ServiceException("实际到期日期 必须小于等于 标的到期日");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!req.SettlementDate.HasValue)
|
|
{
|
|
req.SettlementDate = req.ExerciseDate;
|
|
}
|
|
else if (req.SettlementDate < req.TradeDate)
|
|
{
|
|
throw new ServiceException("结算日期 必须大于等于 成交日期");
|
|
}
|
|
|
|
if (!req.PremiumPayDate.HasValue || isForward)
|
|
{
|
|
req.PremiumPayDate = req.TradeDate;
|
|
}
|
|
|
|
//---------------------------------------------
|
|
// 交易方向
|
|
//---------------------------------------------
|
|
|
|
if (string.IsNullOrWhiteSpace(req.BuySell))
|
|
{
|
|
throw new ServiceException("交易方向 必须填写");
|
|
}
|
|
if (req.BuySell != "买入" && req.BuySell != "卖出")
|
|
{
|
|
throw new ServiceException("交易方向 填写错误:" + req.BuySell);
|
|
}
|
|
|
|
//---------------------------------------------
|
|
// 行权方式
|
|
//---------------------------------------------
|
|
|
|
CheckExerciseMode(req);
|
|
|
|
//---------------------------------------------
|
|
// 看涨看跌 + 远期多空方向
|
|
//---------------------------------------------
|
|
|
|
CheckOptionType(req);
|
|
|
|
//---------------------------------------------
|
|
// 标的价格 + 执行价格
|
|
//---------------------------------------------
|
|
if (!req.SpotPrice.HasValue && req.TradeType != "现金流交易")
|
|
{
|
|
throw new ServiceException("期初标的价格 必须填写");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(req.IsMoneynessOption))
|
|
{
|
|
req.IsMoneynessOption = "否";
|
|
}
|
|
else if (req.IsMoneynessOption != "是" && req.IsMoneynessOption != "否")
|
|
{
|
|
throw new ServiceException("是否相对行权价 填写错误:" + req.IsMoneynessOption);
|
|
}
|
|
|
|
if (!req.Strike.HasValue && !(req.TradeType == "自定义交易" || req.TradeType == "现金流交易" || (req.TradeType == "亚式期权" && req.trade_asian_option?.StrikeType == "Floating")))
|
|
{
|
|
throw new ServiceException("执行价格 必须填写");
|
|
}
|
|
if (req.TradeType == "亚式期权" && req.trade_asian_option != null)
|
|
{
|
|
if (req.trade_asian_option.PayoffType == AsianAverageType.EnhancedArithmeticAverage && req.trade_asian_option.StrikeType != "Floating")
|
|
{
|
|
if (req.trade_asian_option.EnhancedPrice == 0)
|
|
{
|
|
throw new ServiceException("增强价格 必须填写");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
req.trade_asian_option.EnhancedPrice = 0;
|
|
}
|
|
|
|
}
|
|
//--------------------------------------
|
|
// 客户信息
|
|
//--------------------------------------
|
|
|
|
PrepareOtcTradeClient(req, dataSource);
|
|
|
|
//--------------------------------------
|
|
// 交易波动率、平滑过渡天数
|
|
//--------------------------------------
|
|
|
|
PrepareOtcTradeVol(req, dataSource);
|
|
|
|
//--------------------------------------
|
|
// 奇异期权
|
|
//--------------------------------------
|
|
|
|
switch (req.TradeType)
|
|
{
|
|
case "凤凰期权":
|
|
var autocall = req.trade_autocall;
|
|
if (!autocall.IsAnnualized2 || !autocall.AnnualizeFactor2.HasValue)
|
|
{
|
|
autocall.AnnualizeFactor2 = 1;
|
|
req.MetaDic[nameof(autocall.AnnualizeFactor2)] = "/";
|
|
}
|
|
req.Strike = autocall.SpreadStrike1;
|
|
break;
|
|
case "雪球期权":
|
|
CheckSnowball(req);
|
|
break;
|
|
case "亚式期权":
|
|
if (req?.trade_asian_option?.StrikeType == "Floating")
|
|
{
|
|
req.Strike = null;
|
|
}
|
|
if (req?.trade_asian_option?.StrikeType == "Segmented" && req?.trade_asian_option?.PayoffType != "EnhancedArithmeticAverage")
|
|
{
|
|
throw new ServiceException("行权价类型为'分段式'时,均价计算类型必须为'增强算术平均'");
|
|
}
|
|
if (req?.trade_asian_option?.PayoffType == "EnhancedArithmeticAverage" && req?.trade_asian_option?.StrikeType == "Floating")
|
|
{
|
|
throw new ServiceException("均价计算类型为'增强算术平均'时,行权价类型不能为'浮动行权价'");
|
|
}
|
|
break;
|
|
case "自定义交易":
|
|
req.ExerciseMode = null;
|
|
break;
|
|
case "二元期权":
|
|
if (req.ExerciseMode == "American")
|
|
{
|
|
var payoffType = req.trade_binary_option.PayoffType;
|
|
if (payoffType.StartsWith("Down", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
req.OptionType = ConsGlobal.CallPut.Put_CN;
|
|
}
|
|
else if (payoffType.StartsWith("Double", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
req.OptionType = "";
|
|
}
|
|
else
|
|
{
|
|
req.OptionType = ConsGlobal.CallPut.Call_CN;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
req.trade_binary_option.ObservationDates = null;
|
|
}
|
|
break;
|
|
case "障碍期权":
|
|
if (string.IsNullOrEmpty(req.trade_barrier_option.BarrierType))
|
|
{
|
|
throw new ServiceException("障碍类型 必须填写");
|
|
}
|
|
if (!req.trade_barrier_option.BarrierType.EndsWith("敲出"))
|
|
{
|
|
req.trade_barrier_option.RebateAnnualizedAtKO = false;
|
|
}
|
|
break;
|
|
case "累计期权":
|
|
if (req.trade_accumulator_option.AccumulatorStructureType != AccumulatorStructureTypeEnum.Segmented)
|
|
{
|
|
if (req.OptionType == "看涨")
|
|
{
|
|
if (req.Strike > req.trade_accumulator_option.KOBarrier)
|
|
{
|
|
throw new ServiceException("[累计期权]期权看涨时,执行价格必须小于等于障碍价格");
|
|
}
|
|
}
|
|
else if (req.Strike < req.trade_accumulator_option.KOBarrier)
|
|
{
|
|
throw new ServiceException("[累计期权]期权看跌时,执行价格必须大于等于障碍价格");
|
|
}
|
|
|
|
if (req.trade_accumulator_option.EarlyTerminate && req.trade_accumulator_option.SettlementMode == "现金期末")
|
|
{
|
|
throw new ServiceException("[累计期权]结算方式为'现金结算(期末)'时,需要将'敲出是否终止'设为'否'");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (req.trade_accumulator_option.Strike2 == null)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]执行价格2 必须填写");
|
|
}
|
|
if (req.trade_accumulator_option.Strike3 == null)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]执行价格3 必须填写");
|
|
}
|
|
if (req.trade_accumulator_option.KOBarrier == null)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]障碍价格 必须填写");
|
|
}
|
|
if (req.trade_accumulator_option.Strike2 < req.Strike)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]执行价格2需要大于等于执行价格1");
|
|
}
|
|
if (req.trade_accumulator_option.Strike3 < req.trade_accumulator_option.Strike2)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]执行价格3需要大于等于执行价格2");
|
|
}
|
|
if (req.OptionType == "看涨")
|
|
{
|
|
if (req.trade_accumulator_option.Strike2 >= req.trade_accumulator_option.KOBarrier || req.trade_accumulator_option.Strike3 < req.trade_accumulator_option.KOBarrier)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]障碍价格需要大于执行价2小于等于执行价3");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (req.Strike > req.trade_accumulator_option.KOBarrier || req.trade_accumulator_option.Strike2 <= req.trade_accumulator_option.KOBarrier)
|
|
{
|
|
throw new ServiceException("[累计期权 三段式]障碍价格需要大于等于执行价1小于执行价2");
|
|
}
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.SettlementMode2))
|
|
{
|
|
req.trade_accumulator_option.SettlementMode2 = "现金当日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardDateType2))
|
|
{
|
|
req.trade_accumulator_option.ForwardDateType2 =
|
|
PS.Config.Company == CompanyEnum.中粮
|
|
? "标的到期日"
|
|
: PS.Config.Is厦门象屿
|
|
? "标的交割月前一交易日"
|
|
: "期权到期日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardPriceType2))
|
|
{
|
|
req.trade_accumulator_option.ForwardPriceType2 = "期权行权价格";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.SettlementMode3))
|
|
{
|
|
req.trade_accumulator_option.SettlementMode3 = "现金当日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardDateType3))
|
|
{
|
|
req.trade_accumulator_option.ForwardDateType3 =
|
|
PS.Config.Company == CompanyEnum.中粮
|
|
? "标的到期日"
|
|
: PS.Config.Is厦门象屿
|
|
? "标的交割月前一交易日"
|
|
: "期权到期日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardPriceType3))
|
|
{
|
|
req.trade_accumulator_option.ForwardPriceType3 = "期权行权价格";
|
|
}
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.PayoffType))
|
|
{
|
|
req.trade_accumulator_option.PayoffType = "固定";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.SettlementMode))
|
|
{
|
|
req.trade_accumulator_option.SettlementMode = "现金当日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardDateType))
|
|
{
|
|
req.trade_accumulator_option.ForwardDateType =
|
|
PS.Config.Company == CompanyEnum.中粮
|
|
? "标的到期日"
|
|
: PS.Config.Is厦门象屿
|
|
? "标的交割月前一交易日"
|
|
: "期权到期日";
|
|
}
|
|
if (string.IsNullOrWhiteSpace(req.trade_accumulator_option.ForwardPriceType))
|
|
{
|
|
req.trade_accumulator_option.ForwardPriceType = "期权行权价格";
|
|
}
|
|
break;
|
|
case "Risky期权":
|
|
var risky = req.trade_risky_option;
|
|
req.Strike = risky.Strike1;
|
|
break;
|
|
}
|
|
|
|
//检查价差期权(暂时在外部处理)
|
|
//检查互算是否正确(名义本金、权利金、交易总额、交易数量、交易份额)
|
|
|
|
//--------------------------------------
|
|
// 其它处理
|
|
//--------------------------------------
|
|
|
|
if (isAddNew)
|
|
{
|
|
req.TradeStatus = ConsTrade.新增待确认;
|
|
}
|
|
|
|
if (!req.ParticipationRate.HasValue)
|
|
{
|
|
req.ParticipationRate = 1;
|
|
}
|
|
|
|
// 无风险利率
|
|
|
|
if (!req.NoRiskRate.HasValue)
|
|
{
|
|
req.NoRiskRate = valuedateBLL.SystemDate.RiskFreeRate / 100;
|
|
}
|
|
else if (req.NoRiskRate < 0)
|
|
{
|
|
throw new ServiceException("无风险利率 填写错误,必须大于等于0,请求值:" + req.NoRiskRate);
|
|
}
|
|
|
|
//分红率是可以为负值的
|
|
|
|
//--------------------------------------
|
|
// 名义本金或总额成交互算 -- 保底收益率
|
|
//--------------------------------------
|
|
PrepareOtcTradeCalc(req);
|
|
|
|
if (req.SettlementType < 0 || req.SettlementType > SettlementTypeEnum.ReferencePrice)
|
|
{
|
|
throw new ServiceException("结算方式 填写错误,当前支持的类型为:0(收盘价)、1(结算价),、2(参考价),请求值为:" + (int)req.SettlementType);
|
|
}
|
|
|
|
//存续天数
|
|
if (!req.DurationDays.HasValue)
|
|
{
|
|
req.DurationDays = (short)(req.ExerciseDate.Value.Date.Subtract(req.TradeDate.Value.Date).TotalDays + 1);
|
|
}
|
|
}
|
|
|
|
//准备互换交易
|
|
private void PrepareSwapTrade(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
PrepareSwapTradeClient(req);
|
|
|
|
if (!req.SettlementDate.HasValue)
|
|
{
|
|
req.SettlementDate = req.ExerciseDate;
|
|
}
|
|
|
|
req.OriginalNotional = req.Notional;
|
|
req.BuySell = (req.trade_swap.GetTradePrice ?? 0) - (req.trade_swap.PayTradePrice ?? 0) == 0 ? (req.trade_swap.IsPayFloatingProfit ? "卖出" : "买入") : ((req.trade_swap.GetTradePrice ?? 0) - (req.trade_swap.PayTradePrice ?? 0) > 0 ? "卖出" : "买入");
|
|
req.Strike = null;
|
|
req.OriginalStockEqvNotional = req.StockEqvNotional;
|
|
req.StockEqvNotionalReal = req.StockEqvNotional;
|
|
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(req.QuoteCurrency, req.SettlementCurrency, req.TradeDate.Value
|
|
, seekPreday: dataSource == TradeSourceEnum.系统交易);
|
|
req.TradePrice = (req.trade_swap.IsTradePriceWhenOpen ? Math.Abs((req.trade_swap.GetTradePrice ?? 0) - (req.trade_swap.PayTradePrice ?? 0)) : 0) * currencyRate;
|
|
}
|
|
|
|
//场外期权交易波动率、平滑过渡天数
|
|
private static void PrepareOtcTradeVol(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
if (!PS.Config.IsTradeVol)
|
|
{
|
|
//volsuface模式下也存在开仓波动率
|
|
|
|
req.TradeCloseVolatility = null;
|
|
|
|
if (req.Vol.HasValue)
|
|
{
|
|
req.TradeOpenVolatility = req.Vol;
|
|
}
|
|
else
|
|
{
|
|
req.Vol = req.TradeOpenVolatility; //从webapi请求,填写的是TradeOpenVolatility
|
|
}
|
|
}
|
|
else if (req.TradeType != "自定义交易" && req.TradeType != "远期" && req.TradeType != "收益互换" && req.TradeType != "现金流交易")
|
|
{
|
|
if (!req.TradeOpenVolatility.HasValue)
|
|
{
|
|
throw new ServiceException("成交波动率 必须填写");
|
|
}
|
|
|
|
if (req.TradeOpenVolatility.Value < 0)
|
|
{
|
|
throw new ServiceException("成交波动率 必须大于等于0");
|
|
}
|
|
|
|
if (!req.TradeCloseVolatility.HasValue)
|
|
{
|
|
throw new ServiceException("目标波动率 必须填写");
|
|
}
|
|
|
|
if (req.TradeCloseVolatility.Value < 0)
|
|
{
|
|
throw new ServiceException("目标波动率 必须大于等于0");
|
|
}
|
|
|
|
if (req.NumOfSmoothingDays.HasValue)
|
|
{
|
|
if (req.NumOfSmoothingDays.Value < 1)
|
|
{
|
|
throw new ServiceException("平滑过渡天数 必须大于0");
|
|
}
|
|
|
|
var remianDays = QdpCalendarHelper.GetNonHolidayDaysBetween(req.TradeDate.Value, req.ExerciseDate.Value) + 1;
|
|
if (req.NumOfSmoothingDays > remianDays)
|
|
{
|
|
throw new ServiceException($"平滑过渡天数{req.NumOfSmoothingDays}不能大于存续天数{remianDays}天");
|
|
}
|
|
}
|
|
else if (dataSource == TradeSourceEnum.导入交易)
|
|
{
|
|
req.NumOfSmoothingDays = QdpCalendarHelper.GetNonHolidayDaysBetween(req.TradeDate.Value, req.ExerciseDate.Value) + 1;
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceException("平滑过渡天数 必须填写");
|
|
}
|
|
}
|
|
else if (req.TradeType == "自定义交易")
|
|
{
|
|
if (req.NumOfSmoothingDays.HasValue)
|
|
{
|
|
if (req.NumOfSmoothingDays.Value < 1)
|
|
{
|
|
throw new ServiceException("平滑过渡天数 必须大于0");
|
|
}
|
|
|
|
var remianDays = QdpCalendarHelper.GetNonHolidayDaysBetween(req.TradeDate.Value, req.ExerciseDate.Value) + 1;
|
|
if (req.NumOfSmoothingDays > remianDays)
|
|
{
|
|
throw new ServiceException($"平滑过渡天数{req.NumOfSmoothingDays}不能大于存续天数{remianDays}天");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//衍生品交易客户信息
|
|
private void PrepareOtcTradeClient(trade req, TradeSourceEnum dataSource)
|
|
{
|
|
var client = PrepareBaseTradeClient(req);
|
|
var structureTypes = new List<string>() { "牛市价差", "熊市价差" };//OTC-5843此两种组合特殊,去除客户可接受风险等级判断
|
|
if ((client.RiskServiceDegree == null || (client.RiskServiceDegree < 5 && !structureTypes.Contains(req.StructureType))) && req.BuySell == "买入")
|
|
{
|
|
throw new ServiceException($"客户'{client.Name}'为非高风险客户,没有卖出权限");
|
|
}
|
|
|
|
//if (_underlying != null && _underlying.CalcTypeIsStock() && ((client.TradingInstType & TradingInstTypeEnum.Equity) != TradingInstTypeEnum.Equity))
|
|
//{
|
|
// throw new ServiceException($"客户'{client.Name}'的'交易资产'属性不包括'权益',不能进行权益类的期权交易");
|
|
//}
|
|
|
|
if (PS.Config.ErpElement.SecuritiesEnvironment)
|
|
{
|
|
if (client.AccessRule == AccessRuleEnum.MySideAccess.GetHashCode() && req.BuySell == "买入")
|
|
{
|
|
throw new ServiceException($"客户'{client.Name}'为我方准入,不能进行买入的期权交易");
|
|
}
|
|
|
|
if (client.AccessRule == AccessRuleEnum.OpSideAccess.GetHashCode() && req.BuySell == "卖出")
|
|
{
|
|
throw new ServiceException($"客户'{client.Name}'为对手方准入,不能进行卖出的期权交易");
|
|
}
|
|
}
|
|
|
|
req.ClientName = client.Name;
|
|
}
|
|
|
|
//互换交易
|
|
private void PrepareSwapTradeClient(trade req)
|
|
{
|
|
var client = PrepareBaseTradeClient(req);
|
|
|
|
if (_underlying.CalcTypeIsStock() && ((client.TradingInstType & TradingInstTypeEnum.Equity) != TradingInstTypeEnum.Equity))
|
|
{
|
|
throw new ServiceException($"客户'{client.Name}'的'交易资产'属性不包括'权益',不能进行收益互换交易");
|
|
}
|
|
|
|
req.ClientName = client.Name;
|
|
req.SettlementCurrency = client.SettlementCurrency;
|
|
}
|
|
|
|
//客户信息
|
|
private Client PrepareBaseTradeClient(trade req)
|
|
{
|
|
if (req.ClientId < 1)
|
|
{
|
|
throw new ServiceException("客户信息不存在");
|
|
}
|
|
|
|
var client = DbContextFactory.GetClientDbContext(OptUser).client.FirstOrDefault(n => n.id == req.ClientId);
|
|
|
|
if (client == null)
|
|
{
|
|
throw new ServiceException("客户信息不存在");
|
|
}
|
|
|
|
if (client.ProcessStatus == "已休眠" || client.ProcessStatus == "已销户")
|
|
{
|
|
if (!client.ProcessOptDate.HasValue || client.ProcessOptDate.Value < req.TradeDate.Value)
|
|
{
|
|
var tradeDateStr = req.TradeDate.Value.ToString("yyyy-MM-dd");
|
|
var processDateStr = client.ProcessOptDate?.ToString("yyyy-MM-dd HH:mm:ss");
|
|
throw new ServiceException($"客户'{client.Name}'{client.ProcessStatus}并且处理日期{processDateStr}早于交易日期'{tradeDateStr}',无法保存交易!");
|
|
}
|
|
}
|
|
else if (client.ProcessStatus != "已开户")
|
|
{
|
|
throw new ServiceException($"客户'{client.Name}'尚未完成开户!");
|
|
}
|
|
|
|
return client;
|
|
}
|
|
|
|
//衍生品交易名义本金或总额成交互算
|
|
private void PrepareOtcTradeCalc(trade req)
|
|
{
|
|
if (!req.IsAnnualized || !req.AnnualizeFactor.HasValue)
|
|
{
|
|
req.AnnualizeFactor = 1;
|
|
req.MetaDic[nameof(req.AnnualizeFactor)] = "/";
|
|
}
|
|
|
|
var spotPrice = Math.Abs(req.SpotPrice == null ? 0 : req.SpotPrice.Value);
|
|
var CountRatio = _underlying == null ? 0 : _underlying.CountRatio;
|
|
var participationRate = req.ParticipationRate ?? 0;
|
|
|
|
var saveExt = req.SaveExt;
|
|
var annualizeFactor = req.AnnualizeFactor;
|
|
|
|
req.TradePrice = Commons.OtcFormatHelper.GetTradePriceDouble(req.TradePrice ?? 0);
|
|
|
|
//保存初始保底收益总额 实际保底收益率
|
|
//req.PrincipalSum = req.OriginalPrincipalSum;
|
|
req.PrincipalRate = TradeHelper.GetPrincipalRateReal(req.StockEqvNotionalReal, req.OriginalPrincipalSum, annualizeFactor);
|
|
|
|
if (req.TradeType == "现金流交易")
|
|
{
|
|
req.StockEqvNotionalReal = req.StockEqvNotional;
|
|
req.Notional = 0;
|
|
req.TradeSinglePrice = 0;
|
|
}
|
|
else if (req.TradeType == "远期")
|
|
{
|
|
req.Notional = req.TradeAmount * CountRatio;
|
|
req.TradeSinglePrice = req.Notional > 0 ? req.TradePrice / req.Notional : 0;
|
|
if (PS.Config.ErpElement.ForwardTradePriceModel == 0 && !string.IsNullOrWhiteSpace(req.BasisUnderlyingCode))
|
|
{
|
|
var spotInfo = req.MetaDic.ContainsKey("期初信息") ? JsonHelper.Deserialize<string[]>(req.MetaDic["期初信息"]) : null;
|
|
if (spotInfo?.Length < 2)
|
|
{
|
|
throw new SystemException($"MetaDic标的价格有误");
|
|
}
|
|
double.TryParse(spotInfo[0], out var price1);
|
|
double.TryParse(spotInfo[1], out var price2);
|
|
req.StockEqvNotional = price1 > price2 ? TradeHelper.GetStockEqvNotional(req.Notional * price1, 1, 1) : TradeHelper.GetStockEqvNotional(req.Notional * price2, 1, 1);
|
|
}
|
|
else
|
|
{
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.Notional * spotPrice, 1, 1);
|
|
}
|
|
req.StockEqvNotionalReal = req.StockEqvNotional;
|
|
}
|
|
else
|
|
{
|
|
var isAutoCall = false;
|
|
|
|
switch (req.TradeType)
|
|
{
|
|
case "凤凰期权":
|
|
isAutoCall = true;
|
|
annualizeFactor = req.trade_autocall.AnnualizeFactor2;
|
|
break;
|
|
case "雪球期权":
|
|
isAutoCall = true;
|
|
annualizeFactor = req.trade_snowball.AnnualizeFactor2;
|
|
break;
|
|
}
|
|
|
|
if (saveExt != null && saveExt.NotionalFlag != TradingNotionalFlag.None)
|
|
{
|
|
if (req.TradeType == "累计期权")
|
|
{
|
|
var customObservDates = QdpHelper.ParseObservationDate(req.trade_accumulator_option.KOObservationDates);
|
|
customObservDates ??= Qdp.Pricing.Base.Implementations.CalendarImpl.Get("chn")
|
|
.BizDaysBetweenDatesExcluStartDay(req.TradeDate.Value, req.ExerciseDate.Value).ToArray();
|
|
|
|
req.TradeAmount = req.trade_accumulator_option.AccumuTradeAmount * customObservDates.Length;
|
|
req.Notional = req.TradeAmount * CountRatio;
|
|
req.StockEqvNotionalReal = req.Notional * spotPrice;
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
|
|
req.IsTradePricePayType = saveExt.PremiumFlag == TradingPremiumFlag.TradePrice;
|
|
if (req.IsTradePricePayType)
|
|
{
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByTradePrice(req.TradePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradePrice(req.TradePrice, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
}
|
|
else
|
|
{
|
|
req.TradePrice = TradeHelper.GetTradePriceBySinglePrice(req.TradeSinglePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradeSinglePrice(req.TradeSinglePrice, spotPrice);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (saveExt.NotionalFlag)
|
|
{
|
|
case TradingNotionalFlag.StockEqvNotionalReal:
|
|
{
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
var notionalEqv = isAutoCall ? req.StockEqvNotional * participationRate : req.StockEqvNotionalReal;
|
|
req.Notional = Math.Abs(spotPrice) > 0 ? notionalEqv / spotPrice : 0;
|
|
req.TradeAmount = CountRatio == 0 ? 0 : req.Notional / CountRatio;
|
|
}
|
|
break;
|
|
case TradingNotionalFlag.StockEqvNotional:
|
|
{
|
|
req.StockEqvNotionalReal = TradeHelper.GetStockEqvNotionalReal(req.StockEqvNotional, participationRate, annualizeFactor);
|
|
var notionalEqv = isAutoCall ? req.StockEqvNotional * participationRate : req.StockEqvNotionalReal;
|
|
req.Notional = Math.Abs(spotPrice) > 0 ? notionalEqv / spotPrice : 0;
|
|
req.TradeAmount = CountRatio == 0 ? 0 : req.Notional / CountRatio;
|
|
}
|
|
break;
|
|
case TradingNotionalFlag.Notional:
|
|
req.TradeAmount = CountRatio == 0 ? 0 : req.Notional / CountRatio;
|
|
if (isAutoCall)
|
|
{
|
|
req.StockEqvNotional = Math.Abs(participationRate) > 0 ? req.Notional * spotPrice / participationRate : 0;
|
|
req.StockEqvNotionalReal = TradeHelper.GetStockEqvNotionalReal(req.StockEqvNotional, participationRate, annualizeFactor);
|
|
}
|
|
else
|
|
{
|
|
req.StockEqvNotionalReal = req.Notional * spotPrice;
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
}
|
|
break;
|
|
case TradingNotionalFlag.TradeAmount:
|
|
req.Notional = CountRatio == 0 ? 0 : req.TradeAmount * CountRatio;
|
|
if (isAutoCall)
|
|
{
|
|
req.StockEqvNotional = Math.Abs(participationRate) > 0 ? req.Notional * spotPrice / participationRate : 0;
|
|
req.StockEqvNotionalReal = TradeHelper.GetStockEqvNotionalReal(req.StockEqvNotional, participationRate, annualizeFactor);
|
|
}
|
|
else
|
|
{
|
|
req.StockEqvNotionalReal = req.Notional * spotPrice;
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
}
|
|
break;
|
|
default:
|
|
throw new SystemException($"[{nameof(TradeSavePreHandler)}.{nameof(PrepareOtcTradeCalc)}][NotionalFlag]系统程序错误");
|
|
}
|
|
|
|
req.IsUsePremiumRate = false;
|
|
req.IsTradePricePayType = false;
|
|
|
|
switch (saveExt.PremiumFlag)
|
|
{
|
|
case TradingPremiumFlag.TradePrice:
|
|
req.IsTradePricePayType = true;
|
|
req.IsUsePremiumRate = saveExt.NotionalFlag == TradingNotionalFlag.StockEqvNotionalReal || saveExt.NotionalFlag == TradingNotionalFlag.StockEqvNotional;
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByTradePrice(req.TradePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradePrice(req.TradePrice, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
break;
|
|
case TradingPremiumFlag.TradeSinglePrice:
|
|
req.TradePrice = TradeHelper.GetTradePriceBySinglePrice(req.TradeSinglePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradeSinglePrice(req.TradeSinglePrice, spotPrice);
|
|
break;
|
|
case TradingPremiumFlag.PremiumRate:
|
|
req.IsUsePremiumRate = true;
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByPremiumRate(req.PremiumRate, spotPrice);
|
|
req.TradePrice = TradeHelper.GetTradePriceByPremiumRate(req.PremiumRate, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
break;
|
|
case TradingPremiumFlag.AllZero:
|
|
break;
|
|
default:
|
|
throw new SystemException($"[{nameof(TradeSavePreHandler)}.{nameof(PrepareOtcTradeCalc)}][PremiumFlag]系统程序错误");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (req.TradeType == "累计期权")
|
|
{
|
|
var customObservDates = QdpHelper.ParseObservationDate(req.trade_accumulator_option.KOObservationDates);
|
|
customObservDates ??= Qdp.Pricing.Base.Implementations.CalendarImpl.Get("chn")
|
|
.BizDaysBetweenDatesExcluStartDay(req.TradeDate.Value, req.ExerciseDate.Value).ToArray();
|
|
|
|
req.TradeAmount = req.trade_accumulator_option.AccumuTradeAmount * customObservDates.Length;
|
|
req.Notional = req.TradeAmount * CountRatio;
|
|
req.StockEqvNotionalReal = req.Notional * spotPrice;
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
|
|
if (req.IsTradePricePayType)
|
|
{
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByTradePrice(req.TradePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradePrice(req.TradePrice, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
}
|
|
else
|
|
{
|
|
req.TradePrice = TradeHelper.GetTradePriceBySinglePrice(req.TradeSinglePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradeSinglePrice(req.TradeSinglePrice, spotPrice);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (req.IsUsePremiumRate == true)
|
|
{
|
|
if (req.IsTradePricePayType == true)
|
|
{
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradePrice(req.TradePrice, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
}
|
|
else
|
|
{
|
|
req.TradePrice = TradeHelper.GetTradePriceByPremiumRate(req.PremiumRate, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
}
|
|
req.StockEqvNotionalReal = TradeHelper.GetStockEqvNotionalReal(req.StockEqvNotional, participationRate, annualizeFactor);
|
|
if (isAutoCall)
|
|
{
|
|
req.Notional = Math.Abs(spotPrice) > 0 ? req.StockEqvNotional * participationRate / spotPrice : 0;
|
|
}
|
|
else
|
|
{
|
|
req.Notional = Math.Abs(spotPrice) > 0 ? req.StockEqvNotionalReal / spotPrice : 0;
|
|
}
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByPremiumRate(req.PremiumRate, spotPrice);
|
|
}
|
|
else
|
|
{
|
|
if (isAutoCall)
|
|
{
|
|
req.StockEqvNotional = Math.Abs(participationRate) > 0 ? req.Notional * spotPrice / participationRate : 0;
|
|
req.StockEqvNotionalReal = TradeHelper.GetStockEqvNotionalReal(req.StockEqvNotional, participationRate, annualizeFactor);
|
|
}
|
|
else
|
|
{
|
|
req.StockEqvNotionalReal = req.Notional * spotPrice;
|
|
req.StockEqvNotional = TradeHelper.GetStockEqvNotional(req.StockEqvNotionalReal, participationRate, annualizeFactor);
|
|
}
|
|
|
|
if (req.IsTradePricePayType == true)
|
|
{
|
|
req.TradeSinglePrice = TradeHelper.GetTradeSinglePriceByTradePrice(req.TradePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradePrice(req.TradePrice, req.StockEqvNotional, participationRate, req.OriginalPrincipalSum, req.AnnualizeFactor, req.BuySell, req.TradeType, true);
|
|
}
|
|
else
|
|
{
|
|
req.TradePrice = TradeHelper.GetTradePriceBySinglePrice(req.TradeSinglePrice, req.Notional, req.OriginalPrincipalSum, req.BuySell, req.TradeType, true);
|
|
req.PremiumRate = TradeHelper.GetPremiumRateByTradeSinglePrice(req.TradeSinglePrice, spotPrice);
|
|
}
|
|
}
|
|
|
|
req.TradeAmount = CountRatio == 0 ? 0 : req.Notional / CountRatio;
|
|
}
|
|
}
|
|
}
|
|
req.TradePrice = Commons.OtcFormatHelper.GetTradePriceDouble(req.TradePrice ?? 0);
|
|
|
|
//最大名义本金
|
|
if (saveExt != null && saveExt.MaxSpotPrice > 0)
|
|
{
|
|
req.StockEqvNotionalMax = TradeHelper.GetStockEqvNotional(saveExt.MaxSpotPrice.Value * req.Notional, participationRate, annualizeFactor);
|
|
}
|
|
else //if (!req.StockEqvNotionalMax.HasValue || req.StockEqvNotionalMax.Value < 1e-6)
|
|
{
|
|
req.StockEqvNotionalMax = req.StockEqvNotional;
|
|
}
|
|
|
|
req.OriginalNotional = req.Notional;
|
|
req.OriginalStockEqvNotional = req.StockEqvNotional;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检查交易权限
|
|
/// </summary>
|
|
class TradeRightChecker
|
|
{
|
|
IEnumerable<int> _allVarietyIds;
|
|
|
|
readonly Dictionary<string, HashSet<int>> _checkDic;
|
|
|
|
public TradeRightChecker()
|
|
{
|
|
_checkDic = new Dictionary<string, HashSet<int>>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检查是否交易员
|
|
/// </summary>
|
|
public void Check(AssetUnit assetUnit, int traderId, int? clientId, int varietyId, int varietyId2 = 0, string tradeType = null)
|
|
{
|
|
if (traderId < 1)
|
|
{
|
|
throw new ServiceException("请选择交易员");
|
|
}
|
|
|
|
//交易用户组必须和簿记账户一致
|
|
if (ConsUserGroup.HasGroup)
|
|
{
|
|
var userGroup = UserBLL.GetUserGroup(traderId) ?? string.Empty;
|
|
if (userGroup != (assetUnit.UserGroup ?? string.Empty))
|
|
{
|
|
throw new ServiceException($"交易员所在用户组({userGroup})必须和簿记账户所在用户组({assetUnit.UserGroup})一致");
|
|
}
|
|
}
|
|
|
|
if (!_checkDic.TryGetValue("trader:" + traderId, out var userVarietyIds))
|
|
{
|
|
if (!UserBLL.IsTradeOfCurrentLogin(traderId))
|
|
{
|
|
throw new ServiceException("非交易员,不允许录入交易");
|
|
}
|
|
if (!("," + assetUnit.TraderIds + ",").Contains("," + traderId + ","))
|
|
{
|
|
throw new ServiceException("交易员不在所选簿记中,请重新选择簿记");
|
|
}
|
|
|
|
var varietyIds = TradeRightProvider.GetUserVarietyIds(traderId);
|
|
if (varietyIds == null)
|
|
{
|
|
_checkDic["trader:" + traderId] = userVarietyIds = null;
|
|
}
|
|
else
|
|
{
|
|
_checkDic["trader:" + traderId] = userVarietyIds = varietyIds.ToHashSet();
|
|
}
|
|
}
|
|
|
|
if (varietyId < 1 && userVarietyIds == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (userVarietyIds != null && !userVarietyIds.Contains(varietyId) && tradeType != "现金流交易" && varietyId != 0)
|
|
{
|
|
throw new ServiceException("该交易员没有该品种的交易权限,不能添加该品种交易");
|
|
}
|
|
if (varietyId2 != 0)
|
|
{
|
|
if (userVarietyIds != null && !userVarietyIds.Contains(varietyId2))
|
|
{
|
|
throw new ServiceException("该交易员没有该品种的交易权限,不能添加该品种交易");
|
|
}
|
|
}
|
|
|
|
if (!clientId.HasValue || clientId.Value < 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
HashSet<int> clientVarietyidSet = null;
|
|
|
|
if (PS.Config.ClientElement.CanSelectCreditVariety && !_checkDic.TryGetValue("client:" + clientId.Value, out clientVarietyidSet))
|
|
{
|
|
var clientCredits = DbContextFactory.GetYLDbContext()
|
|
.credit.Where(c => c.ClientId == clientId.Value && c.ProcessStatus == "已审批" && (c.Type == CreditTable.RoleType || (c.Type == CreditTable.ClientType && c.IsVariety == true)))
|
|
.Select(n => new
|
|
{
|
|
n.VarietyId,
|
|
IsFeelingWhiteList = n.Type == CreditTable.RoleType || n.IsWhitelist == 1,
|
|
}).ToArray();
|
|
|
|
if (clientCredits.Any())
|
|
{
|
|
clientVarietyidSet = new HashSet<int>();
|
|
|
|
foreach (var c in clientCredits)
|
|
{
|
|
var VarietyIds = DataConvert.ConvertCommaValuesToInt32Array(c.VarietyId);
|
|
|
|
//白名单、黑名单
|
|
if (c.IsFeelingWhiteList)
|
|
{
|
|
foreach (var v in VarietyIds)
|
|
{
|
|
clientVarietyidSet.Add(v);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_allVarietyIds ??= VarietyBLL.GetAllvarietyModel().Select(v => v.id).ToArray();
|
|
|
|
foreach (var v in _allVarietyIds)
|
|
{
|
|
clientVarietyidSet.Add(v);
|
|
}
|
|
|
|
foreach (var v in VarietyIds)
|
|
{
|
|
clientVarietyidSet.Remove(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_checkDic["client:" + clientId.Value] = clientVarietyidSet;
|
|
}
|
|
|
|
if (clientVarietyidSet != null && !clientVarietyidSet.Contains(varietyId))
|
|
{
|
|
throw new ServiceException("该客户没有该品种的交易权限,不能添加该品种交易");
|
|
}
|
|
if (varietyId2 != 0)
|
|
{
|
|
if (clientVarietyidSet != null && !clientVarietyidSet.Contains(varietyId2))
|
|
{
|
|
throw new ServiceException("该客户没有该品种的交易权限,不能添加该品种交易");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检查行权方式
|
|
/// </summary>
|
|
static void CheckExerciseMode(trade req)
|
|
{
|
|
if (req.TradeType == "远期" || req.TradeType == "现金流交易")
|
|
{
|
|
req.ExerciseMode = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
switch (req.ExerciseMode)
|
|
{
|
|
case "欧式":
|
|
req.ExerciseMode = "European";
|
|
break;
|
|
case "美式":
|
|
req.ExerciseMode = "American";
|
|
break;
|
|
case "European":
|
|
case "American":
|
|
break;
|
|
default:
|
|
if (!string.IsNullOrWhiteSpace(req.ExerciseMode))
|
|
{
|
|
throw new ServiceException("行权方式 填写错误:" + req.ExerciseMode);
|
|
}
|
|
if (req.TradeType != "自定义交易")
|
|
{
|
|
throw new ServiceException("行权方式 必须填写");
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (req.ExerciseMode == "American")
|
|
{
|
|
switch (req.TradeType)
|
|
{
|
|
case "双鲨期权":
|
|
case "凤凰期权":
|
|
case "雪球期权":
|
|
case "合成价差期权":
|
|
case "区间累积期权":
|
|
case "Risky期权":
|
|
throw new ServiceException($"行权方式 填写错误:{req.TradeType}不支持美式行权");
|
|
case "亚式期权":
|
|
if (req.trade_asian_option?.StrikeType == "Floating")
|
|
{
|
|
throw new ServiceException("行权方式 填写错误:亚式期权美式行权时不支持浮动行权价方式");
|
|
}
|
|
else if (req.trade_asian_option?.StrikeType == "Segmented")
|
|
{
|
|
throw new ServiceException("行权方式 填写错误:亚式期权美式行权时不支持分段式行权价方式");
|
|
}
|
|
break;
|
|
case "二元期权":
|
|
if (req.trade_binary_option?.RebateType == "AtHit" && req.trade_binary_option?.PayoffType == "DoubleNoTouch")
|
|
{
|
|
throw new ServiceException("行权方式 填写错误:二元美式行权并且二元类型为'DoubleNoTouch'时不支持'立即'补偿支付");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (req.TradeType)
|
|
{
|
|
case "亚式期权":
|
|
if (req.trade_asian_option?.StrikeType == "Segmented" && req.trade_asian_option.PayoffType != "EnhancedArithmeticAverage")
|
|
{
|
|
throw new ServiceException("行权价类型为'分段式'时,均价计算类型只支持'增强算术平均'");
|
|
}
|
|
if (req.trade_asian_option?.StrikeType == "Floating" && req.trade_asian_option.PayoffType == "EnhancedArithmeticAverage")
|
|
{
|
|
throw new ServiceException("均价计算类型为'增强算术平均'时,行权价类型不能为'浮动行权价'");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检查看涨看跌和远期多空方向
|
|
/// </summary>
|
|
static void CheckOptionType(OtcTradeBase req)
|
|
{
|
|
switch (req.OptionType)
|
|
{
|
|
case "看涨":
|
|
case "看跌":
|
|
break;
|
|
case "Call":
|
|
case "多头":
|
|
req.OptionType = "看涨";
|
|
break;
|
|
case "Put":
|
|
case "空头":
|
|
req.OptionType = "看跌";
|
|
break;
|
|
default:
|
|
if (!string.IsNullOrWhiteSpace(req.OptionType))
|
|
{
|
|
throw new ServiceException((req.TradeType == "远期" ? "多空方向" : "看涨看跌") + " 填写错误:" + req.OptionType);
|
|
}
|
|
if (req.TradeType != "自定义交易" && req.TradeType != "现金流交易" && (req.TradeType != "二元期权" || req.ExerciseMode != ConsGlobal.ExerciseMode.American))
|
|
{
|
|
throw new ServiceException((req.TradeType == "远期" ? "多空方向" : "看涨看跌") + " 必须填写");
|
|
}
|
|
break;
|
|
}
|
|
if (req.TradeType == "Risky期权" && req.OptionType != "看涨")
|
|
{
|
|
throw new ServiceException($"看涨看跌 填写错误:{req.TradeType}不支持看跌");
|
|
}
|
|
}
|
|
}
|
|
}
|