1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-17 01:14:35 +08:00

Refine RL todos (#1332)

* Refine several todos

* CI issues

* Remove Dropna limitation of `quote_df` in Exchange  (#1334)

* Remove Dropna limitation of `quote_df` of Exchange

* Impreove docstring

* Fix type error when expression is specified (#1335)

* Refine fill_missing_data()

* Remove several TODO comments

* Add back env for interpreters

* Change Literal import

* Resolve PR comments

* Move  to SAOEState

* Add Trainer.get_policy_state_dict()

* Mypy issue

Co-authored-by: you-n-g <you-n-g@users.noreply.github.com>
This commit is contained in:
Huoran Li
2022-11-10 21:10:11 +08:00
committed by GitHub
parent 49a5bccfec
commit 35794846ff
20 changed files with 461 additions and 530 deletions

View File

@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Union
import pandas as pd import pandas as pd
from .account import Account from .account import Account
from .report import Indicator, PortfolioMetrics
if TYPE_CHECKING: if TYPE_CHECKING:
from ..strategy.base import BaseStrategy from ..strategy.base import BaseStrategy
@@ -20,7 +19,7 @@ if TYPE_CHECKING:
from ..config import C from ..config import C
from ..log import get_module_logger from ..log import get_module_logger
from ..utils import init_instance_by_config from ..utils import init_instance_by_config
from .backtest import backtest_loop, collect_data_loop from .backtest import INDICATOR_METRIC, PORT_METRIC, backtest_loop, collect_data_loop
from .decision import Order from .decision import Order
from .exchange import Exchange from .exchange import Exchange
from .utils import CommonInfrastructure from .utils import CommonInfrastructure
@@ -223,7 +222,7 @@ def backtest(
account: Union[float, int, dict] = 1e9, account: Union[float, int, dict] = 1e9,
exchange_kwargs: dict = {}, exchange_kwargs: dict = {},
pos_type: str = "Position", pos_type: str = "Position",
) -> Tuple[PortfolioMetrics, Indicator]: ) -> Tuple[PORT_METRIC, INDICATOR_METRIC]:
"""initialize the strategy and executor, then backtest function for the interaction of the outermost strategy and """initialize the strategy and executor, then backtest function for the interaction of the outermost strategy and
executor in the nested decision execution executor in the nested decision execution
@@ -256,9 +255,9 @@ def backtest(
Returns Returns
------- -------
portfolio_metrics_dict: Dict[PortfolioMetrics] portfolio_dict: PORT_METRIC
it records the trading portfolio_metrics information it records the trading portfolio_metrics information
indicator_dict: Dict[Indicator] indicator_dict: INDICATOR_METRIC
it computes the trading indicator it computes the trading indicator
It is organized in a dict format It is organized in a dict format
@@ -273,8 +272,7 @@ def backtest(
exchange_kwargs, exchange_kwargs,
pos_type=pos_type, pos_type=pos_type,
) )
portfolio_metrics, indicator = backtest_loop(start_time, end_time, trade_strategy, trade_executor) return backtest_loop(start_time, end_time, trade_strategy, trade_executor)
return portfolio_metrics, indicator
def collect_data( def collect_data(

View File

@@ -3,12 +3,12 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Generator, Optional, Tuple, Union, cast from typing import Dict, TYPE_CHECKING, Generator, Optional, Tuple, Union, cast
import pandas as pd import pandas as pd
from qlib.backtest.decision import BaseTradeDecision from qlib.backtest.decision import BaseTradeDecision
from qlib.backtest.report import Indicator, PortfolioMetrics from qlib.backtest.report import Indicator
if TYPE_CHECKING: if TYPE_CHECKING:
from qlib.strategy.base import BaseStrategy from qlib.strategy.base import BaseStrategy
@@ -19,30 +19,35 @@ from tqdm.auto import tqdm
from ..utils.time import Freq from ..utils.time import Freq
PORT_METRIC = Dict[str, Tuple[pd.DataFrame, dict]]
INDICATOR_METRIC = Dict[str, Tuple[pd.DataFrame, Indicator]]
def backtest_loop( def backtest_loop(
start_time: Union[pd.Timestamp, str], start_time: Union[pd.Timestamp, str],
end_time: Union[pd.Timestamp, str], end_time: Union[pd.Timestamp, str],
trade_strategy: BaseStrategy, trade_strategy: BaseStrategy,
trade_executor: BaseExecutor, trade_executor: BaseExecutor,
) -> Tuple[PortfolioMetrics, Indicator]: ) -> Tuple[PORT_METRIC, INDICATOR_METRIC]:
"""backtest function for the interaction of the outermost strategy and executor in the nested decision execution """backtest function for the interaction of the outermost strategy and executor in the nested decision execution
please refer to the docs of `collect_data_loop` please refer to the docs of `collect_data_loop`
Returns Returns
------- -------
portfolio_metrics: PortfolioMetrics portfolio_dict: PORT_METRIC
it records the trading portfolio_metrics information it records the trading portfolio_metrics information
indicator: Indicator indicator_dict: INDICATOR_METRIC
it computes the trading indicator it computes the trading indicator
""" """
return_value: dict = {} return_value: dict = {}
for _decision in collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value): for _decision in collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value):
pass pass
portfolio_metrics = cast(PortfolioMetrics, return_value.get("portfolio_metrics")) portfolio_dict = cast(PORT_METRIC, return_value.get("portfolio_dict"))
indicator = cast(Indicator, return_value.get("indicator")) indicator_dict = cast(INDICATOR_METRIC, return_value.get("indicator_dict"))
return portfolio_metrics, indicator
return portfolio_dict, indicator_dict
def collect_data_loop( def collect_data_loop(
@@ -89,14 +94,17 @@ def collect_data_loop(
if return_value is not None: if return_value is not None:
all_executors = trade_executor.get_all_executors() all_executors = trade_executor.get_all_executors()
all_portfolio_metrics = {
"{}{}".format(*Freq.parse(_executor.time_per_step)): _executor.trade_account.get_portfolio_metrics() portfolio_dict: PORT_METRIC = {}
for _executor in all_executors indicator_dict: INDICATOR_METRIC = {}
if _executor.trade_account.is_port_metr_enabled()
} for executor in all_executors:
all_indicators = {} key = "{}{}".format(*Freq.parse(executor.time_per_step))
for _executor in all_executors: if executor.trade_account.is_port_metr_enabled():
key = "{}{}".format(*Freq.parse(_executor.time_per_step)) portfolio_dict[key] = executor.trade_account.get_portfolio_metrics()
all_indicators[key] = _executor.trade_account.get_trade_indicator().generate_trade_indicators_dataframe()
all_indicators[key + "_obj"] = _executor.trade_account.get_trade_indicator() indicator_df = executor.trade_account.get_trade_indicator().generate_trade_indicators_dataframe()
return_value.update({"portfolio_metrics": all_portfolio_metrics, "indicator": all_indicators}) indicator_obj = executor.trade_account.get_trade_indicator()
indicator_dict[key] = (indicator_df, indicator_obj)
return_value.update({"portfolio_dict": portfolio_dict, "indicator_dict": indicator_dict})

View File

@@ -26,6 +26,15 @@ from .high_performance_ds import BaseQuote, NumpyQuote
class Exchange: class Exchange:
# `quote_df` is a pd.DataFrame class that contains basic information for backtesting
# After some processing, the data will later be maintained by `quote_cls` object for faster data retriving.
# Some conventions for `quote_df`
# - $close is for calculating the total value at end of each day.
# - if $close is None, the stock on that day is reguarded as suspended.
# - $factor is for rounding to the trading unit;
# - if any $factor is missing when $close exists, trading unit rounding will be disabled
quote_df: pd.DataFrame
def __init__( def __init__(
self, self,
freq: str = "day", freq: str = "day",
@@ -159,6 +168,7 @@ class Exchange:
self.codes = codes self.codes = codes
# Necessary fields # Necessary fields
# $close is for calculating the total value at end of each day. # $close is for calculating the total value at end of each day.
# - if $close is None, the stock on that day is reguarded as suspended.
# $factor is for rounding to the trading unit # $factor is for rounding to the trading unit
# $change is for calculating the limit of the stock # $change is for calculating the limit of the stock
@@ -199,7 +209,7 @@ class Exchange:
self.end_time, self.end_time,
freq=self.freq, freq=self.freq,
disk_cache=True, disk_cache=True,
).dropna(subset=["$close"]) )
self.quote_df.columns = self.all_fields self.quote_df.columns = self.all_fields
# check buy_price data and sell_price data # check buy_price data and sell_price data
@@ -209,7 +219,7 @@ class Exchange:
self.logger.warning("{} field data contains nan.".format(pstr)) self.logger.warning("{} field data contains nan.".format(pstr))
# update trade_w_adj_price # update trade_w_adj_price
if self.quote_df["$factor"].isna().any(): if (self.quote_df["$factor"].isna() & ~self.quote_df["$close"].isna()).any():
# The 'factor.day.bin' file not exists, and `factor` field contains `nan` # The 'factor.day.bin' file not exists, and `factor` field contains `nan`
# Use adjusted price # Use adjusted price
self.trade_w_adj_price = True self.trade_w_adj_price = True
@@ -245,9 +255,9 @@ class Exchange:
assert set(self.extra_quote.columns) == set(self.quote_df.columns) - {"$change"} assert set(self.extra_quote.columns) == set(self.quote_df.columns) - {"$change"}
self.quote_df = pd.concat([self.quote_df, self.extra_quote], sort=False, axis=0) self.quote_df = pd.concat([self.quote_df, self.extra_quote], sort=False, axis=0)
LT_TP_EXP = "(exp)" # Tuple[str, str] LT_TP_EXP = "(exp)" # Tuple[str, str]: the limitation is calculated by a Qlib expression.
LT_FLT = "float" # float LT_FLT = "float" # float: the trading limitation is based on `abs($change) < limit_threshold`
LT_NONE = "none" # none LT_NONE = "none" # none: there is no trading limitation
def _get_limit_type(self, limit_threshold: Union[tuple, float, None]) -> str: def _get_limit_type(self, limit_threshold: Union[tuple, float, None]) -> str:
"""get limit type""" """get limit type"""
@@ -261,20 +271,25 @@ class Exchange:
raise NotImplementedError(f"This type of `limit_threshold` is not supported") raise NotImplementedError(f"This type of `limit_threshold` is not supported")
def _update_limit(self, limit_threshold: Union[Tuple, float, None]) -> None: def _update_limit(self, limit_threshold: Union[Tuple, float, None]) -> None:
# $close is may contains NaN, the nan indicates that the stock is not tradable at that timestamp
suspended = self.quote_df["$close"].isna()
# check limit_threshold # check limit_threshold
limit_type = self._get_limit_type(limit_threshold) limit_type = self._get_limit_type(limit_threshold)
if limit_type == self.LT_NONE: if limit_type == self.LT_NONE:
self.quote_df["limit_buy"] = False self.quote_df["limit_buy"] = suspended
self.quote_df["limit_sell"] = False self.quote_df["limit_sell"] = suspended
elif limit_type == self.LT_TP_EXP: elif limit_type == self.LT_TP_EXP:
# set limit # set limit
limit_threshold = cast(tuple, limit_threshold) limit_threshold = cast(tuple, limit_threshold)
self.quote_df["limit_buy"] = self.quote_df[limit_threshold[0]] # astype bool is necessary, because quote_df is an expression and could be float
self.quote_df["limit_sell"] = self.quote_df[limit_threshold[1]] self.quote_df["limit_buy"] = self.quote_df[limit_threshold[0]].astype("bool") | suspended
self.quote_df["limit_sell"] = self.quote_df[limit_threshold[1]].astype("bool") | suspended
elif limit_type == self.LT_FLT: elif limit_type == self.LT_FLT:
limit_threshold = cast(float, limit_threshold) limit_threshold = cast(float, limit_threshold)
self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold) self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold) | suspended
self.quote_df["limit_sell"] = self.quote_df["$change"].le(-limit_threshold) # pylint: disable=E1130 self.quote_df["limit_sell"] = (
self.quote_df["$change"].le(-limit_threshold) | suspended
) # pylint: disable=E1130
@staticmethod @staticmethod
def _get_vol_limit(volume_threshold: Union[tuple, dict, None]) -> Tuple[Optional[list], Optional[list], set]: def _get_vol_limit(volume_threshold: Union[tuple, dict, None]) -> Tuple[Optional[list], Optional[list], set]:
@@ -338,8 +353,18 @@ class Exchange:
- if direction is None, check if tradable for buying and selling. - if direction is None, check if tradable for buying and selling.
- if direction == Order.BUY, check the if tradable for buying - if direction == Order.BUY, check the if tradable for buying
- if direction == Order.SELL, check the sell limit for selling. - if direction == Order.SELL, check the sell limit for selling.
Returns
-------
True: the trading of the stock is limted (maybe hit the highest/lowest price), hence the stock is not tradable
False: the trading of the stock is not limited, hence the stock may be tradable
""" """
# NOTE:
# **all** is used when checking limitation.
# For example, the stock trading is limited in a day if every miniute is limited in a day if every miniute is limited.
if direction is None: if direction is None:
# The trading limitation is related to the trading direction
# if the direction is not provided, then any limitation from buy or sell will result in trading limitation
buy_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_buy", method="all") buy_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_buy", method="all")
sell_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_sell", method="all") sell_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_sell", method="all")
return bool(buy_limit or sell_limit) return bool(buy_limit or sell_limit)
@@ -356,10 +381,24 @@ class Exchange:
start_time: pd.Timestamp, start_time: pd.Timestamp,
end_time: pd.Timestamp, end_time: pd.Timestamp,
) -> bool: ) -> bool:
"""if stock is suspended(hence not tradable), True will be returned"""
# is suspended # is suspended
if stock_id in self.quote.get_all_stock(): if stock_id in self.quote.get_all_stock():
return self.quote.get_data(stock_id, start_time, end_time, "$close") is None # suspended stocks are represented by None $close stock
# The $close may contains NaN,
close = self.quote.get_data(stock_id, start_time, end_time, "$close")
if close is None:
# if no close record exists
return True
elif isinstance(close, IndexData):
# **any** non-NaN $close represents trading opportunity may exists
# if all returned is nan, then the stock is suspended
return cast(bool, cast(IndexData, close).isna().all())
else:
# it is single value, make sure is is not None
return np.isnan(close)
else: else:
# if the stock is not in the stock list, then it is not tradable and regarded as suspended
return True return True
def is_stock_tradable( def is_stock_tradable(

View File

@@ -8,23 +8,22 @@ import os
import pickle import pickle
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union, cast
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import torch import torch
from joblib import Parallel, delayed from joblib import Parallel, delayed
from qlib.typehint import Literal from qlib.backtest import INDICATOR_METRIC, collect_data_loop, get_strategy_executor
from qlib.backtest import collect_data_loop, get_strategy_executor
from qlib.backtest.decision import BaseTradeDecision, Order, OrderDir, TradeRangeByTime from qlib.backtest.decision import BaseTradeDecision, Order, OrderDir, TradeRangeByTime
from qlib.backtest.executor import BaseExecutor, NestedExecutor, SimulatorExecutor from qlib.backtest.executor import SimulatorExecutor
from qlib.backtest.high_performance_ds import BaseOrderIndicator from qlib.backtest.high_performance_ds import BaseOrderIndicator
from qlib.rl.contrib.naive_config_parser import get_backtest_config_fromfile from qlib.rl.contrib.naive_config_parser import get_backtest_config_fromfile
from qlib.rl.contrib.utils import read_order_file from qlib.rl.contrib.utils import read_order_file
from qlib.rl.data.integration import init_qlib from qlib.rl.data.integration import init_qlib
from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution
from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper from qlib.typehint import Literal
def _get_multi_level_executor_config( def _get_multi_level_executor_config(
@@ -61,15 +60,6 @@ def _get_multi_level_executor_config(
return executor_config return executor_config
def _set_env_for_all_strategy(executor: BaseExecutor) -> None:
if isinstance(executor, NestedExecutor):
if hasattr(executor.inner_strategy, "set_env"):
env = CollectDataEnvWrapper()
env.reset()
executor.inner_strategy.set_env(env)
_set_env_for_all_strategy(executor.inner_executor)
def _convert_indicator_to_dataframe(indicator: dict) -> Optional[pd.DataFrame]: def _convert_indicator_to_dataframe(indicator: dict) -> Optional[pd.DataFrame]:
record_list = [] record_list = []
for time, value_dict in indicator.items(): for time, value_dict in indicator.items():
@@ -94,9 +84,10 @@ def _convert_indicator_to_dataframe(indicator: dict) -> Optional[pd.DataFrame]:
return records return records
# TODO: there should be richer annotation for the input (e.g. report) and the returned report def _generate_report(
# TODO: For example, @ dataclass with typed fields and detailed docstrings. decisions: List[BaseTradeDecision],
def _generate_report(decisions: List[BaseTradeDecision], report_indicators: List[dict]) -> dict: report_indicators: List[INDICATOR_METRIC],
) -> Dict[str, Tuple[pd.DataFrame, pd.DataFrame]]:
"""Generate backtest reports """Generate backtest reports
Parameters Parameters
@@ -109,28 +100,25 @@ def _generate_report(decisions: List[BaseTradeDecision], report_indicators: List
------- -------
""" """
indicator_dict = defaultdict(list) indicator_dict: Dict[str, List[pd.DataFrame]] = defaultdict(list)
indicator_his = defaultdict(list) indicator_his: Dict[str, List[dict]] = defaultdict(list)
for report_indicator in report_indicators: for report_indicator in report_indicators:
for key, value in report_indicator.items(): for key, (indicator_df, indicator_obj) in report_indicator.items():
if key.endswith("_obj"): indicator_dict[key].append(indicator_df)
indicator_his[key].append(value.order_indicator_his) indicator_his[key].append(indicator_obj.order_indicator_his)
else:
indicator_dict[key].append(value)
report = {} report = {}
decision_details = pd.concat([getattr(d, "details") for d in decisions if hasattr(d, "details")]) decision_details = pd.concat([getattr(d, "details") for d in decisions if hasattr(d, "details")])
for key in ["1min", "5min", "30min", "1day"]: for key in indicator_dict:
if key not in indicator_dict: cur_dict = pd.concat(indicator_dict[key])
continue cur_his = pd.concat([_convert_indicator_to_dataframe(his) for his in indicator_his[key]])
report[key] = pd.concat(indicator_dict[key])
report[key + "_obj"] = pd.concat([_convert_indicator_to_dataframe(his) for his in indicator_his[key + "_obj"]])
cur_details = decision_details[decision_details.freq == key].set_index(["instrument", "datetime"]) cur_details = decision_details[decision_details.freq == key].set_index(["instrument", "datetime"])
if len(cur_details) > 0: if len(cur_details) > 0:
cur_details.pop("freq") cur_details.pop("freq")
report[key + "_obj"] = report[key + "_obj"].join(cur_details, how="outer") cur_his = cur_his.join(cur_details, how="outer")
report[key] = (cur_dict, cur_his)
return report return report
@@ -209,25 +197,25 @@ def single_with_simulator(
exchange_config=exchange_config, exchange_config=exchange_config,
qlib_config=None, qlib_config=None,
cash_limit=None, cash_limit=None,
backtest_mode=True,
) )
reports.append(simulator.report_dict) reports.append(simulator.report_dict)
decisions += simulator.decisions decisions += simulator.decisions
indicator = {k: v for report in reports for k, v in report["indicator"]["1day_obj"].order_indicator_his.items()} indicator_1day_objs = [report["indicator"]["1day"][1] for report in reports]
records = _convert_indicator_to_dataframe(indicator) indicator_info = {k: v for obj in indicator_1day_objs for k, v in obj.order_indicator_his.items()}
records = _convert_indicator_to_dataframe(indicator_info)
assert records is None or not np.isnan(records["ffr"]).any() assert records is None or not np.isnan(records["ffr"]).any()
if generate_report: if generate_report:
report = _generate_report(decisions, [report["indicator"] for report in reports]) _report = _generate_report(decisions, [report["indicator"] for report in reports])
if split == "stock": if split == "stock":
stock_id = orders.iloc[0].instrument stock_id = orders.iloc[0].instrument
report = {stock_id: report} report = {stock_id: _report}
else: else:
day = orders.iloc[0].datetime day = orders.iloc[0].datetime
report = {day: report} report = {day: _report}
return records, report return records, report
else: else:
@@ -312,22 +300,22 @@ def single_with_collect_data_loop(
exchange_kwargs=exchange_config, exchange_kwargs=exchange_config,
pos_type="Position" if cash_limit is not None else "InfPosition", pos_type="Position" if cash_limit is not None else "InfPosition",
) )
_set_env_for_all_strategy(executor=executor)
report_dict: dict = {} report_dict: dict = {}
decisions = list(collect_data_loop(trade_start_time, trade_end_time, strategy, executor, report_dict)) decisions = list(collect_data_loop(trade_start_time, trade_end_time, strategy, executor, report_dict))
records = _convert_indicator_to_dataframe(report_dict["indicator"]["1day_obj"].order_indicator_his) indicator_dict = cast(INDICATOR_METRIC, report_dict.get("indicator_dict"))
records = _convert_indicator_to_dataframe(indicator_dict["1day"][1].order_indicator_his)
assert records is None or not np.isnan(records["ffr"]).any() assert records is None or not np.isnan(records["ffr"]).any()
if generate_report: if generate_report:
report = _generate_report(decisions, [report_dict["indicator"]]) _report = _generate_report(decisions, [indicator_dict])
if split == "stock": if split == "stock":
stock_id = orders.iloc[0].instrument stock_id = orders.iloc[0].instrument
report = {stock_id: report} report = {stock_id: _report}
else: else:
day = orders.iloc[0].datetime day = orders.iloc[0].datetime
report = {day: report} report = {day: _report}
return records, report return records, report
else: else:
return records return records
@@ -337,7 +325,7 @@ def backtest(backtest_config: dict, with_simulator: bool = False) -> pd.DataFram
order_df = read_order_file(backtest_config["order_file"]) order_df = read_order_file(backtest_config["order_file"])
cash_limit = backtest_config["exchange"].pop("cash_limit") cash_limit = backtest_config["exchange"].pop("cash_limit")
generate_report = backtest_config["exchange"].pop("generate_report") generate_report = backtest_config.pop("generate_report")
stock_pool = order_df["instrument"].unique().tolist() stock_pool = order_df["instrument"].unique().tolist()
stock_pool.sort() stock_pool.sort()
@@ -382,9 +370,19 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--config_path", type=str, required=True, help="Path to the config file") parser.add_argument("--config_path", type=str, required=True, help="Path to the config file")
parser.add_argument("--use_simulator", action="store_true", help="Whether to use simulator as the backend") parser.add_argument("--use_simulator", action="store_true", help="Whether to use simulator as the backend")
parser.add_argument(
"--n_jobs",
type=int,
required=False,
help="The number of jobs for running backtest parallely(1 for single process)",
)
args = parser.parse_args() args = parser.parse_args()
config = get_backtest_config_fromfile(args.config_path)
if args.n_jobs is not None:
config["concurrency"] = args.n_jobs
backtest( backtest(
backtest_config=get_backtest_config_fromfile(args.config_path), backtest_config=config,
with_simulator=args.use_simulator, with_simulator=args.use_simulator,
) )

View File

@@ -11,11 +11,14 @@ from importlib import import_module
import yaml import yaml
DELETE_KEY = "_delete_"
def merge_a_into_b(a: dict, b: dict) -> dict: def merge_a_into_b(a: dict, b: dict) -> dict:
b = b.copy() b = b.copy()
for k, v in a.items(): for k, v in a.items():
if isinstance(v, dict) and k in b: if isinstance(v, dict) and k in b:
v.pop("_delete_", False) # TODO: make this more elegant v.pop(DELETE_KEY, False)
b[k] = merge_a_into_b(v, b[k]) b[k] = merge_a_into_b(v, b[k])
else: else:
b[k] = v b[k] = v
@@ -86,7 +89,6 @@ def get_backtest_config_fromfile(path: str) -> dict:
"min_cost": 5.0, "min_cost": 5.0,
"trade_unit": 100.0, "trade_unit": 100.0,
"cash_limit": None, "cash_limit": None,
"generate_report": False,
} }
backtest_config["exchange"] = merge_a_into_b(a=backtest_config["exchange"], b=exchange_config_default) backtest_config["exchange"] = merge_a_into_b(a=backtest_config["exchange"], b=exchange_config_default)
backtest_config["exchange"] = _convert_all_list_to_tuple(backtest_config["exchange"]) backtest_config["exchange"] = _convert_all_list_to_tuple(backtest_config["exchange"])
@@ -97,7 +99,7 @@ def get_backtest_config_fromfile(path: str) -> dict:
"concurrency": -1, "concurrency": -1,
"multiplier": 1.0, "multiplier": 1.0,
"output_dir": "outputs/", "output_dir": "outputs/",
# "runtime": {}, "generate_report": False,
} }
backtest_config = merge_a_into_b(a=backtest_config, b=backtest_config_default) backtest_config = merge_a_into_b(a=backtest_config, b=backtest_config_default)

View File

@@ -13,7 +13,6 @@ from qlib.rl.order_execution.utils import get_ticks_slice
from .base import BaseIntradayBacktestData, BaseIntradayProcessedData, ProcessedDataProvider from .base import BaseIntradayBacktestData, BaseIntradayProcessedData, ProcessedDataProvider
from .integration import fetch_features from .integration import fetch_features
from ...data import D
class IntradayBacktestData(BaseIntradayBacktestData): class IntradayBacktestData(BaseIntradayBacktestData):
@@ -81,17 +80,7 @@ def load_backtest_data(
trade_exchange: Exchange, trade_exchange: Exchange,
trade_range: TradeRange, trade_range: TradeRange,
) -> IntradayBacktestData: ) -> IntradayBacktestData:
# TODO: making exchange return data without missing will make it more elegant. Fix this in the future. ticks_index = pd.DatetimeIndex(trade_exchange.quote_df.reset_index()["datetime"])
tmp_data = D.features(
trade_exchange.codes,
trade_exchange.all_fields,
trade_exchange.start_time,
trade_exchange.end_time,
freq=trade_exchange.freq,
disk_cache=True,
)
ticks_index = pd.DatetimeIndex(tmp_data.reset_index()["datetime"])
ticks_index = ticks_index[order.start_time <= ticks_index] ticks_index = ticks_index[order.start_time <= ticks_index]
ticks_index = ticks_index[ticks_index <= order.end_time] ticks_index = ticks_index[ticks_index <= order.end_time]

View File

@@ -3,19 +3,15 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Generic, Optional, TYPE_CHECKING, TypeVar from typing import Any, Generic, TypeVar
import gym
import numpy as np import numpy as np
from gym import spaces
from qlib.typehint import final from qlib.typehint import final
from .simulator import ActType, StateType from .simulator import ActType, StateType
if TYPE_CHECKING:
from .utils.env_wrapper import BaseEnvWrapper
import gym
from gym import spaces
ObsType = TypeVar("ObsType") ObsType = TypeVar("ObsType")
PolicyActType = TypeVar("PolicyActType") PolicyActType = TypeVar("PolicyActType")
@@ -39,8 +35,6 @@ class Interpreter:
class StateInterpreter(Generic[StateType, ObsType], Interpreter): class StateInterpreter(Generic[StateType, ObsType], Interpreter):
"""State Interpreter that interpret execution result of qlib executor into rl env state""" """State Interpreter that interpret execution result of qlib executor into rl env state"""
env: Optional[BaseEnvWrapper] = None
@property @property
def observation_space(self) -> gym.Space: def observation_space(self) -> gym.Space:
raise NotImplementedError() raise NotImplementedError()
@@ -73,8 +67,6 @@ class StateInterpreter(Generic[StateType, ObsType], Interpreter):
class ActionInterpreter(Generic[StateType, PolicyActType, ActType], Interpreter): class ActionInterpreter(Generic[StateType, PolicyActType, ActType], Interpreter):
"""Action Interpreter that interpret rl agent action into qlib orders""" """Action Interpreter that interpret rl agent action into qlib orders"""
env: Optional[BaseEnvWrapper] = None
@property @property
def action_space(self) -> gym.Space: def action_space(self) -> gym.Space:
raise NotImplementedError() raise NotImplementedError()

View File

@@ -69,8 +69,6 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]):
Provider of the processed data. Provider of the processed data.
""" """
# TODO: All implementations related to `data_dir` is coupled with the specific data format for that specific case.
# TODO: So it should be redesigned after the data interface is well-designed.
def __init__( def __init__(
self, self,
max_step: int, max_step: int,
@@ -78,6 +76,8 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]):
data_dim: int, data_dim: int,
processed_data_provider: dict | ProcessedDataProvider, processed_data_provider: dict | ProcessedDataProvider,
) -> None: ) -> None:
super().__init__()
self.max_step = max_step self.max_step = max_step
self.data_ticks = data_ticks self.data_ticks = data_ticks
self.data_dim = data_dim self.data_dim = data_dim
@@ -87,10 +87,6 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]):
) )
def interpret(self, state: SAOEState) -> FullHistoryObs: def interpret(self, state: SAOEState) -> FullHistoryObs:
# TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running
# backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant
# way to decompose interpreter and EnvWrapper in the future.
processed = self.processed_data_provider.get_data( processed = self.processed_data_provider.get_data(
stock_id=state.order.stock_id, stock_id=state.order.stock_id,
date=pd.Timestamp(state.order.start_time.date()), date=pd.Timestamp(state.order.start_time.date()),
@@ -102,8 +98,6 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]):
position_history[0] = state.order.amount position_history[0] = state.order.amount
position_history[1 : len(state.history_steps) + 1] = state.history_steps["position"].to_numpy() position_history[1 : len(state.history_steps) + 1] = state.history_steps["position"].to_numpy()
assert self.env is not None
# The min, slice here are to make sure that indices fit into the range, # The min, slice here are to make sure that indices fit into the range,
# even after the final step of the simulator (in the done step), # even after the final step of the simulator (in the done step),
# to make network in policy happy. # to make network in policy happy.
@@ -115,7 +109,7 @@ class FullHistoryStateInterpreter(StateInterpreter[SAOEState, FullHistoryObs]):
"data_processed_prev": np.array(processed.yesterday), "data_processed_prev": np.array(processed.yesterday),
"acquiring": _to_int32(state.order.direction == state.order.BUY), "acquiring": _to_int32(state.order.direction == state.order.BUY),
"cur_tick": _to_int32(min(int(np.sum(state.ticks_index < state.cur_time)), self.data_ticks - 1)), "cur_tick": _to_int32(min(int(np.sum(state.ticks_index < state.cur_time)), self.data_ticks - 1)),
"cur_step": _to_int32(min(self.env.status["cur_step"], self.max_step - 1)), "cur_step": _to_int32(min(state.cur_step, self.max_step - 1)),
"num_step": _to_int32(self.max_step), "num_step": _to_int32(self.max_step),
"target": _to_float32(state.order.amount), "target": _to_float32(state.order.amount),
"position": _to_float32(state.position), "position": _to_float32(state.position),
@@ -163,6 +157,8 @@ class CurrentStepStateInterpreter(StateInterpreter[SAOEState, CurrentStateObs]):
""" """
def __init__(self, max_step: int) -> None: def __init__(self, max_step: int) -> None:
super().__init__()
self.max_step = max_step self.max_step = max_step
@property @property
@@ -177,15 +173,10 @@ class CurrentStepStateInterpreter(StateInterpreter[SAOEState, CurrentStateObs]):
return spaces.Dict(space) return spaces.Dict(space)
def interpret(self, state: SAOEState) -> CurrentStateObs: def interpret(self, state: SAOEState) -> CurrentStateObs:
# TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running assert state.cur_step <= self.max_step
# backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant
# way to decompose interpreter and EnvWrapper in the future.
assert self.env is not None
assert self.env.status["cur_step"] <= self.max_step
obs = CurrentStateObs( obs = CurrentStateObs(
acquiring=state.order.direction == state.order.BUY, acquiring=state.order.direction == state.order.BUY,
cur_step=self.env.status["cur_step"], cur_step=state.cur_step,
num_step=self.max_step, num_step=self.max_step,
target=state.order.amount, target=state.order.amount,
position=state.position, position=state.position,
@@ -208,6 +199,8 @@ class CategoricalActionInterpreter(ActionInterpreter[SAOEState, int, float]):
""" """
def __init__(self, values: int | List[float], max_step: Optional[int] = None) -> None: def __init__(self, values: int | List[float], max_step: Optional[int] = None) -> None:
super().__init__()
if isinstance(values, int): if isinstance(values, int):
values = [i / values for i in range(0, values + 1)] values = [i / values for i in range(0, values + 1)]
self.action_values = values self.action_values = values
@@ -218,13 +211,8 @@ class CategoricalActionInterpreter(ActionInterpreter[SAOEState, int, float]):
return spaces.Discrete(len(self.action_values)) return spaces.Discrete(len(self.action_values))
def interpret(self, state: SAOEState, action: int) -> float: def interpret(self, state: SAOEState, action: int) -> float:
# TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running
# backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant
# way to decompose interpreter and EnvWrapper in the future.
assert 0 <= action < len(self.action_values) assert 0 <= action < len(self.action_values)
assert self.env is not None if self.max_step is not None and state.cur_step >= self.max_step - 1:
if self.max_step is not None and self.env.status["cur_step"] >= self.max_step - 1:
return state.position return state.position
else: else:
return min(state.position, state.order.amount * self.action_values[action]) return min(state.position, state.order.amount * self.action_values[action])
@@ -244,13 +232,8 @@ class TwapRelativeActionInterpreter(ActionInterpreter[SAOEState, float, float]):
return spaces.Box(0, np.inf, shape=(), dtype=np.float32) return spaces.Box(0, np.inf, shape=(), dtype=np.float32)
def interpret(self, state: SAOEState, action: float) -> float: def interpret(self, state: SAOEState, action: float) -> float:
# TODO: This interpreter relies on EnvWrapper.status, so we have to give it a dummy EnvWrapper when running
# backtest. Currently, the dummy EnvWrapper is CollectDataEnvWrapper. We should find a more elegant
# way to decompose interpreter and EnvWrapper in the future.
assert self.env is not None
estimated_total_steps = math.ceil(len(state.ticks_for_order) / state.ticks_per_step) estimated_total_steps = math.ceil(len(state.ticks_for_order) / state.ticks_per_step)
twap_volume = state.position / (estimated_total_steps - self.env.status["cur_step"]) twap_volume = state.position / (estimated_total_steps - state.cur_step)
return min(state.position, twap_volume * action) return min(state.position, twap_volume * action)

View File

@@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Generator, Iterable, Optional, Tuple, cast from typing import Any, Dict, Generator, Iterable, Optional, OrderedDict, Tuple, cast
import gym import gym
import numpy as np import numpy as np
@@ -14,6 +14,8 @@ from gym.spaces import Discrete
from tianshou.data import Batch, ReplayBuffer, to_torch from tianshou.data import Batch, ReplayBuffer, to_torch
from tianshou.policy import BasePolicy, PPOPolicy from tianshou.policy import BasePolicy, PPOPolicy
from qlib.rl.trainer.trainer import Trainer
__all__ = ["AllOne", "PPO"] __all__ = ["AllOne", "PPO"]
@@ -148,7 +150,7 @@ class PPO(PPOPolicy):
action_space=action_space, action_space=action_space,
) )
if weight_file is not None: if weight_file is not None:
load_weight(self, weight_file) set_weight(self, Trainer.get_policy_state_dict(weight_file))
# utilities: these should be put in a separate (common) file. # # utilities: these should be put in a separate (common) file. #
@@ -160,15 +162,7 @@ def auto_device(module: nn.Module) -> torch.device:
return torch.device("cpu") # fallback to cpu return torch.device("cpu") # fallback to cpu
def load_weight(policy: nn.Module, path: Path) -> None: def set_weight(policy: nn.Module, loaded_weight: OrderedDict) -> None:
assert isinstance(policy, nn.Module), "Policy has to be an nn.Module to load weight."
loaded_weight = torch.load(path, map_location="cpu")
# TODO: this should be handled by whoever calls load_weight.
# TODO: For example, when the outer class receives a weight, it should first unpack it,
# TODO: and send the corresponding part to individual component.
if "vessel" in loaded_weight:
loaded_weight = loaded_weight["vessel"]["policy"]
try: try:
policy.load_state_dict(loaded_weight) policy.load_state_dict(loaded_weight)
except RuntimeError: except RuntimeError:

View File

@@ -9,12 +9,11 @@ import pandas as pd
from qlib.backtest import collect_data_loop, get_strategy_executor from qlib.backtest import collect_data_loop, get_strategy_executor
from qlib.backtest.decision import BaseTradeDecision, Order, TradeRangeByTime from qlib.backtest.decision import BaseTradeDecision, Order, TradeRangeByTime
from qlib.backtest.executor import BaseExecutor, NestedExecutor from qlib.backtest.executor import NestedExecutor
from qlib.rl.data.integration import init_qlib from qlib.rl.data.integration import init_qlib
from qlib.rl.simulator import Simulator from qlib.rl.simulator import Simulator
from .state import SAOEState, SAOEStateAdapter from .state import SAOEState
from .strategy import SAOEStrategy from .strategy import SAOEStateAdapter, SAOEStrategy
from ..utils.env_wrapper import CollectDataEnvWrapper
class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]): class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
@@ -32,8 +31,6 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
Configuration used to initialize Qlib. If it is None, Qlib will not be initialized. Configuration used to initialize Qlib. If it is None, Qlib will not be initialized.
cash_limit: cash_limit:
Cash limit. Cash limit.
backtest_mode
Whether the simulator is under backtest mode.
""" """
def __init__( def __init__(
@@ -43,7 +40,6 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
exchange_config: dict, exchange_config: dict,
qlib_config: dict = None, qlib_config: dict = None,
cash_limit: Optional[float] = None, cash_limit: Optional[float] = None,
backtest_mode: bool = False,
) -> None: ) -> None:
super().__init__(initial=order) super().__init__(initial=order)
@@ -59,7 +55,7 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
} }
self._collect_data_loop: Optional[Generator] = None self._collect_data_loop: Optional[Generator] = None
self.reset(order, strategy_config, executor_config, exchange_config, qlib_config, cash_limit, backtest_mode) self.reset(order, strategy_config, executor_config, exchange_config, qlib_config, cash_limit)
def reset( def reset(
self, self,
@@ -69,7 +65,6 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
exchange_config: dict, exchange_config: dict,
qlib_config: dict = None, qlib_config: dict = None,
cash_limit: Optional[float] = None, cash_limit: Optional[float] = None,
backtest_mode: bool = False,
) -> None: ) -> None:
if qlib_config is not None: if qlib_config is not None:
init_qlib(qlib_config, part="skip") init_qlib(qlib_config, part="skip")
@@ -98,16 +93,6 @@ class SingleAssetOrderExecution(Simulator[Order, SAOEState, float]):
) )
assert isinstance(self._collect_data_loop, Generator) assert isinstance(self._collect_data_loop, Generator)
# TODO: backtest_mode is not a necessary parameter if we carefully design it.
# TODO: It should disappear with CollectDataEnvWrapper in the future.
if backtest_mode:
executor: BaseExecutor = self._executor
while isinstance(executor, NestedExecutor):
if hasattr(executor.inner_strategy, "set_env"):
executor.inner_strategy.set_env(CollectDataEnvWrapper())
executor = executor.inner_executor
# Call `step()` with None action to initialize the internal generator.
self.step(action=None) self.step(action=None)
self._order = order self._order = order

View File

@@ -16,8 +16,6 @@ from qlib.rl.utils import LogLevel
from .state import SAOEMetrics, SAOEState from .state import SAOEMetrics, SAOEState
# TODO: Integrating Qlib's native data with simulator_simple
__all__ = ["SingleAssetOrderExecutionSimple"] __all__ = ["SingleAssetOrderExecutionSimple"]
@@ -98,6 +96,7 @@ class SingleAssetOrderExecutionSimple(Simulator[Order, SAOEState, float]):
self.ticks_for_order = self._get_ticks_slice(self.order.start_time, self.order.end_time) self.ticks_for_order = self._get_ticks_slice(self.order.start_time, self.order.end_time)
self.cur_time = self.ticks_for_order[0] self.cur_time = self.ticks_for_order[0]
self.cur_step = 0
# NOTE: astype(float) is necessary in some systems. # NOTE: astype(float) is necessary in some systems.
# this will align the precision with `.to_numpy()` in `_split_exec_vol` # this will align the precision with `.to_numpy()` in `_split_exec_vol`
self.twap_price = float(self.backtest_data.get_deal_price().loc[self.ticks_for_order].astype(float).mean()) self.twap_price = float(self.backtest_data.get_deal_price().loc[self.ticks_for_order].astype(float).mean())
@@ -194,11 +193,13 @@ class SingleAssetOrderExecutionSimple(Simulator[Order, SAOEState, float]):
self.env.logger.add_any(key, value) self.env.logger.add_any(key, value)
self.cur_time = self._next_time() self.cur_time = self._next_time()
self.cur_step += 1
def get_state(self) -> SAOEState: def get_state(self) -> SAOEState:
return SAOEState( return SAOEState(
order=self.order, order=self.order,
cur_time=self.cur_time, cur_time=self.cur_time,
cur_step=self.cur_step,
position=self.position, position=self.position,
history_exec=self.history_exec, history_exec=self.history_exec,
history_steps=self.history_steps, history_steps=self.history_steps,

View File

@@ -4,290 +4,15 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
from typing import cast, Callable, List, NamedTuple, Optional, Tuple from typing import NamedTuple, Optional
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from qlib.backtest import Exchange, Order from qlib.backtest import Order
from qlib.backtest.executor import BaseExecutor
from qlib.constant import EPS, ONE_MIN, REG_CN
from qlib.rl.order_execution.utils import dataframe_append, price_advantage
from qlib.typehint import TypedDict from qlib.typehint import TypedDict
from qlib.utils.index_data import IndexData
from qlib.utils.time import get_day_min_idx_range
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from qlib.rl.data.base import BaseIntradayBacktestData from qlib.rl.data.base import BaseIntradayBacktestData
from qlib.rl.data.native import IntradayBacktestData
def _get_all_timestamps(
start: pd.Timestamp,
end: pd.Timestamp,
granularity: pd.Timedelta = ONE_MIN,
include_end: bool = True,
) -> pd.DatetimeIndex:
ret = []
while start <= end:
ret.append(start)
start += granularity
if ret[-1] > end:
ret.pop()
if ret[-1] == end and not include_end:
ret.pop()
return pd.DatetimeIndex(ret)
def fill_missing_data(
original_data: np.ndarray,
total_time_list: List[pd.Timestamp],
found_time_list: List[pd.Timestamp],
fill_method: Callable = np.median,
) -> np.ndarray:
"""Fill missing data. We need this function to deal with data that have missing values in some minutes.
TODO: making exchange return data without missing will make it more elegant. Fix this in the future.
Parameters
----------
original_data
Original data without missing values.
total_time_list
All timestamps that required.
found_time_list
Timestamps found in the original data.
fill_method
Method used to fill the missing data.
Returns
-------
The filled data.
"""
assert len(original_data) == len(found_time_list)
tmp = dict(zip(found_time_list, original_data))
fill_val = fill_method(original_data)
return np.array([tmp.get(t, fill_val) for t in total_time_list])
class SAOEStateAdapter:
"""
Maintain states of the environment. SAOEStateAdapter accepts execution results and update its internal state
according to the execution results with additional information acquired from executors & exchange. For example,
it gets the dealt order amount from execution results, and get the corresponding market price / volume from
exchange.
Example usage::
adapter = SAOEStateAdapter(...)
adapter.update(...)
state = adapter.saoe_state
"""
def __init__(
self,
order: Order,
executor: BaseExecutor,
exchange: Exchange,
ticks_per_step: int,
backtest_data: IntradayBacktestData,
) -> None:
self.position = order.amount
self.order = order
self.executor = executor
self.exchange = exchange
self.backtest_data = backtest_data
self.twap_price = self.backtest_data.get_deal_price().mean()
metric_keys = list(SAOEMetrics.__annotations__.keys()) # pylint: disable=no-member
self.history_exec = pd.DataFrame(columns=metric_keys).set_index("datetime")
self.history_steps = pd.DataFrame(columns=metric_keys).set_index("datetime")
self.metrics: Optional[SAOEMetrics] = None
self.cur_time = max(backtest_data.ticks_for_order[0], order.start_time)
self.ticks_per_step = ticks_per_step
def _next_time(self) -> pd.Timestamp:
current_loc = self.backtest_data.ticks_index.get_loc(self.cur_time)
next_loc = current_loc + self.ticks_per_step
next_loc = next_loc - next_loc % self.ticks_per_step
if (
next_loc < len(self.backtest_data.ticks_index)
and self.backtest_data.ticks_index[next_loc] < self.order.end_time
):
return self.backtest_data.ticks_index[next_loc]
else:
return self.order.end_time
def update(
self,
execute_result: list,
last_step_range: Tuple[int, int],
) -> None:
last_step_size = last_step_range[1] - last_step_range[0] + 1
start_time = self.backtest_data.ticks_index[last_step_range[0]]
end_time = self.backtest_data.ticks_index[last_step_range[1]]
exec_vol = np.zeros(last_step_size)
for order, _, __, ___ in execute_result:
idx, _ = get_day_min_idx_range(order.start_time, order.end_time, "1min", REG_CN)
exec_vol[idx - last_step_range[0]] = order.deal_amount
if exec_vol.sum() > self.position and exec_vol.sum() > 0.0:
assert exec_vol.sum() < self.position + 1, f"{exec_vol} too large"
exec_vol *= self.position / (exec_vol.sum())
market_volume = cast(
IndexData,
self.exchange.get_volume(
self.order.stock_id,
pd.Timestamp(start_time),
pd.Timestamp(end_time),
method=None,
),
)
market_price = cast(
IndexData,
self.exchange.get_deal_price(
self.order.stock_id,
pd.Timestamp(start_time),
pd.Timestamp(end_time),
method=None,
direction=self.order.direction,
),
)
found_time_list = [pd.Timestamp(e) for e in list(market_volume.index)]
total_time_list = _get_all_timestamps(start_time, end_time)
market_price = fill_missing_data(np.array(market_price).reshape(-1), total_time_list, found_time_list)
market_volume = fill_missing_data(np.array(market_volume).reshape(-1), total_time_list, found_time_list)
assert market_price.shape == market_volume.shape == exec_vol.shape
# Get data from the current level executor's indicator
current_trade_account = self.executor.trade_account
current_df = current_trade_account.get_trade_indicator().generate_trade_indicators_dataframe()
self.history_exec = dataframe_append(
self.history_exec,
self._collect_multi_order_metric(
order=self.order,
datetime=_get_all_timestamps(start_time, end_time, include_end=True),
market_vol=market_volume,
market_price=market_price,
exec_vol=exec_vol,
pa=current_df.iloc[-1]["pa"],
),
)
self.history_steps = dataframe_append(
self.history_steps,
[
self._collect_single_order_metric(
self.order,
self.cur_time,
market_volume,
market_price,
exec_vol.sum(),
exec_vol,
),
],
)
# TODO: check whether we need this. Can we get this information from Account?
# Do this at the end
self.position -= exec_vol.sum()
self.cur_time = self._next_time()
def generate_metrics_after_done(self) -> None:
"""Generate metrics once the upper level execution is done"""
self.metrics = self._collect_single_order_metric(
self.order,
self.backtest_data.ticks_index[0], # start time
self.history_exec["market_volume"],
self.history_exec["market_price"],
self.history_steps["amount"].sum(),
self.history_exec["deal_amount"],
)
def _collect_multi_order_metric(
self,
order: Order,
datetime: pd.DatetimeIndex,
market_vol: np.ndarray,
market_price: np.ndarray,
exec_vol: np.ndarray,
pa: float,
) -> SAOEMetrics:
return SAOEMetrics(
# It should have the same keys with SAOEMetrics,
# but the values do not necessarily have the annotated type.
# Some values could be vectorized (e.g., exec_vol).
stock_id=order.stock_id,
datetime=datetime,
direction=order.direction,
market_volume=market_vol,
market_price=market_price,
amount=exec_vol,
inner_amount=exec_vol,
deal_amount=exec_vol,
trade_price=market_price,
trade_value=market_price * exec_vol,
position=self.position - np.cumsum(exec_vol),
ffr=exec_vol / order.amount,
pa=pa,
)
def _collect_single_order_metric(
self,
order: Order,
datetime: pd.Timestamp,
market_vol: np.ndarray,
market_price: np.ndarray,
amount: float, # intended to trade such amount
exec_vol: np.ndarray,
) -> SAOEMetrics:
assert len(market_vol) == len(market_price) == len(exec_vol)
if np.abs(np.sum(exec_vol)) < EPS:
exec_avg_price = 0.0
else:
exec_avg_price = cast(float, np.average(market_price, weights=exec_vol)) # could be nan
if hasattr(exec_avg_price, "item"): # could be numpy scalar
exec_avg_price = exec_avg_price.item() # type: ignore
exec_sum = exec_vol.sum()
return SAOEMetrics(
stock_id=order.stock_id,
datetime=datetime,
direction=order.direction,
market_volume=market_vol.sum(),
market_price=market_price.mean() if len(market_price) > 0 else np.nan,
amount=amount,
inner_amount=exec_sum,
deal_amount=exec_sum, # in this simulator, there's no other restrictions
trade_price=exec_avg_price,
trade_value=float(np.sum(market_price * exec_vol)),
position=self.position - exec_sum,
ffr=float(exec_sum / order.amount),
pa=price_advantage(exec_avg_price, self.twap_price, order.direction),
)
@property
def saoe_state(self) -> SAOEState:
return SAOEState(
order=self.order,
cur_time=self.cur_time,
position=self.position,
history_exec=self.history_exec,
history_steps=self.history_steps,
metrics=self.metrics,
backtest_data=self.backtest_data,
ticks_per_step=self.ticks_per_step,
ticks_index=self.backtest_data.ticks_index,
ticks_for_order=self.backtest_data.ticks_for_order,
)
class SAOEMetrics(TypedDict): class SAOEMetrics(TypedDict):
@@ -302,7 +27,7 @@ class SAOEMetrics(TypedDict):
stock_id: str stock_id: str
"""Stock ID of this record.""" """Stock ID of this record."""
datetime: pd.Timestamp | pd.DatetimeIndex # TODO: check this datetime: pd.Timestamp | pd.DatetimeIndex
"""Datetime of this record (this is index in the dataframe).""" """Datetime of this record (this is index in the dataframe)."""
direction: int direction: int
"""Direction of the order. 0 for sell, 1 for buy.""" """Direction of the order. 0 for sell, 1 for buy."""
@@ -349,6 +74,8 @@ class SAOEState(NamedTuple):
"""The order we are dealing with.""" """The order we are dealing with."""
cur_time: pd.Timestamp cur_time: pd.Timestamp
"""Current time, e.g., 9:30.""" """Current time, e.g., 9:30."""
cur_step: int
"""Current step, e.g., 0."""
position: float position: float
"""Current remaining volume to execute.""" """Current remaining volume to execute."""
history_exec: pd.DataFrame history_exec: pd.DataFrame

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import collections import collections
from types import GeneratorType from types import GeneratorType
from typing import Any, cast, Dict, Generator, List, Optional, Union from typing import Any, Callable, cast, Dict, Generator, List, Optional, Tuple, Union
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -15,14 +15,276 @@ from tianshou.policy import BasePolicy
from qlib.backtest import CommonInfrastructure, Order from qlib.backtest import CommonInfrastructure, Order
from qlib.backtest.decision import BaseTradeDecision, TradeDecisionWithDetails, TradeDecisionWO, TradeRange from qlib.backtest.decision import BaseTradeDecision, TradeDecisionWithDetails, TradeDecisionWO, TradeRange
from qlib.backtest.utils import LevelInfrastructure from qlib.backtest.exchange import Exchange
from qlib.constant import ONE_MIN from qlib.backtest.executor import BaseExecutor
from qlib.rl.data.native import load_backtest_data from qlib.backtest.utils import LevelInfrastructure, get_start_end_idx
from qlib.constant import EPS, ONE_MIN, REG_CN
from qlib.rl.data.native import IntradayBacktestData, load_backtest_data
from qlib.rl.interpreter import ActionInterpreter, StateInterpreter from qlib.rl.interpreter import ActionInterpreter, StateInterpreter
from qlib.rl.order_execution.state import SAOEState, SAOEStateAdapter from qlib.rl.order_execution.state import SAOEMetrics, SAOEState
from qlib.rl.utils.env_wrapper import BaseEnvWrapper from qlib.rl.order_execution.utils import dataframe_append, price_advantage
from qlib.strategy.base import RLStrategy from qlib.strategy.base import RLStrategy
from qlib.utils import init_instance_by_config from qlib.utils import init_instance_by_config
from qlib.utils.index_data import IndexData
from qlib.utils.time import get_day_min_idx_range
def _get_all_timestamps(
start: pd.Timestamp,
end: pd.Timestamp,
granularity: pd.Timedelta = ONE_MIN,
include_end: bool = True,
) -> pd.DatetimeIndex:
ret = []
while start <= end:
ret.append(start)
start += granularity
if ret[-1] > end:
ret.pop()
if ret[-1] == end and not include_end:
ret.pop()
return pd.DatetimeIndex(ret)
def fill_missing_data(
original_data: np.ndarray,
fill_method: Callable = np.nanmedian,
) -> np.ndarray:
"""Fill missing data.
Parameters
----------
original_data
Original data without missing values.
fill_method
Method used to fill the missing data.
Returns
-------
The filled data.
"""
return np.nan_to_num(original_data, nan=fill_method(original_data))
class SAOEStateAdapter:
"""
Maintain states of the environment. SAOEStateAdapter accepts execution results and update its internal state
according to the execution results with additional information acquired from executors & exchange. For example,
it gets the dealt order amount from execution results, and get the corresponding market price / volume from
exchange.
Example usage::
adapter = SAOEStateAdapter(...)
adapter.update(...)
state = adapter.saoe_state
"""
def __init__(
self,
order: Order,
trade_decision: BaseTradeDecision,
executor: BaseExecutor,
exchange: Exchange,
ticks_per_step: int,
backtest_data: IntradayBacktestData,
) -> None:
self.position = order.amount
self.order = order
self.executor = executor
self.exchange = exchange
self.backtest_data = backtest_data
self.start_idx, _ = get_start_end_idx(self.executor.trade_calendar, trade_decision)
self.twap_price = self.backtest_data.get_deal_price().mean()
metric_keys = list(SAOEMetrics.__annotations__.keys()) # pylint: disable=no-member
self.history_exec = pd.DataFrame(columns=metric_keys).set_index("datetime")
self.history_steps = pd.DataFrame(columns=metric_keys).set_index("datetime")
self.metrics: Optional[SAOEMetrics] = None
self.cur_time = max(backtest_data.ticks_for_order[0], order.start_time)
self.ticks_per_step = ticks_per_step
def _next_time(self) -> pd.Timestamp:
current_loc = self.backtest_data.ticks_index.get_loc(self.cur_time)
next_loc = current_loc + self.ticks_per_step
next_loc = next_loc - next_loc % self.ticks_per_step
if (
next_loc < len(self.backtest_data.ticks_index)
and self.backtest_data.ticks_index[next_loc] < self.order.end_time
):
return self.backtest_data.ticks_index[next_loc]
else:
return self.order.end_time
def update(
self,
execute_result: list,
last_step_range: Tuple[int, int],
) -> None:
last_step_size = last_step_range[1] - last_step_range[0] + 1
start_time = self.backtest_data.ticks_index[last_step_range[0]]
end_time = self.backtest_data.ticks_index[last_step_range[1]]
exec_vol = np.zeros(last_step_size)
for order, _, __, ___ in execute_result:
idx, _ = get_day_min_idx_range(order.start_time, order.end_time, "1min", REG_CN)
exec_vol[idx - last_step_range[0]] = order.deal_amount
if exec_vol.sum() > self.position and exec_vol.sum() > 0.0:
assert exec_vol.sum() < self.position + 1, f"{exec_vol} too large"
exec_vol *= self.position / (exec_vol.sum())
market_volume = cast(
IndexData,
self.exchange.get_volume(
self.order.stock_id,
pd.Timestamp(start_time),
pd.Timestamp(end_time),
method=None,
),
)
market_price = cast(
IndexData,
self.exchange.get_deal_price(
self.order.stock_id,
pd.Timestamp(start_time),
pd.Timestamp(end_time),
method=None,
direction=self.order.direction,
),
)
market_price = fill_missing_data(np.array(market_price, dtype=float).reshape(-1))
market_volume = fill_missing_data(np.array(market_volume, dtype=float).reshape(-1))
assert market_price.shape == market_volume.shape == exec_vol.shape
# Get data from the current level executor's indicator
current_trade_account = self.executor.trade_account
current_df = current_trade_account.get_trade_indicator().generate_trade_indicators_dataframe()
self.history_exec = dataframe_append(
self.history_exec,
self._collect_multi_order_metric(
order=self.order,
datetime=_get_all_timestamps(start_time, end_time, include_end=True),
market_vol=market_volume,
market_price=market_price,
exec_vol=exec_vol,
pa=current_df.iloc[-1]["pa"],
),
)
self.history_steps = dataframe_append(
self.history_steps,
[
self._collect_single_order_metric(
self.order,
self.cur_time,
market_volume,
market_price,
exec_vol.sum(),
exec_vol,
),
],
)
# Do this at the end
self.position -= exec_vol.sum()
self.cur_time = self._next_time()
def generate_metrics_after_done(self) -> None:
"""Generate metrics once the upper level execution is done"""
self.metrics = self._collect_single_order_metric(
self.order,
self.backtest_data.ticks_index[0], # start time
self.history_exec["market_volume"],
self.history_exec["market_price"],
self.history_steps["amount"].sum(),
self.history_exec["deal_amount"],
)
def _collect_multi_order_metric(
self,
order: Order,
datetime: pd.DatetimeIndex,
market_vol: np.ndarray,
market_price: np.ndarray,
exec_vol: np.ndarray,
pa: float,
) -> SAOEMetrics:
return SAOEMetrics(
# It should have the same keys with SAOEMetrics,
# but the values do not necessarily have the annotated type.
# Some values could be vectorized (e.g., exec_vol).
stock_id=order.stock_id,
datetime=datetime,
direction=order.direction,
market_volume=market_vol,
market_price=market_price,
amount=exec_vol,
inner_amount=exec_vol,
deal_amount=exec_vol,
trade_price=market_price,
trade_value=market_price * exec_vol,
position=self.position - np.cumsum(exec_vol),
ffr=exec_vol / order.amount,
pa=pa,
)
def _collect_single_order_metric(
self,
order: Order,
datetime: pd.Timestamp,
market_vol: np.ndarray,
market_price: np.ndarray,
amount: float, # intended to trade such amount
exec_vol: np.ndarray,
) -> SAOEMetrics:
assert len(market_vol) == len(market_price) == len(exec_vol)
if np.abs(np.sum(exec_vol)) < EPS:
exec_avg_price = 0.0
else:
exec_avg_price = cast(float, np.average(market_price, weights=exec_vol)) # could be nan
if hasattr(exec_avg_price, "item"): # could be numpy scalar
exec_avg_price = exec_avg_price.item() # type: ignore
exec_sum = exec_vol.sum()
return SAOEMetrics(
stock_id=order.stock_id,
datetime=datetime,
direction=order.direction,
market_volume=market_vol.sum(),
market_price=market_price.mean() if len(market_price) > 0 else np.nan,
amount=amount,
inner_amount=exec_sum,
deal_amount=exec_sum, # in this simulator, there's no other restrictions
trade_price=exec_avg_price,
trade_value=float(np.sum(market_price * exec_vol)),
position=self.position - exec_sum,
ffr=float(exec_sum / order.amount),
pa=price_advantage(exec_avg_price, self.twap_price, order.direction),
)
@property
def saoe_state(self) -> SAOEState:
return SAOEState(
order=self.order,
cur_time=self.cur_time,
cur_step=self.executor.trade_calendar.get_trade_step() - self.start_idx,
position=self.position,
history_exec=self.history_exec,
history_steps=self.history_steps,
metrics=self.metrics,
backtest_data=self.backtest_data,
ticks_per_step=self.ticks_per_step,
ticks_index=self.backtest_data.ticks_index,
ticks_for_order=self.backtest_data.ticks_for_order,
)
class SAOEStrategy(RLStrategy): class SAOEStrategy(RLStrategy):
@@ -30,7 +292,7 @@ class SAOEStrategy(RLStrategy):
def __init__( def __init__(
self, self,
policy: object, # TODO: add accurate typehint later. policy: BasePolicy,
outer_trade_decision: BaseTradeDecision = None, outer_trade_decision: BaseTradeDecision = None,
level_infra: LevelInfrastructure = None, level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None, common_infra: CommonInfrastructure = None,
@@ -47,11 +309,17 @@ class SAOEStrategy(RLStrategy):
self.adapter_dict: Dict[tuple, SAOEStateAdapter] = {} self.adapter_dict: Dict[tuple, SAOEStateAdapter] = {}
self._last_step_range = (0, 0) self._last_step_range = (0, 0)
def _create_qlib_backtest_adapter(self, order: Order, trade_range: TradeRange) -> SAOEStateAdapter: def _create_qlib_backtest_adapter(
self,
order: Order,
trade_decision: BaseTradeDecision,
trade_range: TradeRange,
) -> SAOEStateAdapter:
backtest_data = load_backtest_data(order, self.trade_exchange, trade_range) backtest_data = load_backtest_data(order, self.trade_exchange, trade_range)
return SAOEStateAdapter( return SAOEStateAdapter(
order=order, order=order,
trade_decision=trade_decision,
executor=self.executor, executor=self.executor,
exchange=self.trade_exchange, exchange=self.trade_exchange,
ticks_per_step=int(pd.Timedelta(self.trade_calendar.get_freq()) / ONE_MIN), ticks_per_step=int(pd.Timedelta(self.trade_calendar.get_freq()) / ONE_MIN),
@@ -71,7 +339,9 @@ class SAOEStrategy(RLStrategy):
self.adapter_dict = {} self.adapter_dict = {}
for decision in outer_trade_decision.get_decision(): for decision in outer_trade_decision.get_decision():
order = cast(Order, decision) order = cast(Order, decision)
self.adapter_dict[order.key_by_day] = self._create_qlib_backtest_adapter(order, trade_range) self.adapter_dict[order.key_by_day] = self._create_qlib_backtest_adapter(
order, outer_trade_decision, trade_range
)
def get_saoe_state_by_order(self, order: Order) -> SAOEState: def get_saoe_state_by_order(self, order: Order) -> SAOEState:
return self.adapter_dict[order.key_by_day].saoe_state return self.adapter_dict[order.key_by_day].saoe_state
@@ -166,11 +436,10 @@ class SAOEIntStrategy(SAOEStrategy):
policy: dict | BasePolicy, policy: dict | BasePolicy,
state_interpreter: dict | StateInterpreter, state_interpreter: dict | StateInterpreter,
action_interpreter: dict | ActionInterpreter, action_interpreter: dict | ActionInterpreter,
network: object = None, # TODO: add accurate typehint later. network: dict | torch.nn.Module | None = None,
outer_trade_decision: BaseTradeDecision = None, outer_trade_decision: BaseTradeDecision = None,
level_infra: LevelInfrastructure = None, level_infra: LevelInfrastructure = None,
common_infra: CommonInfrastructure = None, common_infra: CommonInfrastructure = None,
backtest: bool = False,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
super(SAOEIntStrategy, self).__init__( super(SAOEIntStrategy, self).__init__(
@@ -181,8 +450,6 @@ class SAOEIntStrategy(SAOEStrategy):
**kwargs, **kwargs,
) )
self._backtest = backtest
self._state_interpreter: StateInterpreter = init_instance_by_config( self._state_interpreter: StateInterpreter = init_instance_by_config(
state_interpreter, state_interpreter,
accept_types=StateInterpreter, accept_types=StateInterpreter,
@@ -221,21 +488,9 @@ class SAOEIntStrategy(SAOEStrategy):
if self._policy is not None: if self._policy is not None:
self._policy.eval() self._policy.eval()
def set_env(self, env: BaseEnvWrapper) -> None:
# TODO: This method is used to set EnvWrapper for interpreters since they rely on EnvWrapper.
# We should decompose the interpreters with EnvWrapper in the future and we should remove this method
# after that.
self._env = env
self._state_interpreter.env = self._action_interpreter.env = self._env
def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs: Any) -> None: def reset(self, outer_trade_decision: BaseTradeDecision = None, **kwargs: Any) -> None:
super().reset(outer_trade_decision=outer_trade_decision, **kwargs) super().reset(outer_trade_decision=outer_trade_decision, **kwargs)
# In backtest, env.reset() needs to be manually called since there is no outer trainer to call it
if self._backtest:
self._env.reset()
def _generate_trade_details(self, act: np.ndarray, exec_vols: List[float]) -> pd.DataFrame: def _generate_trade_details(self, act: np.ndarray, exec_vols: List[float]) -> pd.DataFrame:
assert hasattr(self.outer_trade_decision, "order_list") assert hasattr(self.outer_trade_decision, "order_list")
@@ -268,10 +523,6 @@ class SAOEIntStrategy(SAOEStrategy):
act = policy_out.act.numpy() if torch.is_tensor(policy_out.act) else policy_out.act act = policy_out.act.numpy() if torch.is_tensor(policy_out.act) else policy_out.act
exec_vols = [self._action_interpreter.interpret(s, a) for s, a in zip(states, act)] exec_vols = [self._action_interpreter.interpret(s, a) for s, a in zip(states, act)]
# In backtest, env.step() needs to be manually called since there is no outer trainer to call it
if self._backtest:
self._env.step(None)
oh = self.trade_exchange.get_order_helper() oh = self.trade_exchange.get_order_helper()
order_list = [] order_list = []
for decision, exec_vol in zip(self.outer_trade_decision.get_decision(), exec_vols): for decision, exec_vol in zip(self.outer_trade_decision.get_decision(), exec_vols):

View File

@@ -7,7 +7,7 @@ import collections
import copy import copy
from contextlib import AbstractContextManager, contextmanager from contextlib import AbstractContextManager, contextmanager
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterable, List, Sequence, TypeVar, cast from typing import Any, Dict, Iterable, List, OrderedDict, Sequence, TypeVar, cast
import torch import torch
@@ -152,6 +152,13 @@ class Trainer:
"metrics": self.metrics, "metrics": self.metrics,
} }
@staticmethod
def get_policy_state_dict(ckpt_path: Path) -> OrderedDict:
state_dict = torch.load(ckpt_path, map_location="cpu")
if "vessel" in state_dict:
state_dict = state_dict["vessel"]["policy"]
return state_dict
def load_state_dict(self, state_dict: dict) -> None: def load_state_dict(self, state_dict: dict) -> None:
"""Load all states into current trainer.""" """Load all states into current trainer."""
self.vessel.load_state_dict(state_dict["vessel"]) self.vessel.load_state_dict(state_dict["vessel"])

View File

@@ -48,24 +48,9 @@ class EnvWrapperStatus(TypedDict):
reward_history: list reward_history: list
class BaseEnvWrapper( class EnvWrapper(
gym.Env[ObsType, PolicyActType], gym.Env[ObsType, PolicyActType],
Generic[InitialStateType, StateType, ActType, ObsType, PolicyActType], Generic[InitialStateType, StateType, ActType, ObsType, PolicyActType],
):
"""Base env wrapper for RL environments. It has two implementations:
- EnvWrapper: Qlib-based RL environment used in training.
- CollectDataEnvWrapper: Dummy environment used in collect_data_loop.
"""
def __init__(self) -> None:
self.status: EnvWrapperStatus = cast(EnvWrapperStatus, None)
def render(self, mode: str = "human") -> None:
raise NotImplementedError("Render is not implemented in BaseEnvWrapper.")
class EnvWrapper(
BaseEnvWrapper[InitialStateType, StateType, ActType, ObsType, PolicyActType],
): ):
"""Qlib-based RL environment, subclassing ``gym.Env``. """Qlib-based RL environment, subclassing ``gym.Env``.
A wrapper of components, including simulator, state-interpreter, action-interpreter, reward. A wrapper of components, including simulator, state-interpreter, action-interpreter, reward.
@@ -129,8 +114,6 @@ class EnvWrapper(
# 3. Avoid circular reference. # 3. Avoid circular reference.
# 4. When the components get serialized, we can throw away the env without any burden. # 4. When the components get serialized, we can throw away the env without any burden.
# (though this part is not implemented yet) # (though this part is not implemented yet)
super().__init__()
for obj in [state_interpreter, action_interpreter, reward_fn, aux_info_collector]: for obj in [state_interpreter, action_interpreter, reward_fn, aux_info_collector]:
if obj is not None: if obj is not None:
obj.env = weakref.proxy(self) # type: ignore obj.env = weakref.proxy(self) # type: ignore
@@ -263,19 +246,5 @@ class EnvWrapper(
info_dict = InfoDict(log=self.logger.logs(), aux_info=aux_info) info_dict = InfoDict(log=self.logger.logs(), aux_info=aux_info)
return obs, rew, done, info_dict return obs, rew, done, info_dict
def render(self, mode: str = "human") -> None:
class CollectDataEnvWrapper(BaseEnvWrapper[InitialStateType, StateType, ActType, ObsType, PolicyActType]): raise NotImplementedError("Render is not implemented in EnvWrapper.")
"""Dummy EnvWrapper for collect_data_loop. It only has minimum interfaces to support the collect_data_loop."""
def reset(self, **kwargs: Any) -> None:
self.status = EnvWrapperStatus(
cur_step=0,
done=False,
initial_state=None,
obs_history=[],
action_history=[],
reward_history=[],
)
def step(self, policy_action: Any = None, **kwargs: Any) -> None:
self.status["cur_step"] += 1

View File

@@ -473,7 +473,8 @@ class PortAnaRecord(ACRecordTemp):
self.save(**{f"positions_normal_{_freq}.pkl": positions_normal}) self.save(**{f"positions_normal_{_freq}.pkl": positions_normal})
for _freq, indicators_normal in indicator_dict.items(): for _freq, indicators_normal in indicator_dict.items():
self.save(**{f"indicators_normal_{_freq}.pkl": indicators_normal}) self.save(**{f"indicators_normal_{_freq}.pkl": indicators_normal[0]})
self.save(**{f"indicators_normal_{_freq}_obj.pkl": indicators_normal[1]})
for _analysis_freq in self.risk_analysis_freq: for _analysis_freq in self.risk_analysis_freq:
if _analysis_freq not in portfolio_metric_dict: if _analysis_freq not in portfolio_metric_dict:
@@ -511,7 +512,7 @@ class PortAnaRecord(ACRecordTemp):
if _analysis_freq not in indicator_dict: if _analysis_freq not in indicator_dict:
warnings.warn(f"the freq {_analysis_freq} indicator is not found") warnings.warn(f"the freq {_analysis_freq} indicator is not found")
else: else:
indicators_normal = indicator_dict.get(_analysis_freq) indicators_normal = indicator_dict.get(_analysis_freq)[0]
if self.indicator_analysis_method is None: if self.indicator_analysis_method is None:
analysis_df = indicator_analysis(indicators_normal) analysis_df = indicator_analysis(indicators_normal)
else: else:

View File

@@ -107,7 +107,7 @@ class FileStrTest(TestAutoData):
) )
# ffr valid # ffr valid
ffr_dict = indicator_dict["1day"]["ffr"].to_dict() ffr_dict = indicator_dict["1day"][0]["ffr"].to_dict()
ffr_dict = {str(date).split()[0]: ffr_dict[date] for date in ffr_dict} ffr_dict = {str(date).split()[0]: ffr_dict[date] for date in ffr_dict}
assert np.isclose(ffr_dict["2020-01-03"], dealt_num_for_1000 / 1000) assert np.isclose(ffr_dict["2020-01-03"], dealt_num_for_1000 / 1000)
assert np.isclose(ffr_dict["2020-01-06"], 0) assert np.isclose(ffr_dict["2020-01-06"], 0)

View File

@@ -125,7 +125,7 @@ class TestHFBacktest(TestAutoData):
# NOTE: please refer to the docs of format_decisions # NOTE: please refer to the docs of format_decisions
# NOTE: `"track_data": True,` is very NECESSARY for collecting the decision!!!!! # NOTE: `"track_data": True,` is very NECESSARY for collecting the decision!!!!!
f_dec = format_decisions(decisions) f_dec = format_decisions(decisions)
print(indicator["1day"]) print(indicator["1day"][0])
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -7,11 +7,11 @@ from typing import Tuple
import pandas as pd import pandas as pd
import pytest import pytest
from qlib.backtest.decision import Order, OrderDir, TradeRangeByTime
from qlib.backtest.decision import Order, OrderDir
from qlib.backtest.executor import SimulatorExecutor from qlib.backtest.executor import SimulatorExecutor
from qlib.rl.order_execution import CategoricalActionInterpreter from qlib.rl.order_execution import CategoricalActionInterpreter
from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution
from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper
TOTAL_POSITION = 2100.0 TOTAL_POSITION = 2100.0
@@ -183,8 +183,6 @@ def test_interpreter() -> None:
order = get_order() order = get_order()
simulator = get_simulator(order) simulator = get_simulator(order)
interpreter_action = CategoricalActionInterpreter(values=NUM_EXECUTION) interpreter_action = CategoricalActionInterpreter(values=NUM_EXECUTION)
interpreter_action.env = CollectDataEnvWrapper()
interpreter_action.env.reset()
NUM_STEPS = 7 NUM_STEPS = 7
state = simulator.get_state() state = simulator.get_state()

View File

@@ -20,7 +20,6 @@ from qlib.rl.data.pickle_styled import PickleProcessedDataProvider
from qlib.rl.order_execution import * from qlib.rl.order_execution import *
from qlib.rl.trainer import backtest, train from qlib.rl.trainer import backtest, train
from qlib.rl.utils import ConsoleWriter, CsvWriter, EnvWrapperStatus from qlib.rl.utils import ConsoleWriter, CsvWriter, EnvWrapperStatus
from qlib.rl.utils.env_wrapper import CollectDataEnvWrapper
pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason="Pickle styled data only supports Python >= 3.8") pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason="Pickle styled data only supports Python >= 3.8")
@@ -186,10 +185,6 @@ def test_interpreter():
assert np.sum(obs["data_processed"][60:]) == 0 assert np.sum(obs["data_processed"][60:]) == 0
# second step: action # second step: action
interpreter_action.env = CollectDataEnvWrapper()
interpreter_action_twap.env = CollectDataEnvWrapper()
interpreter_action.env.reset()
interpreter_action_twap.env.reset()
action = interpreter_action(simulator.get_state(), 1) action = interpreter_action(simulator.get_state(), 1)
assert action == 15 / 20 assert action == 15 / 20
@@ -260,8 +255,6 @@ def test_twap_strategy(finite_env_type):
state_interp = FullHistoryStateInterpreter(13, 390, 5, PickleProcessedDataProvider(FEATURE_DATA_DIR)) state_interp = FullHistoryStateInterpreter(13, 390, 5, PickleProcessedDataProvider(FEATURE_DATA_DIR))
action_interp = TwapRelativeActionInterpreter() action_interp = TwapRelativeActionInterpreter()
action_interp.env = CollectDataEnvWrapper()
action_interp.env.reset()
policy = AllOne(state_interp.observation_space, action_interp.action_space) policy = AllOne(state_interp.observation_space, action_interp.action_space)
csv_writer = CsvWriter(Path(__file__).parent / ".output") csv_writer = CsvWriter(Path(__file__).parent / ".output")
@@ -291,8 +284,6 @@ def test_cn_ppo_strategy():
state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR)) state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR))
action_interp = CategoricalActionInterpreter(4) action_interp = CategoricalActionInterpreter(4)
action_interp.env = CollectDataEnvWrapper()
action_interp.env.reset()
network = Recurrent(state_interp.observation_space) network = Recurrent(state_interp.observation_space)
policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4) policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4)
policy.load_state_dict(torch.load(CN_POLICY_WEIGHTS_DIR / "ppo_recurrent_30min.pth", map_location="cpu")) policy.load_state_dict(torch.load(CN_POLICY_WEIGHTS_DIR / "ppo_recurrent_30min.pth", map_location="cpu"))
@@ -324,8 +315,6 @@ def test_ppo_train():
state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR)) state_interp = FullHistoryStateInterpreter(8, 240, 6, PickleProcessedDataProvider(CN_FEATURE_DATA_DIR))
action_interp = CategoricalActionInterpreter(4) action_interp = CategoricalActionInterpreter(4)
action_interp.env = CollectDataEnvWrapper()
action_interp.env.reset()
network = Recurrent(state_interp.observation_space) network = Recurrent(state_interp.observation_space)
policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4) policy = PPO(network, state_interp.observation_space, action_interp.action_space, 1e-4)