mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-12 07:16:54 +08:00
pass the whole workflow
This commit is contained in:
@@ -10,6 +10,7 @@ from ...data import D
|
||||
from .account import Account
|
||||
from ...config import C
|
||||
from ...log import get_module_logger
|
||||
from ...data.dataset.utils import get_level_index
|
||||
|
||||
LOG = get_module_logger("backtest")
|
||||
|
||||
@@ -18,7 +19,8 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark)
|
||||
"""Parameters
|
||||
----------
|
||||
pred : pandas.DataFrame
|
||||
predict should has <instrument, datetime> index and one `score` column
|
||||
predict should has <datetime, instrument> index and one `score` column
|
||||
Qlib want to support multi-singal strategy in the future. So pd.Series is not used.
|
||||
strategy : Strategy()
|
||||
strategy part for backtest
|
||||
trade_exchange : Exchange()
|
||||
@@ -43,6 +45,12 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark)
|
||||
`benchmark` is str, will use the daily change as the 'bench'.
|
||||
benchmark code, default is SH000905 CSI500
|
||||
"""
|
||||
# Convert format if the input format is not expected
|
||||
if get_level_index(pred, level='datetime') == 1:
|
||||
pred = pred.swaplevel().sort_index()
|
||||
if isinstance(pred, pd.Series):
|
||||
pred = pred.to_frame('score')
|
||||
|
||||
trade_account = Account(init_cash=account)
|
||||
_pred_dates = pred.index.get_level_values(level="datetime")
|
||||
predict_dates = D.calendar(start_time=_pred_dates.min(), end_time=_pred_dates.max())
|
||||
@@ -71,10 +79,9 @@ def backtest(pred, strategy, trade_exchange, shift, verbose, account, benchmark)
|
||||
|
||||
# 1. Load the score_series at pred_date
|
||||
try:
|
||||
score = pred.loc(axis=0)[:, pred_date] # (stock_id, trade_date) multi_index, score in pdate
|
||||
score_series = score.reset_index(level="datetime", drop=True)[
|
||||
"score"
|
||||
] # pd.Series(index:stock_id, data: score)
|
||||
score = pred.loc(axis=0)[pred_date, :] # (trade_date, stock_id) multi_index, score in pdate
|
||||
score_series = score.reset_index(level="datetime",
|
||||
drop=True)["score"] # pd.Series(index:stock_id, data: score)
|
||||
except KeyError:
|
||||
LOG.warning("No score found on predict date[{:%Y-%m-%d}]".format(trade_date))
|
||||
score_series = None
|
||||
|
||||
@@ -15,6 +15,7 @@ from .backtest.backtest import backtest as backtest_func, get_date_range
|
||||
|
||||
from ..data import D
|
||||
from ..config import C
|
||||
from ..data.dataset.utils import get_level_index
|
||||
|
||||
logger = get_module_logger("Evaluate")
|
||||
|
||||
@@ -158,11 +159,11 @@ def get_exchange(
|
||||
if deal_price[0] != "$":
|
||||
deal_price = "$" + deal_price
|
||||
if extract_codes:
|
||||
codes = sorted(pred.index.get_level_values(0).unique())
|
||||
codes = sorted(pred.index.get_level_values('instrument').unique())
|
||||
else:
|
||||
codes = "all" # TODO: We must ensure that 'all.txt' includes all the stocks
|
||||
|
||||
dates = sorted(pred.index.get_level_values(1).unique())
|
||||
dates = sorted(pred.index.get_level_values('datetime').unique())
|
||||
dates = np.append(dates, get_date_range(dates[-1], shift=shift))
|
||||
|
||||
exchange = Exchange(
|
||||
@@ -187,7 +188,7 @@ def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, **k
|
||||
|
||||
# backtest workflow related or commmon arguments
|
||||
pred : pandas.DataFrame
|
||||
predict should has <instrument, datetime> index and one `score` column
|
||||
predict should has <datetime, instrument> index and one `score` column
|
||||
account : float
|
||||
init account value
|
||||
shift : int
|
||||
@@ -297,6 +298,8 @@ def long_short_backtest(
|
||||
"short": short_returns(excess),
|
||||
"long_short": long_short_returns}
|
||||
"""
|
||||
if get_level_index(pred, level='datetime') == 1:
|
||||
pred = pred.swaplevel().sort_index()
|
||||
|
||||
if trade_unit is None:
|
||||
trade_unit = C.trade_unit
|
||||
@@ -333,13 +336,13 @@ def long_short_backtest(
|
||||
ls_returns = {}
|
||||
|
||||
for pdate, date in zip(predict_dates, trade_dates):
|
||||
score = pred.loc(axis=0)[:, pdate]
|
||||
score = pred.loc(axis=0)[pdate, :]
|
||||
score = score.reset_index().sort_values(by="score", ascending=False)
|
||||
|
||||
long_stocks = list(score.iloc[:topk]["instrument"])
|
||||
short_stocks = list(score.iloc[-topk:]["instrument"])
|
||||
|
||||
score = score.set_index(["instrument", "datetime"]).sort_index()
|
||||
score = score.set_index(["datetime", "instrument"]).sort_index()
|
||||
|
||||
long_profit = []
|
||||
short_profit = []
|
||||
@@ -363,7 +366,7 @@ def long_short_backtest(
|
||||
else:
|
||||
short_profit.append(-profit)
|
||||
|
||||
for stock in list(score.loc(axis=0)[:, pdate].index.get_level_values(level=0)):
|
||||
for stock in list(score.loc(axis=0)[pdate, :].index.get_level_values(level=0)):
|
||||
# exclude the suspend stock
|
||||
if trade_exchange.check_stock_suspended(stock_id=stock, trade_date=date):
|
||||
continue
|
||||
|
||||
@@ -1,91 +1,60 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import lightgbm as lgb
|
||||
from sklearn.metrics import roc_auc_score, mean_squared_error
|
||||
|
||||
from ...model.base import Model
|
||||
from ...utils import drop_nan_by_y_index
|
||||
from ...data.dataset import DatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
|
||||
|
||||
class LGBModel(Model):
|
||||
"""LightGBM Model
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param_update : dict
|
||||
training parameters
|
||||
"""
|
||||
|
||||
_params = dict()
|
||||
|
||||
"""LightGBM Model"""
|
||||
def __init__(self, loss="mse", **kwargs):
|
||||
if loss not in {"mse", "binary"}:
|
||||
raise NotImplementedError
|
||||
self._scorer = mean_squared_error if loss == "mse" else roc_auc_score
|
||||
self._params.update(objective=loss, **kwargs)
|
||||
self._model = None
|
||||
self._params = {'objective': loss}
|
||||
self._params.update(kwargs)
|
||||
self.model = None
|
||||
|
||||
def fit(self,
|
||||
dataset: DatasetH,
|
||||
num_boost_round=1000,
|
||||
early_stopping_rounds=50,
|
||||
verbose_eval=20,
|
||||
evals_result=dict(),
|
||||
**kwargs):
|
||||
|
||||
df_train, df_valid = dataset.prepare(['train', 'valid'],
|
||||
col_set=['feature', 'label'],
|
||||
data_key=DataHandlerLP.DK_L)
|
||||
x_train, y_train = df_train['feature'], df_train['label']
|
||||
x_valid, y_valid = df_valid['feature'], df_valid['label']
|
||||
|
||||
def fit(
|
||||
self,
|
||||
x_train,
|
||||
y_train,
|
||||
x_valid,
|
||||
y_valid,
|
||||
w_train=None,
|
||||
w_valid=None,
|
||||
num_boost_round=1000,
|
||||
early_stopping_rounds=50,
|
||||
verbose_eval=20,
|
||||
evals_result=dict(),
|
||||
**kwargs
|
||||
):
|
||||
# Lightgbm need 1D array as its label
|
||||
if y_train.values.ndim == 2 and y_train.values.shape[1] == 1:
|
||||
y_train_1d, y_valid_1d = np.squeeze(y_train.values), np.squeeze(y_valid.values)
|
||||
else:
|
||||
raise ValueError("LightGBM doesn't support multi-label training")
|
||||
|
||||
w_train_weight = None if w_train is None else w_train.values
|
||||
w_valid_weight = None if w_valid is None else w_valid.values
|
||||
|
||||
dtrain = lgb.Dataset(x_train.values, label=y_train_1d, weight=w_train_weight)
|
||||
dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d, weight=w_valid_weight)
|
||||
self._model = lgb.train(
|
||||
self._params,
|
||||
dtrain,
|
||||
num_boost_round=num_boost_round,
|
||||
valid_sets=[dtrain, dvalid],
|
||||
valid_names=["train", "valid"],
|
||||
early_stopping_rounds=early_stopping_rounds,
|
||||
verbose_eval=verbose_eval,
|
||||
evals_result=evals_result,
|
||||
**kwargs
|
||||
)
|
||||
dtrain = lgb.Dataset(x_train.values, label=y_train_1d)
|
||||
dvalid = lgb.Dataset(x_valid.values, label=y_valid_1d)
|
||||
self.model = lgb.train(self._params,
|
||||
dtrain,
|
||||
num_boost_round=num_boost_round,
|
||||
valid_sets=[dtrain, dvalid],
|
||||
valid_names=["train", "valid"],
|
||||
early_stopping_rounds=early_stopping_rounds,
|
||||
verbose_eval=verbose_eval,
|
||||
evals_result=evals_result,
|
||||
**kwargs)
|
||||
evals_result["train"] = list(evals_result["train"].values())[0]
|
||||
evals_result["valid"] = list(evals_result["valid"].values())[0]
|
||||
|
||||
def predict(self, x_test):
|
||||
if self._model is None:
|
||||
def predict(self, dataset):
|
||||
if self.model is None:
|
||||
raise ValueError("model is not fitted yet!")
|
||||
return self._model.predict(x_test.values)
|
||||
|
||||
def score(self, x_test, y_test, w_test=None):
|
||||
# Remove rows from x, y and w, which contain Nan in any columns in y_test.
|
||||
x_test, y_test, w_test = drop_nan_by_y_index(x_test, y_test, w_test)
|
||||
preds = self.predict(x_test)
|
||||
w_test_weight = None if w_test is None else w_test.values
|
||||
return self._scorer(y_test.values, preds, sample_weight=w_test_weight)
|
||||
|
||||
def save(self, filename):
|
||||
if self._model is None:
|
||||
raise ValueError("model is not fitted yet!")
|
||||
self._model.save_model(filename)
|
||||
|
||||
def load(self, buffer):
|
||||
self._model = lgb.Booster(params={"model_str": buffer.decode("utf-8")})
|
||||
x_test = dataset.prepare('test', col_set='feature')
|
||||
return pd.Series(self.model.predict(np.squeeze(x_test.values)), index=x_test.index)
|
||||
|
||||
Reference in New Issue
Block a user