总览
其中 workflow 是 qlib 的重点之一,可以分成以下的三个部分,可以从这三个层次来学习源码
- Data
- Loading
- Processing
- Slicing
- Model
- Training and inference
- Saving & loading
- Evaluation
- Forecast signal analysis
- Backtest
Config
类图
默认配置
代码语言:javascript复制_default_config = {
# data provider config
"calendar_provider": "LocalCalendarProvider",
"instrument_provider": "LocalInstrumentProvider",
"feature_provider": "LocalFeatureProvider",
"pit_provider": "LocalPITProvider",
"expression_provider": "LocalExpressionProvider",
"dataset_provider": "LocalDatasetProvider",
"provider": "LocalProvider",
# config it in qlib.init()
# "provider_uri" str or dict:
# # str
# "~/.qlib/stock_data/cn_data"
# # dict
# {"day": "~/.qlib/stock_data/cn_data", "1min": "~/.qlib/stock_data/cn_data_1min"}
# NOTE: provider_uri priority:
# 1. backend_config: backend_obj["kwargs"]["provider_uri"]
# 2. backend_config: backend_obj["kwargs"]["provider_uri_map"]
# 3. qlib.init: provider_uri
"provider_uri": "",
# cache
"expression_cache": None,
"calendar_cache": None,
# for simple dataset cache
"local_cache_path": None,
# kernels can be a fixed value or a callable function lie `def (freq: str) -> int`
# If the kernels are arctic_kernels, `min(NUM_USABLE_CPU, 30)` may be a good value
"kernels": NUM_USABLE_CPU,
# pickle.dump protocol version
"dump_protocol_version": PROTOCOL_VERSION,
# How many tasks belong to one process. Recommend 1 for high-frequency data and None for daily data.
"maxtasksperchild": None,
# If joblib_backend is None, use loky
"joblib_backend": "multiprocessing",
"default_disk_cache": 1, # 0:skip/1:use
"mem_cache_size_limit": 500,
"mem_cache_limit_type": "length",
# memory cache expire second, only in used 'DatasetURICache' and 'client D.calendar'
# default 1 hour
"mem_cache_expire": 60 * 60,
# cache dir name
"dataset_cache_dir_name": "dataset_cache",
"features_cache_dir_name": "features_cache",
# redis
# in order to use cache
"redis_host": "127.0.0.1",
"redis_port": 6379,
"redis_task_db": 1,
# This value can be reset via qlib.init
"logging_level": logging.INFO,
# Global configuration of qlib log
# logging_level can control the logging level more finely
"logging_config": {
"version": 1,
"formatters": {
"logger_format": {
"format": "[%(process)s:%(threadName)s](%(asctime)s) %(levelname)s - %(name)s - [%(filename)s:%(lineno)d] - %(message)s"
}
},
"filters": {
"field_not_found": {
"()": "qlib.log.LogFilter",
"param": [".*?WARN: data not found for.*?"],
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": logging.DEBUG,
"formatter": "logger_format",
"filters": ["field_not_found"],
}
},
"loggers": {"qlib": {"level": logging.DEBUG, "handlers": ["console"]}},
# To let qlib work with other packages, we shouldn't disable existing loggers.
# Note that this param is default to True according to the documentation of logging.
"disable_existing_loggers": False,
},
# Default config for experiment manager
"exp_manager": {
"class": "MLflowExpManager",
"module_path": "qlib.workflow.expm",
"kwargs": {
"uri": "file:" str(Path(os.getcwd()).resolve() / "mlruns"),
"default_exp_name": "Experiment",
},
},
"pit_record_type": {
"date": "I", # uint32
"period": "I", # uint32
"value": "d", # float64
"index": "I", # uint32
},
"pit_record_nan": {
"date": 0,
"period": 0,
"value": float("NAN"),
"index": 0xFFFFFFFF,
},
# Default config for MongoDB
"mongo": {
"task_url": "mongodb://localhost:27017/",
"task_db_name": "default_task_db",
},
# Shift minute for highfreq minute data, used in backtest
# if min_data_shift == 0, use default market time [9:30, 11:29, 1:00, 2:59]
# if min_data_shift != 0, use shifted market time [9:30, 11:29, 1:00, 2:59] - shift*minute
"min_data_shift": 0,
}
例子配置
代码语言:javascript复制qlib_init:
provider_uri: "~/.qlib/qlib_data/cn_data"
region: cn
market: &market csi300
benchmark: &benchmark SH000300
data_handler_config: &data_handler_config
start_time: 2008-01-01
end_time: 2020-08-01
fit_start_time: 2008-01-01
fit_end_time: 2014-12-31
instruments: *market
port_analysis_config: &port_analysis_config
strategy:
class: TopkDropoutStrategy
module_path: qlib.contrib.strategy
kwargs:
model: <MODEL>
dataset: <DATASET>
topk: 50
n_drop: 5
backtest:
start_time: 2017-01-01
end_time: 2020-08-01
account: 100000000
benchmark: *benchmark
exchange_kwargs:
limit_threshold: 0.095
deal_price: close
open_cost: 0.0005
close_cost: 0.0015
min_cost: 5
task:
model:
class: LGBModel
module_path: qlib.contrib.model.gbdt
kwargs:
loss: mse
colsample_bytree: 0.8879
learning_rate: 0.2
subsample: 0.8789
lambda_l1: 205.6999
lambda_l2: 580.9768
max_depth: 8
num_leaves: 210
num_threads: 20
dataset:
class: DatasetH
module_path: qlib.data.dataset
kwargs:
handler:
class: Alpha158
module_path: qlib.contrib.data.handler
kwargs: *data_handler_config
segments:
train: [2008-01-01, 2014-12-31]
valid: [2015-01-01, 2016-12-31]
test: [2017-01-01, 2020-08-01]
record:
- class: SignalRecord
module_path: qlib.workflow.record_temp
kwargs:
model: <MODEL>
dataset: <DATASET>
- class: SigAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
ana_long_short: False
ann_scaler: 252
- class: PortAnaRecord
module_path: qlib.workflow.record_temp
kwargs:
config: *port_analysis_config
- qlib 由配置驱动,所以配置很重要,尤其是 `qlib/config.py: _default_config` 很重要,具体运行时使用的 class 是动态的,由 `xxProvider` 提供, 比如 CalendarProvider, InstrumentProvider, FeatureProvider, PITProvider, ExpressionProvider, DatasetProvider, BaseProvider
- Config 类似 attrDict 让 config 字典表现得像一个类
- QlibConfig 继承 Config; 重要的是 `register` 函数,会做具体的 初始化操作
- register_all_ops:注册所有的 op(即自定义表达式里面用的运算符)到 OpsWrapper
- OpsWrapper 相当于是一个 ops 的 registry
- register_all_wrappers: 是指根据配置把所有的 wrapper 都注入好,这里的 wrapper 是一个封装类,里面的行为由具体的 provider 来提供,这样 可以替换具体的 provider 而代码不需要修改就能改为行为
- 这里其实有点奇怪,用基类也能解决这个问题,不知道为什么多此一举,把代码的可读性降低了很多
- 具体在 register_wrapper 的时候,会根据配置把对应的类初始化,然后注入到 Wrapper 里面,所以正如前面说的,读代码一定要先读配置,因为整个流程是配置(字符串)串连起来的(这样代码无论可读性,可维护性其实都非常差,但是好处是,使用端一个配置就能跑起来,看上去很灵活--其实未必,这个配置其实就是在表达代码了,而且显然没有一段 python 代码表达得更好)
- R.register(QlibRecorder)
- register_all_ops:注册所有的 op(即自定义表达式里面用的运算符)到 OpsWrapper
- 运行过程中,项目的 Config 可以用 C 来引用,下面补充一下几个重要的类似 "C" 的实例
名字 | 含义 | 作用 |
---|---|---|
C | 配置 | 驱动整个流程运行的配置 |
Cal | 日历 | 加载日历数据,涉及文件读取 |
Inst | 股票池 | 加载股票池数据,涉及文件读取 |
FeatureD | 特征 | 加载原始 feature 数据,涉及文件读取 |
PITD | PointInTime 数据 | 加载 PITD 数据 |
ExpressionD | 表达式引擎 | 计算表达式 |
DatasetD | 数据集 | 加载训练推理所需数据 |
D | BaseProvider | 所有没被拆分的 Provider 功能都在这个 Provider 提供 |
R | QlibRecorder | 是整个流程中最重要的管理器,用于管理实验、实验记录 |
Data
DatasetProvider
类图
分析
- 基类 DatasetProvider 中最重要的函数是 dataset_processor,用于加载和处理股票数据,它从给定的数据中提取指定时间范围内的各种股票数据,如开盘价、收盘价、成交量等等。在函数内部,数据加载和计算任务会被分配到多个子进程中进行并行计算,以提高数据处理的速度和效率。最后,它将所有仪器的数据汇总成一个 DataFrame,并且根据需要将其缓存起来,以便下次使用时能够更快地加载数据。
Storage
类图
分析
- qlib 的存储设计是使用 二进制文件来存储 Feature, Instrument 和 Calendar 使用文本存储, 具体例子依次如下图
- BaseStorage、CalendarStorage、FeatureStorage、InstrumentStorage 基本都是空基类,没有具体的实现;FileStorageMixin 是个辅助类,提供如 uri check 等功能
- FileCalendarStorage 定义了 Calendar 的文件存储方式,按行写的文本,比较简单
- FileInstrumentStorage 定义 股票池的文件存储方式,按行写的 csv 格式,格式为 股票id,start_time, end_time,时间范围表示有数据的时间范围(start_time 不同有可能是上市时间)
- FileFeatureStorage 定义了 Feature 的存储数据,这是最核心的股票数据,以股票 ID 为文件夹,以 Feature 的名字 Freq 频率为文件名,存储二进制形式的数据。核心存储格式就一句话 np.hstack([index, data_array]).astype("<f").tofile(fp); 在被调用 __getitem__ 函数的时候会读文件
- 调用流程从下到上如下
- self.backend_obj(instrument=instrument, field=field, freq=freq)[start_index : end_index 1]
- <= 再上层是 LocalFeatureProvider: feature(self, instrument, field, start_index, end_index, freq)
- <= Feature._load_internal(self, instrument, start_index, end_index, freq)
- <= Expression.load(self, instrument, start_index, end_index, *args)
- <= NpPairOperator._load_internal(self, instrument, start_index, end_index, *args) : series_left = self.feature_left.load(instrument, start_index, end_index, *args)
- <= LocalExpressionProvider.expression.load(instrument, query_start, query_end, freq)
- <= DatasetProvider: obj[field] = ExpressionD.expression(inst, field, start_time, end_time, freq)
- <= 这里触发了并发,以上在子线程并发执行
- <=DatasetProvider.dataset_processor: delayed(DatasetProvider.inst_calculator)(inst, start_time, end_time, freq, normalize_column_names, spans, C, inst_processors) 这里会使用多线程并发执行 => DatasetProvider.inst_calculator
- <=BaseProvider.features => DatasetD.dataset(instruments, fields, start_time, end_time, freq, inst_processors=inst_processors)
- <=QlibDataLoader.load => QlibDataLoader.load_group_df
- <=DataHandler.__init__ => DataHandlerLP.setup_data => DataHandler.setup_data
- <=DataHandler = init_instance_by_config(handler, accept_types=DataHandler)
- <=Dataset = init_instance_by_config(task_config["dataset"], accept_types=Dataset) DatasetH 初始化的时候会初始化 DataHandler
- <=task_train => _exe_task(task_config)
- 从上面的流程可以看出,实际上读取文件的操作是在 Dataset 初始化的时候触发,使用 DataHandler 来加载数据,加载的时候会解析 Expression,最终到 Feature,Feature 则对应到文件。
- 调用流程从下到上如下
- 这些是具体的直接关联到文件读写操作的类,
Dataset
类图
分析
- Dataset 相关的类很多 包括 Dataset,DataHandler,DataLoader,DatasetProvider 等等,分别作用如下
- Dataset: dataset 是其中最上层的概念,用于对应到模型训练或者推理使用的一组数据集(这些数据是下层的 DataHandler 处理的);
- Dataset 本身是一个抽象类
- DatasetH 是一个具体的类,其中 H 得意思就是 DataHandler,表示 DatasetH 其实就是 DataHandler 的包装,具体的处理操作应该是 DataHandler 来完成。DatasetH 初始化的时候也会初始化 DataHandler,DataHandler 在初始化的时候会调用 setup_data 函数(也可以通过 DatasetH 的 setup_data 函数调用 );DatasetH 的另一个重要函数是 prepare 函数,这里主要作用是处理 segment(即 train、valid、test);内部调用 DataHandler 的 fetch 函数
- DataHandler:
- 重要的是三个函数 __init__, setup_data (__init__ 里面会调用 setup_data) 和 fetch
- __init__: 内部会初始化 dataloader, 默认对 init_data 即调用 setup_data
- setup_data:这是里面最核心的函数,会调用 dataloader 进行 load 操作,即加载数据
- fetch: 这个是从已经加载了的数据里面获取关心的数据的函数,setup_data 之后实际上数据已经在 _data 了,比如调用 pandas 的函数取数据
- 从上面的类图可以看出 DataHandler 子类非常多,一般在实例中使用子类 DataHandlerLP 或者 DataHandlerLP 的子类,比如 Alpha158
- DataHandlerLP 中的 LP 意思是 (L)earnable (P)rocessor, 包括一般的_data ,他会产生三个数据,而且 learn,infer 数据可以有不同的 processors,即数据预处理 processor(比如 MinMaxNorm,DropNA 等等),这些 processor 在 __init__ 的时候会被动态初始化(调用 fit 函数会依次调用这些 processor,而且支持几种调用方式,比如一个 processor 的输出是另一个的输入,fit 函数会在 setup_data 里面被调用,所以核心还是 __init__、setup_data、fetch 这三个函数)
- DK_R / self._data: the raw data loaded from the loader
- DK_I / self._infer: the data processed for inference
- DK_L / self._learn: the data processed for learning model.
- Alpha158 是 DataHandlerLP 的一个子类,在 contrib 包里,这个子类预设了很多 feature,比如 'kbar', 'price', 'volume', 'rolling', 这些 feature 配置其实是一组 expression 的组合,比如 kbar 包括了 ($close-$open)/$open、($high-$low)/$open、$close-$open)/($high-$low 1e-12) 等等;这部分预设 feature 配置很有用,解释如下:
- 重要的是三个函数 __init__, setup_data (__init__ 里面会调用 setup_data) 和 fetch
- Dataset: dataset 是其中最上层的概念,用于对应到模型训练或者推理使用的一组数据集(这些数据是下层的 DataHandler 处理的);
Alpha158 是指一个包含158种因子(factor)的多因子模型,它通过对股票基本面、技术面和市场因素等多个方面的分析,计算出一个股票的预期收益率。Alpha158 的目标是利用这些因子来打败市场的平均收益率。
Alpha360 则是另一个多因子模型,它包含360种因子,比 Alpha158 更加复杂。Alpha360 的目标也是通过多因子分析来预测股票的收益率,以期在市场中获得超额收益。
- DataLoader:
- 如果是用的 DataHandlerLP 需要在配置里面制定 Dataloader 的配置,比如像下面的配置,但是更多时候是直接写死在 DataHandlerLP 的子类里面的,比如 Alpha158
- DataLoader
- DataLoader 是个抽象基类,只有一个函数 load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame:
- DLWParser 是 DataLoader 的一个子类,还是抽象类,表示 (D)ata(L)oader (W)ith (P)arser for features and names,留了一个重要的方法 load_group_df 给子类来实现
- QlibDataLoader 是 DLWParser 的子类,实现了 load_group_df 方法,这个类是实际使用中最常用的 Load Class,他实现的 load_group_df 又回到了 D 也就是 BaseProvider: instruments = D.instruments(instruments, filter_pipe=self.filter_pipe) ;; df = D.features(instruments, exprs, start_time, end_time, freq=freq, inst_processors=inst_processors) 至此,具体的获取 instrument 和 features 的功能又委托给了 InstrumentProvider 和 DatasetProvider 实现了功能由 Provider 实现的解耦(下面我们会看到 DatasetProvider 由把数据的获取委托给了 ExpressionProvider 简直套娃)
- Dataset => BaseProvider => InstrumentProvider/DatasetProvider => ExpressionProvider => FeatureProvider
- DataLoaderDH 可以组合多个 datahandler 来加载数据
- StaticDataLoader:可以直接从文件中加载数据,而无须委托给 DatasetProvider 下面的配置里给了一个例子
- DataLoader 是个抽象基类,只有一个函数 load(self, instruments, start_time=None, end_time=None) -> pd.DataFrame:
data_handler_config: &data_handler_config
start_time: 2008-01-01
end_time: 2020-08-01
instruments: *market
data_loader:
class: QlibDataLoader
kwargs:
config:
feature:
- ["Resi($close, 15)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5) 1e-12)", "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume 1), 5)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1) 1), 5)", "Corr($close, Log($volume 1), 10)", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1) 1), 60)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1) 1), 10)", "Corr($close, Log($volume 1), 20)", "(Less($open, $close)-$low)/$open"]
- ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"]
label:
- ["Ref($close, -2)/Ref($close, -1) - 1"]
- ["LABEL0"]
freq: day
代码语言:javascript复制data_loader_config: &data_loader_config
class: StaticDataLoader
module_path: qlib.data.dataset.loader
kwargs:
config:
feature: data/feature.pkl
label: data/label.pkl
- DatasetProvider: 其中对外最重要的函数是 dataset 但是父类中并没有提供实现,以子类 LocalDatasetProvider 为例(ClientDatasetProvider 的区别是 dataset 方法变成了网络请求),dataset 方法实际只是执行 去 InstrumentProvider list_instruments 然后 去 CalProvider 获取 calendar, 再运行 dataset_processor
- dataset_processor 是这个类的核心,通过这个函数,耗时的加载和计算操作被 ParallelExt 变成了并发函数,被并发函数的核心则是
- inst_calculator:其中的运算核心可以简化为下面的函数:用 ExpressionProvider 查 feature,CalendarProvider 查 cal,然后做 process 处理
- dataset_processor 是这个类的核心,通过这个函数,耗时的加载和计算操作被 ParallelExt 变成了并发函数,被并发函数的核心则是
for field in column_names: # The client does not have expression provider, the data will be loaded from cache using static method. obj[field] = ExpressionD.expression(inst, field, start_time, end_time, freq) data = pd.DataFrame(obj) data.index = Cal.calendar(freq=freq)[data.index.values.astype(int)] data.index.names = ["datetime"] for _processor in inst_processors: data = init_instance_by_config(_processor, accept_types=InstProcessor)(data, instrument=inst) return data
Expression & Feature
类图
分析
- Expression 是计算如 ($close-$open)/$open、($high-$low)/$open 这样的表达式的核心
- Feature 是 Expression 的一个子类,表示这种 Expression 需要通过 ExpressionProvider 来加载(文件)
- ExpressionOps 是一个空的类,没有任何实现,表示这种 Expression 是通过计算获取的,大部分Expression 的实现都在这个子类下面,ExpressionOps有5个直接子类:
- ElemOperator:表示一元运算符,比如 Not,Abs 等,大部分是通过 NpElemOperator 这个子类实现,表示使用 numpy 来进行计算
- PairOperator:表示二元运算符,比如 加减乘除等,大部分通过 NpPairOperator 这个子类实现,表示使用 numpy 来进行计算
- Rolling:表示一元 Rolling 运算符,Rolling 表示是有滚动窗口的(即计算一定的时间窗口内的统计值),比如: Mean($close, 10) 表示计算过去10填的时间窗口的平均值,Ref($close, 2) 表示前2天的 $close 值 ,内部大部分使用 pandas 来实现,少量通过 numpy 来实现。
- N=0 的时候表示 Expanding,即 累进计算,类似 cumsum,可参考 Pandas进阶之窗口函数rolling()和expanding()
- PairRolling:表示二元 Rolling 运算符,Corr,Cov 等
- If:条件表达式,根据条件取 left 或者 right,使用 np.where 实现
- ExpressionProvider:LocalExpressionProvider 的处理流程为:
- 首先看 expression(即 field )有没有缓存,有缓存直接从缓存获取
- 如果没有缓存,则解析表达式,里面用了一个正则表达式把 $ 符号做了替换,比如 $open $close -> Feature("open") Feature("close"),然后运行了 eval (这个有严重的安全风险),即表达式变成了 Feature 运算的代码 即 通过 Expression 的运算符重载,进行计算,变成 Add(Feature("open"), Feature("close")),Add 会分别 Load 左边和右边的 Feature 出来,然后进行 Add 运算
- Feature("open") 是无法继续解析的元素了,他的 load 会调用 FeatureProvider 的函数 feature(self, instrument, field, start_time, end_time, freq) 进行加载,这里的加载调用函数 self.backend_obj(instrument=instrument, field=field, freq=freq)[start_index : end_index 1] 这里面有个索引操作,实际会调用到默认的 backend 也就是 FileFeatureProvider 的 __getitem__ 函数,在这个函数里面会进行实际的文件读取操作,也就是读取某个 instrument 的 feature open 对应的文件
Processor
类图
分析
- Processor 仍然是数据处理的一部分,这部分比较好理解,就是对数据对一些预处理,processor 定义有两种方式,一种是重载 fit 方法,另一种是重载 __call__ 方法(本身可以是一个可以被调用的函数),内部使用 pandas 实现,比如 DropnaProcessor 使用 df.dropna 方法,FillNa 则使用 df.fillna 方法
Model
BaseModel
类图
- BaseModel 代表了模型的抽象基类,只有两个抽象方法 fit: 训练,predict:预测
分析
- Model,大部分模型都继承自这个子类,还是一个抽象类,没有任何实现,这种抽象类的子类,大部分是为了区分类型,没有其他含义
- 实现的模型非常多,比如 LGBModel、CatBoostModel、TCN、LSTM、ALSTM、SFM、AdaRNN 等等,大部分模型通过 pytorch 实现,少部分通过如 xgboost、gbdt、catboost 等包实现
- 各种模型的学习 TODO
- RiskModel 用来计算股票回报的协方差矩阵:股票的协方差矩阵是一个矩阵,其中每个元素表示两只股票之间的协方差,也就是它们在一段时间内的价格变化趋势是否类似。它是金融学中用于衡量资产间风险相关性的一种重要工具。协方差矩阵可以告诉我们各个股票之间的相关性,从而帮助投资者构建更加优化的投资组合。通常情况下,资产相关性越小,对组合风险的分散作用越大,因此投资者通常会尽量选择具有低相关性的资产来构建投资组合,降低整体风险。除了标准方式,RiskModel 的子类给了几种计算协方差的优化方式,比如
- Structured Covariance:一定结构化形式的协方差矩阵,它不是完全随机的,而是具有某种规律或特殊的性质。例如,对于金融市场中的资产,协方差矩阵通常显示出某些结构化的模式,例如因子结构或稀疏结构等。这种结构化的协方差矩阵可以更好地反映资产之间的相关性和行情变化,从而提高投资组合的效果和风险管理的能力。在实际应用中,Structured Covariance常常用于构建投资组合、风险分析和金融衍生品定价等领域
- Shrinkage Covariance是指对样本协方差矩阵进行收缩(Shrinkage)处理后得到的新协方差矩阵。这种方法被广泛应用于金融、统计和信号处理等领域,用于解决当样本量较少时,估计协方差矩阵不准确的问题。Shrinkage Covariance通过对样本协方差矩阵进行加权平均,使其更接近某些特定结构化形式的协方差矩阵
- Principal Orthogonal Complement Thresholding(POET)是一种用于估计协方差矩阵的方法。它是基于阈值的估计方法,可以有效地处理高维数据和带有噪声或缺失值的数据集。POET的核心思想是利用特征值分解和奇异值分解等技术,将协方差矩阵分解为两部分:一个是主要成分,另一个是与主要成分正交的残差项。在分解过程中,通过设置适当的阈值来控制残差项的影响,从而得到更加准确的协方差矩阵估计值。POET方法具有许多优点,如可扩展性强、计算效率高、对于大规模数据集和复杂模型表现良好等。它已经成功地应用于生物统计学、图像处理、金融分析等领域,并取得了不错的效果。
- FeatureInt:表示 (Int)erpreter,继承了这个类的模型,需要实现一个 get_feature_importance 方法,表示模型是可解释的,比如 LGBModel 就实现了这个方法
Evaluation
Backtest
类图
分析
backtest 表示回测,里面有几个类分别代表了几个重要的概念
类名 | 概念 | 说明 |
---|---|---|
Exchange | 交易所 | 提供交易需要的相关信息,比如开仓费用率、平仓费用率、最低交易费用、交易额/量限制同时也提供了回测相关的信息,比如 回测的频率、开始结束时间、股票代码、交易价格 |
Account | 账户 | 提供账户相关的信息,比如初始现金金额、仓位 |
Order | 订单 | 某一个股票交易订单,提供如股票Id,下单数量,下单方向,下单的起止时间等信息 |
Positon | 持仓 | 当前股票的持仓,属于 account 的一部分 |
BaseTradeDecision | 交易决策 | 由 Strategy 产生,并由 Executor 执行,提供的信息主要是 OrderList |
BaseExecutor | 交易执行器 | 会执行交易生成并生成指标,依赖 Account,Exchange 这些基础组件 |
Signal | 交易信号 | 用来帮助 Strategy 获取生成 decision 的信号,比如模型的预测值等 |
Strategy | 交易策略 | 这是这里面的核心,有很多子类,用来生成交易决策,即 TradeDecision |
BaseInfrastructure | 基础设施 | 这个东西本质是一个 Map,存储了Executor 执行所需要的一些基础依赖,比如 Account 和 Exchange |
TradeCalendarManager |
- 核心流程:
- for _decision in collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value),这里的核心就是 trade_strategy 和 trade_executor 一个是策略,一个是执行器
- 生成交易 Decision: trade_decision = trade_strategy.generate_trade_decision(_execute_result)
- 以 TopkDropoutStrategy 为例:
- 首先会获取 trade_start_time, trade_end_time 表示交易时间 T
- pred_start_time, pred_end_time 表示需要获取预测数据的时间,在这里是 T-1, 因为预测是预测用 T-1 数据预测 T 1: $close 对 T: $close 的涨幅
- 取出预测的 score 并进行排名,选出排名最高的一些,即为要买的;持仓中排名最低的一些,就是要卖的
- 卖就是卖全量,买是按照金额等分到要买的股票上,交易价格都是 $close(默认),假定以收盘价买入或者卖出
- 买卖会考虑涨跌停、停牌等不能交易的情况
- 生成并返回 TradeDecisionWO(OrderList)
- 以 TopkDropoutStrategy 为例:
- 生成交易 Decision: trade_decision = trade_strategy.generate_trade_decision(_execute_result)
In the Alpha158, Qlib uses the label Ref($close, -2)/Ref($close, -1) - 1 that means the change from T 1 to T 2, rather than Ref($close, -1)/$close - 1, of which the reason is that when getting the T day close price of a china stock, the stock can be bought on T 1 day and sold on T 2 day.
- 执行 Decision: _execute_result = yield from trade_executor.collect_data(_trade_decision, level=0)
- 使用 trade_exchange.deal_order 处理订单,返回 trade_val:交易额, rade_cost:费用, trade_price:价格
- 这里会更新持仓 position,账户 account 等信息
- 返回执行结果 (order, trade_val, trade_cost, trade_price)
- 使用 trade_exchange.deal_order 处理订单,返回 trade_val:交易额, rade_cost:费用, trade_price:价格
- 执行 Decision 后处理:trade_strategy.post_exe_step(_execute_result)
- 大部分策略没有实现