def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=2)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) self.add_trait( "IndustryFactor", Enum(*(["无"] + DefaultStrFactorList), arg_type="SingleOption", label="行业因子", order=3)) self.add_trait( "WeightFactor", Enum(*(["等权"] + DefaultNumFactorList), arg_type="SingleOption", label="权重因子", order=4))
def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0])
def _on_RiskDS_changed(self, obj, name, old, new): if new is None: self.add_trait( "CorrMethod", ListStr(arg_type="MultiOption", label="相关性算法", order=3, option_range=("spearman", "pearson", "kendall"))) else: self.add_trait( "CorrMethod", ListStr(arg_type="MultiOption", label="相关性算法", order=3, option_range=("spearman", "pearson", "kendall", "factor-score correlation", "factor-portfolio correlation"))) self.CorrMethod = list( set(self.CorrMethod).intersection(set( self.CorrMethod.option_range)))
class MdAPIDB(FactorDB): """MdAPIDB""" UserID = Str("118073", arg_type="String", label="UserID", order=0) Pwd = Password("shuntai11", arg_type="String", label="Password", order=1) BrokerID = Str("9999", arg_type="String", label="BrokerID", order=2) FrontAddr = Str("tcp://180.168.146.187:10010", arg_type="String", label="前置机地址", order=3) ConDir = Directory(label="流文件目录", arg_type="Directory", order=4) IDs = ListStr(label="订阅ID", arg_type="MultiOption", order=5) def __init__(self, sys_args={}, config_file=None, **kwargs): super().__init__(sys_args=sys_args, config_file=(__QS_ConfigPath__ + os.sep + "MdAPIDBConfig.json" if config_file is None else config_file), **kwargs) self._MdAPI = None self._CacheData = {} # 继承来的属性 self.Name = "MdAPIDB" return def connect(self): self._MdAPI = _MdApi(fdb=self) # 创建 API 对象 self._MdAPI.createFtdcMdApi( self.ConDir) # 在 C++ 环境中创建对象, 传入参数是希望用来保存 .con 文件的地址 self._MdAPI.registerFront(self.FrontAddr) # 注册前置机地址 self._MdAPI.init() # 初始化, 连接前置机 return 0 def disconnect(self): self._MdAPI.disconnect() self._MdAPI = None return 0 def isAvailable(self): return (self._MdAPI is not None) # -------------------------------表的操作--------------------------------- @property def TableNames(self): return ["MarketData"] def getTable(self, table_name, args={}): return _TickTable(name=table_name, fdb=self, sys_args=args)
def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "Portfolio", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="策略组合", order=0)) self.add_trait( "BenchmarkPortfolio", Enum(*(["无"] + DefaultNumFactorList), arg_type="SingleOption", label="基准组合", order=1)) self.add_trait( "AttributeFactors", ListStr(arg_type="MultiOption", label="特征因子", order=2, option_range=tuple(DefaultNumFactorList))) self.AttributeFactors.append(DefaultNumFactorList[-1]) self.add_trait( "IndustryFactor", Enum(*(["无"] + DefaultStrFactorList), arg_type="SingleOption", label="行业因子", order=3)) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=4)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price'])
class OLS(BaseModule): """时间序列 OLS""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=1) ReturnType = Enum("简单收益率", "对数收益率", "价格变化量", arg_type="SingleOption", label="收益率类型", order=2) ForecastPeriod = Int(1, arg_type="Integer", label="预测期数", order=3) Lag = Int(0, arg_type="Integer", label="滞后期数", order=4) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=5) SummaryWindow = Float(np.inf, arg_type="Integer", label="统计窗口", order=6) MinSummaryWindow = Int(2, arg_type="Integer", label="最小统计窗口", order=7) Constant = Bool(True, arg_type="Bool", label="常数项", order=8) def __init__(self, factor_table, price_table, name="时间序列OLS", sys_args={}, **kwargs): self._FactorTable = factor_table self._PriceTable = price_table super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._PriceTable.getFactorMetaData(key="DataType"))) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=1)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {} self._Output["证券ID"] = self._PriceTable.getID() nID = len(self._Output["证券ID"]) self._Output["收益率"] = np.zeros(shape=(0, nID)) self._Output["滚动统计量"] = { "R平方": np.zeros((0, nID)), "调整R平方": np.zeros((0, nID)), "t统计量": {}, "F统计量": np.zeros((0, nID)) } self._Output["因子ID"] = self._FactorTable.getID() nFactorID = len(self._Output["因子ID"]) self._Output["因子值"] = np.zeros((0, nFactorID * len(self.TestFactors))) self._CurCalcInd = 0 self._nMinSample = (max(2, self.MinSummaryWindow) if np.isinf( self.MinSummaryWindow) else max(2, self.MinSummaryWindow)) return (self._FactorTable, self._PriceTable) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd PreInd = self._CurCalcInd - self.ForecastPeriod - self.Lag LastInd = self._CurCalcInd - self.ForecastPeriod PreDateTime = self.CalcDTs[PreInd] LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex PreInd = self._CurCalcInd - self.ForecastPeriod - self.Lag LastInd = self._CurCalcInd - self.ForecastPeriod PreDateTime = self._Model.DateTimeSeries[PreInd] LastDateTime = self._Model.DateTimeSeries[LastInd] if (PreInd < 0) or (LastInd < 0): return 0 Price = self._PriceTable.readData(dts=[LastDateTime, idt], ids=self._Output["证券ID"], factor_names=[self.PriceFactor ]).iloc[0, :, :].values self._Output["收益率"] = np.r_[ self._Output["收益率"], _calcReturn(Price, return_type=self.ReturnType)] FactorData = self._FactorTable.readData( dts=[PreDateTime], ids=self._Output["因子ID"], factor_names=list( self.TestFactors)).iloc[:, 0, :].values.flatten(order="F") self._Output["因子值"] = np.r_[self._Output["因子值"], FactorData.reshape( (1, FactorData.shape[0]))] if self._Output["收益率"].shape[0] < self._nMinSample: return 0 StartInd = int( max(0, self._Output["收益率"].shape[0] - self.SummaryWindow)) X = self._Output["因子值"][StartInd:] if self.Constant: X = sm.add_constant(X, prepend=True) nID = len(self._Output["证券ID"]) Statistics = { "R平方": np.full((1, nID), np.nan), "调整R平方": np.full((1, nID), np.nan), "t统计量": np.full((X.shape[1], nID), np.nan), "F统计量": np.full((1, nID), np.nan) } for i, iID in enumerate(self._Output["证券ID"]): Y = self._Output["收益率"][StartInd:, i] try: Result = sm.OLS(Y, X, missing="drop").fit() Statistics["R平方"][0, i] = Result.rsquared Statistics["调整R平方"][0, i] = Result.rsquared_adj Statistics["F统计量"][0, i] = Result.fvalue Statistics["t统计量"][:, i] = Result.tvalues except: pass self._Output["滚动统计量"]["R平方"] = np.r_[self._Output["滚动统计量"]["R平方"], Statistics["R平方"]] self._Output["滚动统计量"]["调整R平方"] = np.r_[self._Output["滚动统计量"]["调整R平方"], Statistics["调整R平方"]] self._Output["滚动统计量"]["F统计量"] = np.r_[self._Output["滚动统计量"]["F统计量"], Statistics["F统计量"]] self._Output["滚动统计量"]["t统计量"][idt] = Statistics["t统计量"] return 0 def __QS_end__(self): if not self._isStarted: return 0 FactorIDs, PriceIDs = self._Output.pop("因子ID"), self._Output.pop( "证券ID") DTs = sorted(self._Output["滚动统计量"]["t统计量"]) self._Output["最后一期统计量"] = pd.DataFrame( { "R平方": self._Output["滚动统计量"]["R平方"][-1], "调整R平方": self._Output["滚动统计量"]["调整R平方"][-1], "F统计量": self._Output["滚动统计量"]["F统计量"][-1] }, index=PriceIDs).loc[:, ["R平方", "调整R平方", "F统计量"]] Index = pd.MultiIndex.from_product([self.TestFactors, FactorIDs], names=["因子", "因子ID"]) if self.Constant: Index = Index.insert(0, ("Constant", "Constant")) self._Output["最后一期t统计量"] = pd.DataFrame( self._Output["滚动统计量"]["t统计量"][DTs[-1]], index=Index, columns=PriceIDs).reset_index() self._Output["全样本统计量"], self._Output["全样本t统计量"] = pd.DataFrame( index=PriceIDs, columns=["R平方", "调整R平方", "F统计量"]), pd.DataFrame(index=Index, columns=PriceIDs) X = self._Output["因子值"] if self.Constant: X = sm.add_constant(X, prepend=True) for i, iID in enumerate(PriceIDs): Y = self._Output["收益率"][:, i] try: Result = sm.OLS(Y, X, missing="drop").fit() self._Output["全样本统计量"].iloc[i, 0] = Result.rsquared self._Output["全样本统计量"].iloc[i, 1] = Result.rsquared_adj self._Output["全样本统计量"].iloc[i, 2] = Result.fvalue self._Output["全样本t统计量"].iloc[:, i] = Result.tvalues except: pass self._Output["滚动统计量"]["R平方"] = pd.DataFrame( self._Output["滚动统计量"]["R平方"], index=DTs, columns=PriceIDs) self._Output["滚动统计量"]["调整R平方"] = pd.DataFrame( self._Output["滚动统计量"]["调整R平方"], index=DTs, columns=PriceIDs) self._Output["滚动统计量"]["F统计量"] = pd.DataFrame( self._Output["滚动统计量"]["F统计量"], index=DTs, columns=PriceIDs) self._Output["滚动t统计量"] = pd.Panel(self._Output["滚动统计量"].pop("t统计量"), major_axis=Index, minor_axis=PriceIDs) self._Output["滚动t统计量"] = self._Output["滚动t统计量"].swapaxes( 0, 2).to_frame(filter_observations=False).reset_index() self._Output["滚动t统计量"].columns = ["因子", "因子ID", "时点"] + PriceIDs self._Output.pop("收益率"), self._Output.pop("因子值") return 0
class IndustryDistribution(BaseModule): """因子值行业分布""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) #IndustryFactor = Enum(None, arg_type="SingleOption", label="行业因子", order=1) Threshold = Enum("中位数","平均数","25%分位数","75%分位数",arg_type="SingleOption", label="阈值", order=2) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=3) IDFilter = Str(arg_type="IDFilter", label="筛选条件", order=4) def __init__(self, factor_table, name="因子值行业分布", sys_args={}, **kwargs): self._FactorTable = factor_table return super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList(dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait("TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) self.add_trait("IndustryFactor", Enum(*DefaultStrFactorList, arg_type="SingleOption", label="行业因子", order=1)) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor(values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) AllIndustries = pd.unique(self._FactorTable.readData(factor_names=[self.IndustryFactor], dts=self._FactorTable.getDateTime(ifactor_name=self.IndustryFactor), ids=self._FactorTable.getID(ifactor_name=self.IndustryFactor)).iloc[0].values.flatten()) Mask = pd.isnull(AllIndustries) if np.sum(Mask)>0: AllIndustries = AllIndustries[~Mask].tolist()+[None] self._Output = {iFactorName:{iIndustry:[] for iIndustry in AllIndustries} for iFactorName in self.TestFactors} self._Output["历史平均值"] = {iFactorName:[] for iFactorName in self.TestFactors} self._Output["历史标准差"] = {iFactorName:[] for iFactorName in self.TestFactors} self._Output["行业分类"] = AllIndustries self._Output["时点"] = [] self._CurCalcInd = 0 return (self._FactorTable, ) def __QS_move__(self, idt, **kwargs): if self._iDT==idt: return 0 self._iDT = idt if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index(idt) + self._CurCalcInd else: self._CurCalcInd = self._Model.DateTimeIndex IDs = self._FactorTable.getFilteredID(idt=idt, id_filter_str=self.IDFilter) FactorExpose = self._FactorTable.readData(dts=[idt], ids=IDs, factor_names=list(self.TestFactors)+[self.IndustryFactor]).iloc[:,0,:] IndustryData, FactorExpose = FactorExpose.iloc[:, -1], FactorExpose.iloc[:, :-1].astype("float") Threshold = {} Mask = {} for iFactorName in self.TestFactors: Mask[iFactorName] = pd.notnull(FactorExpose[iFactorName]) if self.Threshold=="中位数": Threshold[iFactorName] = FactorExpose[iFactorName].median() elif self.Threshold=="平均值": Threshold[iFactorName] = FactorExpose[iFactorName].mean() elif self.Threshold=="25%分位数": Threshold[iFactorName] = FactorExpose[iFactorName].quantile(0.25) elif self.Threshold=="75%分位数": Threshold[iFactorName] = FactorExpose[iFactorName].quantile(0.75) for jIndustry in self._Output["行业分类"]: if pd.isnull(jIndustry): jMask = pd.isnull(IndustryData) else: jMask = (IndustryData==jIndustry) for iFactorName in self.TestFactors: ijMask = (jMask & Mask[iFactorName]) ijNum = ijMask.sum() if ijNum!=0: self._Output[iFactorName][jIndustry].append((FactorExpose[iFactorName][ijMask]>=Threshold[iFactorName]).sum()/ijNum) else: self._Output[iFactorName][jIndustry].append(np.nan) self._Output["时点"].append(idt) return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() for iFactorName in self.TestFactors: self._Output[iFactorName] = pd.DataFrame(self._Output[iFactorName], index=self._Output["时点"], columns=self._Output["行业分类"]) self._Output["历史平均值"][iFactorName] = self._Output[iFactorName].mean() self._Output["历史标准差"][iFactorName] = self._Output[iFactorName].std() self._Output["历史平均值"] = pd.DataFrame(self._Output["历史平均值"], columns=list(self.TestFactors)) self._Output["历史标准差"] = pd.DataFrame(self._Output["历史标准差"], columns=list(self.TestFactors)) self._Output.pop("行业分类") self._Output.pop("时点") return 0 def genMatplotlibFig(self, file_path=None): nRow, nCol = self._Output["历史平均值"].shape[1]//3+(self._Output["历史平均值"].shape[1]%3!=0), min(3, self._Output["历史平均值"].shape[1]) Fig = Figure(figsize=(min(32, 16+(nCol-1)*8), 8*nRow)) xData = np.arange(0, self._Output["历史平均值"].shape[0]) xTickLabels = [str(iIndustry) for iIndustry in self._Output["历史平均值"].index] yMajorFormatter = FuncFormatter(_QS_formatMatplotlibPercentage) for i, iFactorName in enumerate(self._Output["历史平均值"].columns): iAxes = Fig.add_subplot(nRow, nCol, i+1) iAxes.yaxis.set_major_formatter(yMajorFormatter) iAxes.bar(xData, self._Output["历史平均值"].iloc[:, i].values, color="steelblue", label="历史平均值") iAxes.set_title(iFactorName+"-历史平均值") iAxes.set_xticks(xData) iAxes.set_xticklabels(xTickLabels) if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames)>0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName!="计算时点": HTML += "<li>"+iArgName+": "+str(self.Args[iArgName])+"</li>" elif self.Args[iArgName]: HTML += "<li>"+iArgName+": 自定义时点</li>" else: HTML += "<li>"+iArgName+": 所有时点</li>" HTML += "</ul>" else: HTML = "" Formatters = [_QS_formatPandasPercentage]*self._Output["历史平均值"].shape[1] iHTML = self._Output["历史平均值"].to_html(formatters=Formatters) Pos = iHTML.find(">") HTML += iHTML[:Pos]+' align="center"'+iHTML[Pos:] Fig = self.genMatplotlibFig() # figure 保存为二进制文件 Buffer = BytesIO() Fig.savefig(Buffer, bbox_inches='tight') PlotData = Buffer.getvalue() # 图像数据转化为 HTML 格式 ImgStr = "data:image/png;base64,"+base64.b64encode(PlotData).decode() HTML += ('<img src="%s">' % ImgStr) return HTML
class QuantileTiming(BaseModule): """分位数择时""" #TestFactor = Enum(None, arg_type="SingleOption", label="测试因子", order=0) FactorIDs = ListStr(arg_type="List", label="因子ID", order=1) FactorOrder = Dict(key_trait=Str(), value_trait=Enum("降序", "升序"), arg_type="ArgDict", label="排序方向", order=2) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=3) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=4) SummaryWindow = Float(np.inf, arg_type="Integer", label="统计窗口", order=5) MinSummaryWindow = Int(2, arg_type="Integer", label="最小统计窗口", order=6) GroupNum = Int(3, arg_type="Integer", label="分组数", order=7) def __init__(self, factor_table, price_table, name="分位数择时", sys_args={}, **kwargs): self._FactorTable = factor_table self._PriceTable = price_table super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="测试因子", order=0)) self.FactorIDs = self._FactorTable.getID() DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._PriceTable.getFactorMetaData(key="DataType"))) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=3)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ["价", "Price", "price"]) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {} SecurityIDs = self._PriceTable.getID() nDT, nSecurityID, nFactorID = len(dts), len(SecurityIDs), len( self.FactorIDs) self._Output["证券ID"] = SecurityIDs self._Output["证券收益率"] = np.zeros(shape=(nDT, nSecurityID)) self._Output["因子值"] = np.full(shape=(nDT, nFactorID), fill_value=np.nan) self._Output["因子符号"] = 2 * np.array( [(self.FactorOrder.get(iID, "降序") == "升序") for i, iID in enumerate(self.FactorIDs)]).astype(float) - 1 self._Output["最新信号"] = np.full(shape=(nFactorID, self.GroupNum), fill_value=np.nan) self._Output["信号"] = np.full(shape=(nDT, nFactorID, self.GroupNum), fill_value=np.nan) self._Output["信号收益率"] = np.full(shape=(nFactorID, nSecurityID, nDT, self.GroupNum), fill_value=np.nan) self._CurCalcInd = 0 return (self._FactorTable, self._PriceTable) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 self._iDT = idt DTIdx = self._Model.DateTimeIndex if DTIdx > 0: Price = self._PriceTable.readData( dts=self._Model.DateTimeSeries[DTIdx - 1:DTIdx + 1], ids=self._Output["证券ID"], factor_names=[self.PriceFactor]).iloc[0, :, :].values Return = _calcReturn(Price, return_type="简单收益率") self._Output["证券收益率"][DTIdx:DTIdx + 1, :] = Return Mask = pd.notnull(self._Output["最新信号"]) if np.any(Mask): self._Output["信号收益率"][ np.arange(len(self.FactorIDs))[Mask], :, DTIdx, self._Output["最新信号"][Mask].astype(int)] = np.repeat( Return, np.sum(Mask), axis=0) self._Output["因子值"][DTIdx, :] = self._FactorTable.readData( dts=self._Model.DateTimeSeries[DTIdx:DTIdx + 1], ids=list(self.FactorIDs), factor_names=[self.TestFactor]).iloc[0, 0, :].values if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd else: self._CurCalcInd = self._Model.DateTimeIndex if DTIdx + 1 < self.MinSummaryWindow: return 0 FactorData = self._Output["因子值"][ int(max(0, DTIdx + 1 - self.SummaryWindow)):DTIdx + 1, :] * self._Output["因子符号"] Quantiles = np.nanpercentile(FactorData, np.arange(1, self.GroupNum + 1) / self.GroupNum * 100, axis=0) Signal = np.sum(Quantiles < FactorData[-1, :], axis=0).astype(float) Signal[pd.isnull(FactorData[-1, :])] = np.nan self._Output["最新信号"] = Signal Mask = pd.notnull(Signal) if np.any(Mask): self._Output["信号"][DTIdx, np.arange(len(self.FactorIDs))[Mask], Signal[Mask].astype(int)] = 1 return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() DTs = self._Model.DateTimeSeries self._Output["证券收益率"] = pd.DataFrame(self._Output["证券收益率"], index=DTs, columns=self._Output["证券ID"]) self._Output["证券净值"] = (self._Output["证券收益率"] + 1).cumprod() self._Output["因子值"] = pd.DataFrame(self._Output["因子值"], index=DTs, columns=self.FactorIDs) Groups = np.arange(1, self.GroupNum + 1).astype(str) Signal = self._Output["信号"] self._Output["信号"] = {} for i, iID in enumerate(self.FactorIDs): iSignal = pd.DataFrame(Signal[:, i, :], index=DTs, columns=Groups) iDTs = pd.notnull(iSignal).any(axis=1) iDTs = iDTs[iDTs].index if iDTs.shape[0] > 0: self._Output["信号"][iID] = iSignal.loc[iDTs[0]:] else: self._Output["信号"][iID] = pd.DataFrame(columns=Groups) SignalReturn = self._Output["信号收益率"] self._Output["信号收益率"] = {} self._Output["信号净值"] = {} self._Output["统计数据"] = {} nDate = len(DTs) nYear = (DTs[-1] - DTs[0]).days / 365 for j, jSecurityID in enumerate(self._Output["证券ID"]): self._Output["信号收益率"][jSecurityID] = {} self._Output["信号净值"][jSecurityID] = {} self._Output["统计数据"][jSecurityID] = {} for i, iID in enumerate(self.FactorIDs): ijSignalReturn = pd.DataFrame( SignalReturn[i, j, :, :], index=DTs, columns=Groups).loc[self._Output["信号"][iID].index] if ijSignalReturn.shape[0] == 0: self._Output["信号收益率"][jSecurityID][iID] = ijSignalReturn self._Output["信号净值"][jSecurityID][iID] = ijSignalReturn self._Output["统计数据"][jSecurityID][iID] = pd.DataFrame( index=ijSignalReturn.columns, columns=[ "总收益率", "年化收益率", "波动率", "Sharpe比率", "胜率", "最大回撤率", "最大回撤开始时间", "最大回撤结束时间" ]) else: ijNV = (1 + ijSignalReturn.fillna(0)).cumprod() self._Output["信号收益率"][jSecurityID][iID] = ijSignalReturn self._Output["信号净值"][jSecurityID][iID] = ijNV ijStat = pd.DataFrame(index=ijNV.columns) ijStat["总收益率"] = ijNV.iloc[-1, :] - 1 ijStat["年化收益率"] = ijNV.iloc[-1, :]**(1 / nYear) - 1 ijStat["波动率"] = ijSignalReturn.std() * np.sqrt( nDate / nYear) ijStat["Sharpe比率"] = ijStat["年化收益率"] / ijStat["波动率"] ijStat["胜率"] = (ijSignalReturn > 0 ).sum() / pd.notnull(ijSignalReturn).sum() ijStat["最大回撤率"] = pd.Series(np.nan, index=ijStat.index) ijStat["最大回撤开始时间"] = pd.Series(index=ijStat.index, dtype="O") ijStat["最大回撤结束时间"] = pd.Series(index=ijStat.index, dtype="O") for iCol in ijNV.columns: iMaxDD, iStartPos, iEndPos = calcMaxDrawdownRate( ijNV.loc[:, iCol].values) ijStat.loc[iCol, "最大回撤率"] = abs(iMaxDD) ijStat.loc[iCol, "最大回撤开始时间"] = (ijNV.index[iStartPos] if iStartPos is not None else None) ijStat.loc[iCol, "最大回撤结束时间"] = (ijNV.index[iEndPos] if iEndPos is not None else None) self._Output["统计数据"][jSecurityID][iID] = ijStat self._Output.pop("最新信号") self._Output.pop("因子符号") self._Output.pop("证券ID") return 0
class IC(BaseModule): """IC""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) FactorOrder = Dict(key_trait=Str(), value_trait=Enum("降序", "升序"), arg_type="ArgDict", label="排序方向", order=1) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=2) #IndustryFactor = Enum("无", arg_type="SingleOption", label="行业因子", order=3) #WeightFactor = Enum("等权", arg_type="SingleOption", label="权重因子", order=4) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=5) LookBack = Int(1, arg_type="Integer", label="回溯期数", order=6) CorrMethod = Enum("spearman", "pearson", "kendall", arg_type="SingleOption", label="相关性算法", order=7) IDFilter = Str(arg_type="IDFilter", label="筛选条件", order=8) RollAvgPeriod = Int(12, arg_type="Integer", label="滚动平均期数", order=9) def __init__(self, factor_table, name="IC", sys_args={}, **kwargs): self._FactorTable = factor_table super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=2)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) self.add_trait( "IndustryFactor", Enum(*(["无"] + DefaultStrFactorList), arg_type="SingleOption", label="行业因子", order=3)) self.add_trait( "WeightFactor", Enum(*(["等权"] + DefaultNumFactorList), arg_type="SingleOption", label="权重因子", order=4)) @on_trait_change("TestFactors[]") def _on_TestFactors_changed(self, obj, name, old, new): self.FactorOrder = { iFactorName: self.FactorOrder.get(iFactorName, "降序") for iFactorName in self.TestFactors } def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {} self._Output["IC"] = { iFactorName: [] for iFactorName in self.TestFactors } self._Output["股票数"] = { iFactorName: [] for iFactorName in self.TestFactors } self._Output["时点"] = [] self._CurCalcInd = 0 return (self._FactorTable, ) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd PreInd = self._CurCalcInd - self.LookBack LastInd = self._CurCalcInd - 1 PreDateTime = self.CalcDTs[PreInd] LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex PreInd = self._CurCalcInd - self.LookBack LastInd = self._CurCalcInd - 1 PreDateTime = self._Model.DateTimeSeries[PreInd] LastDateTime = self._Model.DateTimeSeries[LastInd] if (PreInd < 0) or (LastInd < 0): for iFactorName in self.TestFactors: self._Output["IC"][iFactorName].append(np.nan) self._Output["股票数"][iFactorName].append(np.nan) self._Output["时点"].append(idt) return 0 PreIDs = self._FactorTable.getFilteredID(idt=PreDateTime, id_filter_str=self.IDFilter) FactorExpose = self._FactorTable.readData( dts=[PreDateTime], ids=PreIDs, factor_names=list(self.TestFactors)).iloc[:, 0, :] Price = self._FactorTable.readData(dts=[LastDateTime, idt], ids=PreIDs, factor_names=[self.PriceFactor ]).iloc[0, :, :] Ret = Price.iloc[-1] / Price.iloc[0] - 1 if self.IndustryFactor != "无": # 进行收益率的行业调整 IndustryData = self._FactorTable.readData( dts=[LastDateTime], ids=PreIDs, factor_names=[self.IndustryFactor]).iloc[0, 0, :] AllIndustry = IndustryData.unique() if self.WeightFactor == "等权": for iIndustry in AllIndustry: iMask = (IndustryData == iIndustry) Ret[iMask] -= Ret[iMask].mean() else: WeightData = self._FactorTable.readData( dts=[LastDateTime], ids=PreIDs, factor_names=[self.WeightFactor]).iloc[0, 0, :] for iIndustry in AllIndustry: iMask = (IndustryData == iIndustry) iWeight = WeightData[iMask] iRet = Ret[iMask] Ret[iMask] -= (iRet * iWeight).sum() / iWeight[pd.notnull( iWeight) & pd.notnull(iRet)].sum(skipna=False) for iFactorName in self.TestFactors: self._Output["IC"][iFactorName].append( FactorExpose[iFactorName].corr(Ret, method=self.CorrMethod)) self._Output["股票数"][iFactorName].append( pd.notnull(FactorExpose[iFactorName]).sum()) self._Output["时点"].append(idt) return 0 def __QS_end__(self): if not self._isStarted: return 0 CalcDateTimes = self._Output.pop("时点") self._Output["股票数"] = pd.DataFrame(self._Output["股票数"], index=CalcDateTimes) self._Output["IC"] = pd.DataFrame(self._Output["IC"], index=CalcDateTimes) for i, iFactorName in enumerate(self.TestFactors): if self.FactorOrder[iFactorName] == "升序": self._Output["IC"][ iFactorName] = -self._Output["IC"][iFactorName] self._Output["IC的移动平均"] = self._Output["IC"].copy() for i in range(len(CalcDateTimes)): if i < self.RollAvgPeriod - 1: self._Output["IC的移动平均"].iloc[i, :] = np.nan else: self._Output["IC的移动平均"].iloc[i, :] = self._Output["IC"].iloc[ i - self.RollAvgPeriod + 1:i + 1, :].mean() self._Output["统计数据"] = pd.DataFrame(index=self._Output["IC"].columns) self._Output["统计数据"]["平均值"] = self._Output["IC"].mean() self._Output["统计数据"]["标准差"] = self._Output["IC"].std() self._Output["统计数据"]["最小值"] = self._Output["IC"].min() self._Output["统计数据"]["最大值"] = self._Output["IC"].max() self._Output["统计数据"]["IC_IR"] = self._Output["统计数据"][ "平均值"] / self._Output["统计数据"]["标准差"] self._Output["统计数据"]["t统计量"] = np.nan self._Output["统计数据"]["平均股票数"] = self._Output["股票数"].mean() self._Output["统计数据"]["IC×Sqrt(N)"] = self._Output["统计数据"][ "平均值"] * np.sqrt(self._Output["统计数据"]["平均股票数"]) self._Output["统计数据"]["有效期数"] = 0.0 for iFactor in self._Output["IC"]: self._Output["统计数据"].loc[iFactor, "有效期数"] = pd.notnull( self._Output["IC"][iFactor]).sum() self._Output["统计数据"]["t统计量"] = (self._Output["统计数据"]["有效期数"]** 0.5) * self._Output["统计数据"]["IC_IR"] return 0 def genMatplotlibFig(self, file_path=None): nRow, nCol = self._Output["IC"].shape[1] // 3 + ( self._Output["IC"].shape[1] % 3 != 0), min( 3, self._Output["IC"].shape[1]) Fig = plt.figure(figsize=(min(32, 16 + (nCol - 1) * 8), 8 * nRow)) AxesGrid = gridspec.GridSpec(nRow, nCol) xData = np.arange(0, self._Output["IC"].shape[0]) xTicks = np.arange(0, self._Output["IC"].shape[0], max(1, int(self._Output["IC"].shape[0] / 10))) xTickLabels = [ self._Output["IC"].index[i].strftime("%Y-%m-%d") for i in xTicks ] yMajorFormatter = FuncFormatter(_QS_formatMatplotlibPercentage) for i in range(self._Output["IC"].shape[1]): iAxes = plt.subplot(AxesGrid[i // nCol, i % nCol]) iAxes.yaxis.set_major_formatter(yMajorFormatter) iAxes.plot(xData, self._Output["IC的移动平均"].iloc[:, i].values, label="IC的移动平均", color="r", alpha=0.6, lw=3) iAxes.bar(xData, self._Output["IC"].iloc[:, i].values, label="IC", color="b") iAxes.set_xticks(xTicks) iAxes.set_xticklabels(xTickLabels) iAxes.legend(loc='best') iAxes.set_title(self._Output["IC"].columns[i]) if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames) > 0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName != "计算时点": HTML += "<li>" + iArgName + ": " + str( self.Args[iArgName]) + "</li>" elif self.Args[iArgName]: HTML += "<li>" + iArgName + ": 自定义时点</li>" else: HTML += "<li>" + iArgName + ": 所有时点</li>" HTML += "</ul>" else: HTML = "" Formatters = [_QS_formatPandasPercentage] * 4 + [ lambda x: '{0:.4f}'.format(x) ] + [lambda x: '{0:.2f}'.format(x)] * 3 + [ lambda x: '{0:.0f}'.format(x) ] iHTML = self._Output["统计数据"].to_html(formatters=Formatters) Pos = iHTML.find(">") HTML += iHTML[:Pos] + ' align="center"' + iHTML[Pos:] Fig = self.genMatplotlibFig() # figure 保存为二进制文件 Buffer = BytesIO() plt.savefig(Buffer, bbox_inches='tight') PlotData = Buffer.getvalue() # 图像数据转化为 HTML 格式 ImgStr = "data:image/png;base64," + base64.b64encode(PlotData).decode() HTML += ('<img src="%s">' % ImgStr) return HTML
class SectionCorrelation(BaseModule): """因子截面相关性""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) FactorOrder = Dict(key_trait=Str(), value_trait=Enum("降序", "升序"), arg_type="ArgDict", label="排序方向", order=1) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=2) CorrMethod = ListStr(["spearman"], arg_type="MultiOption", label="相关性算法", order=3, option_range=("spearman", "pearson", "kendall")) RiskTable = Instance(RiskTable, arg_type="RiskTable", label="风险表", order=4) IDFilter = Str(arg_type="IDFilter", label="筛选条件", order=5) def __init__(self, factor_table, name="因子截面相关性", sys_args={}, **kwargs): self._FactorTable = factor_table return super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) @on_trait_change("TestFactors[]") def _on_TestFactors_changed(self, obj, name, old, new): self.FactorOrder = { iFactorName: self.FactorOrder.get(iFactorName, "降序") for iFactorName in self.TestFactors } @on_trait_change("RiskTable") def _on_RiskDS_changed(self, obj, name, old, new): if new is None: self.add_trait( "CorrMethod", ListStr(arg_type="MultiOption", label="相关性算法", order=3, option_range=("spearman", "pearson", "kendall"))) else: self.add_trait( "CorrMethod", ListStr(arg_type="MultiOption", label="相关性算法", order=3, option_range=("spearman", "pearson", "kendall", "factor-score correlation", "factor-portfolio correlation"))) self.CorrMethod = list( set(self.CorrMethod).intersection(set( self.CorrMethod.option_range))) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) Items[3].editor = SetEditor( values=self.trait("CorrMethod").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {"FactorPair": []} for i, iFactor in enumerate(self.TestFactors): for j, jFactor in enumerate(self.TestFactors): if j > i: self._Output["FactorPair"].append(iFactor + "-" + jFactor) nPair = len(self._Output["FactorPair"]) self._Output.update({ iMethod: [[] for i in range(nPair)] for iMethod in self.CorrMethod }) self._CorrMatrixNeeded = ( ("factor-score correlation" in self.CorrMethod) or ("factor-portfolio correlation" in self.CorrMethod)) if self._CorrMatrixNeeded and (self.RiskTable is not None): self.RiskTable.start(dts=dts) self._Output["时点"] = [] self._CurCalcInd = 0 return (self._FactorTable, ) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 self._iDT = idt if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd else: self._CurCalcInd = self._Model.DateTimeIndex IDs = self._FactorTable.getFilteredID(idt=idt, id_filter_str=self.IDFilter) FactorExpose = self._FactorTable.readData( dts=[idt], ids=IDs, factor_names=list(self.TestFactors)).iloc[:, 0, :].astype("float") if self._CorrMatrixNeeded and (self.RiskTable is not None): self.RiskTable.move(idt) CovMatrix = dropRiskMatrixNA( self.RiskTable.readCov(dts=[idt], ids=IDs).iloc[0]) FactorIDs = {} else: CovMatrix = None PairInd = 0 for i, iFactor in enumerate(self.TestFactors): iFactorExpose = FactorExpose[iFactor] if self._CorrMatrixNeeded: iIDs = FactorIDs.get(iFactor) if iIDs is None: if CovMatrix is not None: FactorIDs[iFactor] = list( set(CovMatrix.index).intersection( set(iFactorExpose[pd.notnull( iFactorExpose)].index))) else: FactorIDs[iFactor] = list( iFactorExpose[pd.notnull(iFactorExpose)].index) iIDs = FactorIDs[iFactor] for j, jFactor in enumerate(self.TestFactors): if j > i: jFactorExpose = FactorExpose[jFactor] if self._CorrMatrixNeeded: jIDs = FactorIDs.get(jFactor) if jIDs is None: if CovMatrix is not None: FactorIDs[jFactor] = list( set(CovMatrix.index).intersection( set(jFactorExpose[pd.notnull( jFactorExpose)].index))) else: FactorIDs[jFactor] = list(jFactorExpose[ pd.notnull(jFactorExpose)].index) jIDs = FactorIDs[jFactor] IDs = list(set(iIDs).intersection(set(jIDs))) iTempExpose = iFactorExpose.loc[IDs].values jTempExpose = jFactorExpose.loc[IDs].values if CovMatrix is not None: TempCovMatrix = CovMatrix.loc[IDs, IDs].values else: nID = len(IDs) TempCovMatrix = np.eye(nID, nID) for kMethod in self.CorrMethod: if kMethod == "factor-score correlation": ijCov = np.dot(iTempExpose.T, np.dot(TempCovMatrix, jTempExpose)) iStd = np.sqrt( np.dot(iTempExpose.T, np.dot(TempCovMatrix, iTempExpose))) jStd = np.sqrt( np.dot(jTempExpose.T, np.dot(TempCovMatrix, jTempExpose))) self._Output[kMethod][PairInd].append(ijCov / iStd / jStd) elif kMethod == "factor-portfolio correlation": TempCovMatrixInv = np.linalg.inv(TempCovMatrix) ijCov = np.dot( iTempExpose.T, np.dot(TempCovMatrixInv, jTempExpose)) iStd = np.sqrt( np.dot(iTempExpose.T, np.dot(TempCovMatrixInv, iTempExpose))) jStd = np.sqrt( np.dot(jTempExpose.T, np.dot(TempCovMatrixInv, jTempExpose))) self._Output[kMethod][PairInd].append(ijCov / iStd / jStd) else: self._Output[kMethod][PairInd].append( FactorExpose[iFactor].corr( FactorExpose[jFactor], method=kMethod)) PairInd += 1 self._Output["时点"].append(idt) return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() for iMethod in self.CorrMethod: self._Output[iMethod] = pd.DataFrame( np.array(self._Output[iMethod]).T, columns=self._Output["FactorPair"], index=self._Output["时点"]) iAvgName = iMethod + "均值" self._Output[iAvgName] = pd.DataFrame(index=list(self.TestFactors), columns=list( self.TestFactors), dtype="float") for i, iFactor in enumerate(self.TestFactors): for j, jFactor in enumerate(self.TestFactors): if j > i: if self.FactorOrder[iFactor] != self.FactorOrder[ jFactor]: self._Output[iMethod][ iFactor + "-" + jFactor] = -self._Output[iMethod][iFactor + "-" + jFactor] self._Output[iAvgName].loc[ iFactor, jFactor] = self._Output[iMethod][iFactor + "-" + jFactor].mean() elif j < i: self._Output[iAvgName].loc[ iFactor, jFactor] = self._Output[iAvgName].loc[jFactor, iFactor] else: self._Output[iAvgName].loc[iFactor, jFactor] = 1 self._Output.pop("FactorPair") self._Output.pop("时点") if (self.RiskTable is not None) and self._CorrMatrixNeeded: self.RiskTable.end() return 0 def _plotHeatMap(self, axes, df, title): axes.pcolor(df.values, cmap=matplotlib.cm.Reds) axes.set_xticks(np.arange(df.shape[0]) + 0.5, minor=False) axes.set_yticks(np.arange(df.shape[1]) + 0.5, minor=False) axes.invert_yaxis() axes.xaxis.tick_top() axes.set_xticklabels(df.index.tolist(), minor=False) axes.set_yticklabels(df.columns.tolist(), minor=False) axes.set_title(title) return axes def genMatplotlibFig(self, file_path=None): nMethod = len(self.CorrMethod) nRow, nCol = nMethod // 3 + (nMethod % 3 != 0), min(3, nMethod) Fig = plt.figure(figsize=(min(32, 16 + (nCol - 1) * 8), 8 * nRow)) AxesGrid = gridspec.GridSpec(nRow, nCol) for i, iMethod in enumerate(self.CorrMethod): iAvgName = iMethod + "均值" iAxes = plt.subplot(AxesGrid[i // nCol, i % nCol]) self._plotHeatMap(iAxes, self._Output[iAvgName], iAvgName) if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames) > 0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName == "风险表": if self.RiskTable is None: HTML += "<li>" + iArgName + ": None</li>" else: HTML += "<li>" + iArgName + ": " + self.RiskTable.Name + "</li>" elif iArgName != "计算时点": HTML += "<li>" + iArgName + ": " + str( self.Args[iArgName]) + "</li>" elif self.Args[iArgName]: HTML += "<li>" + iArgName + ": 自定义时点</li>" else: HTML += "<li>" + iArgName + ": 所有时点</li>" HTML += "</ul>" else: HTML = "" for i, iMethod in enumerate(self.CorrMethod): iAvgName = iMethod + "均值" iHTML = self._Output[iAvgName].style.background_gradient( cmap="Reds").set_precision(2).render() HTML += '<div align="left" style="font-size:1em"><strong>' + iAvgName + '</strong></div>' + iHTML return HTML
class ExecutionContext(Controller): ''' An execution context contains all the information necessary to start a job. For instance, in order to use FSL, it is necessary to setup a few environment variables whose content depends on the location where FSL is installed. The execution context contains the information about FSL installation necessary to define these environment variable when a job is started. The execution context is shared with every processing nodes and used to build the execution environment of each job. ''' python_path_first = ListStr() python_path_last = ListStr() environ = DictStrStr() #def __init__(self, python_path_first=[], python_path_last=[], #environ = {}): #self.python_path_first = python_path_first #self.python_path_last = python_path_last #self.environ = environ def to_json(self): ''' Returns a dictionary containing JSON compatible representation of the execution context. ''' kwargs = {} if self.python_path_first: kwargs['python_path_first'] = self.python_path_first if self.python_path_last: kwargs['python_path_last'] = self.python_path_last if self.environ: kwargs['environ'] = self.environ return ['capsul.engine.execution_context.from_json', kwargs] def __enter__(self): self._sys_path_first = [osp.expandvars(osp.expanduser(i)) for i in self.python_path_first] self._sys_path_last = [osp.expandvars(osp.expanduser(i)) for i in self.python_path_last] sys.path = self._sys_path_first + sys.path + self._sys_path_last self._environ_backup = {} for n, v in self.environ.items(): self._environ_backup[n] = os.environ.get(n) os.environ[n] = v # This code is specific to Nipype/SPM and should be # in a dedicated module. It is put here until # modularity is added to this method. if os.environ.get('SPM_STANDALONE'): nipype = True try: from nipype.interfaces import spm except ImportError: nipype = False if nipype: import glob spm_directory = os.environ.get('SPM_DIRECTORY', '') spm_exec_glob = osp.join(spm_directory, 'mcr', 'v*') spm_exec = glob.glob(spm_exec_glob) if spm_exec: spm_exec = spm_exec[0] spm.SPMCommand.set_mlab_paths( matlab_cmd=osp.join(spm_directory, 'run_spm%s.sh' % os.environ.get('SPM_VERSION','')) + ' ' + spm_exec + ' script', use_mcr=True) def __exit__(self, exc_type, exc_value, traceback): sys_path_error = False if self._sys_path_first: if sys.path[0:len(self._sys_path_first)] == self._sys_path_first: del sys.path[0:len(self._sys_path_first)] else: sys_path_error = True if self._sys_path_last: if sys.path[-len(self._sys_path_last):] == self._sys_path_last: del sys.path[0:len(self._sys_path_last)] else: sys_path_error = True del self._sys_path_first del self._sys_path_last for n, v in self._environ_backup.items(): if v is None: os.environ.pop(n, None) else: os.environ[n] = v del self._environ_backup if sys_path_error: raise ValueError('sys.path was modified and execution context modifications cannot be undone')
class TimeBarAccount(Account): """基于 Bar 数据的期货账户""" Delay = Bool(True, arg_type="Bool", label="交易延迟", order=2) TargetIDs = ListStr(arg_type="IDList", label="目标ID", order=3) BuyLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="买入限制", order=4) SellLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="卖出限制", order=5) MarketInfo = Instance(_MarketBarInfo, arg_type="ArgObject", label="行情因子", order=6) SecurityInfo = Instance(_SecurityInfo, arg_type="ArgObject", label="证券信息", order=7) def __init__(self, market_ft, security_ft, sys_args={}, config_file=None, **kwargs): # 继承自 Account 的属性 #self._Cash = None# 剩余现金, >=0, array(shape=(nDT+1,)) #self._FrozenCash = 0# 当前被冻结的现金, >=0, float #self._Debt = None# 负债, >=0, array(shape=(nDT+1,)) #self._CashRecord = None# 现金流记录, 现金流入为正, 现金流出为负, DataFrame(columns=["时间点", "现金流", "备注"]) #self._DebtRecord = None# 融资记录, 增加负债为正, 减少负债为负, DataFrame(columns=["时间点", "融资", "备注"]) #self._TradingRecord = None# 交易记录, DataFrame(columns=["时间点", "ID", "买卖数量", "价格", "交易费", "现金收支", "类型"]) self._IDs = []# 本账户支持交易的证券 ID, [] self._Position = None# 持仓数量, DataFrame(index=[时间点]+1, columns=self._IDs) self._PositionAmount = None# 持仓金额, 保证金 - 浮动盈亏, DataFrame(index=[时间点]+1, columns=self._IDs) self._Orders = None# 当前接收到的订单, DataFrame(columns=["ID", "数量", "目标价"]) self._OpenPosition = None# 当前未平仓的持仓信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) self._ClosedPosition = None# 已平仓的交易信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏"]) self._LastPrice = None# 最新价, Series(index=self._IDs) self._MarketFT = market_ft# 行情因子表对象 self._SecurityFT = security_ft# 证券信息因子表对象 super().__init__(sys_args=sys_args, config_file=config_file, **kwargs) self.Name = "FutureAccount" def __QS_initArgs__(self): super().__QS_initArgs__() self.MarketInfo = _MarketBarInfo(self._MarketFT) self.SecurityInfo = _SecurityInfo(self._SecurityFT) self.BuyLimit = _TradeLimit(direction="Buy") self.SellLimit = _TradeLimit(direction="Sell") def __QS_start__(self, mdl, dts, **kwargs): Rslt = super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._IDs = list(self.TargetIDs) if not self._IDs: self._IDs = list(self._MarketFT.getID(ifactor_name=self.MarketInfo.TradePrice)) nDT, nID = len(dts), len(self._IDs) #self._Cash = np.zeros(nDT+1) #self._Debt = np.zeros(nDT+1) self._Position = pd.DataFrame(np.zeros((nDT+1, nID)), index=[dts[0]-dt.timedelta(1)]+dts, columns=self._IDs) self._PositionAmount = self._Position.copy() self._Orders = pd.DataFrame(columns=["ID", "数量", "目标价"]) self._LastPrice = pd.Series(np.nan, index=self._IDs)# 最新价 self._OpenPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) self._ClosedPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏"]) self._iTradingRecord = []# 暂存的交易记录 self._nDT = nDT return Rslt + (self._MarketFT, self._SecurityFT) def __QS_move__(self, idt, **kwargs): super().__QS_move__(idt, **kwargs) self._LastPrice = self._MarketFT.readData(factor_names=[self.MarketInfo.Last], ids=self._IDs, dts=[idt]).iloc[0, 0] if self.Delay:# 撮合成交 iIndex = self._Model.DateTimeIndex self._Position.iloc[iIndex+1] = self._Position.iloc[iIndex] TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame(TradingRecord, index=np.arange(self._TradingRecord.shape[0], self._TradingRecord.shape[0]+len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) else: TradingRecord = self._iTradingRecord self._QS_updatePosition() return TradingRecord def __QS_after_move__(self, idt, **kwargs): super().__QS_after_move__(idt, **kwargs) iIndex = self._Model.DateTimeIndex if not self.Delay:# 撮合成交 TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame(TradingRecord, index=np.arange(self._TradingRecord.shape[0], self._TradingRecord.shape[0]+len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) self._iTradingRecord = TradingRecord self._QS_updatePosition() if iIndex<self._nDT-1: iNextDate = self._Model._QS_TestDateTimes[iIndex+1].date() if iNextDate!=idt.date():# 当前是该交易日的最后一个时点 self._handleRollover(idt)# 处理展期 self._handleSettlement(idt)# 处理结算 return 0 def __QS_end__(self): super().__QS_end__() self._Output["持仓"] = self.getPositionNumSeries() self._Output["持仓金额"] = self.getPositionAmountSeries() self._Output["平仓记录"] = self._ClosedPosition self._Output["未平仓持仓"] = self._OpenPosition return 0 # 处理结算, 对于不满足保证金要求的仓位进行强平 def _handleSettlement(self, idt): if self._OpenPosition.shape[0]==0: return 0 idt = dt.datetime.combine(idt.date(), dt.time(0)) SettlementPrice = self._SecurityFT.readData(factor_names=[self.SecurityInfo.SettlementPrice], dts=[idt], ids=self._IDs).iloc[0,0,:][self._OpenPosition["ID"].tolist()] self._OpenPosition["浮动盈亏"] = self._OpenPosition["数量"] * (SettlementPrice.values - self._OpenPosition["开仓价格"]) * self.SecurityInfo.Multiplier MaintenanceMargin = self._OpenPosition["数量"].abs() * self.SecurityInfo.Multiplier * SettlementPrice.values * self.SecurityInfo.MaintenanceMarginRate# 维持保证金 iGap = (MaintenanceMargin - self._OpenPosition["保证金"] - self._OpenPosition["浮动盈亏"]).clip_lower(0)# 当前保证金和维持保证金之间的缺口 iTotalGap = np.nansum(iGap) if iTotalGap<=0: return 0 AvailableCash = self.AvailableCash iIndex = self._Model.DateTimeIndex if AvailableCash>=iTotalGap: self._updateAccount(-iTotalGap) self._OpenPosition["保证金"] += iGap iPositionAmount = pd.Series((self._OpenPosition["浮动盈亏"]+self._OpenPosition["保证金"]).values, index=self._OpenPosition["ID"].values).groupby(axis=0, level=0).sum() self._PositionAmount.iloc[iIndex+1][iPositionAmount.index] = iPositionAmount return 0 self._OpenPosition["保证金"] += (iGap / iTotalGap) * AvailableCash# 将账户中的现金按照缺口金额大小分配给各个 ID iAllowedPosition = self._OpenPosition["保证金"] / self.SecurityInfo.Multiplier / SettlementPrice.values / self.MaintenanceMarginRate# 计算当前保证金水平下允许的持仓数量 # 对于保证金不足的仓位进行平仓 iNum = (self._OpenPosition["数量"].abs() - iAllowedPosition).clip_lower(0) iOrders = pd.Series(-iNum.values*np.sign(self._OpenPosition["数量"].values), index=self._OpenPosition["ID"].values).groupby(axis=0, level=0).sum() iOrders = iOrders[iOrders!=0] iBookOrders = self._Orders.groupby(by=["ID"])["数量"].sum() if iBookOrders.shape[0]>0: iBookOrders = iBookOrders.loc[iOrders.index].fillna(0) else: iBookOrders = pd.Series(0, index=iOrders.index) iMask = (iOrders>0) iOrders[iMask] = (iOrders[iMask] - iBookOrders[iMask]).clip_lower(0) iMask = (iOrders<0) iOrders[iMask] = (iOrders[iMask] - iBookOrders[iMask]).clip_upper(0) iOrders = iOrders[iOrders!=0] if iOrders.shape[0]==0: return 0 iOrders = pd.DataFrame(iOrders, columns=["数量"]).reset_index() iOrders["目标价"] = np.nan self.order(combined_order=iOrders) return 0 # 处理展期 def _handleRollover(self, idt): if self._OpenPosition.shape[0]==0: return 0 iIndex = self._Model.DateTimeIndex idt = dt.datetime.combine(idt.date(), dt.time(0)) iNextDT = dt.datetime.combine(self._Model._QS_TestDateTimes[iIndex+1].date(), dt.time(0)) IDMapping = self._SecurityFT.readData(factor_names=[self.SecurityInfo.ContractMapping], ids=self._IDs, dts=[idt, iNextDT]).iloc[0] SettlementPrice = self._SecurityFT.readData(factor_names=[self.SecurityInfo.SettlementPrice], dts=[idt], ids=self._IDs).iloc[0,0,:] isRollover = False for iID in self._OpenPosition["ID"].unique(): iCurID, iNextID = IDMapping[iID].iloc[0], IDMapping[iID].iloc[1] if iCurID==iNextID: continue isRollover = True iNextIDPrice = SettlementPrice[IDMapping.iloc[0]==iNextID].iloc[0] iCurIDPrice = SettlementPrice[iID] iMask = (self._OpenPosition["ID"]==iID) iNewNum = self._OpenPosition["数量"][iMask] * iCurIDPrice / iNextIDPrice self._OpenPosition["开仓价格"][iMask] = self._OpenPosition["数量"][iMask] * self._OpenPosition["开仓价格"][iMask] / iNewNum self._OpenPosition["数量"][iMask] = iNewNum if isRollover: self._updateAccount(cash_changed=0.0) return 0 # 当前账户价值 @property def AccountValue(self): return super().AccountValue + np.nansum(self._PositionAmount.iloc[self._Model.DateTimeIndex+1]) # 当前账户的持仓数量 @property def Position(self): return self._Position.iloc[self._Model.DateTimeIndex+1] # 当前账户的持仓金额, 即保证金 @property def PositionAmount(self): self.PositionAmount.iloc[self._Model.DateTimeIndex+1] # 本账户支持交易的证券 ID @property def IDs(self): return self._IDs # 当前账户中还未成交的订单, DataFrame(index=[int], columns=["ID", "数量", "目标价"]) @property def Orders(self): return self._Orders # 当前最新价 @property def LastPrice(self): return self._LastPrice # 获取持仓的历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓, index=[时间点], columns=[ID]) def getPositionNumSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._Position.iloc[1:self._Model.DateTimeIndex+2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取持仓证券的金额历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓金额, index=[时间点], columns=[ID]) def getPositionAmountSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionAmount[1:self._Model.DateTimeIndex+2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取账户价值的历史序列, 以时间点为索引 def getAccountValueSeries(self, dts=None, start_dt=None, end_dt=None): CashSeries = self.getCashSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) PositionAmountSeries = self.getPositionAmountSeries(dts=dts, start_dt=start_dt, end_dt=end_dt).sum(axis=1) DebtSeries = self.getDebtSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) return CashSeries + PositionAmountSeries - DebtSeries # 执行给定数量的证券委托单, target_id: 目标证券 ID, num: 待买卖的数量, target_price: nan 表示市价单, # combined_order: 组合订单, DataFrame(index=[ID],columns=[数量, 目标价]) # 基本的下单函数, 必须实现, 目前只实现了市价单 def order(self, target_id=None, num=0, target_price=np.nan, combined_order=None): if target_id is not None: #if (num>0) and (self.BuyLimit.MinUnit>0): #num = np.fix(num / self.BuyLimit.MinUnit) * self.BuyLimit.MinUnit #elif (num<0) and (self.SellLimit.MinUnit>0): #num = np.fix(num / self.SellLimit.MinUnit) * self.SellLimit.MinUnit self._Orders.loc[self._Orders.shape[0]] = (target_id, num, target_price) return (target_id, num, target_price) if combined_order is not None: #if self.BuyLimit.MinUnit>0: #Mask = (combined_order["数量"]>0) #combined_order["数量"][Mask] = np.fix(combined_order["数量"][Mask] / self.BuyLimit.MinUnit) * self.BuyLimit.MinUnit #if self.SellLimit.MinUnit>0: #Mask = (combined_order["数量"]<0) #combined_order["数量"][Mask] = np.fix(combined_order["数量"][Mask] / self.SellLimit.MinUnit) * self.SellLimit.MinUnit combined_order.index = np.arange(self._Orders.shape[0], self._Orders.shape[0]+combined_order.shape[0]) self._Orders = self._Orders.append(combined_order) return combined_order # 撤销订单, order_ids 是订单在 self.Orders 中的 index def cancelOrder(self, order_ids): self._Orders = self._Orders.loc[self._Orders.index.difference(set(order_ids))] self._Orders.sort_index(axis=0, inplace=True) self._Orders.index = np.arange(self._Orders.shape[0]) return 0 # 更新仓位信息 def _QS_updatePosition(self): iIndex = self._Model.DateTimeIndex iNum = self._OpenPosition.groupby(by=["ID"])["数量"].sum() iPosition = pd.Series(0, index=self._IDs) iPosition[iNum.index] += iNum self._Position.iloc[iIndex+1] = iPosition.values self._OpenPosition["浮动盈亏"] = self._OpenPosition["数量"] * (self._LastPrice[self._OpenPosition["ID"].tolist()].values - self._OpenPosition["开仓价格"]) * self.SecurityInfo.Multiplier iPositionAmount = pd.Series((self._OpenPosition["浮动盈亏"]+self._OpenPosition["保证金"]).values, index=self._OpenPosition["ID"].values).groupby(axis=0, level=0).sum() self._PositionAmount.iloc[iIndex+1] = 0.0 self._PositionAmount.iloc[iIndex+1][iPositionAmount.index] = iPositionAmount return 0 # 撮合成交订单 def _matchOrder(self, idt): MarketOrderMask = pd.isnull(self._Orders["目标价"]) MarketOrders = self._Orders[MarketOrderMask] LimitOrders = self._Orders[~MarketOrderMask] MarketOrders = MarketOrders.groupby(by=["ID"]).sum()["数量"]# Series(数量, index=[ID]) if LimitOrders.shape[0]>0: LimitOrders = LimitOrders.groupby(by=["ID", "目标价"]).sum() LimitOrders.sort_index(ascending=False) LimitOrders = LimitOrders.reset_index(level=1) else: LimitOrders = pd.DataFrame(columns=["ID", "数量", "目标价"]) # 先执行平仓交易 TradingRecord, MarketOpenOrders = self._matchMarketCloseOrder(idt, MarketOrders) iTradingRecord, LimitOrders = self._matchLimitCloseOrder(idt, LimitOrders) TradingRecord.extend(iTradingRecord) # 再执行开仓交易 TradingRecord.extend(self._matchMarketOpenOrder(idt, MarketOpenOrders)) iTradingRecord, LimitOrders = self._matchLimitOpenOrder(idt, LimitOrders) TradingRecord.extend(iTradingRecord) self._Orders = LimitOrders self._Orders.index = np.arange(self._Orders.shape[0]) return TradingRecord # 撮合成交市价平仓单 # 以成交价完成成交, 成交量满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketCloseOrder(self, idt, orders): orders = orders[orders!=0] if orders.shape[0]==0: return ([], pd.Series()) IDs = orders.index.tolist() TradePrice = self._MarketFT.readData(dts=[idt], ids=IDs, factor_names=[self.MarketInfo.TradePrice]).iloc[0, 0]# 成交价 # 过滤限制条件 orders[pd.isnull(TradePrice)] = 0.0# 成交价缺失的不能交易 #if self.SellLimit.LimitIDFilter:# 满足卖出禁止条件的不能卖出 #Mask = (self._MarketFT.getIDMask(idt, ids=IDs, id_filter_str=self.SellLimit.LimitIDFilter) & (orders<0)) #orders[Mask] = orders[Mask].clip_lower(0) #if self.BuyLimit.LimitIDFilter:# 满足买入禁止条件的不能买入 #Mask = (self._MarketFT.getIDMask(idt, ids=IDs, id_filter_str=self.BuyLimit.LimitIDFilter) & (orders>0)) #orders[Mask] = orders[Mask].clip_upper(0) #if self.MarketInfo.Vol:# 成交量限制 #VolLimit = self._MarketFT.readData(factor_names=[self.MarketInfo.Vol], ids=IDs, dts=[idt]).iloc[0, 0] #orders = np.maximum(orders, -VolLimit * self.SellLimit.MarketOrderVolumeLimit) #orders = np.minimum(orders, VolLimit * self.BuyLimit.MarketOrderVolumeLimit) #if self.SellLimit.MinUnit!=0.0:# 最小卖出交易单位限制 #Mask = (orders<0) #orders[Mask] = (orders[Mask] / self.SellLimit.MinUnit).astype("int") * self.SellLimit.MinUnit #if self.BuyLimit.MinUnit!=0.0:# 最小买入交易单位限制 #Mask = (orders>0) #orders[Mask] = (orders[Mask] / self.BuyLimit.MinUnit).astype("int") * self.BuyLimit.MinUnit # 分离平仓单和开仓单 Position = self.Position[IDs] CloseOrders = orders.clip(lower=(-Position).clip_upper(0), upper=(-Position).clip_lower(0))# 平仓单 OpenOrders = orders - CloseOrders# 开仓单 CloseOrders = CloseOrders[CloseOrders!=0] if CloseOrders.shape[0]==0: return ([], OpenOrders) # 处理平仓单 TradePrice = TradePrice[CloseOrders.index] TradeAmounts = CloseOrders * TradePrice * self.SecurityInfo.Multiplier Fees = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee iOpenPosition = self._OpenPosition.set_index(["ID"]) iClosedPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏"]) iNewPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) CashChanged = np.zeros(CloseOrders.shape[0]) for j, jID in enumerate(CloseOrders.index): jNum = CloseOrders.iloc[j] ijPosition = iOpenPosition.loc[[jID]] if jNum<0: ijClosedNum = (ijPosition["数量"] - (ijPosition["数量"].cumsum()+jNum).clip_lower(0)).clip_lower(0) else: ijClosedNum = (ijPosition["数量"] - (ijPosition["数量"].cumsum()+jNum).clip_upper(0)).clip_upper(0) ijClosedPosition = ijPosition.copy() ijClosedPosition["数量"] = ijClosedNum ijClosedPosition["平仓时点"] = idt ijClosedPosition["平仓价格"] = TradePrice[jID] ijClosedPosition["平仓交易费"] = Fees[jID] ijClosedPosition["平仓盈亏"] = ijClosedNum * (TradePrice[jID] - ijPosition["开仓价格"]) * self.SecurityInfo.Multiplier ijClosedPosition["保证金"] = ijClosedPosition["保证金"] * (ijClosedPosition["数量"] / ijPosition["数量"]) ijPosition["保证金"] = ijPosition["保证金"] - ijClosedPosition["保证金"] ijPosition["数量"] -= ijClosedNum CashChanged[j] = ijClosedPosition["平仓盈亏"].sum() - Fees[jID] + ijClosedPosition["保证金"].sum() ijClosedPosition[ijClosedPosition["数量"]!=0] ijClosedPosition.pop("浮动盈亏") iClosedPosition = iClosedPosition.append(ijClosedPosition.reset_index()) iNewPosition = iNewPosition.append(ijPosition[ijPosition["数量"]!=0].reset_index()) iClosedPosition.index = np.arange(self._ClosedPosition.shape[0], self._ClosedPosition.shape[0]+iClosedPosition.shape[0]) self._ClosedPosition = self._ClosedPosition.append(iClosedPosition) iOpenPosition = iOpenPosition.loc[iOpenPosition.index.difference(CloseOrders.index)].reset_index() iNewPosition.index = np.arange(iOpenPosition.shape[0], iOpenPosition.shape[0]+iNewPosition.shape[0]) self._OpenPosition = iOpenPosition.append(iNewPosition) TradingRecord = list(zip([idt]*CloseOrders.shape[0], CloseOrders.index, CloseOrders, TradePrice, Fees, CashChanged, ["close"]*CloseOrders.shape[0])) self._QS_updateCashDebt(CashChanged.sum()) return (TradingRecord, OpenOrders) # 撮合成交市价开仓单 # 以成交价完成成交, 成交量满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketOpenOrder(self, idt, orders): orders = orders[orders!=0] if orders.shape[0]==0: return [] IDs = orders.index.tolist() TradePrice = self._MarketFT.readData(dts=[idt], ids=IDs, factor_names=[self.MarketInfo.TradePrice]).iloc[0, 0]# 成交价 # 处理开仓单 TradeAmounts = orders * TradePrice * self.SecurityInfo.Multiplier MarginAcquired = TradeAmounts.abs() * self.SecurityInfo.InitMarginRate FeesAcquired = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee CashAcquired = MarginAcquired + FeesAcquired CashAllocated = min(CashAcquired.sum(), self.AvailableCash) * CashAcquired / CashAcquired.sum() orders = CashAllocated / TradePrice / self.SecurityInfo.Multiplier / (self.SecurityInfo.InitMarginRate + self.BuyLimit.TradeFee * (orders>0) + self.SellLimit.TradeFee * (orders<0)) * np.sign(orders) #if self.SellLimit.MinUnit!=0.0:# 最小卖出交易单位限制 #Mask = (orders<0) #orders[Mask] = (orders[Mask] / self.SellLimit.MinUnit).astype("int") * self.SellLimit.MinUnit #if self.BuyLimit.MinUnit!=0.0:# 最小买入交易单位限制 #Mask = (orders>0) #orders[Mask] = (orders[Mask] / self.BuyLimit.MinUnit).astype("int") * self.BuyLimit.MinUnit orders = orders[pd.notnull(orders) & (orders!=0)] if orders.shape[0]==0: return [] TradePrice = TradePrice[orders.index] TradeAmounts = orders * TradePrice * self.SecurityInfo.Multiplier Fees = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee MarginAcquired = TradeAmounts.abs() * self.SecurityInfo.InitMarginRate CashChanged = - (MarginAcquired + Fees).sum() TradingRecord = list(zip([idt]*orders.shape[0], orders.index, orders, TradePrice, Fees, -(MarginAcquired+Fees), ["open"]*orders.shape[0])) iNewPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) iNewPosition["ID"] = orders.index iNewPosition["数量"] = orders.values iNewPosition["开仓时点"] = idt iNewPosition["开仓价格"] = TradePrice.values iNewPosition["开仓交易费"] = Fees.values iNewPosition["保证金"] = MarginAcquired.values iNewPosition["浮动盈亏"] = 0.0 iNewPosition.index = np.arange(self._OpenPosition.shape[0], self._OpenPosition.shape[0]+iNewPosition.shape[0]) self._OpenPosition = self._OpenPosition.append(iNewPosition) self._QS_updateCashDebt(CashChanged)# 更新账户信息 return TradingRecord # 撮合成交卖出限价单 # 如果最高价和最低价未指定, 则检验成交价是否优于目标价, 是就以目标价完成成交, 且成交量满足卖出限制中的限价单成交量限比, 否则无法成交 # 如果指定了最高价和最低价, 则假设成交价服从[最低价, 最高价]中的均匀分布, 据此确定可完成成交的数量 # 未成交的限价单继续保留, TODO def _matchLimitCloseOrder(self, idt, sell_orders): return ([], pd.DataFrame(columns=["ID", "数量", "目标价"])) # 撮合成交买入限价单 # 如果最高价和最低价未指定, 则检验成交价是否优于目标价, 是就以目标价完成成交, 且成交量满足买入限制要求 # 如果指定了最高价和最低价, 则假设成交价服从[最低价, 最高价]中的均匀分布, 据此确定可完成成交的数量, 同时满足买入限制要求 # 未成交的限价单继续保留, TODO def _matchLimitOpenOrder(self, idt, buy_orders): return ([], pd.DataFrame(columns=["ID", "数量", "目标价"]))
class TimeBarAccount(Account): """基于 Bar 数据的债券账户""" Delay = Bool(True, arg_type="Bool", label="交易延迟", order=2) TargetIDs = ListStr(arg_type="IDList", label="目标ID", order=3) BuyLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="买入限制", order=4) SellLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="卖出限制", order=5) MarketInfo = Instance(_BarFactorMap, arg_type="ArgObject", label="行情信息", order=6) PaymentInfo = Instance(_SecurityInfo, arg_type="ArgObject", label="支付信息", order=7) def __init__(self, market_ft, payment_ft, sys_args={}, config_file=None, **kwargs): # 继承自 Account 的属性 #self._Cash = None# 剩余现金, >=0, array(shape=(nDT+1,)) #self._Debt = None# 负债, >=0, array(shape=(nDT+1,)) #self._CashRecord = None# 现金流记录, 现金流入为正, 现金流出为负, DataFrame(columns=["时间点", "现金流", "备注"]) #self._DebtRecord = None# 融资记录, 增加负债为正, 减少负债为负, DataFrame(columns=["时间点", "融资", "备注"]) #self._TradingRecord = None# 交易记录, DataFrame(columns=["时间点", "ID", "买卖数量", "价格", "交易费", "现金收支", "类型"]) self._IDs = []# 本账户支持交易的证券 ID, [] self._Position = None# 持仓数量, DataFrame(index=[时间点]+1, columns=self._IDs) self._PositionAmount = None# 持仓金额, DataFrame(index=[时间点]+1, columns=self._IDs) self._Orders = None# 当前接收到的订单, DataFrame(columns=["ID", "数量", "目标价"]) self._OpenPosition = None# 当前未平仓的持仓信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "开仓应计利息", "持有应计利息", "浮动盈亏"]) self._ClosedPosition = None# 已平仓的交易信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "开仓应计利息", "平仓时点", "平仓价格", "平仓交易费", "平仓应计利息", "持有应计利息", "平仓盈亏"]) self._LastPrice = None# 最新价, Series(index=self._IDs) self._AI = None# 应计利息, Series(index=self._IDs) self._MarketFT = market_ft# 行情信息因子表对象 self._PaymentFT = payment_ft# 支付信息因子表对象 self._UnExInterest = None# 待除权利息, DataFrame(index=[ID], columns=[除息日]) self._UnpaidInterest = None# 待支付利息, DataFrame(index=[ID], columns=[付息日]) super().__init__(sys_args=sys_args, config_file=config_file, **kwargs) self.Name = "BondAccount" def __QS_initArgs__(self): super().__QS_initArgs__() self.MarketInfo = _BarFactorMap(self._MarketFT) self.PaymentInfo = _Payment(self._PaymentFT) self.BuyLimit = _TradeLimit(direction="Buy") self.SellLimit = _TradeLimit(direction="Sell") def __QS_start__(self, mdl, dts, **kwargs): Rslt = super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._IDs = list(self.TargetIDs) if not self._IDs: self._IDs = list(self._MarketFT.getID(ifactor_name=self.MarketInfo.TradePrice)) nDT, nID = len(dts), len(self._IDs) #self._Cash = np.zeros(nDT+1) #self._Debt = np.zeros(nDT+1) self._Position = pd.DataFrame(np.zeros((nDT+1, nID)), index=[dts[0]-dt.timedelta(1)]+dts, columns=self._IDs) self._PositionAmount = self._Position.copy() self._Orders = pd.DataFrame(columns=["ID", "数量", "目标价"]) self._LastPrice = pd.Series(np.nan, index=self._IDs) self._AI = pd.Series(np.nan, index=self._IDs) self._OpenPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "开仓支付利息", "持有所得利息", "持有应计利息", "浮动盈亏"]) self._ClosedPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "开仓支付利息", "平仓时点", "平仓价格", "平仓交易费", "平仓收入利息", "持有所得利息", "平仓盈亏"]) self._UnpaidInterest = pd.DataFrame(index=self._IDs) self._iTradingRecord = []# 暂存的交易记录 self._nDT = nDT return Rslt + (self._MarketFT, self._SecurityFT) def __QS_move__(self, idt, **kwargs): super().__QS_move__(idt, **kwargs) # 更新当前的账户信息 iIndex = self._Model.DateTimeIndex self._LastPrice = self._MarketFT.readData(factor_names=[self.MarketInfo.Last, self.MarketInfo.AI], ids=self._IDs, dts=[idt]).iloc[:, 0] self._AI, self._LastPrice = self._LastPrice.iloc[:, 1], self._LastPrice.iloc[:, 0] if iIndex>0: iPreDate, iDate = self._Model._QS_TestDateTimes[iIndex-1].date(), idt.date() if iPreDate!=iDate:# 进入新的一天, 更新应计利息, 接受支付 self._handlePayment(iDate) self._OpenPosition["持有应计利息"] = self._OpenPosition["数量"] * self._AI[self._OpenPosition["ID"].tolist()].values if self.Delay:# 撮合成交 self._Position.iloc[iIndex+1] = self._Position.iloc[iIndex]# 初始化持仓 TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame(TradingRecord, index=np.arange(self._TradingRecord.shape[0], self._TradingRecord.shape[0]+len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) else: TradingRecord = self._iTradingRecord self._QS_updatePosition() return TradingRecord def __QS_after_move__(self, idt, **kwargs): super().__QS_after_move__(self, idt, **kwargs) iIndex = self._Model.DateTimeIndex if not self.Delay:# 撮合成交 TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame(TradingRecord, index=np.arange(self._TradingRecord.shape[0], self._TradingRecord.shape[0]+len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) self._iTradingRecord = TradingRecord self._QS_updatePosition() if iIndex<self._nDT-1: iNextDate, iDate = self._Model._QS_TestDateTimes[iIndex+1].date(), idt.date() if iNextDate!=iDate:# 当前是该交易日的最后一个时点 self._addPaymentInfo(iDate)# 添加新的支付信息 return 0 def __QS_end__(self): super().__QS_end__() self._Output["持仓"] = self.getPositionNumSeries() self._Output["持仓金额"] = self.getPositionAmountSeries() self._Output["平仓记录"] = self._ClosedPosition self._Output["未平仓持仓"] = self._OpenPosition return 0 # 添加新的支付信息 def _addPaymentInfo(self, idate): if self._OpenPosition.shape[0]==0: return 0 iDT = dt.datetime.combine(idate, dt.time(0)) Interest = self._PaymentFT.readData(factor_names=[self.PaymentInfo.Interest, self.PaymentInfo.Principal], ids=self._OpenPosition["ID"].unique().tolist(), dts=[iDT]).iloc[:, 0, :].sum(axis=1) IDs = Interest[Interest>0].index.tolist() if not IDs: return 0 iPosition = self.Position DateInfo = self._PaymentFT.readData(factor_names=[self.PaymentInfo.ExDate, self.PaymentInfo.PayDate], ids=IDs, dts=[iDT]).iloc[:, 0, :] for i, iID in IDs: iExDate, iPayDate = DateInfo.iloc[i, 0], DateInfo.iloc[i, 1] if pd.notnull(iExDate) and pd.notnull(iPayDate): iExDate, iPayDate = dt.date(int(iExDate[:4]), int(iExDate[4:6]), int(iExDate[6:8])), dt.date(int(iPayDate[:4]), int(iPayDate[4:6]), int(iPayDate[6:8])) iInterest = Interest[iID] * iPosition[iID] self._UnExInterest[iPayDate] = self._UnExInterest.get(iPayDate, 0) self._UnExInterest.loc[iID, iPayDate] += iInterest self._UnpaidInterest[iPayDate] = self._UnpaidInterest.get(iPayDate, 0) self._UnpaidInterest.loc[iID, iPayDate] += iInterest return 0 # 处理付息 def _handlePayment(self, idate): if idate in self._UnExInterest: iInterest = self._UnExInterest.pop(idate) iInterest = iInterest[iInterest>0] for i, iID in enumerate(iInterest.index): self.addCash(iInterest.iloc[i], "%s: 除息" % iID) self._FrozenCash += iInterest.iloc[i] if idate in self._UnpaidInterest: iInterest = self._UnpaidInterest.pop(idate) iInterest = iInterest[iInterest>0] for i, iID in enumerate(iInterest.index): self._FrozenCash -= iInterest.iloc[i] return 0 # 当前账户价值 @property def AccountValue(self): return super().AccountValue + np.nansum(self._PositionAmount.iloc[self._Model.DateTimeIndex+1]) # 当前账户的持仓数量 @property def Position(self): return self._Position.iloc[self._Model.DateTimeIndex+1] # 当前账户的持仓金额, 即保证金 @property def PositionAmount(self): self.PositionAmount.iloc[self._Model.DateTimeIndex+1] # 本账户支持交易的证券 ID @property def IDs(self): return self._IDs # 当前账户中还未成交的订单, DataFrame(index=[int], columns=["ID", "数量", "目标价"]) @property def Orders(self): return self._Orders # 当前最新价 @property def LastPrice(self): return self._LastPrice # 获取持仓的历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓, index=[时间点], columns=[ID]) def getPositionNumSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._Position.iloc[1:self._Model.DateTimeIndex+2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取持仓证券的金额历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓金额, index=[时间点], columns=[ID]) def getPositionAmountSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionAmount[1:self._Model.DateTimeIndex+2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取账户价值的历史序列, 以时间点为索引 def getAccountValueSeries(self, dts=None, start_dt=None, end_dt=None): CashSeries = self.getCashSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) PositionAmountSeries = self.getPositionAmountSeries(dts=dts, start_dt=start_dt, end_dt=end_dt).sum(axis=1) DebtSeries = self.getDebtSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) return CashSeries + PositionAmountSeries - DebtSeries # 执行给定数量的证券委托单, target_id: 目标证券 ID, num: 待买卖的数量, target_price: nan 表示市价单, # combined_order: 组合订单, DataFrame(index=[ID],columns=[数量, 目标价]) # 基本的下单函数, 必须实现, 目前只实现了市价单 def order(self, target_id=None, num=0, target_price=np.nan, combined_order=None): if target_id is not None: #if (num>0) and (self.BuyLimit.MinUnit>0): #num = np.fix(num / self.BuyLimit.MinUnit) * self.BuyLimit.MinUnit #elif (num<0) and (self.SellLimit.MinUnit>0): #num = np.fix(num / self.SellLimit.MinUnit) * self.SellLimit.MinUnit self._Orders.loc[self._Orders.shape[0]] = (target_id, num, target_price) return (target_id, num, target_price) if combined_order is not None: #if self.BuyLimit.MinUnit>0: #Mask = (combined_order["数量"]>0) #combined_order["数量"][Mask] = np.fix(combined_order["数量"][Mask] / self.BuyLimit.MinUnit) * self.BuyLimit.MinUnit #if self.SellLimit.MinUnit>0: #Mask = (combined_order["数量"]<0) #combined_order["数量"][Mask] = np.fix(combined_order["数量"][Mask] / self.SellLimit.MinUnit) * self.SellLimit.MinUnit combined_order.index = np.arange(self._Orders.shape[0], self._Orders.shape[0]+combined_order.shape[0]) self._Orders = self._Orders.append(combined_order) return combined_order # 撤销订单, order_ids 是订单在 self.Orders 中的 index def cancelOrder(self, order_ids): self._Orders = self._Orders.loc[self._Orders.index.difference(set(order_ids))] self._Orders.sort_index(axis=0, inplace=True) self._Orders.index = np.arange(self._Orders.shape[0]) return 0 # 更新仓位信息 def _QS_updatePosition(self): iIndex = self._Model.DateTimeIndex iNum = self._OpenPosition.groupby(by=["ID"])["数量"].sum() iPosition = pd.Series(0, index=self._IDs) iPosition[iNum.index] += iNum self._Position.iloc[iIndex+1] = iPosition.values LastPrice = self._LastPrice[self._OpenPosition["ID"].tolist()].values self._OpenPosition["浮动盈亏"] = self._OpenPosition["数量"] * (LastPrice - self._OpenPosition["开仓价格"]) + self._OpenPosition["持有应计利息"] - self._OpenPosition["开仓支付利息"] + self._OpenPosition["持有所得利息"] iPositionAmount = pd.Series(self._OpenPosition["数量"].values * LastPrice + self._OpenPosition["持有应计利息"].values, index=self._OpenPosition["ID"].values).groupby(axis=0, level=0).sum() self._PositionAmount.iloc[iIndex+1] = 0.0 self._PositionAmount.iloc[iIndex+1][iPositionAmount.index] = iPositionAmount return 0 # 撮合成交订单 def _matchOrder(self, idt): MarketOrderMask = pd.isnull(self._Orders["目标价"]) MarketOrders = self._Orders[MarketOrderMask] LimitOrders = self._Orders[~MarketOrderMask] MarketOrders = MarketOrders.groupby(by=["ID"]).sum()["数量"]# Series(数量, index=[ID]) if LimitOrders.shape[0]>0: LimitOrders = LimitOrders.groupby(by=["ID", "目标价"]).sum() LimitOrders.sort_index(ascending=False) LimitOrders = LimitOrders.reset_index(level=1) else: LimitOrders = pd.DataFrame(columns=["ID", "数量", "目标价"]) # 先执行平仓交易 TradingRecord, MarketOpenOrders = self._matchMarketCloseOrder(idt, MarketOrders) iTradingRecord, LimitOrders = self._matchLimitCloseOrder(idt, LimitOrders) TradingRecord.extend(iTradingRecord) # 再执行开仓交易 TradingRecord.extend(self._matchMarketOpenOrder(idt, MarketOpenOrders)) iTradingRecord, LimitOrders = self._matchLimitOpenOrder(idt, LimitOrders) TradingRecord.extend(iTradingRecord) self._Orders = LimitOrders self._Orders.index = np.arange(self._Orders.shape[0]) return TradingRecord # 撮合成交市价平仓单 # 以成交价完成成交, 成交量满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketCloseOrder(self, idt, orders): orders = orders[orders!=0] if orders.shape[0]==0: return ([], pd.Series()) IDs = orders.index.tolist() TradePrice = self._MarketFT.readData(dts=[idt], ids=IDs, factor_names=[self.MarketInfo.TradePrice]).iloc[0, 0]# 成交价 # 过滤限制条件 orders[pd.isnull(TradePrice)] = 0.0# 成交价缺失的不能交易 #if self.SellLimit.LimitIDFilter:# 满足卖出禁止条件的不能卖出 #Mask = (self._MarketFT.getIDMask(idt, ids=IDs, id_filter_str=self.SellLimit.LimitIDFilter) & (orders<0)) #orders[Mask] = orders[Mask].clip_lower(0) #if self.BuyLimit.LimitIDFilter:# 满足买入禁止条件的不能买入 #Mask = (self._MarketFT.getIDMask(idt, ids=IDs, id_filter_str=self.BuyLimit.LimitIDFilter) & (orders>0)) #orders[Mask] = orders[Mask].clip_upper(0) #if self.MarketInfo.Vol:# 成交量限制 #VolLimit = self._MarketFT.readData(factor_names=[self.MarketInfo.Vol], ids=IDs, dts=[idt]).iloc[0, 0] #orders = np.maximum(orders, -VolLimit * self.SellLimit.MarketOrderVolumeLimit) #orders = np.minimum(orders, VolLimit * self.BuyLimit.MarketOrderVolumeLimit) #if self.SellLimit.MinUnit!=0.0:# 最小卖出交易单位限制 #Mask = (orders<0) #orders[Mask] = (orders[Mask] / self.SellLimit.MinUnit).astype("int") * self.SellLimit.MinUnit #if self.BuyLimit.MinUnit!=0.0:# 最小买入交易单位限制 #Mask = (orders>0) #orders[Mask] = (orders[Mask] / self.BuyLimit.MinUnit).astype("int") * self.BuyLimit.MinUnit # 分离平仓单和开仓单 Position = self.Position[IDs] CloseOrders = orders.clip(lower=(-Position).clip_upper(0), upper=(-Position).clip_lower(0))# 平仓单 OpenOrders = orders - CloseOrders# 开仓单 CloseOrders = CloseOrders[CloseOrders!=0] if CloseOrders.shape[0]==0: return ([], OpenOrders) # 处理平仓单 TradePrice = TradePrice[CloseOrders.index] TradeAmounts = CloseOrders * TradePrice * self.SecurityInfo.Multiplier Fees = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee iOpenPosition = self._OpenPosition.set_index(["ID"]) iClosedPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏"]) iNewPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) CashChanged = np.zeros(CloseOrders.shape[0]) for j, jID in enumerate(CloseOrders.index): jNum = CloseOrders.iloc[j] ijPosition = iOpenPosition.loc[[jID]] if jNum<0: ijClosedNum = (ijPosition["数量"] - (ijPosition["数量"].cumsum()+jNum).clip_lower(0)).clip_lower(0) else: ijClosedNum = (ijPosition["数量"] - (ijPosition["数量"].cumsum()+jNum).clip_upper(0)).clip_upper(0) CashChanged[j] = ijClosedNum * (TradePrice[jID] - ijPosition["开仓价格"]) + ijPosition["保证金"] - Fees[jID] ijClosedPosition = ijPosition.copy() ijClosedPosition["数量"] = ijClosedNum ijClosedPosition["平仓时点"] = idt ijClosedPosition["平仓价格"] = TradePrice[jID] ijClosedPosition["平仓交易费"] = Fees[jID] ijClosedPosition["平仓盈亏"] = ijClosedNum * (TradePrice[jID] - ijPosition["开仓价格"]) * self.SecurityInfo.Multiplier ijClosedPosition["保证金"] = ijClosedPosition["保证金"] * (ijClosedPosition["数量"] / ijPosition["数量"]) ijPosition["保证金"] = ijPosition["保证金"] - ijClosedPosition["保证金"] ijPosition["数量"] -= ijClosedNum CashChanged[j] = ijClosedPosition["平仓盈亏"].sum() - Fees[jID] + ijClosedPosition["保证金"].sum() ijClosedPosition[ijClosedPosition["数量"]!=0] ijClosedPosition.pop("浮动盈亏") iClosedPosition = iClosedPosition.append(ijClosedPosition.reset_index()) iNewPosition = iNewPosition.append(ijPosition[ijPosition["数量"]!=0].reset_index()) iClosedPosition.index = np.arange(self._ClosedPosition.shape[0], self._ClosedPosition.shape[0]+iClosedPosition.shape[0]) self._ClosedPosition = self._ClosedPosition.append(iClosedPosition) iOpenPosition = iOpenPosition.loc[iOpenPosition.index.difference(CloseOrders.index)].reset_index() iNewPosition.index = np.arange(iOpenPosition.shape[0], iOpenPosition.shape[0]+iNewPosition.shape[0]) self._OpenPosition = iOpenPosition.append(iNewPosition) TradingRecord = list(zip([idt]*CloseOrders.shape[0], CloseOrders.index, CloseOrders, TradePrice, Fees, CashChanged, ["close"]*CloseOrders.shape[0])) if TradingRecord: self._updateAccount(CashChanged.sum())# 更新账户信息 return (TradingRecord, OpenOrders) # 撮合成交市价开仓单 # 以成交价完成成交, 成交量满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketOpenOrder(self, idt, orders): orders = orders[orders!=0] if orders.shape[0]==0: return [] IDs = orders.index.tolist() TradePrice = self._MarketFT.readData(dts=[idt], ids=IDs, factor_names=[self.MarketInfo.TradePrice]).iloc[0, 0]# 成交价 # 处理开仓单 TradeAmounts = orders * TradePrice * self.SecurityInfo.Multiplier MarginAcquired = TradeAmounts.abs() * self.SecurityInfo.InitMarginRate FeesAcquired = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee CashAcquired = MarginAcquired + FeesAcquired CashAllocated = min(CashAcquired.sum(), self.AvailableCash) * CashAcquired / CashAcquired.sum() orders = CashAllocated / TradePrice / self.SecurityInfo.Multiplier / (self.SecurityInfo.InitMarginRate + self.BuyLimit.TradeFee * (orders>0) + self.SellLimit.TradeFee * (orders<0)) * np.sign(orders) #if self.SellLimit.MinUnit!=0.0:# 最小卖出交易单位限制 #Mask = (orders<0) #orders[Mask] = (orders[Mask] / self.SellLimit.MinUnit).astype("int") * self.SellLimit.MinUnit #if self.BuyLimit.MinUnit!=0.0:# 最小买入交易单位限制 #Mask = (orders>0) #orders[Mask] = (orders[Mask] / self.BuyLimit.MinUnit).astype("int") * self.BuyLimit.MinUnit orders = orders[pd.notnull(orders) & (orders!=0)] if orders.shape[0]==0: return [] TradePrice = TradePrice[orders.index] TradeAmounts = orders * TradePrice * self.SecurityInfo.Multiplier Fees = TradeAmounts.clip_lower(0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper(0).abs() * self.SellLimit.TradeFee MarginAcquired = TradeAmounts.abs() * self.SecurityInfo.InitMarginRate CashChanged = - (MarginAcquired + Fees).sum() TradingRecord = list(zip([idt]*orders.shape[0], orders.index, orders, TradePrice, Fees, -(MarginAcquired+Fees), ["open"]*orders.shape[0])) iNewPosition = pd.DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "保证金", "浮动盈亏"]) iNewPosition["ID"] = orders.index iNewPosition["数量"] = orders.values iNewPosition["开仓时点"] = idt iNewPosition["开仓价格"] = TradePrice.values iNewPosition["开仓交易费"] = Fees.values iNewPosition["保证金"] = MarginAcquired.values iNewPosition["浮动盈亏"] = 0.0 iNewPosition.index = np.arange(self._OpenPosition.shape[0], self._OpenPosition.shape[0]+iNewPosition.shape[0]) self._OpenPosition = self._OpenPosition.append(iNewPosition) self._updateAccount(CashChanged)# 更新账户信息 return TradingRecord # 撮合成交卖出限价单 # 如果最高价和最低价未指定, 则检验成交价是否优于目标价, 是就以目标价完成成交, 且成交量满足卖出限制中的限价单成交量限比, 否则无法成交 # 如果指定了最高价和最低价, 则假设成交价服从[最低价, 最高价]中的均匀分布, 据此确定可完成成交的数量 # 未成交的限价单继续保留, TODO def _matchLimitCloseOrder(self, idt, sell_orders): return ([], pd.DataFrame(columns=["ID", "数量", "目标价"])) # 撮合成交买入限价单 # 如果最高价和最低价未指定, 则检验成交价是否优于目标价, 是就以目标价完成成交, 且成交量满足买入限制要求 # 如果指定了最高价和最低价, 则假设成交价服从[最低价, 最高价]中的均匀分布, 据此确定可完成成交的数量, 同时满足买入限制要求 # 未成交的限价单继续保留, TODO def _matchLimitOpenOrder(self, idt, buy_orders): return ([], pd.DataFrame(columns=["ID", "数量", "目标价"]))
class SQLDB(QSSQLObject, WritableFactorDB): """SQLDB""" CheckWriteData = Bool(False, arg_type="Bool", label="检查写入值", order=100) IgnoreFields = ListStr(arg_type="List", label="忽略字段", order=101) InnerPrefix = Str("qs_", arg_type="String", label="内部前缀", order=102) def __init__(self, sys_args={}, config_file=None, **kwargs): super().__init__(sys_args=sys_args, config_file=(__QS_ConfigPath__ + os.sep + "SQLDBConfig.json" if config_file is None else config_file), **kwargs) self._TableFactorDict = {} # {表名: pd.Series(数据类型, index=[因子名])} self._TableFieldDataType = { } # {表名: pd.Series( 数据库数据类型, index=[因子名])} self.Name = "SQLDB" return def connect(self): super().connect() nPrefix = len(self.InnerPrefix) if self._Connector == "sqlite3": SQLStr = "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%s%%' ORDER BY name" Cursor = self.cursor(SQLStr % self.InnerPrefix) AllTables = Cursor.fetchall() self._TableFactorDict = {} self._TableFieldDataType = {} IgnoreFields = ["code", "datetime"] + list(self.IgnoreFields) for iTableName in AllTables: iTableName = iTableName[0][nPrefix:] Cursor.execute("PRAGMA table_info([%s])" % self.InnerPrefix + iTableName) iDataType = np.array(Cursor.fetchall()) iDataType = pd.Series(iDataType[:, 2], index=iDataType[:, 1]) iDataType = iDataType[iDataType.index.difference(IgnoreFields)] if iDataType.shape[0] > 0: self._TableFieldDataType[iTableName] = iDataType.copy() iDataType[iDataType == "text"] = "string" iDataType[iDataType == "real"] = "double" self._TableFactorDict[iTableName] = iDataType elif self.DBType == "MySQL": SQLStr = ( "SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE table_schema='%s' " % self.DBName) SQLStr += ("AND TABLE_NAME LIKE '%s%%' " % self.InnerPrefix) SQLStr += "AND COLUMN_NAME NOT IN ('code', 'datetime'" if len(self.IgnoreFields) > 0: SQLStr += ",'" + "','".join(self.IgnoreFields) + "') " else: SQLStr += ") " SQLStr += "ORDER BY TABLE_NAME, COLUMN_NAME" Rslt = self.fetchall(SQLStr) if not Rslt: self._TableFieldDataType = {} self._TableFactorDict = {} else: self._TableFieldDataType = pd.DataFrame( np.array(Rslt), columns=["表", "因子", "DataType"]).set_index(["表", "因子"])["DataType"] self._TableFactorDict = self._TableFieldDataType.copy() Mask = (self._TableFactorDict.str.contains("char") | self._TableFactorDict.str.contains("date")) self._TableFactorDict[Mask] = "string" self._TableFactorDict[~Mask] = "double" self._TableFactorDict = { iTable[nPrefix:]: self._TableFactorDict.loc[iTable] for iTable in self._TableFactorDict.index.levels[0] } self._TableFieldDataType = { iTable[nPrefix:]: self._TableFieldDataType.loc[iTable] for iTable in self._TableFieldDataType.index.levels[0] } return 0 @property def TableNames(self): return sorted(self._TableFactorDict) def getTable(self, table_name, args={}): if table_name not in self._TableFactorDict: Msg = ("因子库 '%s' 调用方法 getTable 错误: 不存在因子表: '%s'!" % (self.Name, table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if args.get("因子表类型", "宽表") == "宽表": return _WideTable(name=table_name, fdb=self, sys_args=args, logger=self._QS_Logger) else: return _NarrowTable(name=table_name, fdb=self, sys_args=args, logger=self._QS_Logger) def renameTable(self, old_table_name, new_table_name): if old_table_name not in self._TableFactorDict: Msg = ("因子库 '%s' 调用方法 renameTable 错误: 不存在因子表 '%s'!" % (self.Name, old_table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if (new_table_name != old_table_name) and (new_table_name in self._TableFactorDict): Msg = ("因子库 '%s' 调用方法 renameTable 错误: 新因子表名 '%s' 已经存在于库中!" % (self.Name, new_table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) self.renameDBTable(self.InnerPrefix + old_table_name, self.InnerPrefix + new_table_name) self._TableFactorDict[new_table_name] = self._TableFactorDict.pop( old_table_name) self._TableFieldDataType[ new_table_name] = self._TableFieldDataType.pop(old_table_name) return 0 # 创建表, field_types: {字段名: 数据库数据类型} def createTable(self, table_name, field_types): FieldTypes = field_types.copy() if self.DBType == "MySQL": FieldTypes["datetime"] = field_types.pop("datetime", "DATETIME(6) NOT NULL") FieldTypes["code"] = field_types.pop("code", "VARCHAR(40) NOT NULL") elif self.DBType == "sqlite3": FieldTypes["datetime"] = field_types.pop("datetime", "text NOT NULL") FieldTypes["code"] = field_types.pop("code", "text NOT NULL") self.createDBTable(self.InnerPrefix + table_name, FieldTypes, primary_keys=["datetime", "code"], index_fields=["datetime", "code"]) self._TableFactorDict[table_name] = pd.Series({ iFactorName: ("string" if field_types[iFactorName].find("char") != -1 else "double") for iFactorName in field_types }) self._TableFieldDataType[table_name] = pd.Series(field_types) return 0 # 增加因子,field_types: {字段名: 数据库数据类型} def addFactor(self, table_name, field_types): if table_name not in self._TableFactorDict: return self.createTable(table_name, field_types) self.addField(self.InnerPrefix + table_name, field_types) NewDataType = pd.Series({ iFactorName: ("string" if field_types[iFactorName].find("char") != -1 else "double") for iFactorName in field_types }) self._TableFactorDict[table_name] = self._TableFactorDict[ table_name].append(NewDataType) self._TableFieldDataType[table_name] = self._TableFieldDataType[ table_name].append(pd.Series(field_types)) return 0 def deleteTable(self, table_name): if table_name not in self._TableFactorDict: return 0 self.deleteDBTable(self.InnerPrefix + table_name) self._TableFactorDict.pop(table_name, None) self._TableFieldDataType.pop(table_name, None) return 0 # ----------------------------因子操作--------------------------------- def renameFactor(self, table_name, old_factor_name, new_factor_name): if old_factor_name not in self._TableFactorDict[table_name]: Msg = ("因子库 '%s' 调用方法 renameFactor 错误: 因子表 '%s' 中不存在因子 '%s'!" % (self.Name, table_name, old_factor_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if (new_factor_name != old_factor_name) and ( new_factor_name in self._TableFactorDict[table_name]): Msg = ( "因子库 '%s' 调用方法 renameFactor 错误: 新因子名 '%s' 已经存在于因子表 '%s' 中!" % (self.Name, new_factor_name, table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) self.renameField(self.InnerPrefix + table_name, old_factor_name, new_factor_name) self._TableFactorDict[table_name][ new_factor_name] = self._TableFactorDict[table_name].pop( old_factor_name) self._TableFieldDataType[table_name][ new_factor_name] = self._TableFieldDataType[table_name].pop( old_factor_name) return 0 def deleteFactor(self, table_name, factor_names): if not factor_names: return 0 FactorIndex = self._TableFactorDict.get( table_name, pd.Series()).index.difference(factor_names).tolist() if not FactorIndex: return self.deleteTable(table_name) self.deleteField(self.InnerPrefix + table_name, factor_names) self._TableFactorDict[table_name] = self._TableFactorDict[table_name][ FactorIndex] self._TableFieldDataType[table_name] = self._TableFieldDataType[ table_name][FactorIndex] return 0 def deleteData(self, table_name, ids=None, dts=None): if table_name not in self._TableFactorDict: Msg = ("因子库 '%s' 调用方法 deleteData 错误: 不存在因子表 '%s'!" % (self.Name, table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if (ids is None) and (dts is None): return self.truncateDBTable(self.InnerPrefix + table_name) DBTableName = self.TablePrefix + self.InnerPrefix + table_name SQLStr = "DELETE FROM " + DBTableName if dts is not None: DTs = [iDT.strftime("%Y-%m-%d %H:%M:%S.%f") for iDT in dts] SQLStr += "WHERE " + genSQLInCondition( DBTableName + ".datetime", DTs, is_str=True, max_num=1000) + " " else: SQLStr += "WHERE " + DBTableName + ".datetime IS NOT NULL " if ids is not None: SQLStr += "AND " + genSQLInCondition( DBTableName + ".code", ids, is_str=True, max_num=1000) try: self.execute(SQLStr) except Exception as e: Msg = ("'%s' 调用方法 deleteData 删除表 '%s' 中数据时错误: %s" % (self.Name, table_name, str(e))) self._QS_Logger.error(Msg) raise e return 0 def _adjustWriteData(self, data): NewData = [] DataLen = data.applymap(lambda x: len(x) if isinstance(x, list) else 1) DataLenMax = DataLen.max(axis=1) DataLenMin = DataLen.min(axis=1) if (DataLenMax != DataLenMin).sum() > 0: self._QS_Logger.warning("'%s' 在写入因子 '%s' 时出现因子值长度不一致的情况, 将填充缺失!" % (self.Name, str(data.columns.tolist()))) for i in range(data.shape[0]): iDataLen = DataLen.iloc[i] if iDataLen > 0: iData = data.iloc[i].apply( lambda x: [None] * (iDataLen - len(x)) + x if isinstance(x, list) else [x] * iDataLen).tolist() NewData.extend(zip(*iData)) NewData = pd.DataFrame(NewData, dtype="O") return NewData.where(pd.notnull(NewData), None).to_records(index=False).tolist() def writeData(self, data, table_name, if_exists="update", data_type={}, **kwargs): if table_name not in self._TableFactorDict: FieldTypes = { iFactorName: _identifyDataType(self.DBType, data.iloc[i].dtypes) for i, iFactorName in enumerate(data.items) } self.createTable(table_name, field_types=FieldTypes) SQLStr = "INSERT INTO " + self.TablePrefix + self.InnerPrefix + table_name + " (`datetime`, `code`, " else: NewFactorNames = data.items.difference( self._TableFactorDict[table_name].index).tolist() if NewFactorNames: FieldTypes = { iFactorName: _identifyDataType(self.DBType, data.iloc[i].dtypes) for i, iFactorName in enumerate(NewFactorNames) } self.addFactor(table_name, FieldTypes) AllFactorNames = self._TableFactorDict[table_name].index.tolist() if self.CheckWriteData: OldData = self.getTable(table_name, args={ "因子值类型": "list", "时间转字符串": True }).readData( factor_names=AllFactorNames, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) else: OldData = self.getTable(table_name, args={ "时间转字符串": True }).readData(factor_names=AllFactorNames, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) if if_exists == "append": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = OldData[iFactorName].where( pd.notnull(OldData[iFactorName]), data[iFactorName]) else: data[iFactorName] = OldData[iFactorName] elif if_exists == "update": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = data[iFactorName].where( pd.notnull(data[iFactorName]), OldData[iFactorName]) else: data[iFactorName] = OldData[iFactorName] SQLStr = "REPLACE INTO " + self.TablePrefix + self.InnerPrefix + table_name + " (`datetime`, `code`, " data.major_axis = [ iDT.strftime("%Y-%m-%d %H:%M:%S.%f") for iDT in data.major_axis ] NewData = {} for iFactorName in data.items: iData = data.loc[iFactorName].stack(dropna=False) NewData[iFactorName] = iData SQLStr += "`" + iFactorName + "`, " NewData = pd.DataFrame(NewData).loc[:, data.items] NewData = NewData[pd.notnull(NewData).any(axis=1)] if NewData.shape[0] == 0: return 0 if self._Connector in ("pyodbc", "sqlite3"): SQLStr = SQLStr[:-2] + ") VALUES (" + "?, " * (NewData.shape[1] + 2) else: SQLStr = SQLStr[:-2] + ") VALUES (" + "%s, " * (NewData.shape[1] + 2) SQLStr = SQLStr[:-2] + ") " Cursor = self._Connection.cursor() if self.CheckWriteData: NewData = self._adjustWriteData(NewData.reset_index()) Cursor.executemany(SQLStr, NewData) else: NewData = NewData.astype("O").where(pd.notnull(NewData), None) Cursor.executemany(SQLStr, NewData.reset_index().values.tolist()) self._Connection.commit() Cursor.close() return 0
class FMPModel(BaseModule): """基于特征因子模拟组合的绩效分析模型""" #Portfolio = Enum(None, arg_type="SingleOption", label="策略组合", order=0) #BenchmarkPortfolio = Enum("无", arg_type="SingleOption", label="基准组合", order=1) AttributeFactors = ListStr(arg_type="MultiOption", label="特征因子", order=2, option_range=()) #IndustryFactor = Enum("无", arg_type="SingleOption", label="行业因子", order=3) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=4) RiskTable = Instance(RiskTable, arg_type="RiskTable", label="风险表", order=5) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=6) def __init__(self, factor_table, name="因子模拟组合绩效分析模型", sys_args={}, **kwargs): self._FactorTable = factor_table return super().__init__(name=name, sys_args=sys_args, config_file=None, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "Portfolio", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="策略组合", order=0)) self.add_trait( "BenchmarkPortfolio", Enum(*(["无"] + DefaultNumFactorList), arg_type="SingleOption", label="基准组合", order=1)) self.add_trait( "AttributeFactors", ListStr(arg_type="MultiOption", label="特征因子", order=2, option_range=tuple(DefaultNumFactorList))) self.AttributeFactors.append(DefaultNumFactorList[-1]) self.add_trait( "IndustryFactor", Enum(*(["无"] + DefaultStrFactorList), arg_type="SingleOption", label="行业因子", order=3)) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=4)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) def _normalizePortfolio(self, portfolio): NegMask = (portfolio < 0) TotalNegWeight = portfolio[NegMask].sum() if TotalNegWeight != 0: portfolio[NegMask] = portfolio[NegMask] / TotalNegWeight PosMask = (portfolio > 0) TotalPosWeight = portfolio[PosMask].sum() if TotalPosWeight != 0: portfolio[PosMask] = portfolio[PosMask] / TotalPosWeight portfolio[ NegMask] = portfolio[NegMask] * TotalNegWeight / TotalPosWeight return portfolio def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self.RiskTable.start(dts=dts) self._Output = {} self._Output["因子暴露"] = pd.DataFrame(columns=self.AttributeFactors) self._Output["风险调整的因子暴露"] = pd.DataFrame(columns=self.AttributeFactors) self._Output["风险贡献"] = pd.DataFrame(columns=self.AttributeFactors + ["Alpha"]) self._Output["收益贡献"] = pd.DataFrame(columns=self.AttributeFactors + ["Alpha"]) self._Output["因子收益"] = pd.DataFrame(columns=self.AttributeFactors) self._CurCalcInd = 0 self._IDs = self._FactorTable.getID() return (self._FactorTable, ) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 self._iDT = idt PreDT = None if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd if self._CurCalcInd > 0: PreDT = self.CalcDTs[self._CurCalcInd - 1] else: self._CurCalcInd = self._Model.DateTimeIndex if self._CurCalcInd > 0: PreDT = self._Model.DateTimeSeries[self._CurCalcInd - 1] if PreDT is None: return 0 Portfolio = self._FactorTable.readData(factor_names=[self.Portfolio], dts=[PreDT], ids=self._IDs).iloc[0, 0] Portfolio = self._normalizePortfolio(Portfolio[pd.notnull(Portfolio) & (Portfolio != 0)]) if self.BenchmarkPortfolio != "无": BenchmarkPortfolio = self._FactorTable.readData( factor_names=[self.BenchmarkPortfolio], dts=[PreDT], ids=self._IDs).iloc[0, 0] BenchmarkPortfolio = self._normalizePortfolio( BenchmarkPortfolio[pd.notnull(BenchmarkPortfolio) & (BenchmarkPortfolio != 0)]) IDs = Portfolio.index.union(BenchmarkPortfolio.index) if Portfolio.shape[0] > 0: Portfolio = Portfolio.loc[IDs] Portfolio.fillna(0.0, inplace=True) else: Portfolio = pd.Series(0.0, index=IDs) if BenchmarkPortfolio.shape[0] > 0: BenchmarkPortfolio = BenchmarkPortfolio.loc[IDs] BenchmarkPortfolio.fillna(0.0, inplace=True) else: BenchmarkPortfolio = pd.Series(0.0, index=IDs) Portfolio = Portfolio - BenchmarkPortfolio # 计算因子模拟组合 self.RiskTable.move(PreDT, **kwargs) CovMatrix = dropRiskMatrixNA( self.RiskTable.readCov(dts=[PreDT], ids=Portfolio.index.tolist()).iloc[0]) FactorExpose = self._FactorTable.readData( factor_names=list(self.AttributeFactors), ids=IDs, dts=[PreDT]).iloc[:, 0].dropna(axis=0) IDs = FactorExpose.index.intersection(CovMatrix.index).tolist() CovMatrix, FactorExpose = CovMatrix.loc[IDs, IDs], FactorExpose.loc[IDs, :] if self.IndustryFactor != "无": IndustryData = self._FactorTable.readData( factor_names=[self.IndustryFactor], ids=IDs, dts=[PreDT]).iloc[0, 0, :] DummyData = DummyVarTo01Var(IndustryData, ignore_nonstring=True) DummyData.columns.values[pd.isnull(DummyData.columns)] = "None" FactorExpose = pd.merge(FactorExpose, DummyData, left_index=True, right_index=True) CovMatrixInv = np.linalg.inv(CovMatrix.values) FMPHolding = np.dot( np.dot( np.linalg.inv( np.dot(np.dot(FactorExpose.values.T, CovMatrixInv), FactorExpose.values)), FactorExpose.values.T), CovMatrixInv) # 计算持仓对因子模拟组合的投资组合 Portfolio = self._normalizePortfolio(Portfolio.loc[IDs]) Beta = np.dot( np.dot( np.dot( np.linalg.inv( np.dot(np.dot(FMPHolding, CovMatrix.values), FMPHolding.T)), FMPHolding), CovMatrix.values), Portfolio.values) Price = self._FactorTable.readData(factor_names=[self.PriceFactor], dts=[PreDT, idt], ids=IDs).iloc[0] Return = Price.iloc[1] / Price.iloc[0] - 1 # 计算各统计指标 if FactorExpose.shape[1] > self._Output["因子暴露"].shape[1]: FactorNames = FactorExpose.columns.tolist() self._Output["因子暴露"] = self._Output["因子暴露"].loc[:, FactorNames] self._Output["风险调整的因子暴露"] = self._Output[ "风险调整的因子暴露"].loc[:, FactorNames] self._Output["风险贡献"] = self._Output["风险贡献"].loc[:, FactorNames + ["Alpha"]] self._Output["收益贡献"] = self._Output["收益贡献"].loc[:, FactorNames + ["Alpha"]] self._Output["因子收益"] = self._Output["因子收益"].loc[:, FactorNames] self._Output["因子暴露"].loc[PreDT, FactorExpose.columns] = Beta self._Output["风险调整的因子暴露"].loc[PreDT, FactorExpose.columns] = np.sqrt( np.diag(np.dot(np.dot(FMPHolding, CovMatrix.values), FMPHolding.T))) * Beta RiskContribution = np.dot(np.dot( FMPHolding, CovMatrix.values), Portfolio.values) / np.sqrt( np.dot(np.dot(Portfolio.values, CovMatrix.values), Portfolio.values)) * Beta self._Output["风险贡献"].loc[idt, FactorExpose.columns] = RiskContribution self._Output["风险贡献"].loc[idt, "Alpha"] = np.sqrt( np.dot(np.dot(Portfolio.values, CovMatrix), Portfolio.values)) - np.nansum(RiskContribution) self._Output["因子收益"].loc[idt, FactorExpose.columns] = np.nansum( Return.values * FMPHolding, axis=1) self._Output["收益贡献"].loc[idt, FactorExpose.columns] = self._Output[ "因子收益"].loc[idt, FactorExpose.columns] * self._Output["因子暴露"].loc[ PreDT, FactorExpose.columns] self._Output["收益贡献"].loc[idt, "Alpha"] = (Portfolio * Return).sum( ) - self._Output["收益贡献"].loc[idt, FactorExpose.columns].sum() return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() self.RiskTable.end() self._Output["风险贡献占比"] = self._Output["风险贡献"].divide( self._Output["风险贡献"].sum(axis=1), axis=0) self._Output["历史均值"] = pd.DataFrame( columns=["因子暴露", "风险调整的因子暴露", "风险贡献", "风险贡献占比", "收益贡献"], index=self._Output["收益贡献"].columns) self._Output["历史均值"]["因子暴露"] = self._Output["因子暴露"].mean(axis=0) self._Output["历史均值"]["风险调整的因子暴露"] = self._Output["风险调整的因子暴露"].mean( axis=0) self._Output["历史均值"]["风险贡献"] = self._Output["风险贡献"].mean(axis=0) self._Output["历史均值"]["风险贡献占比"] = self._Output["风险贡献占比"].mean(axis=0) self._Output["历史均值"]["收益贡献"] = self._Output["收益贡献"].mean(axis=0) self._IDs = None return 0 def genMatplotlibFig(self, file_path=None): nRow, nCol = 2, 3 Fig = plt.figure(figsize=(min(32, 16 + (nCol - 1) * 8), 8 * nRow)) AxesGrid = gridspec.GridSpec(nRow, nCol) xData = np.arange(1, self._Output["历史均值"].shape[0]) xTickLabels = [str(iInd) for iInd in self._Output["历史均值"].index] PercentageFormatter = FuncFormatter(_QS_formatMatplotlibPercentage) FloatFormatter = FuncFormatter(lambda x, pos: '%.2f' % (x, )) _QS_plotStatistics(plt.subplot(AxesGrid[0, 0]), xData[:-1], xTickLabels[:-1], self._Output["历史均值"]["因子暴露"].iloc[:-1], FloatFormatter) _QS_plotStatistics(plt.subplot(AxesGrid[0, 1]), xData[:-1], xTickLabels[:-1], self._Output["历史均值"]["风险调整的因子暴露"].iloc[:-1], FloatFormatter) _QS_plotStatistics(plt.subplot(AxesGrid[0, 2]), xData[:-1], xTickLabels[:-1], self._Output["历史均值"]["因子收益"].iloc[:-1], PercentageFormatter) _QS_plotStatistics(plt.subplot(AxesGrid[1, 0]), xData, xTickLabels, self._Output["历史均值"]["收益贡献"], PercentageFormatter) _QS_plotStatistics(plt.subplot(AxesGrid[1, 1]), xData, xTickLabels, self._Output["历史均值"]["风险贡献"], FloatFormatter) _QS_plotStatistics(plt.subplot(AxesGrid[1, 2]), xData, xTickLabels, self._Output["历史均值"]["风险贡献占比"], PercentageFormatter) if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames) > 0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName == "风险表": HTML += "<li>" + iArgName + ": " + self.RiskTable.Name + "</li>" elif iArgName != "计算时点": HTML += "<li>" + iArgName + ": " + str( self.Args[iArgName]) + "</li>" elif self.Args[iArgName]: HTML += "<li>" + iArgName + ": 自定义时点</li>" else: HTML += "<li>" + iArgName + ": 所有时点</li>" HTML += "</ul>" else: HTML = "" Formatters = [lambda x: '{0:.4f}'.format(x)] * 2 + [ _QS_formatPandasPercentage ] * 2 + [lambda x: '{0:.4f}'.format(x), _QS_formatPandasPercentage] iHTML = self._Output["历史均值"].to_html(formatters=Formatters) Pos = iHTML.find(">") HTML += iHTML[:Pos] + ' align="center"' + iHTML[Pos:] Fig = self.genMatplotlibFig() # figure 保存为二进制文件 Buffer = BytesIO() plt.savefig(Buffer, bbox_inches='tight') PlotData = Buffer.getvalue() # 图像数据转化为 HTML 格式 ImgStr = "data:image/png;base64," + base64.b64encode(PlotData).decode() HTML += ('<img src="%s">' % ImgStr) return HTML
class FamaMacBethRegression(BaseModule): """Fama-MacBeth 回归""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=1) #ClassFactor = Enum("无", arg_type="SingleOption", label="类别因子", order=2) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=3) IDFilter = Str(arg_type="IDFilter", label="筛选条件", order=4) RollAvgPeriod = Int(12, arg_type="Integer", label="滚动平均期数", order=5) def __init__(self, factor_table, name="Fama-MacBeth 回归", sys_args={}, **kwargs): self._FactorTable = factor_table return super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=1)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) self.add_trait( "ClassFactor", Enum(*(["无"] + DefaultStrFactorList), arg_type="SingleOption", label="类别因子", order=2)) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = { "Pure Return": [], "Raw Return": [], "时点": [], "回归R平方": [], "回归调整R平方": [], "回归F统计量": [], "回归t统计量(Raw Return)": [], "回归t统计量(Pure Return)": [] } self._CurCalcInd = 0 return (self._FactorTable, ) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 self._iDT = idt if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd LastInd = self._CurCalcInd - 1 LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex LastInd = self._CurCalcInd - 1 LastDateTime = self._Model.DateTimeSeries[LastInd] nFactor = len(self.TestFactors) self._Output["Pure Return"].append( np.full(shape=(nFactor, ), fill_value=np.nan)) self._Output["Raw Return"].append( np.full(shape=(nFactor, ), fill_value=np.nan)) self._Output["回归t统计量(Pure Return)"].append( np.full(shape=(nFactor, ), fill_value=np.nan)) self._Output["回归t统计量(Raw Return)"].append( np.full(shape=(nFactor, ), fill_value=np.nan)) self._Output["回归F统计量"].append( np.full(shape=(nFactor + 1, ), fill_value=np.nan)) self._Output["回归R平方"].append( np.full(shape=(nFactor + 1, ), fill_value=np.nan)) self._Output["回归调整R平方"].append( np.full(shape=(nFactor + 1, ), fill_value=np.nan)) self._Output["时点"].append(idt) if LastInd < 0: return 0 LastIDs = self._FactorTable.getFilteredID(idt=LastDateTime, id_filter_str=self.IDFilter) FactorData = self._FactorTable.readData( dts=[LastDateTime], ids=LastIDs, factor_names=list(self.TestFactors)).iloc[:, 0, :] Price = self._FactorTable.readData(dts=[LastDateTime, idt], ids=LastIDs, factor_names=[self.PriceFactor ]).iloc[0] Ret = Price.iloc[1] / Price.iloc[0] - 1 # 展开Dummy因子 if self.ClassFactor != "无": DummyFactorData = self._FactorTable.readData( dts=[LastDateTime], ids=LastIDs, factor_names=[self.ClassFactor]).iloc[0, 0, :] Mask = pd.notnull(DummyFactorData) DummyFactorData = DummyVarTo01Var(DummyFactorData[Mask], ignore_na=True) FactorData = pd.merge(FactorData.loc[Mask], DummyFactorData, left_index=True, right_index=True) # 回归 yData = Ret[FactorData.index].values xData = FactorData.values if self.ClassFactor == "无": xData = sm.add_constant(xData, prepend=False) LastInds = [nFactor] else: LastInds = [nFactor + i for i in range(xData.shape[1] - nFactor)] try: Result = sm.OLS(yData, xData, missing="drop").fit() self._Output["Pure Return"][-1] = Result.params[0:nFactor] self._Output["回归t统计量(Pure Return)"][-1] = Result.tvalues[0:nFactor] self._Output["回归F统计量"][-1][-1] = Result.fvalue self._Output["回归R平方"][-1][-1] = Result.rsquared self._Output["回归调整R平方"][-1][-1] = Result.rsquared_adj except: pass for i, iFactorName in enumerate(self.TestFactors): iXData = xData[:, [i] + LastInds] try: Result = sm.OLS(yData, iXData, missing="drop").fit() self._Output["Raw Return"][-1][i] = Result.params[0] self._Output["回归t统计量(Raw Return)"][-1][i] = Result.tvalues[0] self._Output["回归F统计量"][-1][i] = Result.fvalue self._Output["回归R平方"][-1][i] = Result.rsquared self._Output["回归调整R平方"][-1][i] = Result.rsquared_adj except: pass return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() FactorNames = list(self.TestFactors) self._Output["Pure Return"] = pd.DataFrame(self._Output["Pure Return"], index=self._Output["时点"], columns=FactorNames) self._Output["Raw Return"] = pd.DataFrame(self._Output["Raw Return"], index=self._Output["时点"], columns=FactorNames) self._Output["滚动t统计量_Pure"] = pd.DataFrame(np.nan, index=self._Output["时点"], columns=FactorNames) self._Output["滚动t统计量_Raw"] = pd.DataFrame(np.nan, index=self._Output["时点"], columns=FactorNames) self._Output["回归t统计量(Raw Return)"] = pd.DataFrame( self._Output["回归t统计量(Raw Return)"], index=self._Output["时点"], columns=FactorNames) self._Output["回归t统计量(Pure Return)"] = pd.DataFrame( self._Output["回归t统计量(Pure Return)"], index=self._Output["时点"], columns=FactorNames) self._Output["回归F统计量"] = pd.DataFrame(self._Output["回归F统计量"], index=self._Output["时点"], columns=FactorNames + ["所有因子"]) self._Output["回归R平方"] = pd.DataFrame(self._Output["回归R平方"], index=self._Output["时点"], columns=FactorNames + ["所有因子"]) self._Output["回归调整R平方"] = pd.DataFrame(self._Output["回归调整R平方"], index=self._Output["时点"], columns=FactorNames + ["所有因子"]) nDT = self._Output["Raw Return"].shape[0] # 计算滚动t统计量 for i in range(nDT): if i < self.RollAvgPeriod - 1: continue iReturn = self._Output["Pure Return"].iloc[i - self.RollAvgPeriod + 1:i + 1, :] self._Output["滚动t统计量_Pure"].iloc[i] = iReturn.mean( axis=0) / iReturn.std(axis=0) * pd.notnull(iReturn).sum( axis=0)**0.5 iReturn = self._Output["Raw Return"].iloc[i - self.RollAvgPeriod + 1:i + 1, :] self._Output["滚动t统计量_Raw"].iloc[i] = iReturn.mean( axis=0) / iReturn.std(axis=0) * pd.notnull(iReturn).sum( axis=0)**0.5 nYear = (self._Output["时点"][-1] - self._Output["时点"][0]).days / 365 self._Output["统计数据"] = pd.DataFrame( index=self._Output["Pure Return"].columns) self._Output["统计数据"]["年化收益率(Pure)"] = ( (1 + self._Output["Pure Return"]).prod())**(1 / nYear) - 1 self._Output["统计数据"]["跟踪误差(Pure)"] = self._Output["Pure Return"].std( ) * np.sqrt(nDT / nYear) self._Output["统计数据"]["信息比率(Pure)"] = self._Output["统计数据"][ "年化收益率(Pure)"] / self._Output["统计数据"]["跟踪误差(Pure)"] self._Output["统计数据"]["胜率(Pure)"] = (self._Output["Pure Return"] > 0).sum() / nDT self._Output["统计数据"]["t统计量(Pure)"] = self._Output["Pure Return"].mean( ) / self._Output["Pure Return"].std() * np.sqrt(nDT) self._Output["统计数据"]["年化收益率(Raw)"] = ( 1 + self._Output["Raw Return"]).prod()**(1 / nYear) - 1 self._Output["统计数据"]["跟踪误差(Raw)"] = self._Output["Raw Return"].std( ) * np.sqrt(nDT / nYear) self._Output["统计数据"]["信息比率(Raw)"] = self._Output["统计数据"][ "年化收益率(Raw)"] / self._Output["统计数据"]["跟踪误差(Raw)"] self._Output["统计数据"]["胜率(Raw)"] = (self._Output["Raw Return"] > 0).sum() / nDT self._Output["统计数据"]["t统计量(Raw)"] = self._Output["Raw Return"].mean( ) / self._Output["Raw Return"].std() * np.sqrt(nDT) self._Output["统计数据"]["年化收益率(Pure-Naive)"] = ( 1 + self._Output["Pure Return"] - self._Output["Raw Return"]).prod()**(1 / nYear) - 1 self._Output["统计数据"]["跟踪误差(Pure-Naive)"] = ( self._Output["Pure Return"] - self._Output["Raw Return"]).std() * np.sqrt(nDT / nYear) self._Output["统计数据"]["信息比率(Pure-Naive)"] = self._Output["统计数据"][ "年化收益率(Pure-Naive)"] / self._Output["统计数据"]["跟踪误差(Pure-Naive)"] self._Output["统计数据"]["胜率(Pure-Naive)"] = ( self._Output["Pure Return"] - self._Output["Raw Return"] > 0).sum() / nDT self._Output["统计数据"]["t统计量(Pure-Naive)"] = ( self._Output["Pure Return"] - self._Output["Raw Return"] ).mean() / (self._Output["Pure Return"] - self._Output["Raw Return"]).std() * np.sqrt(nDT) self._Output["回归统计量均值"] = pd.DataFrame(index=FactorNames + ["所有因子"]) self._Output["回归统计量均值"]["t统计量(Raw Return)"] = self._Output[ "回归t统计量(Raw Return)"].mean() self._Output["回归统计量均值"]["t统计量(Pure Return)"] = self._Output[ "回归t统计量(Pure Return)"].mean() self._Output["回归统计量均值"]["F统计量"] = self._Output["回归F统计量"].mean() self._Output["回归统计量均值"]["R平方"] = self._Output["回归R平方"].mean() self._Output["回归统计量均值"]["调整R平方"] = self._Output["回归调整R平方"].mean() self._Output.pop("时点") return 0 def _plotStatistics(self, axes, x_data, x_ticklabels, left_data, left_formatter, right_data=None, right_formatter=None, right_axes=True): axes.yaxis.set_major_formatter(left_formatter) axes.bar(x_data, left_data.values, label=left_data.name, color="steelblue") if right_data is not None: if right_axes: axes.legend(loc='upper left') right_axes = axes.twinx() right_axes.yaxis.set_major_formatter(right_formatter) right_axes.plot(x_data, right_data.values, label=right_data.name, color="indianred", lw=2.5) right_axes.legend(loc="upper right") else: axes.plot(x_data, right_data.values, label=right_data.name, color="indianred", lw=2.5) axes.legend(loc='best') else: axes.legend(loc='best') axes.set_xticks(x_data) axes.set_xticklabels(x_ticklabels) return axes def genMatplotlibFig(self, file_path=None): nRow, nCol = 1, 3 Fig = Figure(figsize=(min(32, 16 + (nCol - 1) * 8), 8 * nRow)) PercentageFormatter = FuncFormatter(_QS_formatMatplotlibPercentage) FloatFormatter = FuncFormatter(lambda x, pos: '%.2f' % (x, )) xData = np.arange(0, self._Output["统计数据"].shape[0]) xTickLabels = [str(iInd) for iInd in self._Output["统计数据"].index] iAxes = Fig.add_subplot(nRow, nCol, 1) iAxes.yaxis.set_major_formatter(PercentageFormatter) iAxes.bar(xData, self._Output["统计数据"]["年化收益率(Raw)"].values, width=-0.25, align="edge", color="indianred", label="年化收益率(Raw)") iAxes.bar(xData, self._Output["统计数据"]["年化收益率(Pure)"].values, width=0.25, align="edge", color="steelblue", label="年化收益率(Pure)") iAxes.set_xticks(xData) iAxes.set_xticklabels(xTickLabels) iAxes.legend(loc='best') iAxes.set_title("年化收益率") iAxes = Fig.add_subplot(nRow, nCol, 2) iAxes.yaxis.set_major_formatter(FloatFormatter) iAxes.bar(xData, self._Output["统计数据"]["t统计量(Raw)"].values, width=-0.25, align="edge", color="indianred", label="t统计量(Raw)") iAxes.bar(xData, self._Output["统计数据"]["t统计量(Pure)"].values, width=0.25, align="edge", color="steelblue", label="t统计量(Pure)") iAxes.set_xticks(xData) iAxes.set_xticklabels(xTickLabels) iAxes.legend(loc='best') iAxes.set_title("t统计量") iAxes = Fig.add_subplot(nRow, nCol, 3) iAxes.yaxis.set_major_formatter(PercentageFormatter) iAxes.bar(xData, self._Output["统计数据"]["年化收益率(Pure-Naive)"].values, color="steelblue", label="年化收益率(Pure-Naive)") iAxes.set_xticks(xData) iAxes.set_xticklabels(xTickLabels) iAxes.legend(loc='upper left') iAxes.set_title("Pure-Naive") RAxes = iAxes.twinx() RAxes.yaxis.set_major_formatter(FloatFormatter) RAxes.plot(xData, self._Output["统计数据"]["t统计量(Pure-Naive)"].values, color="indianred", lw=2.5, label="t统计量(Pure-Naive)") RAxes.legend(loc='upper right') if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames) > 0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName != "计算时点": HTML += "<li>" + iArgName + ": " + str( self.Args[iArgName]) + "</li>" elif self.Args[iArgName]: HTML += "<li>" + iArgName + ": 自定义时点</li>" else: HTML += "<li>" + iArgName + ": 所有时点</li>" HTML += "</ul>" else: HTML = "" FloatFormatFun = lambda x: '{0:.2f}'.format(x) Formatters = [_QS_formatPandasPercentage] * 2 + [ FloatFormatFun, _QS_formatPandasPercentage, FloatFormatFun ] Formatters += [_QS_formatPandasPercentage] * 2 + [ FloatFormatFun, _QS_formatPandasPercentage, FloatFormatFun ] Formatters += [_QS_formatPandasPercentage] * 2 + [ FloatFormatFun, _QS_formatPandasPercentage, FloatFormatFun ] iHTML = self._Output["统计数据"].to_html(formatters=Formatters) Pos = iHTML.find(">") HTML += iHTML[:Pos] + ' align="center"' + iHTML[Pos:] HTML += '<div align="left" style="font-size:1em"><strong>回归统计量</strong></div>' iHTML = self._Output["回归统计量均值"].to_html(formatters=[FloatFormatFun] * 5) Pos = iHTML.find(">") HTML += iHTML[:Pos] + ' align="center"' + iHTML[Pos:] Fig = self.genMatplotlibFig() # figure 保存为二进制文件 Buffer = BytesIO() Fig.savefig(Buffer, bbox_inches='tight') PlotData = Buffer.getvalue() # 图像数据转化为 HTML 格式 ImgStr = "data:image/png;base64," + base64.b64encode(PlotData).decode() HTML += ('<img src="%s">' % ImgStr) return HTML
class TimeSeriesCorrelation(BaseModule): """时间序列相关性""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) #PriceFactor = Enum(None, arg_type="SingleOption", label="价格因子", order=1) ReturnType = Enum("简单收益率", "对数收益率", "价格变化量", arg_type="SingleOption", label="收益率类型", order=2) ForecastPeriod = Int(1, arg_type="Integer", label="预测期数", order=3) Lag = Int(0, arg_type="Integer", label="滞后期数", order=4) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=5) CorrMethod = Enum("pearson", "spearman", "kendall", arg_type="SingleOption", label="相关性算法", order=6) SummaryWindow = Float(np.inf, arg_type="Integer", label="统计窗口", order=7) MinSummaryWindow = Int(2, arg_type="Integer", label="最小统计窗口", order=8) def __init__(self, factor_table, price_table, name="时间序列相关性", sys_args={}, **kwargs): self._FactorTable = factor_table self._PriceTable = price_table super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._PriceTable.getFactorMetaData(key="DataType"))) self.add_trait( "PriceFactor", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="价格因子", order=1)) self.PriceFactor = searchNameInStrList(DefaultNumFactorList, ['价', 'Price', 'price']) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {} self._Output["滚动相关性"] = { iFactorName: {} for iFactorName in self.TestFactors } # {因子: {时点: DataFrame(index=[因子ID], columns=[证券ID])}}, self._Output["证券ID"] = self._PriceTable.getID() self._Output["收益率"] = np.zeros(shape=(0, len(self._Output["证券ID"]))) self._Output["因子ID"] = self._FactorTable.getID() nFactorID = len(self._Output["因子ID"]) self._Output["因子值"] = { iFactorName: np.zeros(shape=(0, nFactorID)) for iFactorName in self.TestFactors } self._CurCalcInd = 0 self._nMinSample = (max(2, self.MinSummaryWindow) if np.isinf( self.MinSummaryWindow) else max(2, self.MinSummaryWindow)) return (self._FactorTable, self._PriceTable) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd PreInd = self._CurCalcInd - self.ForecastPeriod - self.Lag LastInd = self._CurCalcInd - self.ForecastPeriod PreDateTime = self.CalcDTs[PreInd] LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex PreInd = self._CurCalcInd - self.ForecastPeriod - self.Lag LastInd = self._CurCalcInd - self.ForecastPeriod PreDateTime = self._Model.DateTimeSeries[PreInd] LastDateTime = self._Model.DateTimeSeries[LastInd] if (PreInd < 0) or (LastInd < 0): return 0 Price = self._PriceTable.readData(dts=[LastDateTime, idt], ids=self._Output["证券ID"], factor_names=[self.PriceFactor ]).iloc[0, :, :].values self._Output["收益率"] = np.r_[ self._Output["收益率"], _calcReturn(Price, return_type=self.ReturnType)] FactorData = self._FactorTable.readData( dts=[PreDateTime], ids=self._Output["因子ID"], factor_names=list(self.TestFactors)).iloc[:, 0, :].values.T StartInd = int( max(0, self._Output["收益率"].shape[0] - self.SummaryWindow)) for i, iFactorName in enumerate(self.TestFactors): self._Output["因子值"][iFactorName] = np.r_[ self._Output["因子值"][iFactorName], FactorData[i:i + 1]] if self._Output["收益率"].shape[0] >= self._nMinSample: self._Output["滚动相关性"][iFactorName][idt] = pd.DataFrame( np.c_[self._Output["因子值"][iFactorName][StartInd:], self._Output["收益率"][StartInd:]]).corr( method=self.CorrMethod, min_periods=self._nMinSample ).values[:FactorData.shape[1], FactorData.shape[1]:] return 0 def __QS_end__(self): if not self._isStarted: return 0 FactorIDs, PriceIDs = self._Output.pop("因子ID"), self._Output.pop( "证券ID") LastDT = max(self._Output["滚动相关性"][self.TestFactors[0]]) self._Output["最后一期相关性"], self._Output["全样本相关性"] = {}, {} for iFactorName in self.TestFactors: self._Output["最后一期相关性"][iFactorName] = self._Output["滚动相关性"][ iFactorName][LastDT].T self._Output["全样本相关性"][iFactorName] = pd.DataFrame(np.c_[ self._Output["因子值"][iFactorName], self._Output["收益率"]]).corr( method=self.CorrMethod, min_periods=self._nMinSample).values[:len(FactorIDs), len(FactorIDs):].T self._Output["滚动相关性"][iFactorName] = pd.Panel( self._Output["滚动相关性"][iFactorName], major_axis=FactorIDs, minor_axis=PriceIDs).swapaxes( 0, 2).to_frame(filter_observations=False).reset_index() self._Output["滚动相关性"][iFactorName].columns = ["因子ID", "时点" ] + PriceIDs self._Output["最后一期相关性"] = pd.Panel( self._Output["最后一期相关性"], major_axis=PriceIDs, minor_axis=FactorIDs).swapaxes( 0, 1).to_frame(filter_observations=False).reset_index() self._Output["最后一期相关性"].columns = ["因子", "因子ID"] + PriceIDs self._Output["全样本相关性"] = pd.Panel( self._Output["全样本相关性"], major_axis=PriceIDs, minor_axis=FactorIDs).swapaxes( 0, 1).to_frame(filter_observations=False).reset_index() self._Output["全样本相关性"].columns = ["因子", "因子ID"] + PriceIDs self._Output.pop("收益率"), self._Output.pop("因子值") return 0
class FactorTurnover(BaseModule): """因子换手率""" TestFactors = ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=()) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=1) IDFilter = Str(arg_type="IDFilter", label="筛选条件", order=2) def __init__(self, factor_table, name="因子换手率", sys_args={}, **kwargs): self._FactorTable = factor_table return super().__init__(name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._FactorTable.getFactorMetaData(key="DataType"))) self.add_trait( "TestFactors", ListStr(arg_type="MultiOption", label="测试因子", order=0, option_range=tuple(DefaultNumFactorList))) self.TestFactors.append(DefaultNumFactorList[0]) def getViewItems(self, context_name=""): Items, Context = super().getViewItems(context_name=context_name) Items[0].editor = SetEditor( values=self.trait("TestFactors").option_range) return (Items, Context) def __QS_start__(self, mdl, dts=None, dates=None, times=None): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, dates=dates, times=times) self._Output = {iFactorName: [] for iFactorName in self.TestFactors} self._Output["时点"] = [] self._CurCalcInd = 0 return (self._FactorTable, ) def __QS_move__(self, idt): if self._iDT == idt: return 0 self._iDT = idt if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd LastInd = self._CurCalcInd - 1 LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex LastInd = self._CurCalcInd - 1 LastDateTime = self._Model.DateTimeSeries[LastInd] if LastInd < 0: for iFactorName in self.TestFactors: self._Output[iFactorName].append(0.0) self._Output["时点"].append(idt) return 0 LastIDs = self._FactorTable.getFilteredID(idt=LastDateTime, id_filter_str=self.IDFilter) PreFactorExpose = self._FactorTable.readData( dts=[LastDateTime], ids=LastIDs, factor_names=list(self.TestFactors)).iloc[:, 0, :].astype("float") CurFactorExpose = self._FactorTable.readData( dts=[idt], ids=LastIDs, factor_names=list(self.TestFactors)).iloc[:, 0, :].astype("float") for iFactorName in self.TestFactors: self._Output[iFactorName].append(CurFactorExpose[iFactorName].corr( PreFactorExpose[iFactorName])) self._Output["时点"].append(idt) return 0 def __QS_end__(self): if not self._isStarted: return 0 self._Output = { "因子换手率": pd.DataFrame(self._Output, index=self._Output.pop("时点")) } self._Output["统计数据"] = pd.DataFrame(self._Output["因子换手率"].mean(), columns=["平均值"]) self._Output["统计数据"]["标准差"] = self._Output["因子换手率"].std() self._Output["统计数据"]["最小值"] = self._Output["因子换手率"].min() self._Output["统计数据"]["最大值"] = self._Output["因子换手率"].max() self._Output["统计数据"]["中位数"] = self._Output["因子换手率"].median() return 0 def genMatplotlibFig(self, file_path=None): nRow, nCol = self._Output["因子换手率"].shape[1] // 3 + ( self._Output["因子换手率"].shape[1] % 3 != 0), min( 3, self._Output["因子换手率"].shape[1]) Fig = plt.figure(figsize=(min(32, 16 + (nCol - 1) * 8), 8 * nRow)) AxesGrid = gridspec.GridSpec(nRow, nCol) yMajorFormatter = FuncFormatter(_QS_formatMatplotlibPercentage) for i in range(self._Output["因子换手率"].shape[1]): iAxes = plt.subplot(AxesGrid[i // nCol, i % nCol]) iAxes.yaxis.set_major_formatter(yMajorFormatter) iAxes.xaxis_date() iAxes.xaxis.set_major_formatter(mdate.DateFormatter('%Y-%m-%d')) iAxes.stackplot(self._Output["因子换手率"].index, self._Output["因子换手率"].iloc[:, i].values, color="b") iAxes.set_title(self._Output["因子换手率"].columns[i]) if file_path is not None: Fig.savefig(file_path, dpi=150, bbox_inches='tight') return Fig def _repr_html_(self): if len(self.ArgNames) > 0: HTML = "参数设置: " HTML += '<ul align="left">' for iArgName in self.ArgNames: if iArgName != "计算时点": HTML += "<li>" + iArgName + ": " + str( self.Args[iArgName]) + "</li>" elif self.Args[iArgName]: HTML += "<li>" + iArgName + ": 自定义时点</li>" else: HTML += "<li>" + iArgName + ": 所有时点</li>" HTML += "</ul>" else: HTML = "" iHTML = self._Output["统计数据"].to_html( formatters=[_QS_formatPandasPercentage] * 5) Pos = iHTML.find(">") HTML += iHTML[:Pos] + ' align="center"' + iHTML[Pos:] Fig = self.genMatplotlibFig() # figure 保存为二进制文件 Buffer = BytesIO() plt.savefig(Buffer, bbox_inches='tight') PlotData = Buffer.getvalue() # 图像数据转化为 HTML 格式 ImgStr = "data:image/png;base64," + base64.b64encode(PlotData).decode() HTML += ('<img src="%s">' % ImgStr) return HTML
class DefaultAccount(Account): """默认证券账户""" Delay = Bool(True, arg_type="Bool", label="交易延迟", order=2) TargetIDs = ListStr(arg_type="IDList", label="目标ID", order=3) BuyLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="买入限制", order=4) SellLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="卖出限制", order=5) #Last = Enum(None, arg_type="SingleOption", label="最新价", order=6) def __init__(self, market_ft, name="默认证券账户", sys_args={}, config_file=None, **kwargs): # 继承自 Account 的属性 #self._Cash = None# 剩余现金, >=0, array(shape=(nDT+1,)) #self._FrozenCash = 0# 当前被冻结的现金, >=0, float #self._Debt = None# 负债, >=0, array(shape=(nDT+1,)) #self._CashRecord = None# 现金流记录, 现金流入为正, 现金流出为负, DataFrame(columns=["时间点", "现金流", "备注"]) #self._DebtRecord = None# 融资记录, 增加负债为正, 减少负债为负, DataFrame(columns=["时间点", "融资", "备注"]) #self._TradingRecord = None# 交易记录, DataFrame(columns=["时间点", "ID", "买卖数量", "价格", "交易费", "现金收支", "类型"]) self._IDs = [] # 本账户支持交易的证券 ID, [] self._PositionNum = None # 持仓数量, DataFrame(index=[时间点]+1, columns=self._IDs) self._PositionAmount = None # 持仓金额, DataFrame(index=[时间点]+1, columns=self._IDs) self._Turnover = None # 换手率, Series(index=[时间点]) self._Orders = None # 当前接收到的订单, DataFrame(columns=["ID", "数量", "目标价"]) self._LastPrice = None # 最新价, Series(index=self._IDs) self._BuyPrice = None # 买入成交价, Series(index=self._IDs) self._SellPrice = None # 卖出成交价, Series(index=self._IDs) self._SellVolLimit = None # 卖出成交量限制, Series(index=self._IDs) self._BuyVolLimit = None # 买入成交量限制, Series(index=self._IDs) self._MarketFT = market_ft # 提供净值或者收益率数据的因子表 self._TempData = {} # 临时数据 return super().__init__(name=name, sys_args=sys_args, config_file=config_file, **kwargs) def __QS_initArgs__(self): super().__QS_initArgs__() DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._MarketFT.getFactorMetaData(key="DataType"))) self.add_trait( "Last", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="最新价", order=6, option_range=DefaultNumFactorList)) self.Last = searchNameInStrList( DefaultNumFactorList, ['新', '收', 'Last', 'last', 'close', 'Close']) self.BuyLimit = _TradeLimit(account=self, direction="Buy") self.SellLimit = _TradeLimit(account=self, direction="Sell") def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () Rslt = super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._IDs = list(self.TargetIDs) if not self._IDs: self._IDs = self._MarketFT.getID(ifactor_name=self.Last) nDT, nID = len(dts), len(self._IDs) #self._Cash = np.zeros(nDT+1) #self._FrozenCash = 0 #self._Debt = np.zeros(nDT+1) self._PositionNum = pd.DataFrame(np.zeros((nDT + 1, nID)), index=[dts[0] - dt.timedelta(1)] + dts, columns=self._IDs) self._PositionAmount = self._PositionNum.copy() self._Turnover = pd.Series(np.zeros((nDT, )), index=dts) self._Orders = pd.DataFrame(columns=["ID", "数量", "目标价"]) self._TempData = {} if self.BuyLimit.TradePrice is None: self.BuyLimit.TradePrice, self._TempData[ "BuyPrice"] = self.Last, None if self.SellLimit.TradePrice is None: self.SellLimit.TradePrice, self._TempData[ "SellPrice"] = self.Last, None self._LastPrice = self._BuyPrice = self._SellPrice = None self._iTradingRecord = pd.DataFrame( columns=["时间点", "ID", "买卖数量", "价格", "交易费", "现金收支", "类型"]) # 暂存的交易记录 self._SellVolLimit, self._BuyVolLimit = pd.Series( np.inf, index=self._IDs), pd.Series(np.inf, index=self._IDs) self._nDT = nDT return Rslt + (self._MarketFT, ) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return self._iTradingRecord super().__QS_move__(idt, **kwargs) # 更新当前的账户信息 iIndex = self._Model.DateTimeIndex self._LastPrice = self._MarketFT.readData(factor_names=[self.Last], ids=self._IDs, dts=[idt]).iloc[0, 0] self._PositionNum.iloc[iIndex + 1] = self._PositionNum.iloc[iIndex] # 初始化持仓 if self.Delay: # 撮合成交 self._iTradingRecord = self._matchOrder(idt) self._iTradingRecord = pd.DataFrame( self._iTradingRecord, index=np.arange( self._TradingRecord.shape[0], self._TradingRecord.shape[0] + len(self._iTradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append( self._iTradingRecord) self._PositionAmount.iloc[ iIndex + 1] = self._PositionNum.iloc[iIndex + 1] * self._LastPrice return self._iTradingRecord def __QS_after_move__(self, idt, **kwargs): if self._iDT == idt: return 0 super().__QS_after_move__(idt, **kwargs) if not self.Delay: # 撮合成交 self._iTradingRecord = self._matchOrder(idt) self._iTradingRecord = pd.DataFrame( self._iTradingRecord, index=np.arange( self._TradingRecord.shape[0], self._TradingRecord.shape[0] + len(self._iTradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append( self._iTradingRecord) iIndex = self._Model.DateTimeIndex self._PositionAmount.iloc[ iIndex + 1] = self._PositionNum.iloc[iIndex + 1] * self._LastPrice return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() self.BuyLimit.TradePrice = self._TempData.pop("BuyPrice", self.BuyLimit.TradePrice) self.SellLimit.TradePrice = self._TempData.pop( "SellPrice", self.SellLimit.TradePrice) self._Output["持仓数量"] = self.getPositionNumSeries() self._Output["持仓金额"] = self.getPositionAmountSeries() self._Output["换手率"] = pd.DataFrame(self._Turnover.values, index=self._Turnover.index, columns=["换手率"]) return 0 # 当前账户价值 @property def AccountValue(self): return super().AccountValue + np.nansum( self._PositionAmount.iloc[self._Model.DateTimeIndex + 1]) # 当前账户的持仓数量 @property def PositionNum(self): return self._PositionNum.iloc[self._Model.DateTimeIndex + 1] # 当前账户的持仓金额 @property def PositionAmount(self): return self._PositionAmount.iloc[self._Model.DateTimeIndex + 1] # 本账户支持交易的证券 ID @property def IDs(self): return self._IDs # 当前最新价 @property def LastPrice(self): return self._LastPrice # 当前账户中还未成交的订单, DataFrame(index=[int], columns=["ID", "数量", "目标价"]) @property def Orders(self): return self._Orders # 获取持仓的历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓, index=[时间点], columns=[ID]) def getPositionNumSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionNum.iloc[1:self._Model.DateTimeIndex + 2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取持仓证券的金额历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓金额, index=[时间点], columns=[ID]) def getPositionAmountSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionAmount[1:self._Model.DateTimeIndex + 2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取账户价值的历史序列, 以时间点为索引 def getAccountValueSeries(self, dts=None, start_dt=None, end_dt=None): CashSeries = self.getCashSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) PositionAmountSeries = self.getPositionAmountSeries( dts=dts, start_dt=start_dt, end_dt=end_dt).sum(axis=1) DebtSeries = self.getDebtSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) return CashSeries + PositionAmountSeries - DebtSeries # 执行给定数量的证券委托单, target_id: 目标证券 ID, num: 待买卖的数量, target_price: nan 表示市价单, 返回: (订单ID, # combined_order: 组合订单, DataFrame(index=[ID], columns=[数量, 目标价]) # 基本的下单函数, 必须实现 def order(self, target_id=None, num=0, target_price=np.nan, combined_order=None): if target_id is not None: self._Orders.loc[self._Orders.shape[0]] = (target_id, num, target_price) if pd.notnull(target_price): print("警告: 本账户: '%s' 不支持限价单, 限价单将自动转为市价单!" % self.Name) return (self._Orders.shape[0], target_id, num, target_price) if combined_order is not None: if pd.notnull(combined_order["目标价"]).sum() > 0: print("警告: 本账户: '%s' 不支持限价单, 限价单将自动转为市价单!" % self.Name) combined_order.index.name = "ID" combined_order = combined_order.reset_index() combined_order.index = np.arange( self._Orders.shape[0], self._Orders.shape[0] + combined_order.shape[0]) self._Orders = self._Orders.append(combined_order) return combined_order # 撤销订单, order_ids 是订单在 self.Orders 中的 index def cancelOrder(self, order_ids): self._Orders = self._Orders.loc[self._Orders.index.difference( set(order_ids))] self._Orders.sort_index(axis=0, inplace=True) self._Orders.index = np.arange(self._Orders.shape[0]) return 0 # 更新交易限制条件 def _updateTradeLimit(self, idt): self._SellVolLimit[:] = self._BuyVolLimit[:] = np.inf TradePrice = self._MarketFT.readData( factor_names=[self.BuyLimit.TradePrice, self.SellLimit.TradePrice], ids=self._IDs, dts=[idt]).iloc[:, 0, :] self._BuyPrice, self._SellPrice = TradePrice.iloc[:, 0], TradePrice.iloc[:, 1] self._BuyVolLimit[pd.isnull(self._BuyPrice) | (self._BuyPrice <= 0)] = 0.0 # 买入成交价缺失的不能买入 self._SellVolLimit[pd.isnull(self._SellPrice) | (self._SellPrice <= 0)] = 0.0 # 卖出成交价缺失的不能卖出 if self.SellLimit.LimitIDFilter: # 满足卖出禁止条件的不能卖出 Mask = self._MarketFT.getIDMask( idt, ids=self._IDs, id_filter_str=self.SellLimit.LimitIDFilter) self._SellVolLimit[Mask] = 0.0 if self.BuyLimit.LimitIDFilter: # 满足买入禁止条件的不能买入 Mask = self._MarketFT.getIDMask( idt, ids=self._IDs, id_filter_str=self.BuyLimit.LimitIDFilter) self._BuyVolLimit[Mask] = 0.0 if self.BuyLimit.Amt is not None: # 指定了买入成交额, 成交额满足限制要求 Amount = self._MarketFT.readData(factor_names=[self.BuyLimit.Amt], ids=self._IDs, dts=[idt]).iloc[0, 0, :] self._BuyVolLimit = self._BuyVolLimit.clip_upper( Amount * self.BuyLimit.AmtLimitRatio / self._BuyPrice) if self.SellLimit.Amt is not None: # 指定了卖出成交额, 成交额满足限制要求 Amount = self._MarketFT.readData(factor_names=[self.SellLimit.Amt], ids=self._IDs, dts=[idt]).iloc[0, 0, :] self._SellVolLimit = self._SellVolLimit.clip_upper( Amount * self.SellLimit.AmtLimitRatio / self._SellPrice) if not self.SellLimit.ShortAllowed: PositionNum = self._PositionNum.iloc[self._Model.DateTimeIndex + 1] self._SellVolLimit = self._SellVolLimit.clip_upper( PositionNum.clip_lower(0.0)) return 0 # 撮合成交订单 def _matchOrder(self, idt): if self._Orders.shape[0] == 0: return [] self._updateTradeLimit(idt) MarketOrders = self._Orders.groupby( by=["ID"]).sum()["数量"] # Series(数量, index=[ID]) self._Orders = pd.DataFrame(columns=["ID", "数量", "目标价"]) return self._matchMarketOrder(idt, MarketOrders) # 撮合成交市价单 # 以成交价完成成交, 满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketOrder(self, idt, orders): IDs = orders.index.tolist() orders = orders.clip(upper=self._BuyVolLimit.loc[IDs], lower=-self._SellVolLimit.loc[IDs]) # 过滤限制条件 orders = orders[orders != 0] if orders.shape[0] == 0: return [] # 先执行卖出交易 SellPrice = self._SellPrice[orders.index] SellAmounts = (SellPrice * orders).clip_upper(0).abs() Fees = SellAmounts * self.SellLimit.TradeFee # 卖出交易费 CashChanged = SellAmounts - Fees Mask = (SellAmounts > 0) SellNums = orders.clip_upper(0) TradingRecord = list( zip([idt] * Mask.sum(), orders.index[Mask], SellNums[Mask], SellPrice[Mask], Fees[Mask], CashChanged[Mask], ["sell"] * Mask.sum())) # 再执行买入交易 BuyPrice = self._BuyPrice[orders.index] BuyAmounts = (BuyPrice * orders).clip_lower(0) CashAcquired = BuyAmounts * (1 + self.BuyLimit.TradeFee) TotalCashAcquired = CashAcquired.sum() if TotalCashAcquired > 0: AvailableCash = self.AvailableCash + CashChanged.sum() CashAllocated = min( AvailableCash, TotalCashAcquired) * CashAcquired / TotalCashAcquired BuyAmounts = CashAllocated / (1 + self.BuyLimit.TradeFee) Fees = BuyAmounts * self.BuyLimit.TradeFee BuyNums = BuyAmounts / BuyPrice Mask = (BuyAmounts > 0) TradingRecord.extend( (zip([idt] * Mask.sum(), orders.index[Mask], BuyNums[Mask], BuyPrice[Mask], Fees[Mask], -CashAllocated[Mask], ["buy"] * Mask.sum()))) else: CashAllocated = BuyNums = pd.Series(0, index=BuyAmounts.index) # 更新持仓数量和现金 iIndex = self._Model.DateTimeIndex iPosition = self._PositionNum.iloc[iIndex + 1].copy() TotalAmount = (self._BuyPrice * iPosition.clip_upper(0) + self._SellPrice * iPosition.clip_lower(0)).sum() self._Turnover.iloc[iIndex] = (SellAmounts.sum() + BuyAmounts.sum() ) / (TotalAmount + super().AccountValue) iPosition[orders.index] += BuyNums + SellNums self._PositionNum.iloc[iIndex + 1] = iPosition self._QS_updateCashDebt(CashChanged.sum() - CashAllocated.sum()) return TradingRecord
class EMFields(HasTraits): """ A collection of EM fields output from Sim4Life EM simulations """ # pylint: disable=too-many-instance-attributes #: Configuration parser. configuration = Instance(ConfigParser) #: Current participant ID. participant_id = Str() #: Path to field data file. data_path = File() #: Dictionary of data in `data_path`. data_dict = Dict() #: List of field keys that can be displayed. field_keys = ListStr() #: The currently selected field key. selected_field_key = Str() #: X values of grid in data file. x_vals = Array() #: Y values of grid in data file. y_vals = Array() #: Z values of grid in data file. z_vals = Array() #: Raw data of currently selected field from data file. data_arr = Array() #: X values of regular grid for current field. masked_gr_x = Array() #: Y values of regular grid for current field. masked_gr_y = Array() #: Z values of regular grid for current field. masked_gr_z = Array() #: Data on regular grid for current field. masked_grid_data = Array() @observe('data_path') def _update_data_path(self, event): self.data_dict = loadmat(event.new) g_z = self.data_dict['Axis0'][0] * 1000 g_y = self.data_dict['Axis1'][0] * 1000 g_x = self.data_dict['Axis2'][0] * 1000 self.x_vals = np.array([(g_x[i] + g_x[i + 1]) / 2 for i in range(g_x.size - 1)]) self.y_vals = np.array([(g_y[i] + g_y[i + 1]) / 2 for i in range(g_y.size - 1)]) self.z_vals = np.array([(g_z[i] + g_z[i + 1]) / 2 for i in range(g_z.size - 1)]) tmp = self.x_vals self.x_vals = self.z_vals self.z_vals = tmp self.field_keys = [ key for key in self.data_dict.keys() if 'Snapshot' in key and not any(field in key for field in scalar_fields) ] if self.selected_field_key not in self.field_keys: self.selected_field_key = self.field_keys[0] for key in self.field_keys: if key.lower().startswith( self._get_default_value('initial_field').lower()): self.selected_field_key = key break self.calculate_field() @observe('selected_field_key', post_init=True) def calculate_field(self, event=None): # pylint: disable=unused-argument, too-many-locals """ Calculate the current selected field values and set the grid locations and values Parameters ---------- event : A :py:class:`traits.observation.events.TraitChangeEvent` instance The trait change event for selected_field_key """ data_x, data_y, data_z = abs(self.data_dict[self.selected_field_key]).T self.data_arr = np.sqrt(data_x**2 + data_y**2 + data_z**2).reshape( self.z_vals.size, self.y_vals.size, self.x_vals.size) self.data_arr[self.data_arr == 0] = np.nan self.data_arr = np.swapaxes(self.data_arr, 0, 2) x_min = int(np.ceil(self.x_vals.min())) x_max = int(np.floor(self.x_vals.max())) y_min = int(np.ceil(self.y_vals.min())) y_max = int(np.floor(self.y_vals.max())) z_min = int(np.ceil(self.z_vals.min())) z_max = int(np.floor(self.z_vals.max())) gr_x, gr_y, gr_z = np.mgrid[x_min:x_max:len(self.x_vals) * 1j, y_min:y_max:len(self.y_vals) * 1j, z_min:z_max:len(self.z_vals) * 1j] points = np.array([[gr_x[i, j, k], gr_y[i, j, k], gr_z[i, j, k]] for i in range(gr_x.shape[0]) for j in range(gr_x.shape[1]) for k in range(gr_x.shape[2])]) interp_func = RegularGridInterpolator( (self.x_vals, self.y_vals, self.z_vals), self.data_arr) grid_data = interp_func(points).reshape(self.data_arr.shape) mask = np.all(np.isnan(grid_data), axis=(0, 1)) masked_grid_data = grid_data[:, :, ~mask] masked_gr_x = gr_x[:, :, ~mask] masked_gr_y = gr_y[:, :, ~mask] masked_gr_z = gr_z[:, :, ~mask] maskx = np.all(np.isnan(masked_grid_data), axis=(1, 2)) masked_grid_data = masked_grid_data[~maskx, :, :] masked_gr_x = masked_gr_x[~maskx, :, :] masked_gr_y = masked_gr_y[~maskx, :, :] masked_gr_z = masked_gr_z[~maskx, :, :] masky = np.all(np.isnan(masked_grid_data), axis=(0, 2)) self.masked_grid_data = masked_grid_data[:, ~masky, :] self.masked_gr_x = masked_gr_x[:, ~masky, :] self.masked_gr_y = masked_gr_y[:, ~masky, :] self.masked_gr_z = masked_gr_z[:, ~masky, :] def _get_default_value(self, option): if self.participant_id is not None: if self.participant_id not in self.configuration: self.configuration[self.participant_id] = {} val = self.configuration[self.participant_id][option] else: val = self.configuration[self.participant_id][option] return val
class MongoDB(WritableFactorDB): """MongoDB""" Name = Str("MongoDB") DBType = Enum("Mongo", arg_type="SingleOption", label="数据库类型", order=0) DBName = Str("Scorpion", arg_type="String", label="数据库名", order=1) IPAddr = Str("127.0.0.1", arg_type="String", label="IP地址", order=2) Port = Range(low=0, high=65535, value=27017, arg_type="Integer", label="端口", order=3) User = Str("root", arg_type="String", label="用户名", order=4) Pwd = Password("", arg_type="String", label="密码", order=5) CharSet = Enum("utf8", "gbk", "gb2312", "gb18030", "cp936", "big5", arg_type="SingleOption", label="字符集", order=6) Connector = Enum("default", "pymongo", arg_type="SingleOption", label="连接器", order=7) IgnoreFields = ListStr(arg_type="List", label="忽略字段", order=8) InnerPrefix = Str("qs_", arg_type="String", label="内部前缀", order=9) def __init__(self, sys_args={}, config_file=None, **kwargs): super().__init__(sys_args=sys_args, config_file=(__QS_ConfigPath__ + os.sep + "MongoDBConfig.json" if config_file is None else config_file), **kwargs) self._TableFactorDict = {} # {表名: pd.Series(数据类型, index=[因子名])} self._TableFieldDataType = {} # {表名: pd.Series(数据库数据类型, index=[因子名])} self.Name = "MongoDB" return def __getstate__(self): state = self.__dict__.copy() state["_Connection"] = (True if self.isAvailable() else False) state["_DB"] = None return state def __setstate__(self, state): super().__setstate__(state) if self._Connection: self._connect() else: self._Connection = None @property def Connection(self): if self._Connection is not None: if os.getpid() != self._PID: self._connect() # 如果进程号发生变化, 重连 return self._Connection def _connect(self): self._Connection = None if (self.Connector == "pymongo") or ((self.Connector == "default") and (self.DBType == "Mongo")): try: import pymongo self._Connection = pymongo.MongoClient(host=self.IPAddr, port=self.Port) except Exception as e: Msg = ("'%s' 尝试使用 pymongo 连接(%s@%s:%d)数据库 '%s' 失败: %s" % (self.Name, self.User, self.IPAddr, self.Port, self.DBName, str(e))) self._QS_Logger.error(Msg) if self.Connector != "default": raise e else: self._Connector = "pymongo" else: Msg = ("'%s' 连接(%s@%s:%d)数据库 '%s' 失败: %s" % (self.Name, self.User, self.IPAddr, self.Port, self.DBName, str(e))) self._QS_Logger.error(Msg) raise e self._PID = os.getpid() self._DB = self._Connection[self.DBName] return 0 def connect(self): self._connect() nPrefix = len(self.InnerPrefix) if self.DBType == "Mongo": self._TableFactorDict = {} for iTableName in self._DB.collection_names(): if iTableName[:nPrefix] == self.InnerPrefix: iTableInfo = self._DB[iTableName].find_one( {"code": "_TableInfo"}, { "datetime": 0, "code": 0, "_id": 0 }) if iTableInfo: self._TableFactorDict[ iTableName[nPrefix:]] = pd.Series({ iFactorName: iInfo["DataType"] for iFactorName, iInfo in iTableInfo.items() if iFactorName not in self.IgnoreFields }) return 0 @property def TableNames(self): return sorted(self._TableFactorDict) def getTable(self, table_name, args={}): if table_name not in self._TableFactorDict: Msg = ("因子库 '%s' 调用方法 getTable 错误: 不存在因子表: '%s'!" % (self.Name, table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) TableType = args.get("因子表类型", "宽表") if TableType == "宽表": return _WideTable(name=table_name, fdb=self, sys_args=args, logger=self._QS_Logger) else: Msg = ("因子库 '%s' 调用方法 getTable 错误: 不支持的因子表类型: '%s'" % (self.Name, TableType)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) def renameTable(self, old_table_name, new_table_name): if old_table_name not in self._TableFactorDict: Msg = ("因子库 '%s' 调用方法 renameTable 错误: 不存在因子表 '%s'!" % (self.Name, old_table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if (new_table_name != old_table_name) and (new_table_name in self._TableFactorDict): Msg = ("因子库 '%s' 调用方法 renameTable 错误: 新因子表名 '%s' 已经存在于库中!" % (self.Name, new_table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) self._DB[self.InnerPrefix + old_table_name].rename(self.InnerPrefix + new_table_name) self._TableFactorDict[new_table_name] = self._TableFactorDict.pop( old_table_name) return 0 def deleteTable(self, table_name): if table_name not in self._TableFactorDict: return 0 self._DB.drop_collection(self.InnerPrefix + table_name) self._TableFactorDict.pop(table_name, None) return 0 # 创建表, field_types: {字段名: 数据类型} def createTable(self, table_name, field_types): if self.InnerPrefix + table_name not in self._DB.collection_names(): Doc = { iField: { "DataType": iDataType } for iField, iDataType in field_types.items() } Doc.update({"datetime": None, "code": "_TableInfo"}) Collection = self._DB[self.InnerPrefix + table_name] Collection.insert(Doc) # 添加索引 if self._Connector == "pymongo": import pymongo Index1 = pymongo.IndexModel([("datetime", pymongo.ASCENDING), ("code", pymongo.ASCENDING)], name=self.InnerPrefix + "datetime_code") Index2 = pymongo.IndexModel([("code", pymongo.HASHED)], name=self.InnerPrefix + "code") try: Collection.create_indexes([Index1, Index2]) except Exception as e: self._QS_Logger.warning( "'%s' 调用方法 createTable 在数据库中创建表 '%s' 的索引时错误: %s" % (self.Name, table_name, str(e))) self._TableFactorDict[table_name] = pd.Series(field_types) return 0 # ----------------------------因子操作--------------------------------- def renameFactor(self, table_name, old_factor_name, new_factor_name): if old_factor_name not in self._TableFactorDict[table_name]: Msg = ("因子库 '%s' 调用方法 renameFactor 错误: 因子表 '%s' 中不存在因子 '%s'!" % (self.Name, table_name, old_factor_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) if (new_factor_name != old_factor_name) and ( new_factor_name in self._TableFactorDict[table_name]): Msg = ( "因子库 '%s' 调用方法 renameFactor 错误: 新因子名 '%s' 已经存在于因子表 '%s' 中!" % (self.Name, new_factor_name, table_name)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) self._DB[self.InnerPrefix + table_name].update_many( {}, {"$rename": { old_factor_name: new_factor_name }}) self._TableFactorDict[table_name][ new_factor_name] = self._TableFactorDict[table_name].pop( old_factor_name) return 0 def deleteFactor(self, table_name, factor_names): if not factor_names: return 0 FactorIndex = self._TableFactorDict.get( table_name, pd.Series()).index.difference(factor_names).tolist() if not FactorIndex: return self.deleteTable(table_name) self.deleteField(self.InnerPrefix + table_name, factor_names) for iFactorName in factor_names: self._DB[self.InnerPrefix + table_name].update_many( {}, {'$unset': { iFactorName: 1 }}) self._TableFactorDict[table_name] = self._TableFactorDict[table_name][ FactorIndex] return 0 # 增加因子,field_types: {字段名: 数据类型} def addFactor(self, table_name, field_types): if table_name not in self._TableFactorDict: return self.createTable(table_name, field_types) Doc = { iField: { "DataType": iDataType } for iField, iDataType in field_types.items() } self._DB[self.InnerPrefix + table_name].update({"code": "_TableInfo"}, {"$set": Doc}) self._TableFactorDict[table_name] = self._TableFactorDict[ table_name].append(field_types) return 0 def deleteData(self, table_name, ids=None, dts=None): Doc = {} if dts is not None: Doc["datetime"] = {"$in": dts} if ids is not None: Doc["code"] = {"$in": ids} if Doc: self._DB[self.InnerPrefix + table_name].delete_many(Doc) else: self._DB.drop_collection(self.InnerPrefix + table_name) self._TableFactorDict.pop(table_name) return 0 def writeData(self, data, table_name, if_exists="update", data_type={}, **kwargs): if table_name not in self._TableFactorDict: FieldTypes = { iFactorName: _identifyDataType(data.iloc[i].dtypes) for i, iFactorName in enumerate(data.items) } self.createTable(table_name, field_types=FieldTypes) else: NewFactorNames = data.items.difference( self._TableFactorDict[table_name].index).tolist() if NewFactorNames: FieldTypes = { iFactorName: _identifyDataType(data.iloc[i].dtypes) for i, iFactorName in enumerate(NewFactorNames) } self.addFactor(table_name, FieldTypes) AllFactorNames = self._TableFactorDict[table_name].index.tolist() OldData = self.getTable(table_name).readData( factor_names=AllFactorNames, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) if if_exists == "append": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = OldData[iFactorName].where( pd.notnull(OldData[iFactorName]), data[iFactorName]) else: data[iFactorName] = OldData[iFactorName] elif if_exists == "update": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = data[iFactorName].where( pd.notnull(data[iFactorName]), OldData[iFactorName]) else: data[iFactorName] = OldData[iFactorName] else: Msg = ("因子库 '%s' 调用方法 writeData 错误: 不支持的写入方式 '%s'!" % (self.Name, if_exists)) self._QS_Logger.error(Msg) raise __QS_Error__(Msg) NewData = {} for iFactorName in data.items: iData = data.loc[iFactorName].stack(dropna=False) NewData[iFactorName] = iData NewData = pd.DataFrame(NewData).loc[:, data.items] Mask = pd.notnull(NewData).any(axis=1) NewData = NewData[Mask] if NewData.shape[0] == 0: return 0 self.deleteData(table_name, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) NewData = NewData.reset_index() NewData.columns = ["datetime", "code"] + NewData.columns[2:].tolist() self._DB[self.InnerPrefix + table_name].insert_many( NewData.to_dict(orient="records")) return 0
class RiskAdjustedIC(IC): """风险调整的 IC""" RiskFactors = ListStr(arg_type="MultiOption", label="风险因子", order=2.5, option_range=()) def __init__(self, factor_table, name="风险调整的 IC", sys_args={}, **kwargs): return super().__init__(factor_table=factor_table, name=name, sys_args=sys_args, **kwargs) def __QS_initArgs__(self): super().__QS_initArgs__() self.remove_trait("WeightFactor") def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd PreInd = self._CurCalcInd - self.LookBack LastInd = self._CurCalcInd - 1 PreDateTime = self.CalcDTs[PreInd] LastDateTime = self.CalcDTs[LastInd] else: self._CurCalcInd = self._Model.DateTimeIndex PreInd = self._CurCalcInd - self.LookBack LastInd = self._CurCalcInd - 1 PreDateTime = self._Model.DateTimeSeries[PreInd] LastDateTime = self._Model.DateTimeSeries[LastInd] if (PreInd < 0) or (LastInd < 0): for iFactorName in self.TestFactors: self._Output["IC"][iFactorName].append(np.nan) self._Output["股票数"][iFactorName].append(np.nan) self._Output["时点"].append(idt) return 0 PreIDs = self._FactorTable.getFilteredID(idt=PreDateTime, id_filter_str=self.IDFilter) FactorExpose = self._FactorTable.readData( dts=[PreDateTime], ids=PreIDs, factor_names=list(self.TestFactors)).iloc[:, 0, :] if self.RiskFactors: RiskExpose = self._FactorTable.readData( dts=[PreDateTime], ids=PreIDs, factor_names=list(self.RiskFactors)).iloc[:, 0, :] RiskExpose["constant"] = 1.0 else: RiskExpose = pd.DataFrame(1.0, index=PreIDs, columns=["constant"]) CurPrice = self._FactorTable.readData(dts=[idt], ids=PreIDs, factor_names=[self.PriceFactor ]).iloc[0, 0, :] LastPrice = self._FactorTable.readData(dts=[LastDateTime], ids=PreIDs, factor_names=[self.PriceFactor ]).iloc[0, 0, :] Ret = CurPrice / LastPrice - 1 Mask = (pd.isnull(RiskExpose).sum(axis=1) == 0) # 展开Dummy因子 if self.IndustryFactor != "无": DummyFactorData = self._FactorTable.readData( dts=[PreDateTime], ids=PreIDs, factor_names=[self.IndustryFactor]).iloc[0, 0, :] _, _, _, DummyFactorData = prepareRegressData( np.ones(DummyFactorData.shape[0]), dummy_data=DummyFactorData.values) iMask = (pd.notnull(Ret) & Mask) Ret = Ret[iMask] iX = RiskExpose.loc[iMask].values if self.IndustryFactor != "无": iDummy = DummyFactorData[iMask.values] iDummy = iDummy[:, (np.sum(iDummy == 0, axis=0) < iDummy.shape[0])] iX = np.hstack((iX, iDummy[:, :-1])) try: Result = sm.OLS(Ret.values, iX, missing="drop").fit() except: return self._moveNone(idt) RiskAdjustedRet = pd.Series(Result.resid, index=Ret.index) for iFactorName in self.TestFactors: iFactorExpose = FactorExpose[iFactorName] iMask = (Mask & pd.notnull(iFactorExpose)) iFactorExpose = iFactorExpose[iMask] iX = RiskExpose.loc[iMask].values if self.IndustryFactor != "无": iDummy = DummyFactorData[iMask.values] iDummy = iDummy[:, ( np.sum(iDummy == 0, axis=0) < iDummy.shape[0])] iX = np.hstack((iX, iDummy[:, :-1])) try: Result = sm.OLS(iFactorExpose.values, iX, missing="drop").fit() except: self._Output["IC"][iFactorName].append(np.nan) self._Output["股票数"][iFactorName].append(0) continue iFactorExpose = pd.Series(Result.resid, index=iFactorExpose.index) self._Output["IC"][iFactorName].append( iFactorExpose.corr(RiskAdjustedRet, method=self.CorrMethod)) self._Output["股票数"][iFactorName].append( pd.notnull(iFactorExpose).sum()) self._Output["时点"].append(idt) return 0
class SQLDB(WritableFactorDB): """SQLDB""" DBType = Enum("MySQL", "SQL Server", "Oracle", "sqlite3", arg_type="SingleOption", label="数据库类型", order=0) DBName = Str("Scorpion", arg_type="String", label="数据库名", order=1) IPAddr = Str("127.0.0.1", arg_type="String", label="IP地址", order=2) Port = Range(low=0, high=65535, value=3306, arg_type="Integer", label="端口", order=3) User = Str("root", arg_type="String", label="用户名", order=4) Pwd = Password("", arg_type="String", label="密码", order=5) TablePrefix = Str("", arg_type="String", label="表名前缀", order=6) CharSet = Enum("utf8", "gbk", "gb2312", "gb18030", "cp936", "big5", arg_type="SingleOption", label="字符集", order=7) Connector = Enum("default", "cx_Oracle", "pymssql", "mysql.connector", "pymysql", "sqlite3", "pyodbc", arg_type="SingleOption", label="连接器", order=8) DSN = Str("", arg_type="String", label="数据源", order=9) SQLite3File = File(label="sqlite3文件", arg_type="File", order=10) CheckWriteData = Bool(False, arg_type="Bool", label="检查写入值", order=11) IgnoreFields = ListStr(arg_type="List", label="忽略字段", order=12) InnerPrefix = Str("qs_", arg_type="String", label="内部前缀", order=13) def __init__(self, sys_args={}, config_file=None, **kwargs): self._Connection = None# 数据库链接 self._Connector = None# 实际使用的数据库链接器 self._TableFactorDict = {}# {表名: pd.Series(数据类型, index=[因子名])} self._TableFieldDataType = {}# {表名: pd.Series(数据库数据类型, index=[因子名])} super().__init__(sys_args=sys_args, config_file=(__QS_ConfigPath__+os.sep+"SQLDBConfig.json" if config_file is None else config_file), **kwargs) self._PID = None# 保存数据库连接创建时的进程号 self.Name = "SQLDB" return def __getstate__(self): state = self.__dict__.copy() state["_Connection"] = (True if self.isAvailable() else False) return state def __setstate__(self, state): super().__setstate__(state) if self._Connection: self._connect() else: self._Connection = None # -------------------------------------------数据库相关--------------------------- def _connect(self): self._Connection = None if (self.Connector=="cx_Oracle") or ((self.Connector=="default") and (self.DBType=="Oracle")): try: import cx_Oracle self._Connection = cx_Oracle.connect(self.User, self.Pwd, cx_Oracle.makedsn(self.IPAddr, str(self.Port), self.DBName)) except Exception as e: if self.Connector!="default": raise e else: self._Connector = "cx_Oracle" elif (self.Connector=="pymssql") or ((self.Connector=="default") and (self.DBType=="SQL Server")): try: import pymssql self._Connection = pymssql.connect(server=self.IPAddr, port=str(self.Port), user=self.User, password=self.Pwd, database=self.DBName, charset=self.CharSet) except Exception as e: if self.Connector!="default": raise e else: self._Connector = "pymssql" elif (self.Connector=="mysql.connector") or ((self.Connector=="default") and (self.DBType=="MySQL")): try: import mysql.connector self._Connection = mysql.connector.connect(host=self.IPAddr, port=str(self.Port), user=self.User, password=self.Pwd, database=self.DBName, charset=self.CharSet, autocommit=True) except Exception as e: if self.Connector!="default": raise e else: self._Connector = "mysql.connector" elif self.Connector=='pymysql': try: import pymysql self._Connection = pymysql.connect(host=self.IPAddr, port=self.Port, user=self.User, password=self.Pwd, db=self.DBName, charset=self.CharSet) except Exception as e: if self.Connector!='default': raise e elif (self.Connector=="sqlite3") or ((self.Connector=="default") and (self.DBType=="sqlite3")): import sqlite3 self._Connection = sqlite3.connect(self.SQLite3File) self._Connector = "sqlite3" if self._Connection is None: if self.Connector not in ("default", "pyodbc"): self._Connection = None raise __QS_Error__("不支持该连接器(connector) : "+self.Connector) else: import pyodbc if self.DSN: self._Connection = pyodbc.connect("DSN=%s;PWD=%s" % (self.DSN, self.Pwd)) else: self._Connection = pyodbc.connect("DRIVER={%s};DATABASE=%s;SERVER=%s;UID=%s;PWD=%s" % (self.DBType, self.DBName, self.IPAddr+","+str(self.Port), self.User, self.Pwd)) self._Connector = "pyodbc" self._PID = os.getpid() return 0 def connect(self): self._connect() nPrefix = len(self.InnerPrefix) if self._Connector=="sqlite3": SQLStr = "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%s%%' ORDER BY name" Cursor = self.cursor(SQLStr % self.InnerPrefix) AllTables = Cursor.fetchall() self._TableFactorDict = {} self._TableFieldDataType = {} IgnoreFields = ["code", "datetime"]+list(self.IgnoreFields) for iTableName in AllTables: iTableName = iTableName[0][nPrefix:] Cursor.execute("PRAGMA table_info([%s])" % self.InnerPrefix+iTableName) iDataType = np.array(Cursor.fetchall()) iDataType = pd.Series(iDataType[:, 2], index=iDataType[:, 1]) iDataType = iDataType[iDataType.index.difference(IgnoreFields)] if iDataType.shape[0]>0: self._TableFieldDataType[iTableName] = iDataType.copy() iDataType[iDataType=="text"] = "string" iDataType[iDataType=="real"] = "double" self._TableFactorDict[iTableName] = iDataType elif self.DBType=="MySQL": SQLStr = ("SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE table_schema='%s' " % self.DBName) SQLStr += ("AND TABLE_NAME LIKE '%s%%' " % self.InnerPrefix) SQLStr += "AND COLUMN_NAME NOT IN ('code', 'datetime'" if len(self.IgnoreFields)>0: SQLStr += ",'"+"','".join(self.IgnoreFields)+"') " else: SQLStr += ") " SQLStr += "ORDER BY TABLE_NAME, COLUMN_NAME" Rslt = self.fetchall(SQLStr) if not Rslt: self._TableFieldDataType = {} self._TableFactorDict = {} else: self._TableFieldDataType = pd.DataFrame(np.array(Rslt), columns=["表", "因子", "DataType"]).set_index(["表", "因子"])["DataType"] self._TableFactorDict = self._TableFieldDataType.copy() Mask = (self._TableFactorDict.str.contains("char") | self._TableFactorDict.str.contains("date")) self._TableFactorDict[Mask] = "string" self._TableFactorDict[~Mask] = "double" self._TableFactorDict = {iTable[nPrefix:]:self._TableFactorDict.loc[iTable] for iTable in self._TableFactorDict.index.levels[0]} self._TableFieldDataType = {iTable[nPrefix:]:self._TableFieldDataType.loc[iTable] for iTable in self._TableFieldDataType.index.levels[0]} return 0 def disconnect(self): if self._Connection is not None: try: self._Connection.close() except Exception as e: self._QS_Logger.error("因子库 '%s' 断开错误: %s" % (self.Name, str(e))) finally: self._Connection = None return 0 def isAvailable(self): return (self._Connection is not None) def cursor(self, sql_str=None): if self._Connection is None: raise __QS_Error__("%s尚未连接!" % self.__doc__) if os.getpid()!=self._PID: self._connect()# 如果进程号发生变化, 重连 try:# 连接断开后重连 Cursor = self._Connection.cursor() except: self._connect() Cursor = self._Connection.cursor() if sql_str is None: return Cursor Cursor.execute(sql_str) return Cursor def fetchall(self, sql_str): Cursor = self.cursor(sql_str=sql_str) Data = Cursor.fetchall() Cursor.close() return Data def execute(self, sql_str): if self._Connection is None: raise __QS_Error__("%s尚未连接!" % self.__doc__) if os.getpid()!=self._PID: self._connect()# 如果进程号发生变化, 重连 try: Cursor = self._Connection.cursor() except: self._connect() Cursor = self._Connection.cursor() Cursor.execute(sql_str) self._Connection.commit() Cursor.close() return 0 # -------------------------------表的操作--------------------------------- @property def TableNames(self): return sorted(self._TableFactorDict) def getTable(self, table_name, args={}): if table_name not in self._TableFactorDict: raise __QS_Error__("表 '%s' 不存在!" % table_name) if args.get("因子表类型", "宽表")=="宽表": return _WideTable(name=table_name, fdb=self, sys_args=args, logger=self._QS_Logger) else: return _NarrowTable(name=table_name, fdb=self, sys_args=args, logger=self._QS_Logger) def renameTable(self, old_table_name, new_table_name): if old_table_name not in self._TableFactorDict: raise __QS_Error__("表: '%s' 不存在!" % old_table_name) if (new_table_name!=old_table_name) and (new_table_name in self._TableFactorDict): raise __QS_Error__("表: '"+new_table_name+"' 已存在!") SQLStr = "ALTER TABLE "+self.TablePrefix+self.InnerPrefix+old_table_name+" RENAME TO "+self.TablePrefix+self.InnerPrefix+new_table_name self.execute(SQLStr) self._TableFactorDict[new_table_name] = self._TableFactorDict.pop(old_table_name) self._TableFieldDataType[new_table_name] = self._TableFieldDataType.pop(old_table_name) return 0 # 为某张表增加索引 def addIndex(self, index_name, table_name, fields=["datetime", "code"], index_type="BTREE"): if index_type is not None: SQLStr = "CREATE INDEX "+index_name+" USING "+index_type+" ON "+self.TablePrefix+self.InnerPrefix+table_name+"("+", ".join(fields)+")" else: SQLStr = "CREATE INDEX "+index_name+" ON "+self.TablePrefix+self.InnerPrefix+table_name+"("+", ".join(fields)+")" return self.execute(SQLStr) # 创建表, field_types: {字段名: 数据类型} def createTable(self, table_name, field_types): if self.DBType=="MySQL": SQLStr = "CREATE TABLE IF NOT EXISTS %s (`datetime` DATETIME(6) NOT NULL, `code` VARCHAR(40) NOT NULL, " % (self.TablePrefix+self.InnerPrefix+table_name) for iField in field_types: SQLStr += "`%s` %s, " % (iField, field_types[iField]) SQLStr += "PRIMARY KEY (`datetime`, `code`)) ENGINE=InnoDB DEFAULT CHARSET=utf8" IndexType = "BTREE" elif self.DBType=="sqlite3": SQLStr = "CREATE TABLE IF NOT EXISTS %s (`datetime` text NOT NULL, `code` text NOT NULL, " % (self.TablePrefix+self.InnerPrefix+table_name) for iField in field_types: SQLStr += "`%s` %s, " % (iField, field_types[iField]) SQLStr += "PRIMARY KEY (`datetime`, `code`))" IndexType = None self.execute(SQLStr) try: self.addIndex(table_name+"_index", table_name, index_type=IndexType) except Exception as e: self._QS_Logger.warning("因子表 '%s' 索引创建失败: %s" % (table_name, str(e))) return 0 # 增加字段,field_types: {字段名: 数据类型} def addField(self, table_name, field_types): if table_name not in self._TableFactorDict: return self.createTable(table_name, field_types) SQLStr = "ALTER TABLE %s " % (self.TablePrefix+self.InnerPrefix+table_name) SQLStr += "ADD COLUMN (" for iField in field_types: SQLStr += "%s %s," % (iField, field_types[iField]) SQLStr = SQLStr[:-1]+")" self.execute(SQLStr) return 0 def deleteTable(self, table_name): if table_name not in self._TableFactorDict: return 0 SQLStr = 'DROP TABLE %s' % (self.TablePrefix+self.InnerPrefix+table_name) self.execute(SQLStr) self._TableFactorDict.pop(table_name, None) self._TableFieldDataType.pop(table_name, None) return 0 # 清空表 def truncateTable(self, table_name): if table_name not in self._TableFactorDict: raise __QS_Error__("表: '%s' 不存在!" % table_name) SQLStr = "TRUNCATE TABLE %s" % (self.TablePrefix+self.InnerPrefix+table_name) self.execute(SQLStr) return 0 # ----------------------------因子操作--------------------------------- def renameFactor(self, table_name, old_factor_name, new_factor_name): if old_factor_name not in self._TableFactorDict[table_name]: raise __QS_Error__("因子: '%s' 不存在!" % old_factor_name) if (new_factor_name!=old_factor_name) and (new_factor_name in self._TableFactorDict[table_name]): raise __QS_Error__("表中的因子: '%s' 已存在!" % new_factor_name) if self.DBType!="sqlite3": SQLStr = "ALTER TABLE "+self.TablePrefix+self.InnerPrefix+table_name SQLStr += " CHANGE COLUMN `"+old_factor_name+"` `"+new_factor_name+"`" self.execute(SQLStr) else: # 将表名改为临时表 SQLStr = "ALTER TABLE %s RENAME TO %s" TempTableName = genAvailableName("TempTable", self.TableNames) self.execute(SQLStr % (self.TablePrefix+self.InnerPrefix+table_name, self.TablePrefix+self.InnerPrefix+TempTableName)) # 创建新表 FieldTypes = OrderedDict() for iFactorName, iDataType in self._TableFactorDict[table_name].items(): iDataType = ("text" if iDataType=="string" else "real") if iFactorName==old_factor_name: FieldTypes[new_factor_name] = iDataType else: FieldTypes[iFactorName] = iDataType self.createTable(table_name, field_types=FieldTypes) # 导入数据 OldFactorNames = ", ".join(self._TableFactorDict[table_name].index) NewFactorNames = ", ".join(FieldTypes) SQLStr = "INSERT INTO %s (datetime, code, %s) SELECT datetime, code, %s FROM %s" Cursor = self.cursor(SQLStr % (self.TablePrefix+self.InnerPrefix+table_name, NewFactorNames, OldFactorNames, self.TablePrefix+self.InnerPrefix+TempTableName)) self._Connection.commit() # 删除临时表 Cursor.execute("DROP TABLE %s" % (self.TablePrefix+self.InnerPrefix+TempTableName, )) self._Connection.commit() Cursor.close() self._TableFactorDict[table_name][new_factor_name] = self._TableFactorDict[table_name].pop(old_factor_name) self._TableFieldDataType[table_name][new_factor_name] = self._TableFieldDataType[table_name].pop(old_factor_name) return 0 def deleteFactor(self, table_name, factor_names): if not factor_names: return 0 FactorIndex = self._TableFactorDict.get(table_name, pd.Series()).index.difference(factor_names).tolist() if not FactorIndex: return self.deleteTable(table_name) if self.DBType!="sqlite3": SQLStr = "ALTER TABLE "+self.TablePrefix+self.InnerPrefix+table_name for iFactorName in factor_names: SQLStr += " DROP COLUMN `"+iFactorName+"`," self.execute(SQLStr[:-1]) else: # 将表名改为临时表 SQLStr = "ALTER TABLE %s RENAME TO %s" TempTableName = genAvailableName("TempTable", self.TableNames) self.execute(SQLStr % (self.TablePrefix+self.InnerPrefix+table_name, self.TablePrefix+self.InnerPrefix+TempTableName)) # 创建新表 FieldTypes = OrderedDict() for iFactorName in FactorIndex: FieldTypes[iFactorName] = ("text" if self._TableFactorDict[table_name].loc[iFactorName]=="string" else "real") self.createTable(table_name, field_types=FieldTypes) # 导入数据 FactorNameStr = ", ".join(FactorIndex) SQLStr = "INSERT INTO %s (datetime, code, %s) SELECT datetime, code, %s FROM %s" Cursor = self.cursor(SQLStr % (self.TablePrefix+self.InnerPrefix+table_name, FactorNameStr, FactorNameStr, self.TablePrefix+self.InnerPrefix+TempTableName)) self._Connection.commit() # 删除临时表 Cursor.execute("DROP TABLE %s" % (self.TablePrefix+self.InnerPrefix+TempTableName, )) self._Connection.commit() Cursor.close() self._TableFactorDict[table_name] = self._TableFactorDict[table_name][FactorIndex] self._TableFieldDataType[table_name] = self._TableFieldDataType[table_name][FactorIndex] return 0 def deleteData(self, table_name, ids=None, dts=None): DBTableName = self.TablePrefix+self.InnerPrefix+table_name if (self.DBType!="sqlite3") and (ids is None) and (dts is None): SQLStr = "TRUNCATE TABLE "+DBTableName return self.execute(SQLStr) SQLStr = "DELETE * FROM "+DBTableName if dts is not None: DTs = [iDT.strftime("%Y-%m-%d %H:%M:%S.%f") for iDT in dts] SQLStr += "WHERE "+genSQLInCondition(DBTableName+".datetime", DTs, is_str=True, max_num=1000)+" " else: SQLStr += "WHERE "+DBTableName+".datetime IS NOT NULL " if ids is not None: SQLStr += "AND "+genSQLInCondition(DBTableName+".code", ids, is_str=True, max_num=1000) return self.execute(SQLStr) def _adjustWriteData(self, data): NewData = [] DataLen = data.applymap(lambda x: len(x) if isinstance(x, list) else 1).max(axis=1) for i in range(data.shape[0]): iDataLen = DataLen.iloc[i] iData = data.iloc[i].apply(lambda x: x * int(np.ceil(iDataLen / len(x))) if isinstance(x, list) else [x]*iDataLen).tolist() NewData.extend(zip(*iData)) return NewData def writeData(self, data, table_name, if_exists="update", data_type={}, **kwargs): FieldTypes = {iFactorName:_identifyDataType(self.DBType, data.iloc[i].dtypes) for i, iFactorName in enumerate(data.items)} if table_name not in self._TableFactorDict: self.createTable(table_name, field_types=FieldTypes) self._TableFactorDict[table_name] = pd.Series({iFactorName: ("string" if FieldTypes[iFactorName].find("char")!=-1 else "double") for iFactorName in FieldTypes}) self._TableFieldDataType[table_name] = pd.Series(FieldTypes) SQLStr = "INSERT INTO "+self.TablePrefix+self.InnerPrefix+table_name+" (`datetime`, `code`, " else: NewFactorNames = data.items.difference(self._TableFactorDict[table_name].index).tolist() if NewFactorNames: self.addField(table_name, {iFactorName:FieldTypes[iFactorName] for iFactorName in NewFactorNames}) NewDataType = pd.Series({iFactorName: ("string" if FieldTypes[iFactorName].find("char")!=-1 else "double") for iFactorName in NewFactorNames}) self._TableFactorDict[table_name] = self._TableFactorDict[table_name].append(NewDataType) self._TableFieldDataType[table_name] = self._TableFieldDataType[table_name].append(pd.Series(FieldTypes)) AllFactorNames = self._TableFactorDict[table_name].index.tolist() if self.CheckWriteData: OldData = self.getTable(table_name, args={"因子值类型":"list", "时间转字符串":True}).readData(factor_names=AllFactorNames, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) else: OldData = self.getTable(table_name, args={"时间转字符串":True}).readData(factor_names=AllFactorNames, ids=data.minor_axis.tolist(), dts=data.major_axis.tolist()) if if_exists=="append": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = OldData[iFactorName].where(pd.notnull(OldData[iFactorName]), data[iFactorName]) else: data[iFactorName] = OldData[iFactorName] elif if_exists=="update": for iFactorName in AllFactorNames: if iFactorName in data: data[iFactorName] = data[iFactorName].where(pd.notnull(data[iFactorName]), OldData[iFactorName]) else: data[iFactorName] = OldData[iFactorName] SQLStr = "REPLACE INTO "+self.TablePrefix+self.InnerPrefix+table_name+" (`datetime`, `code`, " data.major_axis = [iDT.strftime("%Y-%m-%d %H:%M:%S.%f") for iDT in data.major_axis] NewData = {} for iFactorName in data.items: iData = data.loc[iFactorName].stack(dropna=False) NewData[iFactorName] = iData SQLStr += "`"+iFactorName+"`, " NewData = pd.DataFrame(NewData).loc[:, data.items] NewData = NewData[pd.notnull(NewData).any(axis=1)] if NewData.shape[0]==0: return 0 NewData = NewData.astype("O").where(pd.notnull(NewData), None) if self._Connector in ("pyodbc", "sqlite3"): SQLStr = SQLStr[:-2] + ") VALUES (" + "?, " * (NewData.shape[1]+2) else: SQLStr = SQLStr[:-2] + ") VALUES (" + "%s, " * (NewData.shape[1]+2) SQLStr = SQLStr[:-2]+") " Cursor = self._Connection.cursor() if self.CheckWriteData: NewData = self._adjustWriteData(NewData.reset_index()) Cursor.executemany(SQLStr, NewData) else: Cursor.executemany(SQLStr, NewData.reset_index().values.tolist()) self._Connection.commit() Cursor.close() return 0
class ReturnBasedStyleModel(BaseModule): """基于收益率回归的风格分析模型""" # TargetNAV = Enum(None, arg_type="SingleOption", label="目标净值", order=0) TargetIDs = ListStr(arg_type="StrList", label="目标ID", order=1) # StyleNAV = Enum(None, arg_type="SingleOption", label="风格净值", order=2) StyleIDs = ListStr(arg_type="StrList", label="风格ID", order=3) ReturnType = Enum("简单收益率", "对数收益率", "价格变化量", arg_type="SingleOption", label="收益率类型", order=4) CalcDTs = List(dt.datetime, arg_type="DateList", label="计算时点", order=5) SummaryWindow = Float(240, arg_type="Integer", label="统计窗口", order=6) MinSummaryWindow = Int(20, arg_type="Integer", label="最小统计窗口", order=7) def __init__(self, target_table, style_table, name="基于收益率回归的风格分析模型", sys_args={}, **kwargs): self._TargetTable = target_table self._StyleTable = style_table return super().__init__(name=name, sys_args=sys_args, config_file=None, **kwargs) def __QS_initArgs__(self): DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._TargetTable.getFactorMetaData(key="DataType"))) self.add_trait( "TargetNAV", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="目标净值", order=0)) DefaultNumFactorList, DefaultStrFactorList = getFactorList( dict(self._StyleTable.getFactorMetaData(key="DataType"))) self.add_trait( "StyleNAV", Enum(*DefaultNumFactorList, arg_type="SingleOption", label="风格净值", order=2)) def __QS_start__(self, mdl, dts, **kwargs): if self._isStarted: return () super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._Output = {} if not self.TargetIDs: self._Output["目标ID"] = self._TargetTable.getID() else: self._Output["目标ID"] = list(self.TargetIDs) nTargetID = len(self._Output["目标ID"]) self._Output["目标净值"] = np.zeros(shape=(0, nTargetID)) if not self.StyleIDs: self._Output["风格ID"] = self._StyleTable.getID() else: self._Output["风格ID"] = list(self.StyleIDs) nStyleID = len(self._Output["风格ID"]) self._Output["风格指数净值"] = np.zeros((0, nStyleID)) self._Output["滚动回归系数"] = {iID: [] for iID in self._Output["目标ID"]} self._Output["滚动回归R平方"] = [] self._Output["时点"] = [] self._CurCalcInd = 0 return (self._StyleTable, self._TargetTable) def __QS_move__(self, idt, **kwargs): if self._iDT == idt: return 0 self._iDT = idt TargetNAV = self._TargetTable.readData( dts=[idt], ids=self._Output["目标ID"], factor_names=[self.TargetNAV]).iloc[0, :, :].values self._Output["目标净值"] = np.r_[self._Output["目标净值"], TargetNAV] StyleNAV = self._StyleTable.readData( dts=[idt], ids=self._Output["风格ID"], factor_names=[self.StyleNAV]).iloc[0, :, :].values self._Output["风格指数净值"] = np.r_[self._Output["风格指数净值"], StyleNAV] if self.CalcDTs: if idt not in self.CalcDTs[self._CurCalcInd:]: return 0 self._CurCalcInd = self.CalcDTs[self._CurCalcInd:].index( idt) + self._CurCalcInd else: self._CurCalcInd = self._Model.DateTimeIndex if self._Output["目标净值"].shape[0] - 1 < self.MinSummaryWindow: return 0 StartInd = int( max(0, self._Output["目标净值"].shape[0] - 1 - self.SummaryWindow)) X = _calcReturn(self._Output["风格指数净值"][StartInd:, :], return_type=self.ReturnType) Y = _calcReturn(self._Output["目标净值"][StartInd:, :], return_type=self.ReturnType) nTargetID, nStyleID = len(self._Output["目标ID"]), len( self._Output["风格ID"]) Rsquared = np.full((nTargetID, ), np.nan) for i, iID in enumerate(self._Output["目标ID"]): iMask = ((np.sum(pd.isnull(X), axis=1) == 0) & (pd.notnull(Y[:, i]))) try: iBeta = regressByCVX(Y[:, i], X, weight=None, constraints={ "Box": { "ub": np.ones((nStyleID, )), "lb": np.zeros((nStyleID, )) }, "LinearEq": { "Aeq": np.ones((1, nStyleID)), "beq": 1 } }) except: iBeta = None if iBeta is None: self._Output["滚动回归系数"][iID].append( np.full((nStyleID, ), np.nan)) else: self._Output["滚动回归系数"][iID].append(iBeta) Rsquared[i] = 1 - np.nansum( (Y[:, i][iMask] - np.dot(X[iMask], iBeta))**2) / np.nansum( (Y[:, i][iMask] - np.nanmean(Y[:, i][iMask]))**2) self._Output["滚动回归R平方"].append(Rsquared) self._Output["时点"].append(idt) return 0 def __QS_end__(self): if not self._isStarted: return 0 super().__QS_end__() DTs, StyleIDs, TargetIDs = self._Output.pop("时点"), self._Output.pop( "风格ID"), self._Output.pop("目标ID") nTargetID, nStyleID = len(TargetIDs), len(StyleIDs) X = _calcReturn(self._Output["风格指数净值"], return_type=self.ReturnType) Y = _calcReturn(self._Output["目标净值"], return_type=self.ReturnType) self._Output["全样本回归系数"] = np.full(shape=(nStyleID, nTargetID), fill_value=np.nan) self._Output["全样本回归R平方"] = np.full(shape=(nTargetID, ), fill_value=np.nan) for i, iID in enumerate(TargetIDs): iMask = ((np.sum(pd.isnull(X), axis=1) == 0) & (pd.notnull(Y[:, i]))) try: iBeta = regressByCVX(Y[:, i], X, weight=None, constraints={ "Box": { "ub": np.ones((nStyleID, )), "lb": np.zeros((nStyleID, )) }, "LinearEq": { "Aeq": np.ones((1, nStyleID)), "beq": 1 } }) except: iBeta = None if iBeta is not None: self._Output["全样本回归系数"][:, i] = iBeta self._Output["全样本回归R平方"][i] = 1 - np.nansum( (Y[:, i][iMask] - np.dot(X[iMask], iBeta))**2) / np.nansum( (Y[:, i][iMask] - np.nanmean(Y[:, i][iMask]))**2) self._Output["滚动回归系数"][iID] = pd.DataFrame( self._Output["滚动回归系数"][iID], index=DTs, columns=self.StyleIDs) self._Output["全样本回归系数"] = pd.DataFrame(self._Output["全样本回归系数"], index=StyleIDs, columns=TargetIDs) self._Output["全样本回归R平方"] = pd.DataFrame(self._Output["全样本回归R平方"], index=TargetIDs, columns=["全样本回归R平方"]) self._Output["滚动回归R平方"] = pd.DataFrame(self._Output["滚动回归R平方"], index=DTs, columns=TargetIDs) self._Output["目标净值"] = pd.DataFrame(self._Output["目标净值"], index=self._Model.DateTimeSeries, columns=self.TargetIDs) self._Output["风格指数净值"] = pd.DataFrame(self._Output["风格指数净值"], index=self._Model.DateTimeSeries, columns=self.StyleIDs) return 0
class SimpleAccount(Account): """简单证券账户""" Delay = Bool(True, arg_type="Bool", label="交易延迟", order=2) TargetIDs = ListStr(arg_type="IDList", label="目标ID", order=3) BuyLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="买入限制", order=4) SellLimit = Instance(_TradeLimit, allow_none=False, arg_type="ArgObject", label="卖出限制", order=5) MarketInfo = Instance(_MarketInfo, allow_none=False, arg_type="ArgObject", label="行情信息", order=6) def __init__(self, market_ft, name="简单证券账户", sys_args={}, config_file=None, **kwargs): # 继承自 Account 的属性 #self._Cash = None# 剩余现金, >=0, array(shape=(nDT+1,)) #self._FrozenCash = 0# 当前被冻结的现金, >=0, float #self._Debt = None# 负债, >=0, array(shape=(nDT+1,)) #self._CashRecord = None# 现金流记录, 现金流入为正, 现金流出为负, DataFrame(columns=["时间点", "现金流", "备注"]) #self._DebtRecord = None# 融资记录, 增加负债为正, 减少负债为负, DataFrame(columns=["时间点", "融资", "备注"]) #self._TradingRecord = None# 交易记录, DataFrame(columns=["时间点", "ID", "买卖数量", "价格", "交易费", "现金收支", "类型"]) self._IDs = [] # 本账户支持交易的证券 ID, [] self._PositionNum = None # 持仓数量, DataFrame(index=[时间点]+1, columns=self._IDs) self._PositionAmount = None # 持仓金额, DataFrame(index=[时间点]+1, columns=self._IDs) self._Orders = None # 当前接收到的订单, DataFrame(columns=["ID", "数量", "目标价"]) self._OpenPosition = None # 当前未平仓的持仓信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "浮动盈亏"]) self._ClosedPosition = None # 已平仓的交易信息, DataFrame(columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏"]) self._LastPrice = None # 最新价, Series(index=self._IDs) self._TradePrice = None # 成交价, Series(index=self._IDs) self._SellVolLimit = None # 卖出成交量限制, Series(index=self._IDs) self._BuyVolLimit = None # 买入成交量限制, Series(index=self._IDs) self._MarketFT = market_ft # 提供净值或者收益率数据的因子表 return super().__init__(name=name, sys_args=sys_args, config_file=config_file, **kwargs) def __QS_initArgs__(self): super().__QS_initArgs__() self.BuyLimit = _TradeLimit(direction="Buy") self.SellLimit = _TradeLimit(direction="Sell") self.MarketInfo = _MarketInfo(ft=self._MarketFT) def __QS_start__(self, mdl, dts, **kwargs): Rslt = super().__QS_start__(mdl=mdl, dts=dts, **kwargs) self._IDs = list(self.TargetIDs) if not self._IDs: self._IDs = self._MarketFT.getID(ifactor_name=self.MarketInfo.Last) nDT, nID = len(dts), len(self._IDs) #self._Cash = np.zeros(nDT+1) #self._FrozenCash = 0 #self._Debt = np.zeros(nDT+1) self._PositionNum = pd.DataFrame(np.zeros((nDT + 1, nID)), index=[dts[0] - dt.timedelta(1)] + dts, columns=self._IDs) self._PositionAmount = self._PositionNum.copy() self._Orders = pd.DataFrame(columns=["ID", "数量", "目标价"]) self._LastPrice = self._TradePrice = None self._OpenPosition = pd.DataFrame( columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "浮动盈亏"]) self._ClosedPosition = pd.DataFrame(columns=[ "ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏" ]) self._iTradingRecord = [] # 暂存的交易记录 self._SellVolLimit, self._BuyVolLimit = pd.Series( np.inf, index=self._IDs), pd.Series(np.inf, index=self._IDs) self._nDT = nDT return Rslt + (self._MarketFT, ) def __QS_move__(self, idt, **kwargs): super().__QS_move__(idt, **kwargs) # 更新当前的账户信息 iIndex = self._Model.DateTimeIndex self._LastPrice = self._MarketFT.readData(factor_names=[ self.MarketInfo.Last ], ids=self._IDs, dts=[idt]).iloc[0, 0] self._TradePrice = self._MarketFT.readData( factor_names=[self.MarketInfo.TradePrice], ids=self._IDs, dts=[idt]).iloc[0, 0] self._PositionNum.iloc[iIndex + 1] = self._PositionNum.iloc[iIndex] # 初始化持仓 if self.Delay: # 撮合成交 TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame( TradingRecord, index=np.arange( self._TradingRecord.shape[0], self._TradingRecord.shape[0] + len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) else: TradingRecord = self._iTradingRecord self._QS_updatePosition() return TradingRecord def __QS_after_move__(self, idt, **kwargs): super().__QS_after_move__(idt, **kwargs) if not self.Delay: # 撮合成交 TradingRecord = self._matchOrder(idt) TradingRecord = pd.DataFrame( TradingRecord, index=np.arange( self._TradingRecord.shape[0], self._TradingRecord.shape[0] + len(TradingRecord)), columns=self._TradingRecord.columns) self._TradingRecord = self._TradingRecord.append(TradingRecord) self._iTradingRecord = TradingRecord self._QS_updatePosition() return 0 def __QS_end__(self): super().__QS_end__() self._Output["持仓数量"] = self.getPositionNumSeries() self._Output["持仓金额"] = self.getPositionAmountSeries() self._Output["平仓记录"] = self._ClosedPosition self._Output["未平仓持仓"] = self._OpenPosition return 0 # 当前账户价值 @property def AccountValue(self): return super().AccountValue + np.nansum( self._PositionAmount.iloc[self._Model.DateTimeIndex + 1]) # 当前账户的持仓数量 @property def PositionNum(self): return self._PositionNum.iloc[self._Model.DateTimeIndex + 1] # 当前账户的持仓金额 @property def PositionAmount(self): return self._PositionAmount.iloc[self._Model.DateTimeIndex + 1] # 本账户支持交易的证券 ID @property def IDs(self): return self._IDs # 当前最新价 @property def LastPrice(self): return self._LastPrice # 当前账户中还未成交的订单, DataFrame(index=[int], columns=["ID", "数量", "目标价"]) @property def Orders(self): return self._Orders # 获取持仓的历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓, index=[时间点], columns=[ID]) def getPositionNumSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionNum.iloc[1:self._Model.DateTimeIndex + 2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取持仓证券的金额历史序列, 以时间点为索引, 返回: pd.DataFrame(持仓金额, index=[时间点], columns=[ID]) def getPositionAmountSeries(self, dts=None, start_dt=None, end_dt=None): Data = self._PositionAmount[1:self._Model.DateTimeIndex + 2] return cutDateTime(Data, dts=dts, start_dt=start_dt, end_dt=end_dt) # 获取账户价值的历史序列, 以时间点为索引 def getAccountValueSeries(self, dts=None, start_dt=None, end_dt=None): CashSeries = self.getCashSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) PositionAmountSeries = self.getPositionAmountSeries( dts=dts, start_dt=start_dt, end_dt=end_dt).sum(axis=1) DebtSeries = self.getDebtSeries(dts=dts, start_dt=start_dt, end_dt=end_dt) return CashSeries + PositionAmountSeries - DebtSeries # 执行给定数量的证券委托单, target_id: 目标证券 ID, num: 待买卖的数量, target_price: nan 表示市价单, 返回: (订单ID, # combined_order: 组合订单, DataFrame(index=[ID], columns=[数量, 目标价]) # 基本的下单函数, 必须实现 def order(self, target_id=None, num=0, target_price=np.nan, combined_order=None): if target_id is not None: self._Orders.loc[self._Orders.shape[0]] = (target_id, num, target_price) return (self._Orders.shape[0], target_id, num, target_price) if combined_order is not None: combined_order.index.name = "ID" combined_order = combined_order.reset_index() combined_order.index = np.arange( self._Orders.shape[0], self._Orders.shape[0] + combined_order.shape[0]) self._Orders = self._Orders.append(combined_order) return combined_order # 撤销订单, order_ids 是订单在 self.Orders 中的 index def cancelOrder(self, order_ids): self._Orders = self._Orders.loc[self._Orders.index.difference( set(order_ids))] self._Orders.sort_index(axis=0, inplace=True) self._Orders.index = np.arange(self._Orders.shape[0]) return 0 # 更新仓位信息 def _QS_updatePosition(self): iIndex = self._Model.DateTimeIndex iNum = self._OpenPosition.groupby(by=["ID"])["数量"].sum() iPosition = pd.Series(0, index=self._IDs) iPosition[iNum.index] += iNum self._PositionNum.iloc[iIndex + 1] = iPosition.values LastPrice = self._LastPrice[self._OpenPosition["ID"].tolist()].values self._OpenPosition["浮动盈亏"] = self._OpenPosition["数量"] * ( LastPrice - self._OpenPosition["开仓价格"]) iPositionAmount = pd.Series( self._OpenPosition["数量"].values * LastPrice, index=self._OpenPosition["ID"].values).groupby(axis=0, level=0).sum() self._PositionAmount.iloc[iIndex + 1] = 0.0 self._PositionAmount.iloc[iIndex + 1][iPositionAmount.index] = iPositionAmount return 0 # 更新交易限制条件 def _updateTradeLimit(self, idt): self._SellVolLimit[:] = self._BuyVolLimit[:] = np.inf if self.SellLimit.LimitIDFilter: # 满足卖出禁止条件的不能卖出 Mask = self._MarketFT.getIDMask( idt, ids=self._IDs, id_filter_str=self.SellLimit.LimitIDFilter) self._SellVolLimit[Mask] = 0.0 if self.BuyLimit.LimitIDFilter: # 满足买入禁止条件的不能买入 Mask = self._MarketFT.getIDMask( idt, ids=self._IDs, id_filter_str=self.BuyLimit.LimitIDFilter) self._BuyVolLimit[Mask] = 0.0 if self.MarketInfo.Amt is not None: # 指定了成交额, 成交额满足限制要求 Amount = self._MarketFT.readData(factor_names=[ self.MarketInfo.Amt ], ids=self._IDs, dts=[idt]).iloc[0, 0, :] self._SellVolLimit = self._SellVolLimit.clip_upper( Amount * self.SellLimit.AmtLimitRatio / self._TradePrice) self._BuyVolLimit = self._BuyVolLimit.clip_upper( Amount * self.BuyLimit.AmtLimitRatio / self._TradePrice) Mask = (pd.isnull(self._TradePrice) | (self._TradePrice <= 0)) self._SellVolLimit[Mask] = self._BuyVolLimit[Mask] = 0.0 # 成交价缺失的不能交易 if not self.SellLimit.ShortAllowed: PositionNum = self._PositionNum.iloc[self._Model.DateTimeIndex + 1] self._SellVolLimit = self._SellVolLimit.clip_upper( PositionNum.clip_lower(0.0)) return 0 # 撮合成交订单 def _matchOrder(self, idt): if self._Orders.shape[0] == 0: return [] self._updateTradeLimit(idt) MarketOrderMask = pd.isnull(self._Orders["目标价"]) MarketOrders = self._Orders[MarketOrderMask] MarketOrders = MarketOrders.groupby( by=["ID"]).sum()["数量"] # Series(数量, index=[ID]) # 先执行平仓交易 TradingRecord, MarketOpenOrders = self._matchMarketCloseOrder( idt, MarketOrders) LimitOrders = self._Orders[~MarketOrderMask] #if LimitOrders.shape[0]>0: #LimitOrders = LimitOrders.groupby(by=["ID", "目标价"]).sum() #LimitOrders = LimitOrders.reset_index(level=1) #iTradingRecord, LimitOrders = self._matchLimitCloseOrder(idt, LimitOrders) #TradingRecord.extend(iTradingRecord) # 再执行开仓交易 TradingRecord.extend(self._matchMarketOpenOrder(idt, MarketOpenOrders)) #if LimitOrders.shape[0]>0: #iTradingRecord, LimitOrders = self._matchLimitOpenOrder(idt, LimitOrders) #TradingRecord.extend(iTradingRecord) #LimitOrders = LimitOrders.reset_index() self._Orders = LimitOrders self._Orders.index = np.arange(self._Orders.shape[0]) return TradingRecord # 撮合成交市价平仓单 # 以成交价完成成交, 满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketCloseOrder(self, idt, orders): IDs = orders.index.tolist() orders = orders.clip(upper=self._BuyVolLimit.loc[IDs], lower=-self._SellVolLimit.loc[IDs]) # 过滤限制条件 orders = orders[orders != 0] if orders.shape[0] == 0: return ([], pd.Series()) # 分离平仓单和开仓单 PositionNum = self.PositionNum[orders.index] CloseOrders = orders.clip(lower=(-PositionNum).clip_upper(0), upper=(-PositionNum).clip_lower(0)) # 平仓单 OpenOrders = orders - CloseOrders # 开仓单 CloseOrders = CloseOrders[CloseOrders != 0] if CloseOrders.shape[0] == 0: return ([], OpenOrders) # 处理平仓单 TradePrice = self._TradePrice[CloseOrders.index] TradeAmounts = CloseOrders * TradePrice Fees = TradeAmounts.clip_lower( 0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper( 0).abs() * self.SellLimit.TradeFee OpenPosition = self._OpenPosition.set_index(["ID"]) ClosedPosition = pd.DataFrame(columns=[ "ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏" ]) RemainderPosition = pd.DataFrame( columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "浮动盈亏"]) CashChanged = np.zeros(CloseOrders.shape[0]) for i, iID in enumerate(CloseOrders.index): iNum = CloseOrders.iloc[i] iOpenPosition = OpenPosition.loc[[iID]] if iNum < 0: iClosedNum = (iOpenPosition["数量"] - (iOpenPosition["数量"].cumsum() + iNum).clip_lower(0)).clip_lower(0) else: iClosedNum = (iOpenPosition["数量"] - (iOpenPosition["数量"].cumsum() + iNum).clip_upper(0)).clip_upper(0) iPNL = iClosedNum * (TradePrice[iID] - iOpenPosition["开仓价格"]) CashChanged[i] = (np.abs(iClosedNum) * iOpenPosition["开仓价格"] + iPNL).sum() - Fees[iID] iClosedPosition = iOpenPosition.copy() iClosedPosition["数量"] = iClosedNum iClosedPosition["平仓时点"] = idt iClosedPosition["平仓价格"] = TradePrice[iID] iClosedPosition["平仓交易费"] = Fees[iID] iClosedPosition["平仓盈亏"] = iPNL iOpenPosition["数量"] -= iClosedNum iClosedPosition = iClosedPosition[iClosedPosition["数量"] != 0] iClosedPosition.pop("浮动盈亏") ClosedPosition = ClosedPosition.append( iClosedPosition.reset_index()) self._SellVolLimit.loc[iID] -= np.clip(iClosedNum.sum(), 0, np.inf) # 调整卖出限制条件 self._BuyVolLimit.loc[iID] += np.clip(iClosedNum.sum(), -np.inf, 0) # 调整买入限制条件 RemainderPosition = RemainderPosition.append( iOpenPosition[iOpenPosition["数量"] != 0].reset_index()) ClosedPosition.index = np.arange( self._ClosedPosition.shape[0], self._ClosedPosition.shape[0] + ClosedPosition.shape[0]) self._ClosedPosition = self._ClosedPosition.append(ClosedPosition) OpenPosition = OpenPosition.loc[OpenPosition.index.difference( CloseOrders.index)].reset_index() RemainderPosition.index = np.arange( OpenPosition.shape[0], OpenPosition.shape[0] + RemainderPosition.shape[0]) self._OpenPosition = OpenPosition.append(RemainderPosition) if self.SellLimit.ShortAllowed: self._OpenPosition = self._OpenPosition[ self._OpenPosition["数量"].abs() > _QS_MinPositionNum] else: self._OpenPosition = self._OpenPosition[ self._OpenPosition["数量"] > _QS_MinPositionNum] TradingRecord = list( zip([idt] * CloseOrders.shape[0], CloseOrders.index, CloseOrders, TradePrice, Fees, CashChanged, ["close"] * CloseOrders.shape[0])) if TradingRecord: self._QS_updateCashDebt(CashChanged.sum()) return (TradingRecord, OpenOrders) # 撮合成交市价开仓单 # 以成交价完成成交, 满足交易限制要求 # 未成交的市价单自动撤销 def _matchMarketOpenOrder(self, idt, orders): orders = orders[orders != 0] if orders.shape[0] == 0: return [] TradePrice = self._TradePrice[orders.index] TradeAmounts = orders * TradePrice FeesAcquired = TradeAmounts.clip_lower( 0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper( 0).abs() * self.SellLimit.TradeFee CashAcquired = TradeAmounts.abs() + FeesAcquired CashAllocated = min( CashAcquired.sum(), self.AvailableCash) * CashAcquired / CashAcquired.sum() orders = CashAllocated / TradePrice / (1 + self.BuyLimit.TradeFee * (orders > 0) + self.SellLimit.TradeFee * (orders < 0)) * np.sign(orders) orders = orders[pd.notnull(orders) & (orders != 0)] if orders.shape[0] == 0: return [] TradePrice = TradePrice[orders.index] TradeAmounts = orders * TradePrice Fees = TradeAmounts.clip_lower( 0) * self.BuyLimit.TradeFee + TradeAmounts.clip_upper( 0).abs() * self.SellLimit.TradeFee CashChanged = -TradeAmounts.abs() - Fees TradingRecord = list( zip([idt] * orders.shape[0], orders.index, orders, TradePrice, Fees, CashChanged, ["open"] * orders.shape[0])) NewPosition = pd.DataFrame( columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "浮动盈亏"]) NewPosition["ID"] = orders.index NewPosition["数量"] = orders.values NewPosition["开仓时点"] = idt NewPosition["开仓价格"] = TradePrice.values NewPosition["开仓交易费"] = Fees.values NewPosition["浮动盈亏"] = 0.0 NewPosition.index = np.arange( self._OpenPosition.shape[0], self._OpenPosition.shape[0] + NewPosition.shape[0]) self._OpenPosition = self._OpenPosition.append(NewPosition) self._QS_updateCashDebt(CashChanged.sum()) return TradingRecord # 撮合成交限价平仓单, TODO # 限价单的报价如果优于当前时段的成交价则可成交, 否则继续挂单 def _matchLimitCloseOrder(self, idt, orders): if orders.shape[0] == 0: return ([], orders) IDs = orders.index.unique().tolist() PositionNum = self.PositionNum RemainderOrders = pd.DataFrame(columns=["目标价", "数量"]) OpenPosition = self._OpenPosition.set_index(["ID"]) CashChanged = zeros(len(IDs)) ClosedPosition = pd.DataFrame(columns=[ "ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "平仓时点", "平仓价格", "平仓交易费", "平仓盈亏" ]) NewPosition = pd.DataFrame( columns=["ID", "数量", "开仓时点", "开仓价格", "开仓交易费", "浮动盈亏"]) for i, iID in enumerate(IDs): iOpenPosition = OpenPosition.loc[[iID]] iPositionNum = iOpenPosition["数量"].sum() iOrders = orders.loc[[iID]] iTradePrice = self._TradePrice.loc[iID] if (iPositionNum == 0) or pd.isnull(iTradePrice): # 当前无仓位 RemainderOrders = RemainderOrders.append(iOrders) continue iSellOrders = iOrders[iOrders["数量"] < 0].sort_values( by=["目标价"], ascending=True) iBuyOrders = iOrders[iOrders["数量"] > 0].sort_values( by=["目标价"], ascending=False) iClosedPosition = iOpenPosition.copy() if (iPositionNum > 0) and (iSellOrders.shape[0] > 0): # 当前多头仓位, 卖出订单为平仓单 iClosedNum = min( iSellOrders["数量"][ iSellOrders["目标价"] <= iTradePrice].abs().sum(), self._SellVolLimit.loc[iID], iPositionNum) iRemainder = (iSellOrders["数量"].cumsum() + iClosedNum).clip_upper(0.0) iRemainder.iloc[1:] = iRemainder.diff().iloc[1:] iSellOrders["数量"] = iRemainder.values RemainderOrders = RemainderOrders.append( iSellOrders[iSellOrders["数量"] != 0]) self._SellVolLimit.loc[iID] -= iClosedNum # 调整卖出限制条件 iClosedNum = (iOpenPosition["数量"] - (iOpenPosition["数量"].cumsum() - iClosedNum).clip_lower(0)).clip_lower(0) iFee = iClosedNum * iTradePrice * self.SellLimit.TradeFee elif (iPositionNum < 0) and (iBuyOrders.shape[0] > 0): iClosedNum = min( iBuyOrders["数量"][iBuyOrders["目标价"] >= iTradePrice].sum(), self._BuyVolLimit.loc[iID], -iPositionNum) iRemainder = (iBuyOrders["数量"].cumsum() - iClosedNum).clip_lower(0.0) iRemainder.iloc[1:] = iRemainder.diff().iloc[1:] iBuyOrders["数量"] = iRemainder.values RemainderOrders = RemainderOrders.append( iBuyOrders[iBuyOrders["数量"] != 0]) self._BuyVolLimit.loc[iID] -= iClosedNum # 调整卖出限制条件 iClosedNum = (iOpenPosition["数量"] - (iOpenPosition["数量"].cumsum() + iClosedNum).clip_upper(0)).clip_upper(0) iFee = iClosedNum.abs() * iTradePrice * self.BuyLimit.TradeFee iPNL = iClosedNum * (iTradePrice - iOpenPosition["开仓价格"]) CashChanged[i] = (np.abs(iClosedNum) * iOpenPosition["开仓价格"] + iPNL).sum() - iFee iClosedPosition["数量"] = iClosedNum iClosedPosition["平仓时点"] = idt iClosedPosition["平仓价格"] = iTradePrice iClosedPosition["平仓交易费"] = iFee iClosedPosition["平仓盈亏"] = iPNL iOpenPosition["数量"] -= iClosedNum iClosedPosition = iClosedPosition[iClosedPosition["数量"] != 0] iClosedPosition.pop("浮动盈亏") ClosedPosition = ClosedPosition.append( iClosedPosition.reset_index()) NewPosition = NewPosition.append( iOpenPosition[iOpenPosition["数量"] != 0].reset_index()) ClosedPosition.index = np.arange( self._ClosedPosition.shape[0], self._ClosedPosition.shape[0] + ClosedPosition.shape[0]) self._ClosedPosition = self._ClosedPosition.append(ClosedPosition) OpenPosition = OpenPosition.loc[OpenPosition.index.difference( set(IDs))].reset_index() NewPosition.index = np.arange( OpenPosition.shape[0], OpenPosition.shape[0] + NewPosition.shape[0]) self._OpenPosition = OpenPosition.append(NewPosition) if self.SellLimit.ShortAllowed: self._OpenPosition = self._OpenPosition[ self._OpenPosition["数量"].abs() > _QS_MinPositionNum] else: self._OpenPosition = self._OpenPosition[ self._OpenPosition["数量"] > _QS_MinPositionNum] TradingRecord = list( zip([idt] * CloseOrders.shape[0], CloseOrders.index, CloseOrders, TradePrice, Fees, CashChanged, ["close"] * CloseOrders.shape[0])) if TradingRecord: self._QS_updateCashDebt(CashChanged.sum()) return (TradingRecord, RemainderOrders) # 撮合成交限价开仓单, TODO # 限价单的报价如果优于当前时段的成交价则可成交, 否则继续挂单 def _matchLimitOpenOrder(self, idt, orders): return ([], pd.DataFrame(columns=["ID", "数量", "目标价"]))