mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-06 20:41:09 +08:00
Qlib RL framework (stage 1) - single-asset order execution (#1076)
* rl init * aux info * Reward config * update * simple * update saoe init * update simulator and seed * minor * minor * update sim * checkpoint * obs * Update interpreter * init qlib simulator * checkpoint * Refine codebase * checkpoint * checkpoint * Add one test * More tests * Simulator checkpoint * checkpoint * First-step tested * Checkpoint * Update data_queue API * Checkpoint * Update test * Move files * Checkpoint * Single-quote -> double-quote * Fix finite env tests * Tested with mypy * pep-574 * No call for env done * Update finite env docs * Fix csv writer * Refine tester * Update logger * Add another logger test * Checkpoint * Add network sanity test * steps per episode is not correct * Cleanup code, ready for PR * Reformat with black * Fix pylint for py37 * Fix lint * Fix lint * Fix flake * update mypy command * mypy * Update exclude pattern * Use pyproject.toml * test * . * . * Refactor pipeline * . * defaults run bash * . * Revert and skip follow_imports * Fix toml issue * fix mypy * . * . * . * Fix install * Minor fix * Fix test * Fix test * Remove requirements * Revert * fix tests * Fix lint * . * . * . * . * . * update install from source command * . * Fix data download * . * . * . * . * . * . * Fix py37 * Ignore tests on non-linux * resolve comments * fix tests * resolve comments * some typo * style updates * More comments * fix dummy * add warning * Align precision in some system * Added some impl notes Co-authored-by: Young <afe.young@gmail.com>
This commit is contained in:
10
tests/conftest.py
Normal file
10
tests/conftest.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
"""Ignore RL tests on non-linux platform."""
|
||||
collect_ignore = []
|
||||
|
||||
if sys.platform != "linux":
|
||||
for root, dirs, files in os.walk("rl"):
|
||||
for file in files:
|
||||
collect_ignore.append(os.path.join(root, file))
|
||||
4
tests/pytest.ini
Normal file
4
tests/pytest.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore:.*rng.randint:DeprecationWarning
|
||||
ignore:.*Casting input x to numpy array:UserWarning
|
||||
88
tests/rl/test_data_queue.py
Normal file
88
tests/rl/test_data_queue.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
from qlib.rl.utils.data_queue import DataQueue
|
||||
|
||||
|
||||
class DummyDataset(Dataset):
|
||||
def __init__(self, length):
|
||||
self.length = length
|
||||
|
||||
def __getitem__(self, index):
|
||||
assert 0 <= index < self.length
|
||||
return pd.DataFrame(np.random.randint(0, 100, size=(index + 1, 4)), columns=list("ABCD"))
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
|
||||
def _worker(dataloader, collector):
|
||||
# for i in range(3):
|
||||
for i, data in enumerate(dataloader):
|
||||
collector.put(len(data))
|
||||
|
||||
|
||||
def _queue_to_list(queue):
|
||||
result = []
|
||||
while not queue.empty():
|
||||
result.append(queue.get())
|
||||
return result
|
||||
|
||||
|
||||
def test_pytorch_dataloader():
|
||||
dataset = DummyDataset(100)
|
||||
dataloader = DataLoader(dataset, batch_size=None, num_workers=1)
|
||||
queue = multiprocessing.Queue()
|
||||
_worker(dataloader, queue)
|
||||
assert len(set(_queue_to_list(queue))) == 100
|
||||
|
||||
|
||||
def test_multiprocess_shared_dataloader():
|
||||
dataset = DummyDataset(100)
|
||||
with DataQueue(dataset, producer_num_workers=1) as data_queue:
|
||||
queue = multiprocessing.Queue()
|
||||
processes = []
|
||||
for _ in range(3):
|
||||
processes.append(multiprocessing.Process(target=_worker, args=(data_queue, queue)))
|
||||
processes[-1].start()
|
||||
for p in processes:
|
||||
p.join()
|
||||
assert len(set(_queue_to_list(queue))) == 100
|
||||
|
||||
|
||||
def test_exit_on_crash_finite():
|
||||
def _exit_finite():
|
||||
dataset = DummyDataset(100)
|
||||
|
||||
with DataQueue(dataset, producer_num_workers=4) as data_queue:
|
||||
time.sleep(3)
|
||||
raise ValueError
|
||||
|
||||
# https://stackoverflow.com/questions/34506638/how-to-register-atexit-function-in-pythons-multiprocessing-subprocess
|
||||
|
||||
process = multiprocessing.Process(target=_exit_finite)
|
||||
process.start()
|
||||
process.join()
|
||||
|
||||
|
||||
def test_exit_on_crash_infinite():
|
||||
def _exit_infinite():
|
||||
dataset = DummyDataset(100)
|
||||
with DataQueue(dataset, repeat=-1, queue_maxsize=100) as data_queue:
|
||||
time.sleep(3)
|
||||
raise ValueError
|
||||
|
||||
process = multiprocessing.Process(target=_exit_infinite)
|
||||
process.start()
|
||||
process.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_multiprocess_shared_dataloader()
|
||||
249
tests/rl/test_finite_env.py
Normal file
249
tests/rl/test_finite_env.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from collections import Counter
|
||||
|
||||
import gym
|
||||
import numpy as np
|
||||
from tianshou.data import Batch, Collector
|
||||
from tianshou.policy import BasePolicy
|
||||
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
||||
from qlib.rl.utils.finite_env import (
|
||||
LogWriter,
|
||||
FiniteDummyVectorEnv,
|
||||
FiniteShmemVectorEnv,
|
||||
FiniteSubprocVectorEnv,
|
||||
check_nan_observation,
|
||||
generate_nan_observation,
|
||||
)
|
||||
|
||||
|
||||
_test_space = gym.spaces.Dict(
|
||||
{
|
||||
"sensors": gym.spaces.Dict(
|
||||
{
|
||||
"position": gym.spaces.Box(low=-100, high=100, shape=(3,)),
|
||||
"velocity": gym.spaces.Box(low=-1, high=1, shape=(3,)),
|
||||
"front_cam": gym.spaces.Tuple(
|
||||
(gym.spaces.Box(low=0, high=1, shape=(10, 10, 3)), gym.spaces.Box(low=0, high=1, shape=(10, 10, 3)))
|
||||
),
|
||||
"rear_cam": gym.spaces.Box(low=0, high=1, shape=(10, 10, 3)),
|
||||
}
|
||||
),
|
||||
"ext_controller": gym.spaces.MultiDiscrete((5, 2, 2)),
|
||||
"inner_state": gym.spaces.Dict(
|
||||
{
|
||||
"charge": gym.spaces.Discrete(100),
|
||||
"system_checks": gym.spaces.MultiBinary(10),
|
||||
"job_status": gym.spaces.Dict(
|
||||
{
|
||||
"task": gym.spaces.Discrete(5),
|
||||
"progress": gym.spaces.Box(low=0, high=100, shape=()),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FiniteEnv(gym.Env):
|
||||
def __init__(self, dataset, num_replicas, rank):
|
||||
self.dataset = dataset
|
||||
self.num_replicas = num_replicas
|
||||
self.rank = rank
|
||||
self.loader = DataLoader(dataset, sampler=DistributedSampler(dataset, num_replicas, rank), batch_size=None)
|
||||
self.iterator = None
|
||||
self.observation_space = gym.spaces.Discrete(255)
|
||||
self.action_space = gym.spaces.Discrete(2)
|
||||
|
||||
def reset(self):
|
||||
if self.iterator is None:
|
||||
self.iterator = iter(self.loader)
|
||||
try:
|
||||
self.current_sample, self.step_count = next(self.iterator)
|
||||
self.current_step = 0
|
||||
return self.current_sample
|
||||
except StopIteration:
|
||||
self.iterator = None
|
||||
return generate_nan_observation(self.observation_space)
|
||||
|
||||
def step(self, action):
|
||||
self.current_step += 1
|
||||
assert self.current_step <= self.step_count
|
||||
return (
|
||||
0,
|
||||
1.0,
|
||||
self.current_step >= self.step_count,
|
||||
{"sample": self.current_sample, "action": action, "metric": 2.0},
|
||||
)
|
||||
|
||||
|
||||
class FiniteEnvWithComplexObs(FiniteEnv):
|
||||
def __init__(self, dataset, num_replicas, rank):
|
||||
self.dataset = dataset
|
||||
self.num_replicas = num_replicas
|
||||
self.rank = rank
|
||||
self.loader = DataLoader(dataset, sampler=DistributedSampler(dataset, num_replicas, rank), batch_size=None)
|
||||
self.iterator = None
|
||||
self.observation_space = gym.spaces.Discrete(255)
|
||||
self.action_space = gym.spaces.Discrete(2)
|
||||
|
||||
def reset(self):
|
||||
if self.iterator is None:
|
||||
self.iterator = iter(self.loader)
|
||||
try:
|
||||
self.current_sample, self.step_count = next(self.iterator)
|
||||
self.current_step = 0
|
||||
return _test_space.sample()
|
||||
except StopIteration:
|
||||
self.iterator = None
|
||||
return generate_nan_observation(self.observation_space)
|
||||
|
||||
def step(self, action):
|
||||
self.current_step += 1
|
||||
assert self.current_step <= self.step_count
|
||||
return (
|
||||
_test_space.sample(),
|
||||
1.0,
|
||||
self.current_step >= self.step_count,
|
||||
{"sample": _test_space.sample(), "action": action, "metric": 2.0},
|
||||
)
|
||||
|
||||
|
||||
class DummyDataset(Dataset):
|
||||
def __init__(self, length):
|
||||
self.length = length
|
||||
self.episodes = [3 * i % 5 + 1 for i in range(self.length)]
|
||||
|
||||
def __getitem__(self, index):
|
||||
assert 0 <= index < self.length
|
||||
return index, self.episodes[index]
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
|
||||
class AnyPolicy(BasePolicy):
|
||||
def forward(self, batch, state=None):
|
||||
return Batch(act=np.stack([1] * len(batch)))
|
||||
|
||||
def learn(self, batch):
|
||||
pass
|
||||
|
||||
|
||||
def _finite_env_factory(dataset, num_replicas, rank, complex=False):
|
||||
if complex:
|
||||
return lambda: FiniteEnvWithComplexObs(dataset, num_replicas, rank)
|
||||
return lambda: FiniteEnv(dataset, num_replicas, rank)
|
||||
|
||||
|
||||
class MetricTracker(LogWriter):
|
||||
def __init__(self, length):
|
||||
super().__init__()
|
||||
self.counter = Counter()
|
||||
self.finished = set()
|
||||
self.length = length
|
||||
|
||||
def on_env_step(self, env_id, obs, rew, done, info):
|
||||
assert rew == 1.0
|
||||
index = info["sample"]
|
||||
if done:
|
||||
# assert index not in self.finished
|
||||
self.finished.add(index)
|
||||
self.counter[index] += 1
|
||||
|
||||
def validate(self):
|
||||
assert len(self.finished) == self.length
|
||||
for k, v in self.counter.items():
|
||||
assert v == k * 3 % 5 + 1
|
||||
|
||||
|
||||
class DoNothingTracker(LogWriter):
|
||||
def on_env_step(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def test_finite_dummy_vector_env():
|
||||
length = 100
|
||||
dataset = DummyDataset(length)
|
||||
envs = FiniteDummyVectorEnv(MetricTracker(length), [_finite_env_factory(dataset, 5, i) for i in range(5)])
|
||||
envs._collector_guarded = True
|
||||
policy = AnyPolicy()
|
||||
test_collector = Collector(policy, envs, exploration_noise=True)
|
||||
|
||||
for _ in range(1):
|
||||
envs._logger = [MetricTracker(length)]
|
||||
try:
|
||||
test_collector.collect(n_step=10**18)
|
||||
except StopIteration:
|
||||
envs._logger[0].validate()
|
||||
|
||||
|
||||
def test_finite_shmem_vector_env():
|
||||
length = 100
|
||||
dataset = DummyDataset(length)
|
||||
envs = FiniteShmemVectorEnv(MetricTracker(length), [_finite_env_factory(dataset, 5, i) for i in range(5)])
|
||||
envs._collector_guarded = True
|
||||
policy = AnyPolicy()
|
||||
test_collector = Collector(policy, envs, exploration_noise=True)
|
||||
|
||||
for _ in range(1):
|
||||
envs._logger = [MetricTracker(length)]
|
||||
try:
|
||||
test_collector.collect(n_step=10**18)
|
||||
except StopIteration:
|
||||
envs._logger[0].validate()
|
||||
|
||||
|
||||
def test_finite_subproc_vector_env():
|
||||
length = 100
|
||||
dataset = DummyDataset(length)
|
||||
envs = FiniteSubprocVectorEnv(MetricTracker(length), [_finite_env_factory(dataset, 5, i) for i in range(5)])
|
||||
envs._collector_guarded = True
|
||||
policy = AnyPolicy()
|
||||
test_collector = Collector(policy, envs, exploration_noise=True)
|
||||
|
||||
for _ in range(1):
|
||||
envs._logger = [MetricTracker(length)]
|
||||
try:
|
||||
test_collector.collect(n_step=10**18)
|
||||
except StopIteration:
|
||||
envs._logger[0].validate()
|
||||
|
||||
|
||||
def test_nan():
|
||||
assert check_nan_observation(generate_nan_observation(_test_space))
|
||||
assert not check_nan_observation(_test_space.sample())
|
||||
|
||||
|
||||
def test_finite_dummy_vector_env_complex():
|
||||
length = 100
|
||||
dataset = DummyDataset(length)
|
||||
envs = FiniteDummyVectorEnv(
|
||||
DoNothingTracker(), [_finite_env_factory(dataset, 5, i, complex=True) for i in range(5)]
|
||||
)
|
||||
envs._collector_guarded = True
|
||||
policy = AnyPolicy()
|
||||
test_collector = Collector(policy, envs, exploration_noise=True)
|
||||
|
||||
try:
|
||||
test_collector.collect(n_step=10**18)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
|
||||
def test_finite_shmem_vector_env_complex():
|
||||
length = 100
|
||||
dataset = DummyDataset(length)
|
||||
envs = FiniteShmemVectorEnv(
|
||||
DoNothingTracker(), [_finite_env_factory(dataset, 5, i, complex=True) for i in range(5)]
|
||||
)
|
||||
envs._collector_guarded = True
|
||||
policy = AnyPolicy()
|
||||
test_collector = Collector(policy, envs, exploration_noise=True)
|
||||
|
||||
try:
|
||||
test_collector.collect(n_step=10**18)
|
||||
except StopIteration:
|
||||
pass
|
||||
156
tests/rl/test_logger.py
Normal file
156
tests/rl/test_logger.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from random import randint, choice
|
||||
from pathlib import Path
|
||||
|
||||
import re
|
||||
import gym
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from gym import spaces
|
||||
from tianshou.data import Collector, Batch
|
||||
from tianshou.policy import BasePolicy
|
||||
|
||||
from qlib.log import set_log_with_config
|
||||
from qlib.config import C
|
||||
from qlib.constant import INF
|
||||
from qlib.rl.interpreter import StateInterpreter, ActionInterpreter
|
||||
from qlib.rl.simulator import Simulator
|
||||
from qlib.rl.utils.data_queue import DataQueue
|
||||
from qlib.rl.utils.env_wrapper import InfoDict, EnvWrapper
|
||||
from qlib.rl.utils.log import LogLevel, LogCollector, CsvWriter, ConsoleWriter
|
||||
from qlib.rl.utils.finite_env import vectorize_env
|
||||
|
||||
|
||||
class SimpleEnv(gym.Env[int, int]):
|
||||
def __init__(self):
|
||||
self.logger = LogCollector()
|
||||
self.observation_space = gym.spaces.Discrete(2)
|
||||
self.action_space = gym.spaces.Discrete(2)
|
||||
|
||||
def reset(self):
|
||||
self.step_count = 0
|
||||
return 0
|
||||
|
||||
def step(self, action: int):
|
||||
self.logger.reset()
|
||||
|
||||
self.logger.add_scalar("reward", 42.0)
|
||||
|
||||
self.logger.add_scalar("a", randint(1, 10))
|
||||
self.logger.add_array("b", pd.DataFrame({"a": [1, 2], "b": [3, 4]}))
|
||||
|
||||
if self.step_count >= 3:
|
||||
done = choice([False, True])
|
||||
else:
|
||||
done = False
|
||||
|
||||
if 2 <= self.step_count <= 3:
|
||||
self.logger.add_scalar("c", randint(11, 20))
|
||||
|
||||
self.step_count += 1
|
||||
|
||||
return 1, 42.0, done, InfoDict(log=self.logger.logs(), aux_info={})
|
||||
|
||||
|
||||
class AnyPolicy(BasePolicy):
|
||||
def forward(self, batch, state=None):
|
||||
return Batch(act=np.stack([1] * len(batch)))
|
||||
|
||||
def learn(self, batch):
|
||||
pass
|
||||
|
||||
|
||||
def test_simple_env_logger(caplog):
|
||||
set_log_with_config(C.logging_config)
|
||||
for venv_cls_name in ["dummy", "shmem", "subproc"]:
|
||||
writer = ConsoleWriter()
|
||||
csv_writer = CsvWriter(Path(__file__).parent / ".output")
|
||||
venv = vectorize_env(lambda: SimpleEnv(), venv_cls_name, 4, [writer, csv_writer])
|
||||
with venv.collector_guard():
|
||||
collector = Collector(AnyPolicy(), venv)
|
||||
collector.collect(n_episode=30)
|
||||
|
||||
output_file = pd.read_csv(Path(__file__).parent / ".output" / "result.csv")
|
||||
assert output_file.columns.tolist() == ["reward", "a", "c"]
|
||||
assert len(output_file) >= 30
|
||||
|
||||
line_counter = 0
|
||||
for line in caplog.text.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
line_counter += 1
|
||||
assert re.match(r".*reward 42\.0000 \(42.0000\) a .* \((4|5|6)\.\d+\) c .* \((14|15|16)\.\d+\)", line)
|
||||
assert line_counter >= 3
|
||||
|
||||
|
||||
class SimpleSimulator(Simulator[int, float, float]):
|
||||
def __init__(self, initial: int, **kwargs) -> None:
|
||||
self.initial = float(initial)
|
||||
|
||||
def step(self, action: float) -> None:
|
||||
import torch
|
||||
|
||||
self.initial += action
|
||||
self.env.logger.add_scalar("test_a", torch.tensor(233.0))
|
||||
self.env.logger.add_scalar("test_b", np.array(200))
|
||||
|
||||
def get_state(self) -> float:
|
||||
return self.initial
|
||||
|
||||
def done(self) -> bool:
|
||||
return self.initial % 1 > 0.5
|
||||
|
||||
|
||||
class DummyStateInterpreter(StateInterpreter[float, float]):
|
||||
def interpret(self, state: float) -> float:
|
||||
return state
|
||||
|
||||
@property
|
||||
def observation_space(self) -> spaces.Box:
|
||||
return spaces.Box(0, np.inf, shape=(), dtype=np.float32)
|
||||
|
||||
|
||||
class DummyActionInterpreter(ActionInterpreter[float, int, float]):
|
||||
def interpret(self, state: float, action: int) -> float:
|
||||
return action / 100
|
||||
|
||||
@property
|
||||
def action_space(self) -> spaces.Box:
|
||||
return spaces.Discrete(5)
|
||||
|
||||
|
||||
class RandomFivePolicy(BasePolicy):
|
||||
def forward(self, batch, state=None):
|
||||
return Batch(act=np.random.randint(5, size=len(batch)))
|
||||
|
||||
def learn(self, batch):
|
||||
pass
|
||||
|
||||
|
||||
def test_logger_with_env_wrapper():
|
||||
with DataQueue(list(range(20)), shuffle=False) as data_iterator:
|
||||
env_wrapper_factory = lambda: EnvWrapper(
|
||||
SimpleSimulator,
|
||||
DummyStateInterpreter(),
|
||||
DummyActionInterpreter(),
|
||||
data_iterator,
|
||||
logger=LogCollector(LogLevel.DEBUG),
|
||||
)
|
||||
|
||||
# loglevel can be debug here because metrics can all dump into csv
|
||||
# otherwise, csv writer might crash
|
||||
csv_writer = CsvWriter(Path(__file__).parent / ".output", loglevel=LogLevel.DEBUG)
|
||||
venv = vectorize_env(env_wrapper_factory, "shmem", 4, csv_writer)
|
||||
with venv.collector_guard():
|
||||
collector = Collector(RandomFivePolicy(), venv)
|
||||
collector.collect(n_episode=INF * len(venv))
|
||||
|
||||
output_df = pd.read_csv(Path(__file__).parent / ".output" / "result.csv")
|
||||
assert len(output_df) == 20
|
||||
# obs has a increasing trend
|
||||
assert output_df["obs"].to_numpy()[:10].sum() < output_df["obs"].to_numpy()[10:].sum()
|
||||
assert (output_df["test_a"] == 233).all()
|
||||
assert (output_df["test_b"] == 200).all()
|
||||
assert "steps_per_episode" in output_df and "reward" in output_df
|
||||
308
tests/rl/test_saoe_simple.py
Normal file
308
tests/rl/test_saoe_simple.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
import torch
|
||||
from tianshou.data import Batch
|
||||
|
||||
from qlib.backtest import Order
|
||||
from qlib.config import C
|
||||
from qlib.log import set_log_with_config
|
||||
from qlib.rl.data import pickle_styled
|
||||
from qlib.rl.entries.test import backtest
|
||||
from qlib.rl.order_execution import *
|
||||
from qlib.rl.utils import ConsoleWriter, CsvWriter, EnvWrapperStatus
|
||||
|
||||
pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason="Pickle styled data only supports Python >= 3.8")
|
||||
|
||||
|
||||
DATA_ROOT_DIR = Path(__file__).parent.parent / ".data" / "rl" / "intraday_saoe"
|
||||
DATA_DIR = DATA_ROOT_DIR / "us"
|
||||
BACKTEST_DATA_DIR = DATA_DIR / "backtest"
|
||||
FEATURE_DATA_DIR = DATA_DIR / "processed"
|
||||
ORDER_DIR = DATA_DIR / "order" / "valid_bidir"
|
||||
|
||||
CN_DATA_DIR = DATA_ROOT_DIR / "cn"
|
||||
CN_BACKTEST_DATA_DIR = CN_DATA_DIR / "backtest"
|
||||
CN_FEATURE_DATA_DIR = CN_DATA_DIR / "processed"
|
||||
CN_ORDER_DIR = CN_DATA_DIR / "order" / "test"
|
||||
CN_POLICY_WEIGHTS_DIR = CN_DATA_DIR / "weights"
|
||||
|
||||
|
||||
def test_pickle_data_inspect():
|
||||
data = pickle_styled.load_intraday_backtest_data(BACKTEST_DATA_DIR, "AAL", "2013-12-11", "close", 0)
|
||||
assert len(data) == 390
|
||||
|
||||
data = pickle_styled.load_intraday_processed_data(
|
||||
DATA_DIR / "processed", "AAL", "2013-12-11", 5, data.get_time_index()
|
||||
)
|
||||
assert len(data.today) == len(data.yesterday) == 390
|
||||
|
||||
|
||||
def test_simulator_first_step():
|
||||
order = Order("AAL", 30.0, 0, pd.Timestamp("2013-12-11 00:00:00"), pd.Timestamp("2013-12-11 23:59:59"))
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
state = simulator.get_state()
|
||||
assert state.cur_time == pd.Timestamp("2013-12-11 09:30:00")
|
||||
assert state.position == 30.0
|
||||
|
||||
simulator.step(15.0)
|
||||
state = simulator.get_state()
|
||||
assert len(state.history_exec) == 30
|
||||
assert state.history_exec.index[0] == pd.Timestamp("2013-12-11 09:30:00")
|
||||
assert state.history_exec["market_volume"].iloc[0] == 450072.0
|
||||
assert abs(state.history_exec["market_price"].iloc[0] - 25.370001) < 1e-4
|
||||
assert (state.history_exec["amount"] == 0.5).all()
|
||||
assert (state.history_exec["deal_amount"] == 0.5).all()
|
||||
assert abs(state.history_exec["trade_price"].iloc[0] - 25.370001) < 1e-4
|
||||
assert abs(state.history_exec["trade_value"].iloc[0] - 12.68500) < 1e-4
|
||||
assert state.history_exec["position"].iloc[0] == 29.5
|
||||
assert state.history_exec["ffr"].iloc[0] == 1 / 60
|
||||
|
||||
assert state.history_steps["market_volume"].iloc[0] == 5041147.0
|
||||
assert state.history_steps["amount"].iloc[0] == 15.0
|
||||
assert state.history_steps["deal_amount"].iloc[0] == 15.0
|
||||
assert state.history_steps["ffr"].iloc[0] == 0.5
|
||||
assert (
|
||||
state.history_steps["pa"].iloc[0]
|
||||
== (state.history_steps["trade_price"].iloc[0] / simulator.twap_price - 1) * 10000
|
||||
)
|
||||
|
||||
assert state.position == 15.0
|
||||
assert state.cur_time == pd.Timestamp("2013-12-11 10:00:00")
|
||||
|
||||
|
||||
def test_simulator_stop_twap():
|
||||
order = Order("AAL", 13.0, 0, pd.Timestamp("2013-12-11 00:00:00"), pd.Timestamp("2013-12-11 23:59:59"))
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
for _ in range(13):
|
||||
simulator.step(1.0)
|
||||
|
||||
state = simulator.get_state()
|
||||
assert len(state.history_exec) == 390
|
||||
assert (state.history_exec["deal_amount"] == 13 / 390).all()
|
||||
assert state.history_steps["position"].iloc[0] == 12 and state.history_steps["position"].iloc[-1] == 0
|
||||
|
||||
assert (state.metrics["ffr"] - 1) < 1e-3
|
||||
assert abs(state.metrics["market_price"] - state.backtest_data.get_deal_price().mean()) < 1e-4
|
||||
assert np.isclose(state.metrics["market_volume"], state.backtest_data.get_volume().sum())
|
||||
assert state.position == 0.0
|
||||
assert abs(state.metrics["trade_price"] - state.metrics["market_price"]) < 1e-4
|
||||
assert abs(state.metrics["pa"]) < 1e-2
|
||||
|
||||
assert simulator.done()
|
||||
|
||||
|
||||
def test_simulator_stop_early():
|
||||
order = Order("AAL", 1.0, 1, pd.Timestamp("2013-12-11 00:00:00"), pd.Timestamp("2013-12-11 23:59:59"))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
simulator.step(2.0)
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
simulator.step(1.0)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
simulator.step(1.0)
|
||||
|
||||
|
||||
def test_simulator_start_middle():
|
||||
order = Order("AAL", 15.0, 1, pd.Timestamp("2013-12-11 10:15:00"), pd.Timestamp("2013-12-11 15:44:59"))
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
assert len(simulator.ticks_for_order) == 330
|
||||
assert simulator.cur_time == pd.Timestamp("2013-12-11 10:15:00")
|
||||
simulator.step(2.0)
|
||||
assert simulator.cur_time == pd.Timestamp("2013-12-11 10:30:00")
|
||||
|
||||
for _ in range(10):
|
||||
simulator.step(1.0)
|
||||
|
||||
simulator.step(2.0)
|
||||
assert len(simulator.history_exec) == 330
|
||||
assert simulator.done()
|
||||
assert abs(simulator.history_exec["amount"].iloc[-1] - (1 + 2 / 15)) < 1e-4
|
||||
assert abs(simulator.metrics["ffr"] - 1) < 1e-4
|
||||
|
||||
|
||||
def test_interpreter():
|
||||
order = Order("AAL", 15.0, 1, pd.Timestamp("2013-12-11 10:15:00"), pd.Timestamp("2013-12-11 15:44:59"))
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
assert len(simulator.ticks_for_order) == 330
|
||||
assert simulator.cur_time == pd.Timestamp("2013-12-11 10:15:00")
|
||||
|
||||
# emulate a env status
|
||||
class EmulateEnvWrapper(NamedTuple):
|
||||
status: EnvWrapperStatus
|
||||
|
||||
interpreter = FullHistoryStateInterpreter(FEATURE_DATA_DIR, 13, 390, 5)
|
||||
interpreter_step = CurrentStepStateInterpreter(13)
|
||||
interpreter_action = CategoricalActionInterpreter(20)
|
||||
interpreter_action_twap = TwapRelativeActionInterpreter()
|
||||
|
||||
wrapper_status_kwargs = dict(initial_state=order, obs_history=[], action_history=[], reward_history=[])
|
||||
|
||||
# first step
|
||||
interpreter.env = EmulateEnvWrapper(status=EnvWrapperStatus(cur_step=0, done=False, **wrapper_status_kwargs))
|
||||
|
||||
obs = interpreter(simulator.get_state())
|
||||
assert obs["cur_tick"] == 45
|
||||
assert obs["cur_step"] == 0
|
||||
assert obs["position"] == 15.0
|
||||
assert obs["position_history"][0] == 15.0
|
||||
assert all(np.sum(obs["data_processed"][i]) != 0 for i in range(45))
|
||||
assert np.sum(obs["data_processed"][45:]) == 0
|
||||
assert obs["data_processed_prev"].shape == (390, 5)
|
||||
|
||||
# first step: second interpreter
|
||||
interpreter_step.env = EmulateEnvWrapper(status=EnvWrapperStatus(cur_step=0, done=False, **wrapper_status_kwargs))
|
||||
|
||||
obs = interpreter_step(simulator.get_state())
|
||||
assert obs["acquiring"] == 1
|
||||
assert obs["position"] == 15.0
|
||||
|
||||
# second step
|
||||
simulator.step(5.0)
|
||||
interpreter.env = EmulateEnvWrapper(status=EnvWrapperStatus(cur_step=1, done=False, **wrapper_status_kwargs))
|
||||
|
||||
obs = interpreter(simulator.get_state())
|
||||
assert obs["cur_tick"] == 60
|
||||
assert obs["cur_step"] == 1
|
||||
assert obs["position"] == 10.0
|
||||
assert obs["position_history"][:2].tolist() == [15.0, 10.0]
|
||||
assert all(np.sum(obs["data_processed"][i]) != 0 for i in range(60))
|
||||
assert np.sum(obs["data_processed"][60:]) == 0
|
||||
|
||||
# second step: action
|
||||
action = interpreter_action(simulator.get_state(), 1)
|
||||
assert action == 15 / 20
|
||||
|
||||
interpreter_action_twap.env = EmulateEnvWrapper(
|
||||
status=EnvWrapperStatus(cur_step=1, done=False, **wrapper_status_kwargs)
|
||||
)
|
||||
action = interpreter_action_twap(simulator.get_state(), 1.5)
|
||||
assert action == 1.5
|
||||
|
||||
# fast-forward
|
||||
for _ in range(10):
|
||||
simulator.step(0.0)
|
||||
|
||||
# last step
|
||||
simulator.step(5.0)
|
||||
interpreter.env = EmulateEnvWrapper(
|
||||
status=EnvWrapperStatus(cur_step=12, done=simulator.done(), **wrapper_status_kwargs)
|
||||
)
|
||||
|
||||
assert interpreter.env.status["done"]
|
||||
|
||||
obs = interpreter(simulator.get_state())
|
||||
assert obs["cur_tick"] == 375
|
||||
assert obs["cur_step"] == 12
|
||||
assert obs["position"] == 0.0
|
||||
assert obs["position_history"][1:11].tolist() == [10.0] * 10
|
||||
assert all(np.sum(obs["data_processed"][i]) != 0 for i in range(375))
|
||||
assert np.sum(obs["data_processed"][375:]) == 0
|
||||
|
||||
|
||||
def test_network_sanity():
|
||||
# we won't check the correctness of networks here
|
||||
order = Order("AAL", 15.0, 1, pd.Timestamp("2013-12-11 9:30:00"), pd.Timestamp("2013-12-11 15:59:59"))
|
||||
|
||||
simulator = SingleAssetOrderExecution(order, BACKTEST_DATA_DIR)
|
||||
assert len(simulator.ticks_for_order) == 390
|
||||
|
||||
class EmulateEnvWrapper(NamedTuple):
|
||||
status: EnvWrapperStatus
|
||||
|
||||
interpreter = FullHistoryStateInterpreter(FEATURE_DATA_DIR, 13, 390, 5)
|
||||
action_interp = CategoricalActionInterpreter(13)
|
||||
|
||||
wrapper_status_kwargs = dict(initial_state=order, obs_history=[], action_history=[], reward_history=[])
|
||||
|
||||
network = Recurrent(interpreter.observation_space)
|
||||
policy = PPO(network, interpreter.observation_space, action_interp.action_space, 1e-3)
|
||||
|
||||
for i in range(14):
|
||||
interpreter.env = EmulateEnvWrapper(status=EnvWrapperStatus(cur_step=i, done=False, **wrapper_status_kwargs))
|
||||
obs = interpreter(simulator.get_state())
|
||||
batch = Batch(obs=[obs])
|
||||
output = policy(batch)
|
||||
assert 0 <= output["act"].item() <= 13
|
||||
if i < 13:
|
||||
simulator.step(1.0)
|
||||
else:
|
||||
assert obs["cur_tick"] == 389
|
||||
assert obs["cur_step"] == 12
|
||||
assert obs["position_history"][-1] == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("finite_env_type", ["dummy", "subproc", "shmem"])
|
||||
def test_twap_strategy(finite_env_type):
|
||||
set_log_with_config(C.logging_config)
|
||||
orders = pickle_styled.load_orders(ORDER_DIR)
|
||||
assert len(orders) == 248
|
||||
|
||||
state_interp = FullHistoryStateInterpreter(FEATURE_DATA_DIR, 13, 390, 5)
|
||||
action_interp = TwapRelativeActionInterpreter()
|
||||
policy = AllOne(state_interp.observation_space, action_interp.action_space)
|
||||
csv_writer = CsvWriter(Path(__file__).parent / ".output")
|
||||
|
||||
backtest(
|
||||
partial(SingleAssetOrderExecution, data_dir=BACKTEST_DATA_DIR, ticks_per_step=30),
|
||||
state_interp,
|
||||
action_interp,
|
||||
orders,
|
||||
policy,
|
||||
[ConsoleWriter(total_episodes=len(orders)), csv_writer],
|
||||
concurrency=4,
|
||||
finite_env_type=finite_env_type,
|
||||
)
|
||||
|
||||
metrics = pd.read_csv(Path(__file__).parent / ".output" / "result.csv")
|
||||
assert len(metrics) == 248
|
||||
assert np.isclose(metrics["ffr"].mean(), 1.0)
|
||||
assert np.isclose(metrics["pa"].mean(), 0.0)
|
||||
assert np.allclose(metrics["pa"], 0.0, atol=2e-3)
|
||||
|
||||
|
||||
def test_cn_ppo_strategy():
|
||||
set_log_with_config(C.logging_config)
|
||||
# The data starts with 9:31 and ends with 15:00
|
||||
orders = pickle_styled.load_orders(CN_ORDER_DIR, start_time=pd.Timestamp("9:31"), end_time=pd.Timestamp("14:58"))
|
||||
assert len(orders) == 40
|
||||
|
||||
state_interp = FullHistoryStateInterpreter(CN_FEATURE_DATA_DIR, 8, 240, 6)
|
||||
action_interp = CategoricalActionInterpreter(4)
|
||||
network = Recurrent(state_interp.observation_space)
|
||||
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"))
|
||||
csv_writer = CsvWriter(Path(__file__).parent / ".output")
|
||||
|
||||
backtest(
|
||||
partial(SingleAssetOrderExecution, data_dir=CN_BACKTEST_DATA_DIR, ticks_per_step=30),
|
||||
state_interp,
|
||||
action_interp,
|
||||
orders,
|
||||
policy,
|
||||
[ConsoleWriter(total_episodes=len(orders)), csv_writer],
|
||||
concurrency=4,
|
||||
)
|
||||
|
||||
metrics = pd.read_csv(Path(__file__).parent / ".output" / "result.csv")
|
||||
assert len(metrics) == len(orders)
|
||||
assert np.isclose(metrics["ffr"].mean(), 1.0)
|
||||
assert np.isclose(metrics["pa"].mean(), -16.21578303474833)
|
||||
assert np.isclose(metrics["market_price"].mean(), 58.68277690875527)
|
||||
assert np.isclose(metrics["trade_price"].mean(), 58.76063985000002)
|
||||
Reference in New Issue
Block a user