mirror of
https://github.com/microsoft/qlib.git
synced 2026-06-06 05:51:17 +08:00
* 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>
196 lines
7.2 KiB
Python
196 lines
7.2 KiB
Python
# Copyright (c) Microsoft Corporation.
|
|
# Licensed under the MIT License.
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from qlib.backtest.decision import Order, OrderDir
|
|
from qlib.backtest.executor import SimulatorExecutor
|
|
from qlib.rl.order_execution import CategoricalActionInterpreter
|
|
from qlib.rl.order_execution.simulator_qlib import SingleAssetOrderExecution
|
|
|
|
TOTAL_POSITION = 2100.0
|
|
|
|
python_version_request = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher")
|
|
|
|
|
|
def is_close(a: float, b: float, epsilon: float = 1e-4) -> bool:
|
|
return abs(a - b) <= epsilon
|
|
|
|
|
|
def get_order() -> Order:
|
|
return Order(
|
|
stock_id="SH600000",
|
|
amount=TOTAL_POSITION,
|
|
direction=OrderDir.BUY,
|
|
start_time=pd.Timestamp("2019-03-04 09:30:00"),
|
|
end_time=pd.Timestamp("2019-03-04 14:29:00"),
|
|
)
|
|
|
|
|
|
def get_configs(order: Order) -> Tuple[dict, dict]:
|
|
executor_config = {
|
|
"class": "NestedExecutor",
|
|
"module_path": "qlib.backtest.executor",
|
|
"kwargs": {
|
|
"time_per_step": "1day",
|
|
"inner_strategy": {"class": "ProxySAOEStrategy", "module_path": "qlib.rl.order_execution.strategy"},
|
|
"track_data": True,
|
|
"inner_executor": {
|
|
"class": "NestedExecutor",
|
|
"module_path": "qlib.backtest.executor",
|
|
"kwargs": {
|
|
"time_per_step": "30min",
|
|
"inner_strategy": {
|
|
"class": "TWAPStrategy",
|
|
"module_path": "qlib.contrib.strategy.rule_strategy",
|
|
},
|
|
"inner_executor": {
|
|
"class": "SimulatorExecutor",
|
|
"module_path": "qlib.backtest.executor",
|
|
"kwargs": {
|
|
"time_per_step": "1min",
|
|
"verbose": False,
|
|
"trade_type": SimulatorExecutor.TT_SERIAL,
|
|
"generate_report": False,
|
|
"track_data": True,
|
|
},
|
|
},
|
|
"track_data": True,
|
|
},
|
|
},
|
|
"start_time": pd.Timestamp(order.start_time.date()),
|
|
"end_time": pd.Timestamp(order.start_time.date()),
|
|
},
|
|
}
|
|
|
|
exchange_config = {
|
|
"freq": "1min",
|
|
"codes": [order.stock_id],
|
|
"limit_threshold": ("$ask == 0", "$bid == 0"),
|
|
"deal_price": ("If($ask == 0, $bid, $ask)", "If($bid == 0, $ask, $bid)"),
|
|
"volume_threshold": {
|
|
"all": ("cum", "0.2 * DayCumsum($volume, '9:30', '14:29')"),
|
|
"buy": ("current", "$askV1"),
|
|
"sell": ("current", "$bidV1"),
|
|
},
|
|
"open_cost": 0.0005,
|
|
"close_cost": 0.0015,
|
|
"min_cost": 5.0,
|
|
"trade_unit": None,
|
|
}
|
|
|
|
return executor_config, exchange_config
|
|
|
|
|
|
def get_simulator(order: Order) -> SingleAssetOrderExecution:
|
|
DATA_ROOT_DIR = Path(__file__).parent.parent / ".data" / "rl" / "qlib_simulator"
|
|
|
|
# fmt: off
|
|
qlib_config = {
|
|
"provider_uri_day": DATA_ROOT_DIR / "qlib_1d",
|
|
"provider_uri_1min": DATA_ROOT_DIR / "qlib_1min",
|
|
"feature_root_dir": DATA_ROOT_DIR / "qlib_handler_stock",
|
|
"feature_columns_today": [
|
|
"$open", "$high", "$low", "$close", "$vwap", "$bid", "$ask", "$volume",
|
|
"$bidV", "$bidV1", "$bidV3", "$bidV5", "$askV", "$askV1", "$askV3", "$askV5",
|
|
],
|
|
"feature_columns_yesterday": [
|
|
"$open_1", "$high_1", "$low_1", "$close_1", "$vwap_1", "$bid_1", "$ask_1", "$volume_1",
|
|
"$bidV_1", "$bidV1_1", "$bidV3_1", "$bidV5_1", "$askV_1", "$askV1_1", "$askV3_1", "$askV5_1",
|
|
],
|
|
}
|
|
# fmt: on
|
|
|
|
executor_config, exchange_config = get_configs(order)
|
|
|
|
return SingleAssetOrderExecution(
|
|
order=order,
|
|
qlib_config=qlib_config,
|
|
executor_config=executor_config,
|
|
exchange_config=exchange_config,
|
|
)
|
|
|
|
|
|
@python_version_request
|
|
def test_simulator_first_step():
|
|
order = get_order()
|
|
simulator = get_simulator(order)
|
|
state = simulator.get_state()
|
|
assert state.cur_time == pd.Timestamp("2019-03-04 09:30:00")
|
|
assert state.position == TOTAL_POSITION
|
|
|
|
AMOUNT = 300.0
|
|
simulator.step(AMOUNT)
|
|
state = simulator.get_state()
|
|
assert state.cur_time == pd.Timestamp("2019-03-04 10:00:00")
|
|
assert state.position == TOTAL_POSITION - AMOUNT
|
|
assert len(state.history_exec) == 30
|
|
assert state.history_exec.index[0] == pd.Timestamp("2019-03-04 09:30:00")
|
|
|
|
assert is_close(state.history_exec["market_volume"].iloc[0], 109382.382812)
|
|
assert is_close(state.history_exec["market_price"].iloc[0], 149.566483)
|
|
assert (state.history_exec["amount"] == AMOUNT / 30).all()
|
|
assert (state.history_exec["deal_amount"] == AMOUNT / 30).all()
|
|
assert is_close(state.history_exec["trade_price"].iloc[0], 149.566483)
|
|
assert is_close(state.history_exec["trade_value"].iloc[0], 1495.664825)
|
|
assert is_close(state.history_exec["position"].iloc[0], TOTAL_POSITION - AMOUNT / 30)
|
|
assert is_close(state.history_exec["ffr"].iloc[0], AMOUNT / TOTAL_POSITION / 30)
|
|
|
|
assert is_close(state.history_steps["market_volume"].iloc[0], 1254848.5756835938)
|
|
assert state.history_steps["amount"].iloc[0] == AMOUNT
|
|
assert state.history_steps["deal_amount"].iloc[0] == AMOUNT
|
|
assert state.history_steps["ffr"].iloc[0] == AMOUNT / TOTAL_POSITION
|
|
assert is_close(
|
|
state.history_steps["pa"].iloc[0] * (1.0 if order.direction == OrderDir.SELL else -1.0),
|
|
(state.history_steps["trade_price"].iloc[0] / simulator.twap_price - 1) * 10000,
|
|
)
|
|
|
|
|
|
@python_version_request
|
|
def test_simulator_stop_twap() -> None:
|
|
order = get_order()
|
|
simulator = get_simulator(order)
|
|
NUM_STEPS = 7
|
|
for i in range(NUM_STEPS):
|
|
simulator.step(TOTAL_POSITION / NUM_STEPS)
|
|
|
|
HISTORY_STEP_LENGTH = 30 * NUM_STEPS
|
|
state = simulator.get_state()
|
|
assert len(state.history_exec) == HISTORY_STEP_LENGTH
|
|
|
|
assert (state.history_exec["deal_amount"] == TOTAL_POSITION / HISTORY_STEP_LENGTH).all()
|
|
assert is_close(state.history_steps["position"].iloc[0], TOTAL_POSITION * (NUM_STEPS - 1) / NUM_STEPS)
|
|
assert is_close(state.history_steps["position"].iloc[-1], 0.0)
|
|
assert is_close(state.position, 0.0)
|
|
assert is_close(state.metrics["ffr"], 1.0)
|
|
|
|
assert is_close(state.metrics["market_price"], state.backtest_data.get_deal_price().mean())
|
|
assert is_close(state.metrics["market_volume"], state.backtest_data.get_volume().sum())
|
|
assert is_close(state.metrics["trade_price"], state.metrics["market_price"])
|
|
assert is_close(state.metrics["pa"], 0.0)
|
|
|
|
assert simulator.done()
|
|
|
|
|
|
@python_version_request
|
|
def test_interpreter() -> None:
|
|
NUM_EXECUTION = 3
|
|
order = get_order()
|
|
simulator = get_simulator(order)
|
|
interpreter_action = CategoricalActionInterpreter(values=NUM_EXECUTION)
|
|
|
|
NUM_STEPS = 7
|
|
state = simulator.get_state()
|
|
position_history = []
|
|
for i in range(NUM_STEPS):
|
|
simulator.step(interpreter_action(state, 1))
|
|
state = simulator.get_state()
|
|
position_history.append(state.position)
|
|
|
|
assert position_history[-1] == max(TOTAL_POSITION - TOTAL_POSITION / NUM_EXECUTION * (i + 1), 0.0)
|