mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-17 01:14:35 +08:00
Refine backtest codes (#1120)
* Refine backtest code * Keep working * Minor * Resolve PR comments * Fix import error * Fix import error
This commit is contained in:
@@ -2,24 +2,29 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import List, Tuple, Union, TYPE_CHECKING
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Generator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
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
|
||||||
from .executor import BaseExecutor
|
from .executor import BaseExecutor
|
||||||
from .decision import BaseTradeDecision
|
from .decision import BaseTradeDecision
|
||||||
from .position import Position
|
|
||||||
from .exchange import Exchange
|
|
||||||
from .backtest import backtest_loop
|
|
||||||
from .backtest import collect_data_loop
|
|
||||||
from .utils import CommonInfrastructure
|
|
||||||
from .decision import Order
|
|
||||||
from ..utils import init_instance_by_config
|
|
||||||
from ..log import get_module_logger
|
|
||||||
from ..config import C
|
from ..config import C
|
||||||
|
from ..log import get_module_logger
|
||||||
|
from ..utils import init_instance_by_config
|
||||||
|
from .backtest import backtest_loop, collect_data_loop
|
||||||
|
from .decision import Order
|
||||||
|
from .exchange import Exchange
|
||||||
|
from .position import Position
|
||||||
|
from .utils import CommonInfrastructure
|
||||||
|
|
||||||
# make import more user-friendly by adding `from qlib.backtest import STH`
|
# make import more user-friendly by adding `from qlib.backtest import STH`
|
||||||
|
|
||||||
@@ -28,26 +33,34 @@ logger = get_module_logger("backtest caller")
|
|||||||
|
|
||||||
|
|
||||||
def get_exchange(
|
def get_exchange(
|
||||||
exchange=None,
|
exchange: Union[str, dict, object, Path] = None,
|
||||||
freq="day",
|
freq: str = "day",
|
||||||
start_time=None,
|
start_time: Union[pd.Timestamp, str] = None,
|
||||||
end_time=None,
|
end_time: Union[pd.Timestamp, str] = None,
|
||||||
codes="all",
|
codes: Union[list, str] = "all",
|
||||||
subscribe_fields=[],
|
subscribe_fields: list = [],
|
||||||
open_cost=0.0015,
|
open_cost: float = 0.0015,
|
||||||
close_cost=0.0025,
|
close_cost: float = 0.0025,
|
||||||
min_cost=5.0,
|
min_cost: float = 5.0,
|
||||||
limit_threshold=None,
|
limit_threshold: Union[Tuple[str, str], float, None] = None,
|
||||||
deal_price: Union[str, Tuple[str], List[str]] = None,
|
deal_price: Union[str, Tuple[str], List[str]] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> Exchange:
|
||||||
"""get_exchange
|
"""get_exchange
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
||||||
# exchange related arguments
|
# exchange related arguments
|
||||||
exchange: Exchange().
|
exchange: Exchange(). It could be None or any types that are acceptable by `init_instance_by_config`.
|
||||||
|
freq: str
|
||||||
|
frequency of data.
|
||||||
|
start_time: Union[pd.Timestamp, str]
|
||||||
|
closed start time for backtest.
|
||||||
|
end_time: Union[pd.Timestamp, str]
|
||||||
|
closed end time for backtest.
|
||||||
|
codes: list|str
|
||||||
|
list stock_id list or a string of instruments (i.e. all, csi500, sse50)
|
||||||
subscribe_fields: list
|
subscribe_fields: list
|
||||||
subscribe fields.
|
subscribe fields.
|
||||||
open_cost : float
|
open_cost : float
|
||||||
@@ -57,8 +70,6 @@ def get_exchange(
|
|||||||
min_cost : float
|
min_cost : float
|
||||||
min transaction cost. It is an absolute amount of cost instead of a ratio of your order's deal amount.
|
min transaction cost. It is an absolute amount of cost instead of a ratio of your order's deal amount.
|
||||||
e.g. You must pay at least 5 yuan of commission regardless of your order's deal amount.
|
e.g. You must pay at least 5 yuan of commission regardless of your order's deal amount.
|
||||||
trade_unit : int
|
|
||||||
Included in kwargs. Please refer to the docs of `__init__` of `Exchange`
|
|
||||||
deal_price: Union[str, Tuple[str], List[str]]
|
deal_price: Union[str, Tuple[str], List[str]]
|
||||||
The `deal_price` supports following two types of input
|
The `deal_price` supports following two types of input
|
||||||
- <deal_price> : str
|
- <deal_price> : str
|
||||||
@@ -101,10 +112,14 @@ def get_exchange(
|
|||||||
|
|
||||||
|
|
||||||
def create_account_instance(
|
def create_account_instance(
|
||||||
start_time, end_time, benchmark: str, account: Union[float, int, dict], pos_type: str = "Position"
|
start_time: Union[pd.Timestamp, str],
|
||||||
|
end_time: Union[pd.Timestamp, str],
|
||||||
|
benchmark: str,
|
||||||
|
account: Union[float, int, dict],
|
||||||
|
pos_type: str = "Position",
|
||||||
) -> Account:
|
) -> Account:
|
||||||
"""
|
"""
|
||||||
# TODO: is very strange pass benchmark_config in the account(maybe for report)
|
# TODO: is very strange pass benchmark_config in the account (maybe for report)
|
||||||
# There should be a post-step to process the report.
|
# There should be a post-step to process the report.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -132,6 +147,8 @@ def create_account_instance(
|
|||||||
key "cash" means initial cash.
|
key "cash" means initial cash.
|
||||||
key "stock1" means the information of first stock with amount and price(optional).
|
key "stock1" means the information of first stock with amount and price(optional).
|
||||||
...
|
...
|
||||||
|
pos_type: str
|
||||||
|
Postion type.
|
||||||
"""
|
"""
|
||||||
if isinstance(account, (int, float)):
|
if isinstance(account, (int, float)):
|
||||||
pos_kwargs = {"init_cash": account}
|
pos_kwargs = {"init_cash": account}
|
||||||
@@ -159,15 +176,15 @@ def create_account_instance(
|
|||||||
|
|
||||||
|
|
||||||
def get_strategy_executor(
|
def get_strategy_executor(
|
||||||
start_time,
|
start_time: Union[pd.Timestamp, str],
|
||||||
end_time,
|
end_time: Union[pd.Timestamp, str],
|
||||||
strategy: BaseStrategy,
|
strategy: Union[str, dict, object, Path],
|
||||||
executor: BaseExecutor,
|
executor: Union[str, dict, object, Path],
|
||||||
benchmark: str = "SH000300",
|
benchmark: str = "SH000300",
|
||||||
account: Union[float, int, Position] = 1e9,
|
account: Union[float, int, Position] = 1e9,
|
||||||
exchange_kwargs: dict = {},
|
exchange_kwargs: dict = {},
|
||||||
pos_type: str = "Position",
|
pos_type: str = "Position",
|
||||||
):
|
) -> Tuple[BaseStrategy, BaseExecutor]:
|
||||||
|
|
||||||
# NOTE:
|
# NOTE:
|
||||||
# - for avoiding recursive import
|
# - for avoiding recursive import
|
||||||
@@ -176,7 +193,11 @@ def get_strategy_executor(
|
|||||||
from .executor import BaseExecutor # pylint: disable=C0415
|
from .executor import BaseExecutor # pylint: disable=C0415
|
||||||
|
|
||||||
trade_account = create_account_instance(
|
trade_account = create_account_instance(
|
||||||
start_time=start_time, end_time=end_time, benchmark=benchmark, account=account, pos_type=pos_type
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
benchmark=benchmark,
|
||||||
|
account=account,
|
||||||
|
pos_type=pos_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
exchange_kwargs = copy.copy(exchange_kwargs)
|
exchange_kwargs = copy.copy(exchange_kwargs)
|
||||||
@@ -196,29 +217,31 @@ def get_strategy_executor(
|
|||||||
|
|
||||||
|
|
||||||
def backtest(
|
def backtest(
|
||||||
start_time,
|
start_time: Union[pd.Timestamp, str],
|
||||||
end_time,
|
end_time: Union[pd.Timestamp, str],
|
||||||
strategy,
|
strategy: Union[str, dict, object, Path],
|
||||||
executor,
|
executor: Union[str, dict, object, Path],
|
||||||
benchmark="SH000300",
|
benchmark: str = "SH000300",
|
||||||
account=1e9,
|
account: Union[float, int, Position] = 1e9,
|
||||||
exchange_kwargs={},
|
exchange_kwargs: dict = {},
|
||||||
pos_type: str = "Position",
|
pos_type: str = "Position",
|
||||||
):
|
) -> Tuple[PortfolioMetrics, Indicator]:
|
||||||
"""initialize the strategy and executor, then backtest function for the interaction of the outermost strategy and executor in the nested decision execution
|
"""initialize the strategy and executor, then backtest function for the interaction of the outermost strategy and
|
||||||
|
executor in the nested decision execution
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start_time : pd.Timestamp|str
|
start_time : Union[pd.Timestamp, str]
|
||||||
closed start time for backtest
|
closed start time for backtest
|
||||||
**NOTE**: This will be applied to the outmost executor's calendar.
|
**NOTE**: This will be applied to the outmost executor's calendar.
|
||||||
end_time : pd.Timestamp|str
|
end_time : Union[pd.Timestamp, str]
|
||||||
closed end time for backtest
|
closed end time for backtest
|
||||||
**NOTE**: This will be applied to the outmost executor's calendar.
|
**NOTE**: This will be applied to the outmost executor's calendar.
|
||||||
E.g. Executor[day](Executor[1min]), setting `end_time == 20XX0301` will include all the minutes on 20XX0301
|
E.g. Executor[day](Executor[1min]), setting `end_time == 20XX0301` will include all the minutes on 20XX0301
|
||||||
strategy : Union[str, dict, BaseStrategy]
|
strategy : Union[str, dict, object, Path]
|
||||||
for initializing outermost portfolio strategy. Please refer to the docs of init_instance_by_config for more information.
|
for initializing outermost portfolio strategy. Please refer to the docs of init_instance_by_config for more
|
||||||
executor : Union[str, dict, BaseExecutor]
|
information.
|
||||||
|
executor : Union[str, dict, object, Path]
|
||||||
for initializing the outermost executor.
|
for initializing the outermost executor.
|
||||||
benchmark: str
|
benchmark: str
|
||||||
the benchmark for reporting.
|
the benchmark for reporting.
|
||||||
@@ -257,16 +280,16 @@ def backtest(
|
|||||||
|
|
||||||
|
|
||||||
def collect_data(
|
def collect_data(
|
||||||
start_time,
|
start_time: Union[pd.Timestamp, str],
|
||||||
end_time,
|
end_time: Union[pd.Timestamp, str],
|
||||||
strategy,
|
strategy: Union[str, dict, object, Path],
|
||||||
executor,
|
executor: Union[str, dict, object, Path],
|
||||||
benchmark="SH000300",
|
benchmark: str = "SH000300",
|
||||||
account=1e9,
|
account: Union[float, int, Position] = 1e9,
|
||||||
exchange_kwargs={},
|
exchange_kwargs: dict = {},
|
||||||
pos_type: str = "Position",
|
pos_type: str = "Position",
|
||||||
return_value: dict = None,
|
return_value: dict = None,
|
||||||
):
|
) -> Generator[object, None, None]:
|
||||||
"""initialize the strategy and executor, then collect the trade decision data for rl training
|
"""initialize the strategy and executor, then collect the trade decision data for rl training
|
||||||
|
|
||||||
please refer to the docs of the backtest for the explanation of the parameters
|
please refer to the docs of the backtest for the explanation of the parameters
|
||||||
@@ -291,7 +314,7 @@ def collect_data(
|
|||||||
|
|
||||||
def format_decisions(
|
def format_decisions(
|
||||||
decisions: List[BaseTradeDecision],
|
decisions: List[BaseTradeDecision],
|
||||||
) -> Tuple[str, List[Tuple[BaseTradeDecision, Union[Tuple, None]]]]:
|
) -> Optional[Tuple[str, List[Tuple[BaseTradeDecision, Union[Tuple, None]]]]]:
|
||||||
"""
|
"""
|
||||||
format the decisions collected by `qlib.backtest.collect_data`
|
format the decisions collected by `qlib.backtest.collect_data`
|
||||||
The decisions will be organized into a tree-like structure.
|
The decisions will be organized into a tree-like structure.
|
||||||
@@ -326,4 +349,4 @@ def format_decisions(
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Order"]
|
__all__ = ["Order", "backtest"]
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# Copyright (c) Microsoft Corporation.
|
# Copyright (c) Microsoft Corporation.
|
||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
from qlib.utils import init_instance_by_config
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from .position import BasePosition
|
from qlib.utils import init_instance_by_config
|
||||||
from .report import PortfolioMetrics, Indicator
|
|
||||||
from .decision import BaseTradeDecision, Order
|
from .decision import BaseTradeDecision, Order
|
||||||
from .exchange import Exchange
|
from .exchange import Exchange
|
||||||
|
from .position import BasePosition
|
||||||
|
from .report import Indicator, PortfolioMetrics
|
||||||
|
|
||||||
"""
|
"""
|
||||||
rtn & earning in the Account
|
rtn & earning in the Account
|
||||||
@@ -34,40 +37,42 @@ class AccumulatedInfo:
|
|||||||
AccumulatedInfo should be shared across different levels
|
AccumulatedInfo should be shared across different levels
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
self.rtn = 0 # accumulated return, do not consider cost
|
self.rtn: float = 0.0 # accumulated return, do not consider cost
|
||||||
self.cost = 0 # accumulated cost
|
self.cost: float = 0.0 # accumulated cost
|
||||||
self.to = 0 # accumulated turnover
|
self.to: float = 0.0 # accumulated turnover
|
||||||
|
|
||||||
def add_return_value(self, value):
|
def add_return_value(self, value: float) -> None:
|
||||||
self.rtn += value
|
self.rtn += value
|
||||||
|
|
||||||
def add_cost(self, value):
|
def add_cost(self, value: float) -> None:
|
||||||
self.cost += value
|
self.cost += value
|
||||||
|
|
||||||
def add_turnover(self, value):
|
def add_turnover(self, value: float) -> None:
|
||||||
self.to += value
|
self.to += value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_return(self):
|
def get_return(self) -> float:
|
||||||
return self.rtn
|
return self.rtn
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_cost(self):
|
def get_cost(self) -> float:
|
||||||
return self.cost
|
return self.cost
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_turnover(self):
|
def get_turnover(self) -> float:
|
||||||
return self.to
|
return self.to
|
||||||
|
|
||||||
|
|
||||||
class Account:
|
class Account:
|
||||||
"""
|
"""
|
||||||
The correctness of the metrics of Account in nested execution depends on the shallow copy of `trade_account` in qlib/backtest/executor.py:NestedExecutor
|
The correctness of the metrics of Account in nested execution depends on the shallow copy of `trade_account` in
|
||||||
Different level of executor has different Account object when calculating metrics. But the position object is shared cross all the Account object.
|
qlib/backtest/executor.py:NestedExecutor
|
||||||
|
Different level of executor has different Account object when calculating metrics. But the position object is
|
||||||
|
shared cross all the Account object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -78,7 +83,7 @@ class Account:
|
|||||||
benchmark_config: dict = {},
|
benchmark_config: dict = {},
|
||||||
pos_type: str = "Position",
|
pos_type: str = "Position",
|
||||||
port_metr_enabled: bool = True,
|
port_metr_enabled: bool = True,
|
||||||
):
|
) -> None:
|
||||||
"""the trade account of backtest.
|
"""the trade account of backtest.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -102,7 +107,7 @@ class Account:
|
|||||||
self.benchmark_config = None # avoid no attribute error
|
self.benchmark_config = None # avoid no attribute error
|
||||||
self.init_vars(init_cash, position_dict, freq, benchmark_config)
|
self.init_vars(init_cash, position_dict, freq, benchmark_config)
|
||||||
|
|
||||||
def init_vars(self, init_cash, position_dict, freq: str, benchmark_config: dict):
|
def init_vars(self, init_cash: float, position_dict: dict, freq: str, benchmark_config: dict) -> None:
|
||||||
# 1) the following variables are shared by multiple layers
|
# 1) the following variables are shared by multiple layers
|
||||||
# - you will see a shallow copy instead of deepcopy in the NestedExecutor;
|
# - you will see a shallow copy instead of deepcopy in the NestedExecutor;
|
||||||
self.init_cash = init_cash
|
self.init_cash = init_cash
|
||||||
@@ -114,7 +119,7 @@ class Account:
|
|||||||
"position_dict": position_dict,
|
"position_dict": position_dict,
|
||||||
},
|
},
|
||||||
"module_path": "qlib.backtest.position",
|
"module_path": "qlib.backtest.position",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
self.accum_info = AccumulatedInfo()
|
self.accum_info = AccumulatedInfo()
|
||||||
|
|
||||||
@@ -123,13 +128,13 @@ class Account:
|
|||||||
self.hist_positions = {}
|
self.hist_positions = {}
|
||||||
self.reset(freq=freq, benchmark_config=benchmark_config)
|
self.reset(freq=freq, benchmark_config=benchmark_config)
|
||||||
|
|
||||||
def is_port_metr_enabled(self):
|
def is_port_metr_enabled(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Is portfolio-based metrics enabled.
|
Is portfolio-based metrics enabled.
|
||||||
"""
|
"""
|
||||||
return self._port_metr_enabled and not self.current_position.skip_update()
|
return self._port_metr_enabled and not self.current_position.skip_update()
|
||||||
|
|
||||||
def reset_report(self, freq, benchmark_config):
|
def reset_report(self, freq: str, benchmark_config: dict) -> None:
|
||||||
# portfolio related metrics
|
# portfolio related metrics
|
||||||
if self.is_port_metr_enabled():
|
if self.is_port_metr_enabled():
|
||||||
# NOTE:
|
# NOTE:
|
||||||
@@ -140,13 +145,13 @@ class Account:
|
|||||||
# fill stock value
|
# fill stock value
|
||||||
# The frequency of account may not align with the trading frequency.
|
# The frequency of account may not align with the trading frequency.
|
||||||
# This may result in obscure bugs when data quality is low.
|
# This may result in obscure bugs when data quality is low.
|
||||||
if isinstance(self.benchmark_config, dict) and self.benchmark_config.get("start_time") is not None:
|
if isinstance(self.benchmark_config, dict) and "start_time" in self.benchmark_config:
|
||||||
self.current_position.fill_stock_value(self.benchmark_config["start_time"], self.freq)
|
self.current_position.fill_stock_value(self.benchmark_config["start_time"], self.freq)
|
||||||
|
|
||||||
# trading related metrics(e.g. high-frequency trading)
|
# trading related metrics(e.g. high-frequency trading)
|
||||||
self.indicator = Indicator()
|
self.indicator = Indicator()
|
||||||
|
|
||||||
def reset(self, freq=None, benchmark_config=None, port_metr_enabled: bool = None):
|
def reset(self, freq: str = None, benchmark_config: dict = None, port_metr_enabled: bool = None) -> None:
|
||||||
"""reset freq and report of account
|
"""reset freq and report of account
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -155,6 +160,7 @@ class Account:
|
|||||||
frequency of account & report, by default None
|
frequency of account & report, by default None
|
||||||
benchmark_config : {}, optional
|
benchmark_config : {}, optional
|
||||||
benchmark config of report, by default None
|
benchmark config of report, by default None
|
||||||
|
port_metr_enabled: bool
|
||||||
"""
|
"""
|
||||||
if freq is not None:
|
if freq is not None:
|
||||||
self.freq = freq
|
self.freq = freq
|
||||||
@@ -165,13 +171,13 @@ class Account:
|
|||||||
|
|
||||||
self.reset_report(self.freq, self.benchmark_config)
|
self.reset_report(self.freq, self.benchmark_config)
|
||||||
|
|
||||||
def get_hist_positions(self):
|
def get_hist_positions(self) -> dict:
|
||||||
return self.hist_positions
|
return self.hist_positions
|
||||||
|
|
||||||
def get_cash(self):
|
def get_cash(self) -> float:
|
||||||
return self.current_position.get_cash()
|
return self.current_position.get_cash()
|
||||||
|
|
||||||
def _update_state_from_order(self, order, trade_val, cost, trade_price):
|
def _update_state_from_order(self, order: Order, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
if self.is_port_metr_enabled():
|
if self.is_port_metr_enabled():
|
||||||
# update turnover
|
# update turnover
|
||||||
self.accum_info.add_turnover(trade_val)
|
self.accum_info.add_turnover(trade_val)
|
||||||
@@ -191,13 +197,14 @@ class Account:
|
|||||||
profit = self.current_position.get_stock_price(order.stock_id) * trade_amount - trade_val
|
profit = self.current_position.get_stock_price(order.stock_id) * trade_amount - trade_val
|
||||||
self.accum_info.add_return_value(profit) # note here do not consider cost
|
self.accum_info.add_return_value(profit) # note here do not consider cost
|
||||||
|
|
||||||
def update_order(self, order, trade_val, cost, trade_price):
|
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
if self.current_position.skip_update():
|
if self.current_position.skip_update():
|
||||||
# TODO: supporting polymorphism for account
|
# TODO: supporting polymorphism for account
|
||||||
# updating order for infinite position is meaningless
|
# updating order for infinite position is meaningless
|
||||||
return
|
return
|
||||||
|
|
||||||
# if stock is sold out, no stock price information in Position, then we should update account first, then update current position
|
# if stock is sold out, no stock price information in Position, then we should update account first,
|
||||||
|
# then update current position
|
||||||
# if stock is bought, there is no stock in current position, update current, then update account
|
# if stock is bought, there is no stock in current position, update current, then update account
|
||||||
# The cost will be subtracted from the cash at last. So the trading logic can ignore the cost calculation
|
# The cost will be subtracted from the cash at last. So the trading logic can ignore the cost calculation
|
||||||
if order.direction == Order.SELL:
|
if order.direction == Order.SELL:
|
||||||
@@ -212,8 +219,15 @@ class Account:
|
|||||||
self.current_position.update_order(order, trade_val, cost, trade_price)
|
self.current_position.update_order(order, trade_val, cost, trade_price)
|
||||||
self._update_state_from_order(order, trade_val, cost, trade_price)
|
self._update_state_from_order(order, trade_val, cost, trade_price)
|
||||||
|
|
||||||
def update_current_position(self, trade_start_time, trade_end_time, trade_exchange):
|
def update_current_position(
|
||||||
"""update current to make rtn consistent with earning at the end of bar, and update holding bar count of stock"""
|
self,
|
||||||
|
trade_start_time: pd.Timestamp,
|
||||||
|
trade_end_time: pd.Timestamp,
|
||||||
|
trade_exchange: Exchange,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Update current to make rtn consistent with earning at the end of bar, and update holding bar count of stock
|
||||||
|
"""
|
||||||
# update price for stock in the position and the profit from changed_price
|
# update price for stock in the position and the profit from changed_price
|
||||||
# NOTE: updating position does not only serve portfolio metrics, it also serve the strategy
|
# NOTE: updating position does not only serve portfolio metrics, it also serve the strategy
|
||||||
if not self.current_position.skip_update():
|
if not self.current_position.skip_update():
|
||||||
@@ -228,7 +242,7 @@ class Account:
|
|||||||
# NOTE: updating bar_count does not only serve portfolio metrics, it also serve the strategy
|
# NOTE: updating bar_count does not only serve portfolio metrics, it also serve the strategy
|
||||||
self.current_position.add_count_all(bar=self.freq)
|
self.current_position.add_count_all(bar=self.freq)
|
||||||
|
|
||||||
def update_portfolio_metrics(self, trade_start_time, trade_end_time):
|
def update_portfolio_metrics(self, trade_start_time: pd.Timestamp, trade_end_time: pd.Timestamp) -> None:
|
||||||
"""update portfolio_metrics"""
|
"""update portfolio_metrics"""
|
||||||
# calculate earning
|
# calculate earning
|
||||||
# account_value - last_account_value
|
# account_value - last_account_value
|
||||||
@@ -243,14 +257,16 @@ class Account:
|
|||||||
last_account_value = self.portfolio_metrics.get_latest_account_value()
|
last_account_value = self.portfolio_metrics.get_latest_account_value()
|
||||||
last_total_cost = self.portfolio_metrics.get_latest_total_cost()
|
last_total_cost = self.portfolio_metrics.get_latest_total_cost()
|
||||||
last_total_turnover = self.portfolio_metrics.get_latest_total_turnover()
|
last_total_turnover = self.portfolio_metrics.get_latest_total_turnover()
|
||||||
|
|
||||||
# get now_account_value, now_stock_value, now_earning, now_cost, now_turnover
|
# get now_account_value, now_stock_value, now_earning, now_cost, now_turnover
|
||||||
now_account_value = self.current_position.calculate_value()
|
now_account_value = self.current_position.calculate_value()
|
||||||
now_stock_value = self.current_position.calculate_stock_value()
|
now_stock_value = self.current_position.calculate_stock_value()
|
||||||
now_earning = now_account_value - last_account_value
|
now_earning = now_account_value - last_account_value
|
||||||
now_cost = self.accum_info.get_cost - last_total_cost
|
now_cost = self.accum_info.get_cost - last_total_cost
|
||||||
now_turnover = self.accum_info.get_turnover - last_total_turnover
|
now_turnover = self.accum_info.get_turnover - last_total_turnover
|
||||||
|
|
||||||
# update portfolio_metrics for today
|
# update portfolio_metrics for today
|
||||||
# judge whether the the trading is begin.
|
# judge whether the trading is begin.
|
||||||
# and don't add init account state into portfolio_metrics, due to we don't have excess return in those days.
|
# and don't add init account state into portfolio_metrics, due to we don't have excess return in those days.
|
||||||
self.portfolio_metrics.update_portfolio_metrics_record(
|
self.portfolio_metrics.update_portfolio_metrics_record(
|
||||||
trade_start_time=trade_start_time,
|
trade_start_time=trade_start_time,
|
||||||
@@ -267,7 +283,7 @@ class Account:
|
|||||||
stock_value=now_stock_value,
|
stock_value=now_stock_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_hist_positions(self, trade_start_time):
|
def update_hist_positions(self, trade_start_time: pd.Timestamp) -> None:
|
||||||
"""update history position"""
|
"""update history position"""
|
||||||
now_account_value = self.current_position.calculate_value()
|
now_account_value = self.current_position.calculate_value()
|
||||||
# set now_account_value to position
|
# set now_account_value to position
|
||||||
@@ -287,7 +303,7 @@ class Account:
|
|||||||
inner_order_indicators: List[Dict[str, pd.Series]] = None,
|
inner_order_indicators: List[Dict[str, pd.Series]] = None,
|
||||||
decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]] = None,
|
decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]] = None,
|
||||||
indicator_config: dict = {},
|
indicator_config: dict = {},
|
||||||
):
|
) -> None:
|
||||||
"""update trade indicators and order indicators in each bar end"""
|
"""update trade indicators and order indicators in each bar end"""
|
||||||
# TODO: will skip empty decisions make it faster? `outer_trade_decision.empty():`
|
# TODO: will skip empty decisions make it faster? `outer_trade_decision.empty():`
|
||||||
|
|
||||||
@@ -323,7 +339,7 @@ class Account:
|
|||||||
inner_order_indicators: List[Dict[str, pd.Series]] = None,
|
inner_order_indicators: List[Dict[str, pd.Series]] = None,
|
||||||
decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]] = None,
|
decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]] = None,
|
||||||
indicator_config: dict = {},
|
indicator_config: dict = {},
|
||||||
):
|
) -> None:
|
||||||
"""update account at each trading bar step
|
"""update account at each trading bar step
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -338,6 +354,8 @@ class Account:
|
|||||||
whether the trading executor is atomic, which means there is no higher-frequency trading executor inside it
|
whether the trading executor is atomic, which means there is no higher-frequency trading executor inside it
|
||||||
- if atomic is True, calculate the indicators with trade_info
|
- if atomic is True, calculate the indicators with trade_info
|
||||||
- else, aggregate indicators with inner indicators
|
- else, aggregate indicators with inner indicators
|
||||||
|
outer_trade_decision: BaseTradeDecision
|
||||||
|
external trade decision
|
||||||
trade_info : List[(Order, float, float, float)], optional
|
trade_info : List[(Order, float, float, float)], optional
|
||||||
trading information, by default None
|
trading information, by default None
|
||||||
- necessary if atomic is True
|
- necessary if atomic is True
|
||||||
@@ -377,7 +395,7 @@ class Account:
|
|||||||
indicator_config=indicator_config,
|
indicator_config=indicator_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_portfolio_metrics(self):
|
def get_portfolio_metrics(self) -> Tuple[pd.DataFrame, dict]:
|
||||||
"""get the history portfolio_metrics and positions instance"""
|
"""get the history portfolio_metrics and positions instance"""
|
||||||
if self.is_port_metr_enabled():
|
if self.is_port_metr_enabled():
|
||||||
_portfolio_metrics = self.portfolio_metrics.generate_portfolio_metrics_dataframe()
|
_portfolio_metrics = self.portfolio_metrics.generate_portfolio_metrics_dataframe()
|
||||||
|
|||||||
@@ -2,17 +2,29 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Generator, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from qlib.backtest.decision import BaseTradeDecision
|
from qlib.backtest.decision import BaseTradeDecision
|
||||||
from typing import TYPE_CHECKING
|
from qlib.backtest.report import Indicator, PortfolioMetrics
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qlib.strategy.base import BaseStrategy
|
from qlib.strategy.base import BaseStrategy
|
||||||
from qlib.backtest.executor import BaseExecutor
|
from qlib.backtest.executor import BaseExecutor
|
||||||
from ..utils.time import Freq
|
|
||||||
from tqdm.auto import tqdm
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
from ..utils.time import Freq
|
||||||
|
|
||||||
def backtest_loop(start_time, end_time, trade_strategy: BaseStrategy, trade_executor: BaseExecutor):
|
|
||||||
|
def backtest_loop(
|
||||||
|
start_time: Union[pd.Timestamp, str],
|
||||||
|
end_time: Union[pd.Timestamp, str],
|
||||||
|
trade_strategy: BaseStrategy,
|
||||||
|
trade_executor: BaseExecutor,
|
||||||
|
) -> Tuple[PortfolioMetrics, Indicator]:
|
||||||
"""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`
|
||||||
@@ -31,19 +43,23 @@ def backtest_loop(start_time, end_time, trade_strategy: BaseStrategy, trade_exec
|
|||||||
|
|
||||||
|
|
||||||
def collect_data_loop(
|
def collect_data_loop(
|
||||||
start_time, end_time, trade_strategy: BaseStrategy, trade_executor: BaseExecutor, return_value: dict = None
|
start_time: Union[pd.Timestamp, str],
|
||||||
):
|
end_time: Union[pd.Timestamp, str],
|
||||||
|
trade_strategy: BaseStrategy,
|
||||||
|
trade_executor: BaseExecutor,
|
||||||
|
return_value: dict = None,
|
||||||
|
) -> Generator[BaseTradeDecision, Optional[BaseTradeDecision], None]:
|
||||||
"""Generator for collecting the trade decision data for rl training
|
"""Generator for collecting the trade decision data for rl training
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start_time : pd.Timestamp|str
|
start_time : Union[pd.Timestamp, str]
|
||||||
closed start time for backtest
|
closed start time for backtest
|
||||||
**NOTE**: This will be applied to the outmost executor's calendar.
|
**NOTE**: This will be applied to the outmost executor's calendar.
|
||||||
end_time : pd.Timestamp|str
|
end_time : Union[pd.Timestamp, str]
|
||||||
closed end time for backtest
|
closed end time for backtest
|
||||||
**NOTE**: This will be applied to the outmost executor's calendar.
|
**NOTE**: This will be applied to the outmost executor's calendar.
|
||||||
E.g. Executor[day](Executor[1min]), setting `end_time == 20XX0301` will include all the minutes on 20XX0301
|
E.g. Executor[day](Executor[1min]), setting `end_time == 20XX0301` will include all the minutes on 20XX0301
|
||||||
trade_strategy : BaseStrategy
|
trade_strategy : BaseStrategy
|
||||||
the outermost portfolio strategy
|
the outermost portfolio strategy
|
||||||
trade_executor : BaseExecutor
|
trade_executor : BaseExecutor
|
||||||
|
|||||||
@@ -2,23 +2,26 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from enum import IntEnum
|
|
||||||
from qlib.data.data import Cal
|
|
||||||
from qlib.utils.time import concat_date_time, epsilon_change
|
|
||||||
from qlib.log import get_module_logger
|
|
||||||
|
|
||||||
from typing import ClassVar, Optional, Union, List, Tuple
|
from abc import abstractmethod
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
# try to fix circular imports when enabling type hints
|
# try to fix circular imports when enabling type hints
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, ClassVar, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from qlib.backtest.utils import TradeCalendarManager
|
||||||
|
from qlib.data.data import Cal
|
||||||
|
from qlib.log import get_module_logger
|
||||||
|
from qlib.utils.time import concat_date_time, epsilon_change
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qlib.strategy.base import BaseStrategy
|
from qlib.strategy.base import BaseStrategy
|
||||||
from qlib.backtest.exchange import Exchange
|
from qlib.backtest.exchange import Exchange
|
||||||
from qlib.backtest.utils import TradeCalendarManager
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
class OrderDir(IntEnum):
|
class OrderDir(IntEnum):
|
||||||
@@ -46,7 +49,7 @@ class Order:
|
|||||||
# - they are set by users and is time-invariant.
|
# - they are set by users and is time-invariant.
|
||||||
stock_id: str
|
stock_id: str
|
||||||
amount: float # `amount` is a non-negative and adjusted value
|
amount: float # `amount` is a non-negative and adjusted value
|
||||||
direction: int
|
direction: OrderDir
|
||||||
|
|
||||||
# 2) time variant values:
|
# 2) time variant values:
|
||||||
# - Users may want to set these values when using lower level APIs
|
# - Users may want to set these values when using lower level APIs
|
||||||
@@ -61,7 +64,7 @@ class Order:
|
|||||||
# What the value should be about in all kinds of cases
|
# What the value should be about in all kinds of cases
|
||||||
# - not tradable: the deal_amount == 0 , factor is None
|
# - not tradable: the deal_amount == 0 , factor is None
|
||||||
# - the stock is suspended and the entire order fails. No cost for this order
|
# - the stock is suspended and the entire order fails. No cost for this order
|
||||||
# - dealed or partially dealed: deal_amount >= 0 and factor is not None
|
# - dealt or partially dealt: deal_amount >= 0 and factor is not None
|
||||||
deal_amount: Optional[float] = None # `deal_amount` is a non-negative value
|
deal_amount: Optional[float] = None # `deal_amount` is a non-negative value
|
||||||
factor: Optional[float] = None
|
factor: Optional[float] = None
|
||||||
|
|
||||||
@@ -74,10 +77,10 @@ class Order:
|
|||||||
SELL: ClassVar[OrderDir] = OrderDir.SELL
|
SELL: ClassVar[OrderDir] = OrderDir.SELL
|
||||||
BUY: ClassVar[OrderDir] = OrderDir.BUY
|
BUY: ClassVar[OrderDir] = OrderDir.BUY
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self) -> None:
|
||||||
if self.direction not in {Order.SELL, Order.BUY}:
|
if self.direction not in {Order.SELL, Order.BUY}:
|
||||||
raise NotImplementedError("direction not supported, `Order.SELL` for sell, `Order.BUY` for buy")
|
raise NotImplementedError("direction not supported, `Order.SELL` for sell, `Order.BUY` for buy")
|
||||||
self.deal_amount = 0
|
self.deal_amount = 0.0
|
||||||
self.factor = None
|
self.factor = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -99,7 +102,7 @@ class Order:
|
|||||||
return self.deal_amount * self.sign
|
return self.deal_amount * self.sign
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sign(self) -> float:
|
def sign(self) -> int:
|
||||||
"""
|
"""
|
||||||
return the sign of trading
|
return the sign of trading
|
||||||
- `+1` indicates buying
|
- `+1` indicates buying
|
||||||
@@ -112,15 +115,12 @@ class Order:
|
|||||||
if isinstance(direction, OrderDir):
|
if isinstance(direction, OrderDir):
|
||||||
return direction
|
return direction
|
||||||
elif isinstance(direction, (int, float, np.integer, np.floating)):
|
elif isinstance(direction, (int, float, np.integer, np.floating)):
|
||||||
if direction > 0:
|
return Order.BUY if direction > 0 else Order.SELL
|
||||||
return Order.BUY
|
|
||||||
else:
|
|
||||||
return Order.SELL
|
|
||||||
elif isinstance(direction, str):
|
elif isinstance(direction, str):
|
||||||
dl = direction.lower()
|
dl = direction.lower().strip()
|
||||||
if dl.strip() == "sell":
|
if dl == "sell":
|
||||||
return OrderDir.SELL
|
return OrderDir.SELL
|
||||||
elif dl.strip() == "buy":
|
elif dl == "buy":
|
||||||
return OrderDir.BUY
|
return OrderDir.BUY
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
@@ -138,14 +138,14 @@ class OrderHelper:
|
|||||||
Motivation
|
Motivation
|
||||||
- Make generating order easier
|
- Make generating order easier
|
||||||
- User may have no knowledge about the adjust-factor information about the system.
|
- User may have no knowledge about the adjust-factor information about the system.
|
||||||
- It involves to much interaction with the exchange when generating orders.
|
- It involves too much interaction with the exchange when generating orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, exchange: Exchange):
|
def __init__(self, exchange: Exchange) -> None:
|
||||||
self.exchange = exchange
|
self.exchange = exchange
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def create(
|
def create(
|
||||||
self,
|
|
||||||
code: str,
|
code: str,
|
||||||
amount: float,
|
amount: float,
|
||||||
direction: OrderDir,
|
direction: OrderDir,
|
||||||
@@ -175,21 +175,18 @@ class OrderHelper:
|
|||||||
Order:
|
Order:
|
||||||
The created order
|
The created order
|
||||||
"""
|
"""
|
||||||
if start_time is not None:
|
|
||||||
start_time = pd.Timestamp(start_time)
|
|
||||||
if end_time is not None:
|
|
||||||
end_time = pd.Timestamp(end_time)
|
|
||||||
# NOTE: factor is a value belongs to the results section. User don't have to care about it when creating orders
|
# NOTE: factor is a value belongs to the results section. User don't have to care about it when creating orders
|
||||||
return Order(
|
return Order(
|
||||||
stock_id=code,
|
stock_id=code,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
start_time=start_time,
|
start_time=start_time if start_time is not None else pd.Timestamp(start_time),
|
||||||
end_time=end_time,
|
end_time=end_time if end_time is not None else pd.Timestamp(end_time),
|
||||||
direction=direction,
|
direction=direction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TradeRange:
|
class TradeRange:
|
||||||
|
@abstractmethod
|
||||||
def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
|
def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
This method will be call with following way
|
This method will be call with following way
|
||||||
@@ -216,6 +213,7 @@ class TradeRange:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `__call__` method")
|
raise NotImplementedError(f"Please implement the `__call__` method")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
@@ -234,23 +232,26 @@ class TradeRange:
|
|||||||
|
|
||||||
|
|
||||||
class IdxTradeRange(TradeRange):
|
class IdxTradeRange(TradeRange):
|
||||||
def __init__(self, start_idx: int, end_idx: int):
|
def __init__(self, start_idx: int, end_idx: int) -> None:
|
||||||
self._start_idx = start_idx
|
self._start_idx = start_idx
|
||||||
self._end_idx = end_idx
|
self._end_idx = end_idx
|
||||||
|
|
||||||
def __call__(self, trade_calendar: TradeCalendarManager = None) -> Tuple[int, int]:
|
def __call__(self, trade_calendar: TradeCalendarManager = None) -> Tuple[int, int]:
|
||||||
return self._start_idx, self._end_idx
|
return self._start_idx, self._end_idx
|
||||||
|
|
||||||
|
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class TradeRangeByTime(TradeRange):
|
class TradeRangeByTime(TradeRange):
|
||||||
"""This is a helper function for make decisions"""
|
"""This is a helper function for make decisions"""
|
||||||
|
|
||||||
def __init__(self, start_time: str, end_time: str):
|
def __init__(self, start_time: str, end_time: str) -> None:
|
||||||
"""
|
"""
|
||||||
This is a callable class.
|
This is a callable class.
|
||||||
|
|
||||||
**NOTE**:
|
**NOTE**:
|
||||||
- It is designed for minute-bar for intraday trading!!!!!
|
- It is designed for minute-bar for intra-day trading!!!!!
|
||||||
- Both start_time and end_time are **closed** in the range
|
- Both start_time and end_time are **closed** in the range
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -264,26 +265,25 @@ class TradeRangeByTime(TradeRange):
|
|||||||
self.end_time = pd.Timestamp(end_time).time()
|
self.end_time = pd.Timestamp(end_time).time()
|
||||||
assert self.start_time < self.end_time
|
assert self.start_time < self.end_time
|
||||||
|
|
||||||
def __call__(self, trade_calendar: TradeCalendarManager = None) -> Tuple[int, int]:
|
def __call__(self, trade_calendar: TradeCalendarManager) -> Tuple[int, int]:
|
||||||
if trade_calendar is None:
|
if trade_calendar is None:
|
||||||
raise NotImplementedError("trade_calendar is necessary for getting TradeRangeByTime.")
|
raise NotImplementedError("trade_calendar is necessary for getting TradeRangeByTime.")
|
||||||
start = trade_calendar.start_time
|
|
||||||
val_start, val_end = concat_date_time(start.date(), self.start_time), concat_date_time(
|
start_date = trade_calendar.start_time.date()
|
||||||
start.date(), self.end_time
|
val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
|
||||||
)
|
|
||||||
return trade_calendar.get_range_idx(val_start, val_end)
|
return trade_calendar.get_range_idx(val_start, val_end)
|
||||||
|
|
||||||
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
def clip_time_range(self, start_time: pd.Timestamp, end_time: pd.Timestamp) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
start_date = start_time.date()
|
start_date = start_time.date()
|
||||||
val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
|
val_start, val_end = concat_date_time(start_date, self.start_time), concat_date_time(start_date, self.end_time)
|
||||||
# NOTE: `end_date` should not be used. Because the `end_date` is for slicing. It may be in the next day
|
# NOTE: `end_date` should not be used. Because the `end_date` is for slicing. It may be in the next day
|
||||||
# Assumption: start_time and end_time is for intraday trading. So it is OK for only using start_date
|
# Assumption: start_time and end_time is for intra-day trading. So it is OK for only using start_date
|
||||||
return max(val_start, start_time), min(val_end, end_time)
|
return max(val_start, start_time), min(val_end, end_time)
|
||||||
|
|
||||||
|
|
||||||
class BaseTradeDecision:
|
class BaseTradeDecision:
|
||||||
"""
|
"""
|
||||||
Trade decisions ara made by strategy and executed by exeuter
|
Trade decisions ara made by strategy and executed by executor
|
||||||
|
|
||||||
Motivation:
|
Motivation:
|
||||||
Here are several typical scenarios for `BaseTradeDecision`
|
Here are several typical scenarios for `BaseTradeDecision`
|
||||||
@@ -297,7 +297,7 @@ class BaseTradeDecision:
|
|||||||
2. Same as `case 1.3`
|
2. Same as `case 1.3`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, strategy: BaseStrategy, trade_range: Union[Tuple[int, int], TradeRange] = None):
|
def __init__(self, strategy: BaseStrategy, trade_range: Union[Tuple[int, int], TradeRange] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -339,7 +339,7 @@ class BaseTradeDecision:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
def update(self, trade_calendar: TradeCalendarManager) -> Union["BaseTradeDecision", None]:
|
def update(self, trade_calendar: TradeCalendarManager) -> Optional[BaseTradeDecision]:
|
||||||
"""
|
"""
|
||||||
Be called at the **start** of each step.
|
Be called at the **start** of each step.
|
||||||
|
|
||||||
@@ -354,10 +354,8 @@ class BaseTradeDecision:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
None:
|
|
||||||
No update, use previous decision(or unavailable)
|
|
||||||
BaseTradeDecision:
|
BaseTradeDecision:
|
||||||
New update, use new decision
|
New update, use new decision. If no updates, return None (use previous decision (or unavailable))
|
||||||
"""
|
"""
|
||||||
# purpose 1)
|
# purpose 1)
|
||||||
self.total_step = trade_calendar.get_trade_len()
|
self.total_step = trade_calendar.get_trade_len()
|
||||||
@@ -412,12 +410,12 @@ class BaseTradeDecision:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_start_idx, _end_idx = self._get_range_limit(**kwargs)
|
_start_idx, _end_idx = self._get_range_limit(**kwargs)
|
||||||
except NotImplementedError:
|
except NotImplementedError as e:
|
||||||
if "default_value" in kwargs:
|
if "default_value" in kwargs:
|
||||||
return kwargs["default_value"]
|
return kwargs["default_value"]
|
||||||
else:
|
else:
|
||||||
# Default to get full index
|
# Default to get full index
|
||||||
raise NotImplementedError(f"The decision didn't provide an index range") from NotImplementedError
|
raise NotImplementedError(f"The decision didn't provide an index range") from e
|
||||||
|
|
||||||
# clip index
|
# clip index
|
||||||
if getattr(self, "total_step", None) is not None:
|
if getattr(self, "total_step", None) is not None:
|
||||||
@@ -426,7 +424,7 @@ class BaseTradeDecision:
|
|||||||
if _start_idx < 0 or _end_idx >= self.total_step:
|
if _start_idx < 0 or _end_idx >= self.total_step:
|
||||||
logger = get_module_logger("decision")
|
logger = get_module_logger("decision")
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[{_start_idx},{_end_idx}] go beyoud the total_step({self.total_step}), it will be clipped"
|
f"[{_start_idx},{_end_idx}] go beyond the total_step({self.total_step}), it will be clipped.",
|
||||||
)
|
)
|
||||||
_start_idx, _end_idx = max(0, _start_idx), min(self.total_step - 1, _end_idx)
|
_start_idx, _end_idx = max(0, _start_idx), min(self.total_step - 1, _end_idx)
|
||||||
return _start_idx, _end_idx
|
return _start_idx, _end_idx
|
||||||
@@ -444,7 +442,7 @@ class BaseTradeDecision:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
rtype: str
|
rtype: str
|
||||||
- "full": return the full limitation of the deicsion in the day
|
- "full": return the full limitation of the decision in the day
|
||||||
- "step": return the limitation of current step
|
- "step": return the limitation of current step
|
||||||
|
|
||||||
raise_error: bool
|
raise_error: bool
|
||||||
@@ -497,11 +495,10 @@ class BaseTradeDecision:
|
|||||||
return True
|
return True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mod_inner_decision(self, inner_trade_decision: BaseTradeDecision):
|
def mod_inner_decision(self, inner_trade_decision: BaseTradeDecision) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
This method will be called on the inner_trade_decision after it is generated.
|
This method will be called on the inner_trade_decision after it is generated.
|
||||||
`inner_trade_decision` will be changed **inplaced**.
|
`inner_trade_decision` will be changed **inplace**.
|
||||||
|
|
||||||
Motivation of the `mod_inner_decision`
|
Motivation of the `mod_inner_decision`
|
||||||
- Leave a hook for outer decision to affect the decision generated by the inner strategy
|
- Leave a hook for outer decision to affect the decision generated by the inner strategy
|
||||||
@@ -520,6 +517,9 @@ class BaseTradeDecision:
|
|||||||
|
|
||||||
|
|
||||||
class EmptyTradeDecision(BaseTradeDecision):
|
class EmptyTradeDecision(BaseTradeDecision):
|
||||||
|
def get_decision(self) -> List[object]:
|
||||||
|
return []
|
||||||
|
|
||||||
def empty(self) -> bool:
|
def empty(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -544,4 +544,9 @@ class TradeDecisionWO(BaseTradeDecision):
|
|||||||
return self.order_list
|
return self.order_list
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"class: {self.__class__.__name__}; strategy: {self.strategy}; trade_range: {self.trade_range}; order_list[{len(self.order_list)}]"
|
return (
|
||||||
|
f"class: {self.__class__.__name__}; "
|
||||||
|
f"strategy: {self.strategy}; "
|
||||||
|
f"trade_range: {self.trade_range}; "
|
||||||
|
f"order_list[{len(self.order_list)}]"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
# Copyright (c) Microsoft Corporation.
|
# Copyright (c) Microsoft Corporation.
|
||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Type, Union
|
||||||
from typing import List, Tuple, Union
|
|
||||||
|
from ..utils.index_data import IndexData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|
||||||
from qlib.backtest.position import BasePosition, Position
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..data.data import D
|
from qlib.backtest.position import BasePosition
|
||||||
|
|
||||||
from ..config import C
|
from ..config import C
|
||||||
from ..constant import REG_CN
|
from ..constant import REG_CN
|
||||||
|
from ..data.data import D
|
||||||
from ..log import get_module_logger
|
from ..log import get_module_logger
|
||||||
from .decision import Order, OrderDir, OrderHelper
|
from .decision import Order, OrderDir, OrderHelper
|
||||||
from .high_performance_ds import BaseQuote, NumpyQuote
|
from .high_performance_ds import BaseQuote, NumpyQuote
|
||||||
@@ -24,22 +28,22 @@ from .high_performance_ds import BaseQuote, NumpyQuote
|
|||||||
class Exchange:
|
class Exchange:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
freq="day",
|
freq: str = "day",
|
||||||
start_time=None,
|
start_time: Union[pd.Timestamp, str] = None,
|
||||||
end_time=None,
|
end_time: Union[pd.Timestamp, str] = None,
|
||||||
codes="all",
|
codes: Union[list, str] = "all",
|
||||||
deal_price: Union[str, Tuple[str], List[str]] = None,
|
deal_price: Union[str, Tuple[str], List[str]] = None,
|
||||||
subscribe_fields=[],
|
subscribe_fields: list = [],
|
||||||
limit_threshold: Union[Tuple[str, str], float, None] = None,
|
limit_threshold: Union[Tuple[str, str], float, None] = None,
|
||||||
volume_threshold=None,
|
volume_threshold: Union[tuple, dict] = None,
|
||||||
open_cost=0.0015,
|
open_cost: float = 0.0015,
|
||||||
close_cost=0.0025,
|
close_cost: float = 0.0025,
|
||||||
min_cost=5,
|
min_cost: float = 5.0,
|
||||||
impact_cost=0.0,
|
impact_cost: float = 0.0,
|
||||||
extra_quote=None,
|
extra_quote: pd.DataFrame = None,
|
||||||
quote_cls=NumpyQuote,
|
quote_cls: Type[BaseQuote] = NumpyQuote,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""__init__
|
"""__init__
|
||||||
:param freq: frequency of data
|
:param freq: frequency of data
|
||||||
:param start_time: closed start time for backtest
|
:param start_time: closed start time for backtest
|
||||||
@@ -72,11 +76,12 @@ class Exchange:
|
|||||||
]
|
]
|
||||||
1) ("cum" or "current", limit_str) denotes a single volume limit.
|
1) ("cum" or "current", limit_str) denotes a single volume limit.
|
||||||
- limit_str is qlib data expression which is allowed to define your own Operator.
|
- limit_str is qlib data expression which is allowed to define your own Operator.
|
||||||
Please refer to qlib/contrib/ops/high_freq.py, here are any custom operator for high frequency,
|
Please refer to qlib/contrib/ops/high_freq.py, here are any custom operator for
|
||||||
such as DayCumsum. !!!NOTE: if you want you use the custom operator, you need to
|
high frequency, such as DayCumsum. !!!NOTE: if you want you use the custom
|
||||||
register it in qlib_init.
|
operator, you need to register it in qlib_init.
|
||||||
- "cum" means that this is a cumulative value over time, such as cumulative market volume.
|
- "cum" means that this is a cumulative value over time, such as cumulative market
|
||||||
So when it is used as a volume limit, it is necessary to subtract the dealt amount.
|
volume. So when it is used as a volume limit, it is necessary to subtract the dealt
|
||||||
|
amount.
|
||||||
- "current" means that this is a real-time value and will not accumulate over time,
|
- "current" means that this is a real-time value and will not accumulate over time,
|
||||||
so it can be directly used as a capacity limit.
|
so it can be directly used as a capacity limit.
|
||||||
e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1")
|
e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1")
|
||||||
@@ -84,7 +89,7 @@ class Exchange:
|
|||||||
"buy" means the volume limits of buying. "sell" means the volume limits of selling.
|
"buy" means the volume limits of buying. "sell" means the volume limits of selling.
|
||||||
Different volume limits will be aggregated with min(). If volume_threshold is only
|
Different volume limits will be aggregated with min(). If volume_threshold is only
|
||||||
("cum" or "current", limit_str) instead of a dict, the volume limits are for
|
("cum" or "current", limit_str) instead of a dict, the volume limits are for
|
||||||
both by deault. In other words, it is same as {"all": ("cum" or "current", limit_str)}.
|
both by default. In other words, it is same as {"all": ("cum" or "current", limit_str)}.
|
||||||
3) e.g. "volume_threshold": {
|
3) e.g. "volume_threshold": {
|
||||||
"all": ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"),
|
"all": ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"),
|
||||||
"buy": ("current", "$askV1"),
|
"buy": ("current", "$askV1"),
|
||||||
@@ -104,13 +109,14 @@ class Exchange:
|
|||||||
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.
|
||||||
Optional fields:
|
Optional fields:
|
||||||
$volume is only necessary when we limit the trade amount or calculate PA(vwap) indicator
|
$volume is only necessary when we limit the trade amount or calculate
|
||||||
|
PA(vwap) indicator
|
||||||
$vwap is only necessary when we use the $vwap price as the deal price
|
$vwap is only necessary when we use the $vwap price as the deal price
|
||||||
$factor is for rounding to the trading unit
|
$factor is for rounding to the trading unit
|
||||||
limit_sell will be set to False by default(False indicates we can sell this
|
limit_sell will be set to False by default (False indicates we can sell
|
||||||
target on this day).
|
this target on this day).
|
||||||
limit_buy will be set to False by default(False indicates we can buy this
|
limit_buy will be set to False by default (False indicates we can buy
|
||||||
target on this day).
|
this target on this day).
|
||||||
index: MultipleIndex(instrument, pd.Datetime)
|
index: MultipleIndex(instrument, pd.Datetime)
|
||||||
"""
|
"""
|
||||||
self.freq = freq
|
self.freq = freq
|
||||||
@@ -163,7 +169,7 @@ class Exchange:
|
|||||||
if self.limit_type == self.LT_TP_EXP:
|
if self.limit_type == self.LT_TP_EXP:
|
||||||
for exp in limit_threshold:
|
for exp in limit_threshold:
|
||||||
necessary_fields.add(exp)
|
necessary_fields.add(exp)
|
||||||
all_fields = necessary_fields | vol_lt_fields
|
all_fields = necessary_fields | set(vol_lt_fields)
|
||||||
all_fields = list(all_fields | set(subscribe_fields))
|
all_fields = list(all_fields | set(subscribe_fields))
|
||||||
|
|
||||||
self.all_fields = all_fields
|
self.all_fields = all_fields
|
||||||
@@ -182,17 +188,22 @@ class Exchange:
|
|||||||
self.quote_cls = quote_cls
|
self.quote_cls = quote_cls
|
||||||
self.quote: BaseQuote = self.quote_cls(self.quote_df, freq)
|
self.quote: BaseQuote = self.quote_cls(self.quote_df, freq)
|
||||||
|
|
||||||
def get_quote_from_qlib(self):
|
def get_quote_from_qlib(self) -> None:
|
||||||
# get stock data from qlib
|
# get stock data from qlib
|
||||||
if len(self.codes) == 0:
|
if len(self.codes) == 0:
|
||||||
self.codes = D.instruments()
|
self.codes = D.instruments()
|
||||||
self.quote_df = D.features(
|
self.quote_df = D.features(
|
||||||
self.codes, self.all_fields, self.start_time, self.end_time, freq=self.freq, disk_cache=True
|
self.codes,
|
||||||
|
self.all_fields,
|
||||||
|
self.start_time,
|
||||||
|
self.end_time,
|
||||||
|
freq=self.freq,
|
||||||
|
disk_cache=True,
|
||||||
).dropna(subset=["$close"])
|
).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
|
||||||
for attr in "buy_price", "sell_price":
|
for attr in ("buy_price", "sell_price"):
|
||||||
pstr = getattr(self, attr) # price string
|
pstr = getattr(self, attr) # price string
|
||||||
if self.quote_df[pstr].isna().any():
|
if self.quote_df[pstr].isna().any():
|
||||||
self.logger.warning("{} field data contains nan.".format(pstr))
|
self.logger.warning("{} field data contains nan.".format(pstr))
|
||||||
@@ -238,7 +249,7 @@ class Exchange:
|
|||||||
LT_FLT = "float" # float
|
LT_FLT = "float" # float
|
||||||
LT_NONE = "none" # none
|
LT_NONE = "none" # none
|
||||||
|
|
||||||
def _get_limit_type(self, limit_threshold):
|
def _get_limit_type(self, limit_threshold: Union[Tuple, float, None]) -> str:
|
||||||
"""get limit type"""
|
"""get limit type"""
|
||||||
if isinstance(limit_threshold, Tuple):
|
if isinstance(limit_threshold, Tuple):
|
||||||
return self.LT_TP_EXP
|
return self.LT_TP_EXP
|
||||||
@@ -249,7 +260,7 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
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):
|
def _update_limit(self, limit_threshold: Union[Tuple, float, None]) -> None:
|
||||||
# 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:
|
||||||
@@ -263,9 +274,10 @@ class Exchange:
|
|||||||
self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold)
|
self.quote_df["limit_buy"] = self.quote_df["$change"].ge(limit_threshold)
|
||||||
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) # pylint: disable=E1130
|
||||||
|
|
||||||
def _get_vol_limit(self, volume_threshold):
|
@staticmethod
|
||||||
|
def _get_vol_limit(volume_threshold: Union[tuple, dict]) -> Tuple[Optional[list], Optional[list], set]:
|
||||||
"""
|
"""
|
||||||
preproccess the volume limit.
|
preprocess the volume limit.
|
||||||
get the fields need to get from qlib.
|
get the fields need to get from qlib.
|
||||||
get the volume limit list of buying and selling which is composed of all limits.
|
get the volume limit list of buying and selling which is composed of all limits.
|
||||||
Parameters
|
Parameters
|
||||||
@@ -295,8 +307,7 @@ class Exchange:
|
|||||||
volume_threshold = {"all": volume_threshold}
|
volume_threshold = {"all": volume_threshold}
|
||||||
|
|
||||||
assert isinstance(volume_threshold, dict)
|
assert isinstance(volume_threshold, dict)
|
||||||
for key in volume_threshold:
|
for key, vol_limit in volume_threshold.items():
|
||||||
vol_limit = volume_threshold[key]
|
|
||||||
assert isinstance(vol_limit, tuple)
|
assert isinstance(vol_limit, tuple)
|
||||||
fields.add(vol_limit[1])
|
fields.add(vol_limit[1])
|
||||||
|
|
||||||
@@ -307,10 +318,19 @@ class Exchange:
|
|||||||
|
|
||||||
return buy_vol_limit, sell_vol_limit, fields
|
return buy_vol_limit, sell_vol_limit, fields
|
||||||
|
|
||||||
def check_stock_limit(self, stock_id, start_time, end_time, direction=None):
|
def check_stock_limit(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
direction: int = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
stock_id : str
|
||||||
|
start_time: pd.Timestamp
|
||||||
|
end_time: pd.Timestamp
|
||||||
direction : int, optional
|
direction : int, optional
|
||||||
trade direction, by default None
|
trade direction, by default None
|
||||||
- if direction is None, check if tradable for buying and selling.
|
- if direction is None, check if tradable for buying and selling.
|
||||||
@@ -328,39 +348,42 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"direction {direction} is not supported!")
|
raise ValueError(f"direction {direction} is not supported!")
|
||||||
|
|
||||||
def check_stock_suspended(self, stock_id, start_time, end_time):
|
def check_stock_suspended(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
) -> bool:
|
||||||
# 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
|
return self.quote.get_data(stock_id, start_time, end_time, "$close") is None
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_stock_tradable(self, stock_id, start_time, end_time, direction=None):
|
def is_stock_tradable(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
direction: int = None,
|
||||||
|
) -> bool:
|
||||||
# check if stock can be traded
|
# check if stock can be traded
|
||||||
# same as check in check_order
|
return not (
|
||||||
if self.check_stock_suspended(stock_id, start_time, end_time) or self.check_stock_limit(
|
self.check_stock_suspended(stock_id, start_time, end_time)
|
||||||
stock_id, start_time, end_time, direction
|
or self.check_stock_limit(stock_id, start_time, end_time, direction)
|
||||||
):
|
)
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_order(self, order):
|
def check_order(self, order: Order) -> bool:
|
||||||
# check limit and suspended
|
# check limit and suspended
|
||||||
if self.check_stock_suspended(order.stock_id, order.start_time, order.end_time) or self.check_stock_limit(
|
return self.is_stock_tradable(order.stock_id, order.start_time, order.end_time, order.direction)
|
||||||
order.stock_id, order.start_time, order.end_time, order.direction
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def deal_order(
|
def deal_order(
|
||||||
self,
|
self,
|
||||||
order,
|
order: Order,
|
||||||
trade_account: Account = None,
|
trade_account: Account = None,
|
||||||
position: BasePosition = None,
|
position: BasePosition = None,
|
||||||
dealt_order_amount: defaultdict = defaultdict(float),
|
dealt_order_amount: defaultdict = defaultdict(float),
|
||||||
):
|
) -> Tuple[float, float, float]:
|
||||||
"""
|
"""
|
||||||
Deal order when the actual transaction
|
Deal order when the actual transaction
|
||||||
the results section in `Order` will be changed.
|
the results section in `Order` will be changed.
|
||||||
@@ -371,9 +394,9 @@ class Exchange:
|
|||||||
:return: trade_val, trade_cost, trade_price
|
:return: trade_val, trade_cost, trade_price
|
||||||
"""
|
"""
|
||||||
# check order first.
|
# check order first.
|
||||||
if self.check_order(order) is False:
|
if not self.check_order(order):
|
||||||
order.deal_amount = 0.0
|
order.deal_amount = 0.0
|
||||||
# using np.nan instead of None to make it more convenient to should the value in format string
|
# using np.nan instead of None to make it more convenient to show the value in format string
|
||||||
self.logger.debug(f"Order failed due to trading limitation: {order}")
|
self.logger.debug(f"Order failed due to trading limitation: {order}")
|
||||||
return 0.0, 0.0, np.nan
|
return 0.0, 0.0, np.nan
|
||||||
|
|
||||||
@@ -382,7 +405,9 @@ class Exchange:
|
|||||||
|
|
||||||
# NOTE: order will be changed in this function
|
# NOTE: order will be changed in this function
|
||||||
trade_price, trade_val, trade_cost = self._calc_trade_info_by_order(
|
trade_price, trade_val, trade_cost = self._calc_trade_info_by_order(
|
||||||
order, trade_account.current_position if trade_account else position, dealt_order_amount
|
order,
|
||||||
|
trade_account.current_position if trade_account else position,
|
||||||
|
dealt_order_amount,
|
||||||
)
|
)
|
||||||
if trade_val > 1e-5:
|
if trade_val > 1e-5:
|
||||||
# If the order can only be deal 0 value. Nothing to be updated
|
# If the order can only be deal 0 value. Nothing to be updated
|
||||||
@@ -396,23 +421,49 @@ class Exchange:
|
|||||||
|
|
||||||
return trade_val, trade_cost, trade_price
|
return trade_val, trade_cost, trade_price
|
||||||
|
|
||||||
def get_quote_info(self, stock_id, start_time, end_time, method="ts_data_last"):
|
def get_quote_info(
|
||||||
return self.quote.get_data(stock_id, start_time, end_time, method=method)
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
method: str = "ts_data_last",
|
||||||
|
) -> Union[None, int, float, bool, IndexData]:
|
||||||
|
return self.quote.get_data(stock_id, start_time, end_time, method=method) # TODO: missing `field`?
|
||||||
|
|
||||||
def get_close(self, stock_id, start_time, end_time, method="ts_data_last"):
|
def get_close(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
method: str = "ts_data_last",
|
||||||
|
) -> Union[None, int, float, bool, IndexData]:
|
||||||
return self.quote.get_data(stock_id, start_time, end_time, field="$close", method=method)
|
return self.quote.get_data(stock_id, start_time, end_time, field="$close", method=method)
|
||||||
|
|
||||||
def get_volume(self, stock_id, start_time, end_time, method="sum"):
|
def get_volume(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
method: str = "sum",
|
||||||
|
) -> float:
|
||||||
"""get the total deal volume of stock with `stock_id` between the time interval [start_time, end_time)"""
|
"""get the total deal volume of stock with `stock_id` between the time interval [start_time, end_time)"""
|
||||||
return self.quote.get_data(stock_id, start_time, end_time, field="$volume", method=method)
|
return self.quote.get_data(stock_id, start_time, end_time, field="$volume", method=method)
|
||||||
|
|
||||||
def get_deal_price(self, stock_id, start_time, end_time, direction: OrderDir, method="ts_data_last"):
|
def get_deal_price(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
direction: OrderDir,
|
||||||
|
method: str = "ts_data_last",
|
||||||
|
) -> float:
|
||||||
if direction == OrderDir.SELL:
|
if direction == OrderDir.SELL:
|
||||||
pstr = self.sell_price
|
pstr = self.sell_price
|
||||||
elif direction == OrderDir.BUY:
|
elif direction == OrderDir.BUY:
|
||||||
pstr = self.buy_price
|
pstr = self.buy_price
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
deal_price = self.quote.get_data(stock_id, start_time, end_time, field=pstr, method=method)
|
deal_price = self.quote.get_data(stock_id, start_time, end_time, field=pstr, method=method)
|
||||||
if method is not None and (deal_price is None or np.isnan(deal_price) or deal_price <= 1e-08):
|
if method is not None and (deal_price is None or np.isnan(deal_price) or deal_price <= 1e-08):
|
||||||
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!")
|
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!")
|
||||||
@@ -420,11 +471,16 @@ class Exchange:
|
|||||||
deal_price = self.get_close(stock_id, start_time, end_time, method)
|
deal_price = self.get_close(stock_id, start_time, end_time, method)
|
||||||
return deal_price
|
return deal_price
|
||||||
|
|
||||||
def get_factor(self, stock_id, start_time, end_time) -> Union[float, None]:
|
def get_factor(
|
||||||
|
self,
|
||||||
|
stock_id: str,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Union[float, None]:
|
Optional[float]:
|
||||||
`None`: if the stock is suspended `None` may be returned
|
`None`: if the stock is suspended `None` may be returned
|
||||||
`float`: return factor if the factor exists
|
`float`: return factor if the factor exists
|
||||||
"""
|
"""
|
||||||
@@ -434,11 +490,16 @@ class Exchange:
|
|||||||
return self.quote.get_data(stock_id, start_time, end_time, field="$factor", method="ts_data_last")
|
return self.quote.get_data(stock_id, start_time, end_time, field="$factor", method="ts_data_last")
|
||||||
|
|
||||||
def generate_amount_position_from_weight_position(
|
def generate_amount_position_from_weight_position(
|
||||||
self, weight_position, cash, start_time, end_time, direction=OrderDir.BUY
|
self,
|
||||||
):
|
weight_position: dict,
|
||||||
|
cash: float,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
direction: OrderDir = OrderDir.BUY,
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
The generate the target position according to the weight and the cash.
|
The generate the target position according to the weight and the cash.
|
||||||
NOTE: All the cash will assigned to the tadable stock.
|
NOTE: All the cash will assigned to the tradable stock.
|
||||||
Parameter:
|
Parameter:
|
||||||
weight_position : dict {stock_id : weight}; allocate cash by weight_position
|
weight_position : dict {stock_id : weight}; allocate cash by weight_position
|
||||||
among then, weight must be in this range: 0 < weight < 1
|
among then, weight must be in this range: 0 < weight < 1
|
||||||
@@ -451,15 +512,14 @@ class Exchange:
|
|||||||
|
|
||||||
# calculate the total weight of tradable value
|
# calculate the total weight of tradable value
|
||||||
tradable_weight = 0.0
|
tradable_weight = 0.0
|
||||||
for stock_id in weight_position:
|
for stock_id, wp in weight_position.items():
|
||||||
if self.is_stock_tradable(stock_id=stock_id, start_time=start_time, end_time=end_time):
|
if self.is_stock_tradable(stock_id=stock_id, start_time=start_time, end_time=end_time):
|
||||||
# weight_position must be greater than 0 and less than 1
|
# weight_position must be greater than 0 and less than 1
|
||||||
if weight_position[stock_id] < 0 or weight_position[stock_id] > 1:
|
if wp < 0 or wp > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"weight_position is {}, "
|
"weight_position is {}, " "weight_position is not in the range of (0, 1).".format(wp),
|
||||||
"weight_position is not in the range of (0, 1).".format(weight_position[stock_id])
|
|
||||||
)
|
)
|
||||||
tradable_weight += weight_position[stock_id]
|
tradable_weight += wp
|
||||||
|
|
||||||
if tradable_weight - 1.0 >= 1e-5:
|
if tradable_weight - 1.0 >= 1e-5:
|
||||||
raise ValueError("tradable_weight is {}, can not greater than 1.".format(tradable_weight))
|
raise ValueError("tradable_weight is {}, can not greater than 1.".format(tradable_weight))
|
||||||
@@ -467,19 +527,24 @@ class Exchange:
|
|||||||
amount_dict = {}
|
amount_dict = {}
|
||||||
for stock_id in weight_position:
|
for stock_id in weight_position:
|
||||||
if weight_position[stock_id] > 0.0 and self.is_stock_tradable(
|
if weight_position[stock_id] > 0.0 and self.is_stock_tradable(
|
||||||
stock_id=stock_id, start_time=start_time, end_time=end_time
|
stock_id=stock_id,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
):
|
):
|
||||||
amount_dict[stock_id] = (
|
amount_dict[stock_id] = (
|
||||||
cash
|
cash
|
||||||
* weight_position[stock_id]
|
* weight_position[stock_id]
|
||||||
/ tradable_weight
|
/ tradable_weight
|
||||||
// self.get_deal_price(
|
// self.get_deal_price(
|
||||||
stock_id=stock_id, start_time=start_time, end_time=end_time, direction=direction
|
stock_id=stock_id,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
direction=direction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return amount_dict
|
return amount_dict
|
||||||
|
|
||||||
def get_real_deal_amount(self, current_amount, target_amount, factor):
|
def get_real_deal_amount(self, current_amount: float, target_amount: float, factor: float) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the real adjust deal amount when considering the trading unit
|
Calculate the real adjust deal amount when considering the trading unit
|
||||||
:param current_amount:
|
:param current_amount:
|
||||||
@@ -501,7 +566,13 @@ class Exchange:
|
|||||||
deal_amount = self.round_amount_by_trade_unit(deal_amount, factor)
|
deal_amount = self.round_amount_by_trade_unit(deal_amount, factor)
|
||||||
return -deal_amount
|
return -deal_amount
|
||||||
|
|
||||||
def generate_order_for_target_amount_position(self, target_position, current_position, start_time, end_time):
|
def generate_order_for_target_amount_position(
|
||||||
|
self,
|
||||||
|
target_position: dict,
|
||||||
|
current_position: dict,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
) -> list:
|
||||||
"""
|
"""
|
||||||
Note: some future information is used in this function
|
Note: some future information is used in this function
|
||||||
Parameter:
|
Parameter:
|
||||||
@@ -517,7 +588,8 @@ class Exchange:
|
|||||||
# three parts: kept stock_id, dropped stock_id, new stock_id
|
# three parts: kept stock_id, dropped stock_id, new stock_id
|
||||||
# handle kept stock_id
|
# handle kept stock_id
|
||||||
|
|
||||||
# because the order of the set is not fixed, the trading order of the stock is different, so that the backtest results of the same parameter are different;
|
# because the order of the set is not fixed, the trading order of the stock is different, so that the backtest
|
||||||
|
# results of the same parameter are different;
|
||||||
# so here we sort stock_id, and then randomly shuffle the order of stock_id
|
# so here we sort stock_id, and then randomly shuffle the order of stock_id
|
||||||
# because the same random seed is used, the final stock_id order is fixed
|
# because the same random seed is used, the final stock_id order is fixed
|
||||||
sorted_ids = sorted(set(list(current_position.keys()) + list(target_position.keys())))
|
sorted_ids = sorted(set(list(current_position.keys()) + list(target_position.keys())))
|
||||||
@@ -546,7 +618,7 @@ class Exchange:
|
|||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
factor=factor,
|
factor=factor,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# sell stock
|
# sell stock
|
||||||
@@ -558,14 +630,19 @@ class Exchange:
|
|||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
factor=factor,
|
factor=factor,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
# return order_list : buy + sell
|
# return order_list : buy + sell
|
||||||
return sell_order_list + buy_order_list
|
return sell_order_list + buy_order_list
|
||||||
|
|
||||||
def calculate_amount_position_value(
|
def calculate_amount_position_value(
|
||||||
self, amount_dict, start_time, end_time, only_tradable=False, direction=OrderDir.SELL
|
self,
|
||||||
):
|
amount_dict: dict,
|
||||||
|
start_time: pd.Timestamp,
|
||||||
|
end_time: pd.Timestamp,
|
||||||
|
only_tradable: bool = False,
|
||||||
|
direction: OrderDir = OrderDir.SELL,
|
||||||
|
) -> float:
|
||||||
"""Parameter
|
"""Parameter
|
||||||
position : Position()
|
position : Position()
|
||||||
amount_dict : {stock_id : amount}
|
amount_dict : {stock_id : amount}
|
||||||
@@ -576,21 +653,28 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
value = 0
|
value = 0
|
||||||
for stock_id in amount_dict:
|
for stock_id in amount_dict:
|
||||||
if (
|
if not only_tradable or (
|
||||||
only_tradable is True
|
not self.check_stock_suspended(stock_id=stock_id, start_time=start_time, end_time=end_time)
|
||||||
and self.check_stock_suspended(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
and not self.check_stock_limit(stock_id=stock_id, start_time=start_time, end_time=end_time)
|
||||||
and self.check_stock_limit(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
|
||||||
or only_tradable is False
|
|
||||||
):
|
):
|
||||||
value += (
|
value += (
|
||||||
self.get_deal_price(
|
self.get_deal_price(
|
||||||
stock_id=stock_id, start_time=start_time, end_time=end_time, direction=direction
|
stock_id=stock_id,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
direction=direction,
|
||||||
)
|
)
|
||||||
* amount_dict[stock_id]
|
* amount_dict[stock_id]
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _get_factor_or_raise_error(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None):
|
def _get_factor_or_raise_error(
|
||||||
|
self,
|
||||||
|
factor: float = None,
|
||||||
|
stock_id: str = None,
|
||||||
|
start_time: pd.Timestamp = None,
|
||||||
|
end_time: pd.Timestamp = None,
|
||||||
|
) -> float:
|
||||||
"""Please refer to the docs of get_amount_of_trade_unit"""
|
"""Please refer to the docs of get_amount_of_trade_unit"""
|
||||||
if factor is None:
|
if factor is None:
|
||||||
if stock_id is not None and start_time is not None and end_time is not None:
|
if stock_id is not None and start_time is not None and end_time is not None:
|
||||||
@@ -599,7 +683,13 @@ class Exchange:
|
|||||||
raise ValueError(f"`factor` and (`stock_id`, `start_time`, `end_time`) can't both be None")
|
raise ValueError(f"`factor` and (`stock_id`, `start_time`, `end_time`) can't both be None")
|
||||||
return factor
|
return factor
|
||||||
|
|
||||||
def get_amount_of_trade_unit(self, factor: float = None, stock_id: str = None, start_time=None, end_time=None):
|
def get_amount_of_trade_unit(
|
||||||
|
self,
|
||||||
|
factor: float = None,
|
||||||
|
stock_id: str = None,
|
||||||
|
start_time: pd.Timestamp = None,
|
||||||
|
end_time: pd.Timestamp = None,
|
||||||
|
) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
get the trade unit of amount based on **factor**
|
get the trade unit of amount based on **factor**
|
||||||
the factor can be given directly or calculated in given time range and stock id.
|
the factor can be given directly or calculated in given time range and stock id.
|
||||||
@@ -617,14 +707,22 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not self.trade_w_adj_price and self.trade_unit is not None:
|
if not self.trade_w_adj_price and self.trade_unit is not None:
|
||||||
factor = self._get_factor_or_raise_error(
|
factor = self._get_factor_or_raise_error(
|
||||||
factor=factor, stock_id=stock_id, start_time=start_time, end_time=end_time
|
factor=factor,
|
||||||
|
stock_id=stock_id,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
)
|
)
|
||||||
return self.trade_unit / factor
|
return self.trade_unit / factor
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def round_amount_by_trade_unit(
|
def round_amount_by_trade_unit(
|
||||||
self, deal_amount, factor: float = None, stock_id: str = None, start_time=None, end_time=None
|
self,
|
||||||
|
deal_amount,
|
||||||
|
factor: float = None,
|
||||||
|
stock_id: str = None,
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
):
|
):
|
||||||
"""Parameter
|
"""Parameter
|
||||||
Please refer to the docs of get_amount_of_trade_unit
|
Please refer to the docs of get_amount_of_trade_unit
|
||||||
@@ -635,7 +733,10 @@ class Exchange:
|
|||||||
if not self.trade_w_adj_price and self.trade_unit is not None:
|
if not self.trade_w_adj_price and self.trade_unit is not None:
|
||||||
# the minimal amount is 1. Add 0.1 for solving precision problem.
|
# the minimal amount is 1. Add 0.1 for solving precision problem.
|
||||||
factor = self._get_factor_or_raise_error(
|
factor = self._get_factor_or_raise_error(
|
||||||
factor=factor, stock_id=stock_id, start_time=start_time, end_time=end_time
|
factor=factor,
|
||||||
|
stock_id=stock_id,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
)
|
)
|
||||||
return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor
|
return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor
|
||||||
return deal_amount
|
return deal_amount
|
||||||
@@ -714,7 +815,12 @@ class Exchange:
|
|||||||
max_trade_amount = (cash - self.min_cost) / trade_price
|
max_trade_amount = (cash - self.min_cost) / trade_price
|
||||||
return max_trade_amount
|
return max_trade_amount
|
||||||
|
|
||||||
def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount):
|
def _calc_trade_info_by_order(
|
||||||
|
self,
|
||||||
|
order: Order,
|
||||||
|
position: Optional[BasePosition],
|
||||||
|
dealt_order_amount: dict,
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
"""
|
"""
|
||||||
Calculation of trade info
|
Calculation of trade info
|
||||||
**NOTE**: Order will be changed in this function
|
**NOTE**: Order will be changed in this function
|
||||||
@@ -753,7 +859,8 @@ class Exchange:
|
|||||||
if not np.isclose(order.deal_amount, current_amount):
|
if not np.isclose(order.deal_amount, current_amount):
|
||||||
# when not selling last stock. rounding is necessary
|
# when not selling last stock. rounding is necessary
|
||||||
order.deal_amount = self.round_amount_by_trade_unit(
|
order.deal_amount = self.round_amount_by_trade_unit(
|
||||||
min(current_amount, order.deal_amount), order.factor
|
min(current_amount, order.deal_amount),
|
||||||
|
order.factor,
|
||||||
)
|
)
|
||||||
|
|
||||||
# in case of negative value of cash
|
# in case of negative value of cash
|
||||||
@@ -778,7 +885,8 @@ class Exchange:
|
|||||||
# The money is not enough
|
# The money is not enough
|
||||||
max_buy_amount = self._get_buy_amount_by_cash_limit(trade_price, cash, cost_ratio)
|
max_buy_amount = self._get_buy_amount_by_cash_limit(trade_price, cash, cost_ratio)
|
||||||
order.deal_amount = self.round_amount_by_trade_unit(
|
order.deal_amount = self.round_amount_by_trade_unit(
|
||||||
min(max_buy_amount, order.deal_amount), order.factor
|
min(max_buy_amount, order.deal_amount),
|
||||||
|
order.factor,
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Order clipped due to cash limitation: {order}")
|
self.logger.debug(f"Order clipped due to cash limitation: {order}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
from abc import abstractmethod
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from abc import abstractmethod
|
||||||
|
from collections import defaultdict
|
||||||
|
from types import GeneratorType
|
||||||
|
from typing import Generator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from qlib.backtest.account import Account
|
||||||
from qlib.backtest.position import BasePosition
|
from qlib.backtest.position import BasePosition
|
||||||
from qlib.log import get_module_logger
|
from qlib.log import get_module_logger
|
||||||
from types import GeneratorType
|
|
||||||
from qlib.backtest.account import Account
|
|
||||||
import pandas as pd
|
|
||||||
from typing import List, Tuple, Union
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from .decision import Order, BaseTradeDecision
|
|
||||||
from .exchange import Exchange
|
|
||||||
from .utils import TradeCalendarManager, CommonInfrastructure, LevelInfrastructure, get_start_end_idx
|
|
||||||
|
|
||||||
from ..utils import init_instance_by_config
|
|
||||||
from ..strategy.base import BaseStrategy
|
from ..strategy.base import BaseStrategy
|
||||||
|
from ..utils import init_instance_by_config
|
||||||
|
from .decision import BaseTradeDecision, Order
|
||||||
|
from .exchange import Exchange
|
||||||
|
from .utils import (
|
||||||
|
BaseInfrastructure,
|
||||||
|
CommonInfrastructure,
|
||||||
|
LevelInfrastructure,
|
||||||
|
TradeCalendarManager,
|
||||||
|
get_start_end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseExecutor:
|
class BaseExecutor:
|
||||||
@@ -30,9 +39,9 @@ class BaseExecutor:
|
|||||||
track_data: bool = False,
|
track_data: bool = False,
|
||||||
trade_exchange: Exchange = None,
|
trade_exchange: Exchange = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
settle_type=BasePosition.ST_NO,
|
settle_type=BasePosition.ST_NO, # TODO: add typehint
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -53,15 +62,21 @@ class BaseExecutor:
|
|||||||
- 'base_price': the based price than which the trading price is advanced, Optional, default by 'twap'
|
- 'base_price': the based price than which the trading price is advanced, Optional, default by 'twap'
|
||||||
- If 'base_price' is 'twap', the based price is the time weighted average price
|
- If 'base_price' is 'twap', the based price is the time weighted average price
|
||||||
- If 'base_price' is 'vwap', the based price is the volume weighted average price
|
- If 'base_price' is 'vwap', the based price is the volume weighted average price
|
||||||
- 'weight_method': weighted method when calculating total trading pa by different orders' pa in each step, optional, default by 'mean'
|
- 'weight_method': weighted method when calculating total trading pa by different orders' pa in each
|
||||||
|
step, optional, default by 'mean'
|
||||||
- If 'weight_method' is 'mean', calculating mean value of different orders' pa
|
- If 'weight_method' is 'mean', calculating mean value of different orders' pa
|
||||||
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different orders' pa
|
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different
|
||||||
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different orders' pa
|
orders' pa
|
||||||
|
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different
|
||||||
|
orders' pa
|
||||||
- 'ffr_config': config for calculating fulfill rate(ffr), optional
|
- 'ffr_config': config for calculating fulfill rate(ffr), optional
|
||||||
- 'weight_method': weighted method when calculating total trading ffr by different orders' ffr in each step, optional, default by 'mean'
|
- 'weight_method': weighted method when calculating total trading ffr by different orders' ffr in each
|
||||||
|
step, optional, default by 'mean'
|
||||||
- If 'weight_method' is 'mean', calculating mean value of different orders' ffr
|
- If 'weight_method' is 'mean', calculating mean value of different orders' ffr
|
||||||
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different orders' ffr
|
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different
|
||||||
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different orders' ffr
|
orders' ffr
|
||||||
|
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different
|
||||||
|
orders' ffr
|
||||||
Example:
|
Example:
|
||||||
{
|
{
|
||||||
'show_indicator': True,
|
'show_indicator': True,
|
||||||
@@ -79,7 +94,8 @@ class BaseExecutor:
|
|||||||
whether to print trading info, by default False
|
whether to print trading info, by default False
|
||||||
track_data : bool, optional
|
track_data : bool, optional
|
||||||
whether to generate trade_decision, will be used when training rl agent
|
whether to generate trade_decision, will be used when training rl agent
|
||||||
- If `self.track_data` is true, when making data for training, the input `trade_decision` of `execute` will be generated by `collect_data`
|
- If `self.track_data` is true, when making data for training, the input `trade_decision` of `execute` will
|
||||||
|
be generated by `collect_data`
|
||||||
- Else, `trade_decision` will not be generated
|
- Else, `trade_decision` will not be generated
|
||||||
|
|
||||||
trade_exchange : Exchange
|
trade_exchange : Exchange
|
||||||
@@ -114,7 +130,7 @@ class BaseExecutor:
|
|||||||
self.dealt_order_amount = defaultdict(float)
|
self.dealt_order_amount = defaultdict(float)
|
||||||
self.deal_day = None
|
self.deal_day = None
|
||||||
|
|
||||||
def reset_common_infra(self, common_infra, copy_trade_account=False):
|
def reset_common_infra(self, common_infra: BaseInfrastructure, copy_trade_account: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
reset infrastructure for trading
|
reset infrastructure for trading
|
||||||
- reset trade_account
|
- reset trade_account
|
||||||
@@ -132,7 +148,7 @@ class BaseExecutor:
|
|||||||
# 2. Others are not shared, so each level has it own metrics (portfolio and trading metrics)
|
# 2. Others are not shared, so each level has it own metrics (portfolio and trading metrics)
|
||||||
self.trade_account: Account = copy.copy(common_infra.get("trade_account"))
|
self.trade_account: Account = copy.copy(common_infra.get("trade_account"))
|
||||||
else:
|
else:
|
||||||
self.trade_account = common_infra.get("trade_account")
|
self.trade_account: Account = common_infra.get("trade_account")
|
||||||
self.trade_account.reset(freq=self.time_per_step, port_metr_enabled=self.generate_portfolio_metrics)
|
self.trade_account.reset(freq=self.time_per_step, port_metr_enabled=self.generate_portfolio_metrics)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -148,7 +164,7 @@ class BaseExecutor:
|
|||||||
"""
|
"""
|
||||||
return self.level_infra.get("trade_calendar")
|
return self.level_infra.get("trade_calendar")
|
||||||
|
|
||||||
def reset(self, common_infra: CommonInfrastructure = None, **kwargs):
|
def reset(self, common_infra: CommonInfrastructure = None, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
- reset `start_time` and `end_time`, used in trade calendar
|
- reset `start_time` and `end_time`, used in trade calendar
|
||||||
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
|
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
|
||||||
@@ -161,13 +177,13 @@ class BaseExecutor:
|
|||||||
if common_infra is not None:
|
if common_infra is not None:
|
||||||
self.reset_common_infra(common_infra)
|
self.reset_common_infra(common_infra)
|
||||||
|
|
||||||
def get_level_infra(self):
|
def get_level_infra(self) -> LevelInfrastructure:
|
||||||
return self.level_infra
|
return self.level_infra
|
||||||
|
|
||||||
def finished(self):
|
def finished(self) -> bool:
|
||||||
return self.trade_calendar.finished()
|
return self.trade_calendar.finished()
|
||||||
|
|
||||||
def execute(self, trade_decision: BaseTradeDecision, level: int = 0):
|
def execute(self, trade_decision: BaseTradeDecision, level: int = 0) -> List[object]:
|
||||||
"""execute the trade decision and return the executed result
|
"""execute the trade decision and return the executed result
|
||||||
|
|
||||||
NOTE: this function is never used directly in the framework. Should we delete it?
|
NOTE: this function is never used directly in the framework. Should we delete it?
|
||||||
@@ -189,9 +205,15 @@ class BaseExecutor:
|
|||||||
pass
|
pass
|
||||||
return return_value.get("execute_result")
|
return return_value.get("execute_result")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _collect_data(cls, trade_decision: BaseTradeDecision, level: int = 0) -> Tuple[List[object], dict]:
|
def _collect_data(
|
||||||
|
self,
|
||||||
|
trade_decision: BaseTradeDecision,
|
||||||
|
level: int = 0,
|
||||||
|
) -> Union[
|
||||||
|
Generator[BaseTradeDecision, Optional[BaseTradeDecision], Tuple[List[object], dict]],
|
||||||
|
Tuple[List[object], dict],
|
||||||
|
]:
|
||||||
"""
|
"""
|
||||||
Please refer to the doc of collect_data
|
Please refer to the doc of collect_data
|
||||||
The only difference between `_collect_data` and `collect_data` is that some common steps are moved into
|
The only difference between `_collect_data` and `collect_data` is that some common steps are moved into
|
||||||
@@ -209,8 +231,11 @@ class BaseExecutor:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def collect_data(
|
def collect_data(
|
||||||
self, trade_decision: BaseTradeDecision, return_value: dict = None, level: int = 0
|
self,
|
||||||
) -> List[object]:
|
trade_decision: BaseTradeDecision,
|
||||||
|
return_value: dict = None,
|
||||||
|
level: int = 0,
|
||||||
|
) -> Generator[BaseTradeDecision, Optional[BaseTradeDecision], List[object]]:
|
||||||
"""Generator for collecting the trade decision data for rl training
|
"""Generator for collecting the trade decision data for rl training
|
||||||
|
|
||||||
his function will make a step forward
|
his function will make a step forward
|
||||||
@@ -253,7 +278,9 @@ class BaseExecutor:
|
|||||||
obj = self._collect_data(trade_decision=trade_decision, level=level)
|
obj = self._collect_data(trade_decision=trade_decision, level=level)
|
||||||
|
|
||||||
if isinstance(obj, GeneratorType):
|
if isinstance(obj, GeneratorType):
|
||||||
res, kwargs = yield from obj
|
yield_res = yield from obj
|
||||||
|
assert isinstance(yield_res, tuple) and len(yield_res) == 2
|
||||||
|
res, kwargs = yield_res
|
||||||
else:
|
else:
|
||||||
# Some concrete executor don't have inner decisions
|
# Some concrete executor don't have inner decisions
|
||||||
res, kwargs = obj
|
res, kwargs = obj
|
||||||
@@ -279,7 +306,7 @@ class BaseExecutor:
|
|||||||
return_value.update({"execute_result": res})
|
return_value.update({"execute_result": res})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_all_executors(self):
|
def get_all_executors(self) -> List[BaseExecutor]:
|
||||||
"""get all executors"""
|
"""get all executors"""
|
||||||
return [self]
|
return [self]
|
||||||
|
|
||||||
@@ -287,7 +314,8 @@ class BaseExecutor:
|
|||||||
class NestedExecutor(BaseExecutor):
|
class NestedExecutor(BaseExecutor):
|
||||||
"""
|
"""
|
||||||
Nested Executor with inner strategy and executor
|
Nested Executor with inner strategy and executor
|
||||||
- At each time `execute` is called, it will call the inner strategy and executor to execute the `trade_decision` in a higher frequency env.
|
- At each time `execute` is called, it will call the inner strategy and executor to execute the `trade_decision`
|
||||||
|
in a higher frequency env.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -305,7 +333,7 @@ class NestedExecutor(BaseExecutor):
|
|||||||
align_range_limit: bool = True,
|
align_range_limit: bool = True,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -323,10 +351,14 @@ class NestedExecutor(BaseExecutor):
|
|||||||
It is only for nested executor, because range_limit is given by outer strategy
|
It is only for nested executor, because range_limit is given by outer strategy
|
||||||
"""
|
"""
|
||||||
self.inner_executor: BaseExecutor = init_instance_by_config(
|
self.inner_executor: BaseExecutor = init_instance_by_config(
|
||||||
inner_executor, common_infra=common_infra, accept_types=BaseExecutor
|
inner_executor,
|
||||||
|
common_infra=common_infra,
|
||||||
|
accept_types=BaseExecutor,
|
||||||
)
|
)
|
||||||
self.inner_strategy: BaseStrategy = init_instance_by_config(
|
self.inner_strategy: BaseStrategy = init_instance_by_config(
|
||||||
inner_strategy, common_infra=common_infra, accept_types=BaseStrategy
|
inner_strategy,
|
||||||
|
common_infra=common_infra,
|
||||||
|
accept_types=BaseStrategy,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._skip_empty_decision = skip_empty_decision
|
self._skip_empty_decision = skip_empty_decision
|
||||||
@@ -344,10 +376,10 @@ class NestedExecutor(BaseExecutor):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset_common_infra(self, common_infra, copy_trade_account=False):
|
def reset_common_infra(self, common_infra: CommonInfrastructure, copy_trade_account: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
reset infrastructure for trading
|
reset infrastructure for trading
|
||||||
- reset inner_strategyand inner_executor common infra
|
- reset inner_strategy and inner_executor common infra
|
||||||
"""
|
"""
|
||||||
# NOTE: please refer to the docs of BaseExecutor.reset_common_infra for the meaning of `copy_trade_account`
|
# NOTE: please refer to the docs of BaseExecutor.reset_common_infra for the meaning of `copy_trade_account`
|
||||||
|
|
||||||
@@ -358,7 +390,7 @@ class NestedExecutor(BaseExecutor):
|
|||||||
self.inner_executor.reset_common_infra(common_infra, copy_trade_account=True)
|
self.inner_executor.reset_common_infra(common_infra, copy_trade_account=True)
|
||||||
self.inner_strategy.reset_common_infra(common_infra)
|
self.inner_strategy.reset_common_infra(common_infra)
|
||||||
|
|
||||||
def _init_sub_trading(self, trade_decision):
|
def _init_sub_trading(self, trade_decision: BaseTradeDecision) -> None:
|
||||||
trade_start_time, trade_end_time = self.trade_calendar.get_step_time()
|
trade_start_time, trade_end_time = self.trade_calendar.get_step_time()
|
||||||
self.inner_executor.reset(start_time=trade_start_time, end_time=trade_end_time)
|
self.inner_executor.reset(start_time=trade_start_time, end_time=trade_end_time)
|
||||||
sub_level_infra = self.inner_executor.get_level_infra()
|
sub_level_infra = self.inner_executor.get_level_infra()
|
||||||
@@ -368,14 +400,18 @@ class NestedExecutor(BaseExecutor):
|
|||||||
def _update_trade_decision(self, trade_decision: BaseTradeDecision) -> BaseTradeDecision:
|
def _update_trade_decision(self, trade_decision: BaseTradeDecision) -> BaseTradeDecision:
|
||||||
# outer strategy have chance to update decision each iterator
|
# outer strategy have chance to update decision each iterator
|
||||||
updated_trade_decision = trade_decision.update(self.inner_executor.trade_calendar)
|
updated_trade_decision = trade_decision.update(self.inner_executor.trade_calendar)
|
||||||
if updated_trade_decision is not None:
|
if updated_trade_decision is not None: # TODO: always is None for now?
|
||||||
trade_decision = updated_trade_decision
|
trade_decision = updated_trade_decision
|
||||||
# NEW UPDATE
|
# NEW UPDATE
|
||||||
# create a hook for inner strategy to update outer decision
|
# create a hook for inner strategy to update outer decision
|
||||||
self.inner_strategy.alter_outer_trade_decision(trade_decision)
|
self.inner_strategy.alter_outer_trade_decision(trade_decision)
|
||||||
return trade_decision
|
return trade_decision
|
||||||
|
|
||||||
def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0):
|
def _collect_data(
|
||||||
|
self,
|
||||||
|
trade_decision: BaseTradeDecision,
|
||||||
|
level: int = 0,
|
||||||
|
) -> Generator[BaseTradeDecision, Optional[BaseTradeDecision], Tuple[List[object], dict]]:
|
||||||
execute_result = []
|
execute_result = []
|
||||||
inner_order_indicators = []
|
inner_order_indicators = []
|
||||||
decision_list = []
|
decision_list = []
|
||||||
@@ -390,8 +426,8 @@ class NestedExecutor(BaseExecutor):
|
|||||||
|
|
||||||
if trade_decision.empty() and self._skip_empty_decision:
|
if trade_decision.empty() and self._skip_empty_decision:
|
||||||
# give one chance for outer strategy to update the strategy
|
# give one chance for outer strategy to update the strategy
|
||||||
# - For updating some information in the sub executor(the strategy have no knowledge of the inner
|
# - For updating some information in the sub executor (the strategy have no knowledge of the inner
|
||||||
# executor when generating the decision)
|
# executor when generating the decision)
|
||||||
break
|
break
|
||||||
|
|
||||||
sub_cal: TradeCalendarManager = self.inner_executor.trade_calendar
|
sub_cal: TradeCalendarManager = self.inner_executor.trade_calendar
|
||||||
@@ -405,15 +441,19 @@ class NestedExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# NOTE: !!!!!
|
# NOTE: !!!!!
|
||||||
# the two lines below is for a special case in RL
|
# the two lines below is for a special case in RL
|
||||||
# To solve the confliction below
|
# To solve the conflicts below
|
||||||
# - Normally, user will create a strategy and embed it into Qlib's executor and simulator interaction loop
|
# - Normally, user will create a strategy and embed it into Qlib's executor and simulator interaction
|
||||||
# For a _nested qlib example_, (Qlib Strategy) <=> (Qlib Executor[(inner Qlib Strategy) <=> (inner Qlib Executor)])
|
# loop For a _nested qlib example_, (Qlib Strategy) <=> (Qlib Executor[(inner Qlib Strategy) <=>
|
||||||
|
# (inner Qlib Executor)])
|
||||||
# - However, RL-based framework has it's own script to run the loop
|
# - However, RL-based framework has it's own script to run the loop
|
||||||
# For an _RL learning example_, (RL Policy) <=> (RL Env[(inner Qlib Executor)])
|
# For an _RL learning example_, (RL Policy) <=> (RL Env[(inner Qlib Executor)])
|
||||||
# To make it possible to run _nested qlib example_ and _RL learning example_ together, the solution below is proposed
|
# To make it possible to run _nested qlib example_ and _RL learning example_ together, the solution
|
||||||
# - The entry script follow the example of _RL learning example_ to be compatible with all kinds of RL Framework
|
# below is proposed
|
||||||
|
# - The entry script follow the example of _RL learning example_ to be compatible with all kinds of
|
||||||
|
# RL Framework
|
||||||
# - Each step of (RL Env) will make (inner Qlib Executor) one step forward
|
# - Each step of (RL Env) will make (inner Qlib Executor) one step forward
|
||||||
# - (inner Qlib Strategy) is a proxy strategy, it will give the program control right to (RL Env) by `yield from` and wait for the action from the policy
|
# - (inner Qlib Strategy) is a proxy strategy, it will give the program control right to (RL Env)
|
||||||
|
# by `yield from` and wait for the action from the policy
|
||||||
# So the two lines below is the implementation of yielding control rights
|
# So the two lines below is the implementation of yielding control rights
|
||||||
if isinstance(res, GeneratorType):
|
if isinstance(res, GeneratorType):
|
||||||
res = yield from res
|
res = yield from res
|
||||||
@@ -427,13 +467,15 @@ class NestedExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# NOTE: Trade Calendar will step forward in the follow line
|
# NOTE: Trade Calendar will step forward in the follow line
|
||||||
_inner_execute_result = yield from self.inner_executor.collect_data(
|
_inner_execute_result = yield from self.inner_executor.collect_data(
|
||||||
trade_decision=_inner_trade_decision, level=level + 1
|
trade_decision=_inner_trade_decision,
|
||||||
|
level=level + 1,
|
||||||
)
|
)
|
||||||
|
assert isinstance(_inner_execute_result, list)
|
||||||
self.post_inner_exe_step(_inner_execute_result)
|
self.post_inner_exe_step(_inner_execute_result)
|
||||||
execute_result.extend(_inner_execute_result)
|
execute_result.extend(_inner_execute_result)
|
||||||
|
|
||||||
inner_order_indicators.append(
|
inner_order_indicators.append(
|
||||||
self.inner_executor.trade_account.get_trade_indicator().get_order_indicator(raw=True)
|
self.inner_executor.trade_account.get_trade_indicator().get_order_indicator(raw=True),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# do nothing and just step forward
|
# do nothing and just step forward
|
||||||
@@ -441,7 +483,7 @@ class NestedExecutor(BaseExecutor):
|
|||||||
|
|
||||||
return execute_result, {"inner_order_indicators": inner_order_indicators, "decision_list": decision_list}
|
return execute_result, {"inner_order_indicators": inner_order_indicators, "decision_list": decision_list}
|
||||||
|
|
||||||
def post_inner_exe_step(self, inner_exe_res):
|
def post_inner_exe_step(self, inner_exe_res: List[object]) -> None:
|
||||||
"""
|
"""
|
||||||
A hook for doing sth after each step of inner strategy
|
A hook for doing sth after each step of inner strategy
|
||||||
|
|
||||||
@@ -451,11 +493,23 @@ class NestedExecutor(BaseExecutor):
|
|||||||
the execution result of inner task
|
the execution result of inner task
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_all_executors(self):
|
def get_all_executors(self) -> List[object]:
|
||||||
"""get all executors, including self and inner_executor.get_all_executors()"""
|
"""get all executors, including self and inner_executor.get_all_executors()"""
|
||||||
return [self, *self.inner_executor.get_all_executors()]
|
return [self, *self.inner_executor.get_all_executors()]
|
||||||
|
|
||||||
|
|
||||||
|
def _retrieve_orders_from_decision(trade_decision: BaseTradeDecision) -> List[Order]:
|
||||||
|
"""
|
||||||
|
IDE-friendly helper function.
|
||||||
|
"""
|
||||||
|
decisions = trade_decision.get_decision()
|
||||||
|
orders: List[Order] = []
|
||||||
|
for decision in decisions:
|
||||||
|
assert isinstance(decision, Order)
|
||||||
|
orders.append(decision)
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
class SimulatorExecutor(BaseExecutor):
|
class SimulatorExecutor(BaseExecutor):
|
||||||
"""Executor that simulate the true market"""
|
"""Executor that simulate the true market"""
|
||||||
|
|
||||||
@@ -464,10 +518,10 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# available trade_types
|
# available trade_types
|
||||||
TT_SERIAL = "serial"
|
TT_SERIAL = "serial"
|
||||||
## The orders will be executed serially in a sequence
|
# The orders will be executed serially in a sequence
|
||||||
# In each trading step, it is possible that users sell instruments first and use the money to buy new instruments
|
# In each trading step, it is possible that users sell instruments first and use the money to buy new instruments
|
||||||
TT_PARAL = "parallel"
|
TT_PARAL = "parallel"
|
||||||
## The orders will be executed parallelly
|
# The orders will be executed in parallel
|
||||||
# In each trading step, if users try to sell instruments first and buy new instruments with money, failure will
|
# In each trading step, if users try to sell instruments first and buy new instruments with money, failure will
|
||||||
# occur
|
# occur
|
||||||
|
|
||||||
@@ -483,7 +537,7 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
trade_type: str = TT_SERIAL,
|
trade_type: str = TT_SERIAL,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -517,7 +571,7 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
List[Order]:
|
List[Order]:
|
||||||
get a list orders according to `self.trade_type`
|
get a list orders according to `self.trade_type`
|
||||||
"""
|
"""
|
||||||
orders = trade_decision.get_decision()
|
orders = _retrieve_orders_from_decision(trade_decision)
|
||||||
|
|
||||||
if self.trade_type == self.TT_SERIAL:
|
if self.trade_type == self.TT_SERIAL:
|
||||||
# Orders will be traded in a parallel way
|
# Orders will be traded in a parallel way
|
||||||
@@ -525,15 +579,15 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
elif self.trade_type == self.TT_PARAL:
|
elif self.trade_type == self.TT_PARAL:
|
||||||
# NOTE: !!!!!!!
|
# NOTE: !!!!!!!
|
||||||
# Assumption: there will not be orders in different trading direction in a single step of a strategy !!!!
|
# Assumption: there will not be orders in different trading direction in a single step of a strategy !!!!
|
||||||
# The parallel trading failure will be caused only by the confliction of money
|
# The parallel trading failure will be caused only by the conflicts of money
|
||||||
# Therefore, make the buying go first will make sure the confliction happen.
|
# Therefore, make the buying go first will make sure the conflicts happen.
|
||||||
# It equals to parallel trading after sorting the order by direction
|
# It equals to parallel trading after sorting the order by direction
|
||||||
order_it = sorted(orders, key=lambda order: -order.direction)
|
order_it = sorted(orders, key=lambda order: -order.direction)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
return order_it
|
return order_it
|
||||||
|
|
||||||
def _update_dealt_order_amount(self, order):
|
def _update_dealt_order_amount(self, order: Order) -> None:
|
||||||
"""update date and dealt order amount in the day."""
|
"""update date and dealt order amount in the day."""
|
||||||
|
|
||||||
now_deal_day = self.trade_calendar.get_step_time()[0].floor(freq="D")
|
now_deal_day = self.trade_calendar.get_step_time()[0].floor(freq="D")
|
||||||
@@ -542,8 +596,7 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
self.deal_day = now_deal_day
|
self.deal_day = now_deal_day
|
||||||
self.dealt_order_amount[order.stock_id] += order.deal_amount
|
self.dealt_order_amount[order.stock_id] += order.deal_amount
|
||||||
|
|
||||||
def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0):
|
def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0) -> Tuple[List[object], dict]:
|
||||||
|
|
||||||
trade_start_time, _ = self.trade_calendar.get_step_time()
|
trade_start_time, _ = self.trade_calendar.get_step_time()
|
||||||
execute_result = []
|
execute_result = []
|
||||||
|
|
||||||
@@ -559,7 +612,8 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
self._update_dealt_order_amount(order)
|
self._update_dealt_order_amount(order)
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(
|
print(
|
||||||
"[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}, cash {:.2f}.".format(
|
"[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, "
|
||||||
|
"value {:.2f}, cash {:.2f}.".format(
|
||||||
trade_start_time,
|
trade_start_time,
|
||||||
"sell" if order.direction == Order.SELL else "buy",
|
"sell" if order.direction == Order.SELL else "buy",
|
||||||
order.stock_id,
|
order.stock_id,
|
||||||
@@ -569,6 +623,6 @@ class SimulatorExecutor(BaseExecutor):
|
|||||||
order.factor,
|
order.factor,
|
||||||
trade_val,
|
trade_val,
|
||||||
self.trade_account.get_cash(),
|
self.trade_account.get_cash(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
return execute_result, {"trade_info": execute_result}
|
return execute_result, {"trade_info": execute_result}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
# Copyright (c) Microsoft Corporation.
|
# Copyright (c) Microsoft Corporation.
|
||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
from functools import lru_cache
|
|
||||||
import logging
|
|
||||||
from typing import List, Text, Union, Callable, Iterable, Dict
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import pandas as pd
|
import logging
|
||||||
import numpy as np
|
from collections import OrderedDict
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Callable, Dict, Iterable, List, Text, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
import qlib.utils.index_data as idd
|
||||||
|
|
||||||
|
from ..log import get_module_logger
|
||||||
from ..utils.index_data import IndexData, SingleData
|
from ..utils.index_data import IndexData, SingleData
|
||||||
from ..utils.resam import resam_ts_data, ts_data_last
|
from ..utils.resam import resam_ts_data, ts_data_last
|
||||||
from ..log import get_module_logger
|
from ..utils.time import Freq, is_single_value
|
||||||
from ..utils.time import is_single_value, Freq
|
|
||||||
import qlib.utils.index_data as idd
|
|
||||||
|
|
||||||
|
|
||||||
class BaseQuote:
|
class BaseQuote:
|
||||||
@@ -627,7 +628,9 @@ class NumpyOrderIndicator(BaseOrderIndicator):
|
|||||||
metrics = [metrics]
|
metrics = [metrics]
|
||||||
for metric in metrics:
|
for metric in metrics:
|
||||||
order_indicator.data[metric] = idd.sum_by_index(
|
order_indicator.data[metric] = idd.sum_by_index(
|
||||||
[indicator.data[metric] for indicator in indicators], stocks, fill_value
|
[indicator.data[metric] for indicator in indicators],
|
||||||
|
stocks,
|
||||||
|
fill_value,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
@@ -2,24 +2,28 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from datetime import timedelta
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from .decision import Order
|
|
||||||
from ..data.data import D
|
from ..data.data import D
|
||||||
|
from .decision import Order
|
||||||
|
|
||||||
|
|
||||||
class BasePosition:
|
class BasePosition:
|
||||||
"""
|
"""
|
||||||
The Position want to maintain the position like a dictionary
|
The Position wants to maintain the position like a dictionary
|
||||||
Please refer to the `Position` class for the position
|
Please refer to the `Position` class for the position
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, cash=0.0, **kwargs):
|
def __init__(self, *args, cash: float = 0.0, **kwargs) -> None:
|
||||||
self._settle_type = self.ST_NO
|
self._settle_type = self.ST_NO
|
||||||
|
self.position = {}
|
||||||
|
|
||||||
|
def fill_stock_value(self, start_time: Union[str, pd.Timestamp], freq: str, last_days: int = 30) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def skip_update(self) -> bool:
|
def skip_update(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -49,7 +53,7 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `check_stock` method")
|
raise NotImplementedError(f"Please implement the `check_stock` method")
|
||||||
|
|
||||||
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float):
|
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -64,7 +68,7 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `update_order` method")
|
raise NotImplementedError(f"Please implement the `update_order` method")
|
||||||
|
|
||||||
def update_stock_price(self, stock_id, price: float):
|
def update_stock_price(self, stock_id: str, price: float) -> None:
|
||||||
"""
|
"""
|
||||||
Updating the latest price of the order
|
Updating the latest price of the order
|
||||||
The useful when clearing balance at each bar end
|
The useful when clearing balance at each bar end
|
||||||
@@ -89,6 +93,9 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `calculate_stock_value` method")
|
raise NotImplementedError(f"Please implement the `calculate_stock_value` method")
|
||||||
|
|
||||||
|
def calculate_value(self) -> float:
|
||||||
|
raise NotImplementedError(f"Please implement the `calculate_value` method")
|
||||||
|
|
||||||
def get_stock_list(self) -> List:
|
def get_stock_list(self) -> List:
|
||||||
"""
|
"""
|
||||||
Get the list of stocks in the position.
|
Get the list of stocks in the position.
|
||||||
@@ -124,14 +131,16 @@ class BasePosition:
|
|||||||
|
|
||||||
def get_cash(self, include_settle: bool = False) -> float:
|
def get_cash(self, include_settle: bool = False) -> float:
|
||||||
"""
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
include_settle:
|
||||||
|
will the unsettled(delayed) cash included
|
||||||
|
Default: not include those unavailable cash
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
float:
|
float:
|
||||||
the available(tradable) cash in position
|
the available(tradable) cash in position
|
||||||
include_settle:
|
|
||||||
will the unsettled(delayed) cash included
|
|
||||||
Default: not include those unavailable cash
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `get_cash` method")
|
raise NotImplementedError(f"Please implement the `get_cash` method")
|
||||||
|
|
||||||
@@ -165,7 +174,7 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `get_stock_weight_dict` method")
|
raise NotImplementedError(f"Please implement the `get_stock_weight_dict` method")
|
||||||
|
|
||||||
def add_count_all(self, bar):
|
def add_count_all(self, bar) -> None:
|
||||||
"""
|
"""
|
||||||
Will be called at the end of each bar on each level
|
Will be called at the end of each bar on each level
|
||||||
|
|
||||||
@@ -176,24 +185,19 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `add_count_all` method")
|
raise NotImplementedError(f"Please implement the `add_count_all` method")
|
||||||
|
|
||||||
def update_weight_all(self):
|
def update_weight_all(self) -> None:
|
||||||
"""
|
"""
|
||||||
Updating the position weight;
|
Updating the position weight;
|
||||||
|
|
||||||
# TODO: this function is a little weird. The weight data in the position is in a wrong state after dealing order
|
# TODO: this function is a little weird. The weight data in the position is in a wrong state after dealing order
|
||||||
# and before updating weight.
|
# and before updating weight.
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
bar :
|
|
||||||
The level to be updated
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `add_count_all` method")
|
raise NotImplementedError(f"Please implement the `add_count_all` method")
|
||||||
|
|
||||||
ST_CASH = "cash"
|
ST_CASH = "cash"
|
||||||
ST_NO = None
|
ST_NO = None
|
||||||
|
|
||||||
def settle_start(self, settle_type: str):
|
def settle_start(self, settle_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
settlement start
|
settlement start
|
||||||
It will act like start and commit a transaction
|
It will act like start and commit a transaction
|
||||||
@@ -210,14 +214,9 @@ class BasePosition:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `settle_conf` method")
|
raise NotImplementedError(f"Please implement the `settle_conf` method")
|
||||||
|
|
||||||
def settle_commit(self):
|
def settle_commit(self) -> None:
|
||||||
"""
|
"""
|
||||||
settlement commit
|
settlement commit
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
settle_type : str
|
|
||||||
please refer to the documents of Executor
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f"Please implement the `settle_commit` method")
|
raise NotImplementedError(f"Please implement the `settle_commit` method")
|
||||||
|
|
||||||
@@ -242,13 +241,11 @@ class Position(BasePosition):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cash: float = 0, position_dict: Dict[str, Dict[str, float]] = {}):
|
def __init__(self, cash: float = 0, position_dict: Dict[str, Union[Dict[str, float], float]] = {}) -> None:
|
||||||
"""Init position by cash and position_dict.
|
"""Init position by cash and position_dict.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start_time :
|
|
||||||
the start time of backtest. It's for filling the initial value of stocks.
|
|
||||||
cash : float, optional
|
cash : float, optional
|
||||||
initial cash in account, by default 0
|
initial cash in account, by default 0
|
||||||
position_dict : Dict[
|
position_dict : Dict[
|
||||||
@@ -268,9 +265,9 @@ class Position(BasePosition):
|
|||||||
# Otherwise the initial value
|
# Otherwise the initial value
|
||||||
self.init_cash = cash
|
self.init_cash = cash
|
||||||
self.position = position_dict.copy()
|
self.position = position_dict.copy()
|
||||||
for stock in self.position:
|
for stock, value in self.position.items():
|
||||||
if isinstance(self.position[stock], int):
|
if isinstance(value, int):
|
||||||
self.position[stock] = {"amount": self.position[stock]}
|
self.position[stock] = {"amount": value}
|
||||||
self.position["cash"] = cash
|
self.position["cash"] = cash
|
||||||
|
|
||||||
# If the stock price information is missing, the account value will not be calculated temporarily
|
# If the stock price information is missing, the account value will not be calculated temporarily
|
||||||
@@ -279,21 +276,23 @@ class Position(BasePosition):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def fill_stock_value(self, start_time: Union[str, pd.Timestamp], freq: str, last_days: int = 30):
|
def fill_stock_value(self, start_time: Union[str, pd.Timestamp], freq: str, last_days: int = 30) -> None:
|
||||||
"""fill the stock value by the close price of latest last_days from qlib.
|
"""fill the stock value by the close price of latest last_days from qlib.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
start_time :
|
start_time :
|
||||||
the start time of backtest.
|
the start time of backtest.
|
||||||
|
freq : str
|
||||||
|
Frequency
|
||||||
last_days : int, optional
|
last_days : int, optional
|
||||||
the days to get the latest close price, by default 30.
|
the days to get the latest close price, by default 30.
|
||||||
"""
|
"""
|
||||||
stock_list = []
|
stock_list = []
|
||||||
for stock in self.position:
|
for stock, value in self.position.items():
|
||||||
if not isinstance(self.position[stock], dict):
|
if not isinstance(value, dict):
|
||||||
continue
|
continue
|
||||||
if ("price" not in self.position[stock]) or (self.position[stock]["price"] is None):
|
if value.get("price", None) is None:
|
||||||
stock_list.append(stock)
|
stock_list.append(stock)
|
||||||
|
|
||||||
if len(stock_list) == 0:
|
if len(stock_list) == 0:
|
||||||
@@ -304,7 +303,12 @@ class Position(BasePosition):
|
|||||||
price_end_time = start_time
|
price_end_time = start_time
|
||||||
price_start_time = start_time - timedelta(days=last_days)
|
price_start_time = start_time - timedelta(days=last_days)
|
||||||
price_df = D.features(
|
price_df = D.features(
|
||||||
stock_list, ["$close"], price_start_time, price_end_time, freq=freq, disk_cache=True
|
stock_list,
|
||||||
|
["$close"],
|
||||||
|
price_start_time,
|
||||||
|
price_end_time,
|
||||||
|
freq=freq,
|
||||||
|
disk_cache=True,
|
||||||
).dropna()
|
).dropna()
|
||||||
price_dict = price_df.groupby(["instrument"]).tail(1).reset_index(level=1, drop=True)["$close"].to_dict()
|
price_dict = price_df.groupby(["instrument"]).tail(1).reset_index(level=1, drop=True)["$close"].to_dict()
|
||||||
|
|
||||||
@@ -316,7 +320,7 @@ class Position(BasePosition):
|
|||||||
self.position[stock]["price"] = price_dict[stock]
|
self.position[stock]["price"] = price_dict[stock]
|
||||||
self.position["now_account_value"] = self.calculate_value()
|
self.position["now_account_value"] = self.calculate_value()
|
||||||
|
|
||||||
def _init_stock(self, stock_id, amount, price=None):
|
def _init_stock(self, stock_id: str, amount: float, price: float = None) -> None:
|
||||||
"""
|
"""
|
||||||
initialization the stock in current position
|
initialization the stock in current position
|
||||||
|
|
||||||
@@ -334,7 +338,7 @@ class Position(BasePosition):
|
|||||||
self.position[stock_id]["price"] = price
|
self.position[stock_id]["price"] = price
|
||||||
self.position[stock_id]["weight"] = 0 # update the weight in the end of the trade date
|
self.position[stock_id]["weight"] = 0 # update the weight in the end of the trade date
|
||||||
|
|
||||||
def _buy_stock(self, stock_id, trade_val, cost, trade_price):
|
def _buy_stock(self, stock_id: str, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
trade_amount = trade_val / trade_price
|
trade_amount = trade_val / trade_price
|
||||||
if stock_id not in self.position:
|
if stock_id not in self.position:
|
||||||
self._init_stock(stock_id=stock_id, amount=trade_amount, price=trade_price)
|
self._init_stock(stock_id=stock_id, amount=trade_amount, price=trade_price)
|
||||||
@@ -344,15 +348,16 @@ class Position(BasePosition):
|
|||||||
|
|
||||||
self.position["cash"] -= trade_val + cost
|
self.position["cash"] -= trade_val + cost
|
||||||
|
|
||||||
def _sell_stock(self, stock_id, trade_val, cost, trade_price):
|
def _sell_stock(self, stock_id: str, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
trade_amount = trade_val / trade_price
|
trade_amount = trade_val / trade_price
|
||||||
if stock_id not in self.position:
|
if stock_id not in self.position:
|
||||||
raise KeyError("{} not in current position".format(stock_id))
|
raise KeyError("{} not in current position".format(stock_id))
|
||||||
else:
|
else:
|
||||||
if np.isclose(self.position[stock_id]["amount"], trade_amount):
|
if np.isclose(self.position[stock_id]["amount"], trade_amount):
|
||||||
# Selling all the stocks
|
# Selling all the stocks
|
||||||
# we use np.isclose instead of abs(<the final amount>) <= 1e-5 because `np.isclose` consider both ralative amount and absolute amount
|
# we use np.isclose instead of abs(<the final amount>) <= 1e-5 because `np.isclose` consider both
|
||||||
# Using abs(<the final amount>) <= 1e-5 will result in error when the amount is large
|
# relative amount and absolute amount
|
||||||
|
# Using abs(<the final amount>) <= 1e-5 will result in error when the amount is large
|
||||||
self._del_stock(stock_id)
|
self._del_stock(stock_id)
|
||||||
else:
|
else:
|
||||||
# decrease the amount of stock
|
# decrease the amount of stock
|
||||||
@@ -361,8 +366,10 @@ class Position(BasePosition):
|
|||||||
if self.position[stock_id]["amount"] < -1e-5:
|
if self.position[stock_id]["amount"] < -1e-5:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"only have {} {}, require {}".format(
|
"only have {} {}, require {}".format(
|
||||||
self.position[stock_id]["amount"] + trade_amount, stock_id, trade_amount
|
self.position[stock_id]["amount"] + trade_amount,
|
||||||
)
|
stock_id,
|
||||||
|
trade_amount,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
new_cash = trade_val - cost
|
new_cash = trade_val - cost
|
||||||
@@ -373,13 +380,13 @@ class Position(BasePosition):
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
def _del_stock(self, stock_id):
|
def _del_stock(self, stock_id: str) -> None:
|
||||||
del self.position[stock_id]
|
del self.position[stock_id]
|
||||||
|
|
||||||
def check_stock(self, stock_id):
|
def check_stock(self, stock_id: str) -> bool:
|
||||||
return stock_id in self.position
|
return stock_id in self.position
|
||||||
|
|
||||||
def update_order(self, order, trade_val, cost, trade_price):
|
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
# handle order, order is a order class, defined in exchange.py
|
# handle order, order is a order class, defined in exchange.py
|
||||||
if order.direction == Order.BUY:
|
if order.direction == Order.BUY:
|
||||||
# BUY
|
# BUY
|
||||||
@@ -390,54 +397,54 @@ class Position(BasePosition):
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError("do not support order direction {}".format(order.direction))
|
raise NotImplementedError("do not support order direction {}".format(order.direction))
|
||||||
|
|
||||||
def update_stock_price(self, stock_id, price):
|
def update_stock_price(self, stock_id: str, price: float) -> None:
|
||||||
self.position[stock_id]["price"] = price
|
self.position[stock_id]["price"] = price
|
||||||
|
|
||||||
def update_stock_count(self, stock_id, bar, count):
|
def update_stock_count(self, stock_id: str, bar: str, count: float) -> None: # TODO: check type of `bar`
|
||||||
self.position[stock_id][f"count_{bar}"] = count
|
self.position[stock_id][f"count_{bar}"] = count
|
||||||
|
|
||||||
def update_stock_weight(self, stock_id, weight):
|
def update_stock_weight(self, stock_id: str, weight: float) -> None:
|
||||||
self.position[stock_id]["weight"] = weight
|
self.position[stock_id]["weight"] = weight
|
||||||
|
|
||||||
def calculate_stock_value(self):
|
def calculate_stock_value(self) -> float:
|
||||||
stock_list = self.get_stock_list()
|
stock_list = self.get_stock_list()
|
||||||
value = 0
|
value = 0
|
||||||
for stock_id in stock_list:
|
for stock_id in stock_list:
|
||||||
value += self.position[stock_id]["amount"] * self.position[stock_id]["price"]
|
value += self.position[stock_id]["amount"] * self.position[stock_id]["price"]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def calculate_value(self):
|
def calculate_value(self) -> float:
|
||||||
value = self.calculate_stock_value()
|
value = self.calculate_stock_value()
|
||||||
value += self.position["cash"] + self.position.get("cash_delay", 0.0)
|
value += self.position["cash"] + self.position.get("cash_delay", 0.0)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get_stock_list(self):
|
def get_stock_list(self) -> List[str]:
|
||||||
stock_list = list(set(self.position.keys()) - {"cash", "now_account_value", "cash_delay"})
|
stock_list = list(set(self.position.keys()) - {"cash", "now_account_value", "cash_delay"})
|
||||||
return stock_list
|
return stock_list
|
||||||
|
|
||||||
def get_stock_price(self, code):
|
def get_stock_price(self, code: str) -> float:
|
||||||
return self.position[code]["price"]
|
return self.position[code]["price"]
|
||||||
|
|
||||||
def get_stock_amount(self, code):
|
def get_stock_amount(self, code: str) -> float:
|
||||||
return self.position[code]["amount"] if code in self.position else 0
|
return self.position[code]["amount"] if code in self.position else 0
|
||||||
|
|
||||||
def get_stock_count(self, code, bar):
|
def get_stock_count(self, code: str, bar: str) -> float:
|
||||||
"""the days the account has been hold, it may be used in some special strategies"""
|
"""the days the account has been hold, it may be used in some special strategies"""
|
||||||
if f"count_{bar}" in self.position[code]:
|
if f"count_{bar}" in self.position[code]:
|
||||||
return self.position[code][f"count_{bar}"]
|
return self.position[code][f"count_{bar}"]
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_stock_weight(self, code):
|
def get_stock_weight(self, code: str) -> float:
|
||||||
return self.position[code]["weight"]
|
return self.position[code]["weight"]
|
||||||
|
|
||||||
def get_cash(self, include_settle=False):
|
def get_cash(self, include_settle: bool = False) -> float:
|
||||||
cash = self.position["cash"]
|
cash = self.position["cash"]
|
||||||
if include_settle:
|
if include_settle:
|
||||||
cash += self.position.get("cash_delay", 0.0)
|
cash += self.position.get("cash_delay", 0.0)
|
||||||
return cash
|
return cash
|
||||||
|
|
||||||
def get_stock_amount_dict(self):
|
def get_stock_amount_dict(self) -> dict:
|
||||||
"""generate stock amount dict {stock_id : amount of stock}"""
|
"""generate stock amount dict {stock_id : amount of stock}"""
|
||||||
d = {}
|
d = {}
|
||||||
stock_list = self.get_stock_list()
|
stock_list = self.get_stock_list()
|
||||||
@@ -445,7 +452,7 @@ class Position(BasePosition):
|
|||||||
d[stock_code] = self.get_stock_amount(code=stock_code)
|
d[stock_code] = self.get_stock_amount(code=stock_code)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_stock_weight_dict(self, only_stock=False):
|
def get_stock_weight_dict(self, only_stock: bool = False) -> dict:
|
||||||
"""get_stock_weight_dict
|
"""get_stock_weight_dict
|
||||||
generate stock weight dict {stock_id : value weight of stock in the position}
|
generate stock weight dict {stock_id : value weight of stock in the position}
|
||||||
it is meaningful in the beginning or the end of each trade date
|
it is meaningful in the beginning or the end of each trade date
|
||||||
@@ -463,7 +470,7 @@ class Position(BasePosition):
|
|||||||
d[stock_code] = self.position[stock_code]["amount"] * self.position[stock_code]["price"] / position_value
|
d[stock_code] = self.position[stock_code]["amount"] * self.position[stock_code]["price"] / position_value
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def add_count_all(self, bar):
|
def add_count_all(self, bar: str) -> None:
|
||||||
stock_list = self.get_stock_list()
|
stock_list = self.get_stock_list()
|
||||||
for code in stock_list:
|
for code in stock_list:
|
||||||
if f"count_{bar}" in self.position[code]:
|
if f"count_{bar}" in self.position[code]:
|
||||||
@@ -471,18 +478,18 @@ class Position(BasePosition):
|
|||||||
else:
|
else:
|
||||||
self.position[code][f"count_{bar}"] = 1
|
self.position[code][f"count_{bar}"] = 1
|
||||||
|
|
||||||
def update_weight_all(self):
|
def update_weight_all(self) -> None:
|
||||||
weight_dict = self.get_stock_weight_dict()
|
weight_dict = self.get_stock_weight_dict()
|
||||||
for stock_code, weight in weight_dict.items():
|
for stock_code, weight in weight_dict.items():
|
||||||
self.update_stock_weight(stock_code, weight)
|
self.update_stock_weight(stock_code, weight)
|
||||||
|
|
||||||
def settle_start(self, settle_type):
|
def settle_start(self, settle_type: str) -> None:
|
||||||
assert self._settle_type == self.ST_NO, "Currently, settlement can't be nested!!!!!"
|
assert self._settle_type == self.ST_NO, "Currently, settlement can't be nested!!!!!"
|
||||||
self._settle_type = settle_type
|
self._settle_type = settle_type
|
||||||
if settle_type == self.ST_CASH:
|
if settle_type == self.ST_CASH:
|
||||||
self.position["cash_delay"] = 0.0
|
self.position["cash_delay"] = 0.0
|
||||||
|
|
||||||
def settle_commit(self):
|
def settle_commit(self) -> None:
|
||||||
if self._settle_type != self.ST_NO:
|
if self._settle_type != self.ST_NO:
|
||||||
if self._settle_type == self.ST_CASH:
|
if self._settle_type == self.ST_CASH:
|
||||||
self.position["cash"] += self.position["cash_delay"]
|
self.position["cash"] += self.position["cash_delay"]
|
||||||
@@ -507,10 +514,10 @@ class InfPosition(BasePosition):
|
|||||||
# InfPosition always have any stocks
|
# InfPosition always have any stocks
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float):
|
def update_order(self, order: Order, trade_val: float, cost: float, trade_price: float) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_stock_price(self, stock_id, price: float):
|
def update_stock_price(self, stock_id: str, price: float) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def calculate_stock_value(self) -> float:
|
def calculate_stock_value(self) -> float:
|
||||||
@@ -522,17 +529,20 @@ class InfPosition(BasePosition):
|
|||||||
"""
|
"""
|
||||||
return np.inf
|
return np.inf
|
||||||
|
|
||||||
def get_stock_list(self) -> List:
|
def calculate_value(self) -> float:
|
||||||
|
raise NotImplementedError(f"InfPosition doesn't support calculating value")
|
||||||
|
|
||||||
|
def get_stock_list(self) -> list:
|
||||||
raise NotImplementedError(f"InfPosition doesn't support stock list position")
|
raise NotImplementedError(f"InfPosition doesn't support stock list position")
|
||||||
|
|
||||||
def get_stock_price(self, code) -> float:
|
def get_stock_price(self, code: str) -> float:
|
||||||
"""the price of the inf position is meaningless"""
|
"""the price of the inf position is meaningless"""
|
||||||
return np.nan
|
return np.nan
|
||||||
|
|
||||||
def get_stock_amount(self, code) -> float:
|
def get_stock_amount(self, code: str) -> float:
|
||||||
return np.inf
|
return np.inf
|
||||||
|
|
||||||
def get_cash(self, include_settle=False) -> float:
|
def get_cash(self, include_settle: bool = False) -> float:
|
||||||
return np.inf
|
return np.inf
|
||||||
|
|
||||||
def get_stock_amount_dict(self) -> Dict:
|
def get_stock_amount_dict(self) -> Dict:
|
||||||
@@ -541,14 +551,14 @@ class InfPosition(BasePosition):
|
|||||||
def get_stock_weight_dict(self, only_stock: bool = False) -> Dict:
|
def get_stock_weight_dict(self, only_stock: bool = False) -> Dict:
|
||||||
raise NotImplementedError(f"InfPosition doesn't support get_stock_weight_dict")
|
raise NotImplementedError(f"InfPosition doesn't support get_stock_weight_dict")
|
||||||
|
|
||||||
def add_count_all(self, bar):
|
def add_count_all(self, bar: str) -> None:
|
||||||
raise NotImplementedError(f"InfPosition doesn't support add_count_all")
|
raise NotImplementedError(f"InfPosition doesn't support add_count_all")
|
||||||
|
|
||||||
def update_weight_all(self):
|
def update_weight_all(self) -> None:
|
||||||
raise NotImplementedError(f"InfPosition doesn't support update_weight_all")
|
raise NotImplementedError(f"InfPosition doesn't support update_weight_all")
|
||||||
|
|
||||||
def settle_start(self, settle_type: str):
|
def settle_start(self, settle_type: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def settle_commit(self):
|
def settle_commit(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
This module is not well maintained.
|
This module is not well maintained.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from .position import Position
|
|
||||||
from ..data import D
|
|
||||||
from ..config import C
|
|
||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from ..config import C
|
||||||
|
from ..data import D
|
||||||
|
from .position import Position
|
||||||
|
|
||||||
|
|
||||||
def get_benchmark_weight(
|
def get_benchmark_weight(
|
||||||
bench,
|
bench,
|
||||||
@@ -214,7 +216,9 @@ def get_stock_group(stock_group_field_df, bench_stock_weight_df, group_method, g
|
|||||||
for idx, row in (~bench_stock_weight_df.isna()).iterrows():
|
for idx, row in (~bench_stock_weight_df.isna()).iterrows():
|
||||||
bench_values = stock_group_field_df.loc[idx, row[row].index]
|
bench_values = stock_group_field_df.loc[idx, row[row].index]
|
||||||
new_stock_group_df.loc[idx] = get_daily_bin_group(
|
new_stock_group_df.loc[idx] = get_daily_bin_group(
|
||||||
bench_values, stock_group_field_df.loc[idx], group_n=group_n
|
bench_values,
|
||||||
|
stock_group_field_df.loc[idx],
|
||||||
|
group_n=group_n,
|
||||||
)
|
)
|
||||||
return new_stock_group_df
|
return new_stock_group_df
|
||||||
|
|
||||||
@@ -315,7 +319,7 @@ def brinson_pa(
|
|||||||
# The excess profit from the interaction of assets allocation and stocks selection
|
# The excess profit from the interaction of assets allocation and stocks selection
|
||||||
"RIN": Q4 - Q3 - Q2 + Q1,
|
"RIN": Q4 - Q3 - Q2 + Q1,
|
||||||
"RTotal": Q4 - Q1, # The totoal excess profit
|
"RTotal": Q4 - Q1, # The totoal excess profit
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
"port_group_ret": port_group_ret_df,
|
"port_group_ret": port_group_ret_df,
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import Dict, List, Tuple, Union
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from qlib.backtest.exchange import Exchange
|
import qlib.utils.index_data as idd
|
||||||
from qlib.backtest.decision import BaseTradeDecision, Order, OrderDir
|
from qlib.backtest.decision import BaseTradeDecision, Order, OrderDir
|
||||||
from .high_performance_ds import BaseOrderIndicator, NumpyOrderIndicator, SingleMetric
|
from qlib.backtest.exchange import Exchange
|
||||||
|
|
||||||
from ..tests.config import CSI300_BENCH
|
from ..tests.config import CSI300_BENCH
|
||||||
from ..utils.resam import get_higher_eq_freq_feature, resam_ts_data
|
from ..utils.resam import get_higher_eq_freq_feature, resam_ts_data
|
||||||
import qlib.utils.index_data as idd
|
from .high_performance_ds import BaseOrderIndicator, NumpyOrderIndicator, SingleMetric
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMetrics:
|
class PortfolioMetrics:
|
||||||
@@ -161,7 +162,8 @@ class PortfolioMetrics:
|
|||||||
stock_value,
|
stock_value,
|
||||||
]:
|
]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"None in [trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, total_cost, cost_rate, stock_value]"
|
"None in [trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, "
|
||||||
|
"total_cost, cost_rate, stock_value]",
|
||||||
)
|
)
|
||||||
|
|
||||||
if trade_end_time is None and bench_value is None:
|
if trade_end_time is None and bench_value is None:
|
||||||
@@ -335,7 +337,10 @@ class Indicator:
|
|||||||
# sum inner order indicators with same metric.
|
# sum inner order indicators with same metric.
|
||||||
all_metric = ["inner_amount", "deal_amount", "trade_price", "trade_value", "trade_cost", "trade_dir"]
|
all_metric = ["inner_amount", "deal_amount", "trade_price", "trade_value", "trade_cost", "trade_dir"]
|
||||||
self.order_indicator_cls.sum_all_indicators(
|
self.order_indicator_cls.sum_all_indicators(
|
||||||
self.order_indicator, inner_order_indicators, all_metric, fill_value=0
|
self.order_indicator,
|
||||||
|
inner_order_indicators,
|
||||||
|
all_metric,
|
||||||
|
fill_value=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def func(trade_price, deal_amount):
|
def func(trade_price, deal_amount):
|
||||||
@@ -378,12 +383,17 @@ class Indicator:
|
|||||||
|
|
||||||
if decision.trade_range is not None:
|
if decision.trade_range is not None:
|
||||||
trade_start_time, trade_end_time = decision.trade_range.clip_time_range(
|
trade_start_time, trade_end_time = decision.trade_range.clip_time_range(
|
||||||
start_time=trade_start_time, end_time=trade_end_time
|
start_time=trade_start_time,
|
||||||
|
end_time=trade_end_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
if price == "deal_price":
|
if price == "deal_price":
|
||||||
price_s = trade_exchange.get_deal_price(
|
price_s = trade_exchange.get_deal_price(
|
||||||
inst, trade_start_time, trade_end_time, direction=direction, method=None
|
inst,
|
||||||
|
trade_start_time,
|
||||||
|
trade_end_time,
|
||||||
|
direction=direction,
|
||||||
|
method=None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"This type of input is not supported")
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
@@ -599,8 +609,12 @@ class Indicator:
|
|||||||
if show_indicator:
|
if show_indicator:
|
||||||
print(
|
print(
|
||||||
"[Indicator({}) {:%Y-%m-%d %H:%M:%S}]: FFR: {}, PA: {}, POS: {}".format(
|
"[Indicator({}) {:%Y-%m-%d %H:%M:%S}]: FFR: {}, PA: {}, POS: {}".format(
|
||||||
freq, trade_start_time, fulfill_rate, price_advantage, positive_rate
|
freq,
|
||||||
)
|
trade_start_time,
|
||||||
|
fulfill_rate,
|
||||||
|
price_advantage,
|
||||||
|
positive_rate,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_order_indicator(self, raw: bool = True):
|
def get_order_indicator(self, raw: bool = True):
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# Copyright (c) Microsoft Corporation.
|
# Copyright (c) Microsoft Corporation.
|
||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
from qlib.utils import init_instance_by_config
|
import abc
|
||||||
from typing import Dict, List, Text, Tuple, Union
|
from typing import Dict, List, Text, Tuple, Union
|
||||||
from ..model.base import BaseModel
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from qlib.utils import init_instance_by_config
|
||||||
|
|
||||||
from ..data.dataset import Dataset
|
from ..data.dataset import Dataset
|
||||||
from ..data.dataset.utils import convert_index_format
|
from ..data.dataset.utils import convert_index_format
|
||||||
|
from ..model.base import BaseModel
|
||||||
from ..utils.resam import resam_ts_data
|
from ..utils.resam import resam_ts_data
|
||||||
import pandas as pd
|
|
||||||
import abc
|
|
||||||
|
|
||||||
|
|
||||||
class Signal(metaclass=abc.ABCMeta):
|
class Signal(metaclass=abc.ABCMeta):
|
||||||
@@ -82,7 +85,7 @@ class ModelSignal(SignalWCache):
|
|||||||
|
|
||||||
|
|
||||||
def create_signal_from(
|
def create_signal_from(
|
||||||
obj: Union[Signal, Tuple[BaseModel, Dataset], List, Dict, Text, pd.Series, pd.DataFrame]
|
obj: Union[Signal, Tuple[BaseModel, Dataset], List, Dict, Text, pd.Series, pd.DataFrame],
|
||||||
) -> Signal:
|
) -> Signal:
|
||||||
"""
|
"""
|
||||||
create signal from diverse information
|
create signal from diverse information
|
||||||
|
|||||||
@@ -2,16 +2,22 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import bisect
|
import bisect
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Any, Set, Tuple, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from qlib.utils.time import epsilon_change
|
from qlib.utils.time import epsilon_change
|
||||||
from typing import TYPE_CHECKING, Tuple, Union
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qlib.backtest.decision import BaseTradeDecision
|
from qlib.backtest.decision import BaseTradeDecision
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from ..data.data import Cal
|
from ..data.data import Cal
|
||||||
|
|
||||||
|
|
||||||
@@ -26,8 +32,8 @@ class TradeCalendarManager:
|
|||||||
freq: str,
|
freq: str,
|
||||||
start_time: Union[str, pd.Timestamp] = None,
|
start_time: Union[str, pd.Timestamp] = None,
|
||||||
end_time: Union[str, pd.Timestamp] = None,
|
end_time: Union[str, pd.Timestamp] = None,
|
||||||
level_infra: "LevelInfrastructure" = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -43,19 +49,26 @@ class TradeCalendarManager:
|
|||||||
self.level_infra = level_infra
|
self.level_infra = level_infra
|
||||||
self.reset(freq=freq, start_time=start_time, end_time=end_time)
|
self.reset(freq=freq, start_time=start_time, end_time=end_time)
|
||||||
|
|
||||||
def reset(self, freq, start_time, end_time):
|
def reset(
|
||||||
|
self,
|
||||||
|
freq: str,
|
||||||
|
start_time: Union[str, pd.Timestamp] = None,
|
||||||
|
end_time: Union[str, pd.Timestamp] = None,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Please refer to the docs of `__init__`
|
Please refer to the docs of `__init__`
|
||||||
|
|
||||||
Reset the trade calendar
|
Reset the trade calendar
|
||||||
- self.trade_len : The total count for trading step
|
- self.trade_len : The total count for trading step
|
||||||
- self.trade_step : The number of trading step finished, self.trade_step can be [0, 1, 2, ..., self.trade_len - 1]
|
- self.trade_step : The number of trading step finished, self.trade_step can be
|
||||||
|
[0, 1, 2, ..., self.trade_len - 1]
|
||||||
"""
|
"""
|
||||||
self.freq = freq
|
self.freq = freq
|
||||||
self.start_time = pd.Timestamp(start_time) if start_time else None
|
self.start_time = pd.Timestamp(start_time) if start_time else None
|
||||||
self.end_time = pd.Timestamp(end_time) if end_time else None
|
self.end_time = pd.Timestamp(end_time) if end_time else None
|
||||||
|
|
||||||
_calendar = Cal.calendar(freq=freq, future=True)
|
_calendar = Cal.calendar(freq=freq, future=True)
|
||||||
|
assert isinstance(_calendar, np.ndarray)
|
||||||
self._calendar = _calendar
|
self._calendar = _calendar
|
||||||
_, _, _start_index, _end_index = Cal.locate_index(start_time, end_time, freq=freq, future=True)
|
_, _, _start_index, _end_index = Cal.locate_index(start_time, end_time, freq=freq, future=True)
|
||||||
self.start_index = _start_index
|
self.start_index = _start_index
|
||||||
@@ -63,7 +76,7 @@ class TradeCalendarManager:
|
|||||||
self.trade_len = _end_index - _start_index + 1
|
self.trade_len = _end_index - _start_index + 1
|
||||||
self.trade_step = 0
|
self.trade_step = 0
|
||||||
|
|
||||||
def finished(self):
|
def finished(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the trading finished
|
Check if the trading finished
|
||||||
- Should check before calling strategy.generate_decisions and executor.execute
|
- Should check before calling strategy.generate_decisions and executor.execute
|
||||||
@@ -72,29 +85,32 @@ class TradeCalendarManager:
|
|||||||
"""
|
"""
|
||||||
return self.trade_step >= self.trade_len
|
return self.trade_step >= self.trade_len
|
||||||
|
|
||||||
def step(self):
|
def step(self) -> None:
|
||||||
if self.finished():
|
if self.finished():
|
||||||
raise RuntimeError(f"The calendar is finished, please reset it if you want to call it!")
|
raise RuntimeError(f"The calendar is finished, please reset it if you want to call it!")
|
||||||
self.trade_step = self.trade_step + 1
|
self.trade_step += 1
|
||||||
|
|
||||||
def get_freq(self):
|
def get_freq(self) -> str:
|
||||||
return self.freq
|
return self.freq
|
||||||
|
|
||||||
def get_trade_len(self):
|
def get_trade_len(self) -> int:
|
||||||
"""get the total step length"""
|
"""get the total step length"""
|
||||||
return self.trade_len
|
return self.trade_len
|
||||||
|
|
||||||
def get_trade_step(self):
|
def get_trade_step(self) -> int:
|
||||||
return self.trade_step
|
return self.trade_step
|
||||||
|
|
||||||
def get_step_time(self, trade_step=None, shift=0):
|
def get_step_time(self, trade_step: int = None, shift: int = 0) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
"""
|
"""
|
||||||
Get the left and right endpoints of the trade_step'th trading interval
|
Get the left and right endpoints of the trade_step'th trading interval
|
||||||
|
|
||||||
About the endpoints:
|
About the endpoints:
|
||||||
- Qlib uses the closed interval in time-series data selection, which has the same performance as pandas.Series.loc
|
- Qlib uses the closed interval in time-series data selection, which has the same performance as
|
||||||
# - The returned right endpoints should minus 1 seconds because of the closed interval representation in Qlib.
|
pandas.Series.loc
|
||||||
# Note: Qlib supports up to minutely decision execution, so 1 seconds is less than any trading time interval.
|
# - The returned right endpoints should minus 1 seconds because of the closed interval representation in
|
||||||
|
# Qlib.
|
||||||
|
# Note: Qlib supports up to minutely decision execution, so 1 seconds is less than any trading time
|
||||||
|
# interval.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -105,15 +121,14 @@ class TradeCalendarManager:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Tuple[pd.Timestamp, pd.Timestap]
|
Tuple[pd.Timestamp, pd.Timestamp]
|
||||||
- If shift == 0, return the trading time range
|
- If shift == 0, return the trading time range
|
||||||
- If shift > 0, return the trading time range of the earlier shift bars
|
- If shift > 0, return the trading time range of the earlier shift bars
|
||||||
- If shift < 0, return the trading time range of the later shift bar
|
- If shift < 0, return the trading time range of the later shift bar
|
||||||
"""
|
"""
|
||||||
if trade_step is None:
|
if trade_step is None:
|
||||||
trade_step = self.get_trade_step()
|
trade_step = self.get_trade_step()
|
||||||
trade_step = trade_step - shift
|
calendar_index = self.start_index + trade_step - shift
|
||||||
calendar_index = self.start_index + trade_step
|
|
||||||
return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1])
|
return self._calendar[calendar_index], epsilon_change(self._calendar[calendar_index + 1])
|
||||||
|
|
||||||
def get_data_cal_range(self, rtype: str = "full") -> Tuple[int, int]:
|
def get_data_cal_range(self, rtype: str = "full") -> Tuple[int, int]:
|
||||||
@@ -126,7 +141,7 @@ class TradeCalendarManager:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
rtype: str
|
rtype: str
|
||||||
- "full": return the full limitation of the deicsion in the day
|
- "full": return the full limitation of the decision in the day
|
||||||
- "step": return the limitation of current step
|
- "step": return the limitation of current step
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -148,7 +163,7 @@ class TradeCalendarManager:
|
|||||||
|
|
||||||
return start_idx - day_start_idx, end_index - day_start_idx
|
return start_idx - day_start_idx, end_index - day_start_idx
|
||||||
|
|
||||||
def get_all_time(self):
|
def get_all_time(self) -> Tuple[pd.Timestamp, pd.Timestamp]:
|
||||||
"""Get the start_time and end_time for trading"""
|
"""Get the start_time and end_time for trading"""
|
||||||
return self.start_time, self.end_time
|
return self.start_time, self.end_time
|
||||||
|
|
||||||
@@ -167,30 +182,33 @@ class TradeCalendarManager:
|
|||||||
Tuple[int, int]:
|
Tuple[int, int]:
|
||||||
the index of the range. **the left and right are closed**
|
the index of the range. **the left and right are closed**
|
||||||
"""
|
"""
|
||||||
left, right = (
|
left = bisect.bisect_right(self._calendar, start_time) - 1
|
||||||
bisect.bisect_right(self._calendar, start_time) - 1,
|
right = bisect.bisect_right(self._calendar, end_time) - 1
|
||||||
bisect.bisect_right(self._calendar, end_time) - 1,
|
|
||||||
)
|
|
||||||
left -= self.start_index
|
left -= self.start_index
|
||||||
right -= self.start_index
|
right -= self.start_index
|
||||||
|
|
||||||
def clip(idx):
|
def clip(idx: int) -> int:
|
||||||
return min(max(0, idx), self.trade_len - 1)
|
return min(max(0, idx), self.trade_len - 1)
|
||||||
|
|
||||||
return clip(left), clip(right)
|
return clip(left), clip(right)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"class: {self.__class__.__name__}; {self.start_time}[{self.start_index}]~{self.end_time}[{self.end_index}]: [{self.trade_step}/{self.trade_len}]"
|
return (
|
||||||
|
f"class: {self.__class__.__name__}; "
|
||||||
|
f"{self.start_time}[{self.start_index}]~{self.end_time}[{self.end_index}]: "
|
||||||
|
f"[{self.trade_step}/{self.trade_len}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseInfrastructure:
|
class BaseInfrastructure:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs) -> None:
|
||||||
self.reset_infra(**kwargs)
|
self.reset_infra(**kwargs)
|
||||||
|
|
||||||
def get_support_infra(self):
|
@abstractmethod
|
||||||
|
def get_support_infra(self) -> Set[str]:
|
||||||
raise NotImplementedError("`get_support_infra` is not implemented!")
|
raise NotImplementedError("`get_support_infra` is not implemented!")
|
||||||
|
|
||||||
def reset_infra(self, **kwargs):
|
def reset_infra(self, **kwargs) -> None:
|
||||||
support_infra = self.get_support_infra()
|
support_infra = self.get_support_infra()
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
if k in support_infra:
|
if k in support_infra:
|
||||||
@@ -198,53 +216,58 @@ class BaseInfrastructure:
|
|||||||
else:
|
else:
|
||||||
warnings.warn(f"{k} is ignored in `reset_infra`!")
|
warnings.warn(f"{k} is ignored in `reset_infra`!")
|
||||||
|
|
||||||
def get(self, infra_name):
|
def get(self, infra_name: str) -> Any:
|
||||||
if hasattr(self, infra_name):
|
if hasattr(self, infra_name):
|
||||||
return getattr(self, infra_name)
|
return getattr(self, infra_name)
|
||||||
else:
|
else:
|
||||||
warnings.warn(f"infra {infra_name} is not found!")
|
warnings.warn(f"infra {infra_name} is not found!")
|
||||||
|
|
||||||
def has(self, infra_name):
|
def has(self, infra_name: str) -> bool:
|
||||||
return infra_name in self.get_support_infra() and hasattr(self, infra_name)
|
return infra_name in self.get_support_infra() and hasattr(self, infra_name)
|
||||||
|
|
||||||
def update(self, other):
|
def update(self, other: BaseInfrastructure) -> None:
|
||||||
support_infra = other.get_support_infra()
|
support_infra = other.get_support_infra()
|
||||||
infra_dict = {_infra: getattr(other, _infra) for _infra in support_infra if hasattr(other, _infra)}
|
infra_dict = {_infra: getattr(other, _infra) for _infra in support_infra if hasattr(other, _infra)}
|
||||||
self.reset_infra(**infra_dict)
|
self.reset_infra(**infra_dict)
|
||||||
|
|
||||||
|
|
||||||
class CommonInfrastructure(BaseInfrastructure):
|
class CommonInfrastructure(BaseInfrastructure):
|
||||||
def get_support_infra(self):
|
def get_support_infra(self) -> Set[str]:
|
||||||
return ["trade_account", "trade_exchange"]
|
return {"trade_account", "trade_exchange"}
|
||||||
|
|
||||||
|
|
||||||
class LevelInfrastructure(BaseInfrastructure):
|
class LevelInfrastructure(BaseInfrastructure):
|
||||||
"""level infrastructure is created by executor, and then shared to strategies on the same level"""
|
"""level infrastructure is created by executor, and then shared to strategies on the same level"""
|
||||||
|
|
||||||
def get_support_infra(self):
|
def get_support_infra(self) -> Set[str]:
|
||||||
"""
|
"""
|
||||||
Descriptions about the infrastructure
|
Descriptions about the infrastructure
|
||||||
|
|
||||||
sub_level_infra:
|
sub_level_infra:
|
||||||
- **NOTE**: this will only work after _init_sub_trading !!!
|
- **NOTE**: this will only work after _init_sub_trading !!!
|
||||||
"""
|
"""
|
||||||
return ["trade_calendar", "sub_level_infra", "common_infra"]
|
return {"trade_calendar", "sub_level_infra", "common_infra"}
|
||||||
|
|
||||||
def reset_cal(self, freq, start_time, end_time):
|
def reset_cal(
|
||||||
|
self,
|
||||||
|
freq: str,
|
||||||
|
start_time: Union[str, pd.Timestamp, None],
|
||||||
|
end_time: Union[str, pd.Timestamp, None],
|
||||||
|
) -> None:
|
||||||
"""reset trade calendar manager"""
|
"""reset trade calendar manager"""
|
||||||
if self.has("trade_calendar"):
|
if self.has("trade_calendar"):
|
||||||
self.get("trade_calendar").reset(freq, start_time=start_time, end_time=end_time)
|
self.get("trade_calendar").reset(freq, start_time=start_time, end_time=end_time)
|
||||||
else:
|
else:
|
||||||
self.reset_infra(
|
self.reset_infra(
|
||||||
trade_calendar=TradeCalendarManager(freq, start_time=start_time, end_time=end_time, level_infra=self)
|
trade_calendar=TradeCalendarManager(freq, start_time=start_time, end_time=end_time, level_infra=self),
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_sub_level_infra(self, sub_level_infra: LevelInfrastructure):
|
def set_sub_level_infra(self, sub_level_infra: LevelInfrastructure) -> None:
|
||||||
"""this will make the calendar access easier when acrossing multi-levels"""
|
"""this will make the calendar access easier when crossing multi-levels"""
|
||||||
self.reset_infra(sub_level_infra=sub_level_infra)
|
self.reset_infra(sub_level_infra=sub_level_infra)
|
||||||
|
|
||||||
|
|
||||||
def get_start_end_idx(trade_calendar: TradeCalendarManager, outer_trade_decision: BaseTradeDecision) -> Union[int, int]:
|
def get_start_end_idx(trade_calendar: TradeCalendarManager, outer_trade_decision: BaseTradeDecision) -> Tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
A helper function for getting the decision-level index range limitation for inner strategy
|
A helper function for getting the decision-level index range limitation for inner strategy
|
||||||
- NOTE: this function is not applicable to order-level
|
- NOTE: this function is not applicable to order-level
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
# Copyright (c) Microsoft Corporation.
|
# Copyright (c) Microsoft Corporation.
|
||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qlib.backtest.exchange import Exchange
|
from qlib.backtest.exchange import Exchange
|
||||||
from qlib.backtest.position import BasePosition
|
from qlib.backtest.position import BasePosition
|
||||||
|
|
||||||
from typing import Tuple, Union
|
from typing import Tuple, Union
|
||||||
|
|
||||||
|
from ..backtest.decision import BaseTradeDecision
|
||||||
|
from ..backtest.utils import CommonInfrastructure, LevelInfrastructure, TradeCalendarManager
|
||||||
from ..rl.interpreter import ActionInterpreter, StateInterpreter
|
from ..rl.interpreter import ActionInterpreter, StateInterpreter
|
||||||
from ..utils import init_instance_by_config
|
from ..utils import init_instance_by_config
|
||||||
from ..backtest.utils import CommonInfrastructure, LevelInfrastructure, TradeCalendarManager
|
|
||||||
from ..backtest.decision import BaseTradeDecision
|
|
||||||
|
|
||||||
__all__ = ["BaseStrategy", "RLStrategy", "RLIntStrategy"]
|
__all__ = ["BaseStrategy", "RLStrategy", "RLIntStrategy"]
|
||||||
|
|
||||||
@@ -25,12 +28,13 @@ class BaseStrategy:
|
|||||||
level_infra: LevelInfrastructure = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
trade_exchange: Exchange = None,
|
trade_exchange: Exchange = None,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
outer_trade_decision : BaseTradeDecision, optional
|
outer_trade_decision : BaseTradeDecision, optional
|
||||||
the trade decision of outer strategy which this strategy relies, and it will be traded in [start_time, end_time], by default None
|
the trade decision of outer strategy which this strategy relies, and it will be traded in
|
||||||
|
[start_time, end_time], by default None
|
||||||
- If the strategy is used to split trade decision, it will be used
|
- If the strategy is used to split trade decision, it will be used
|
||||||
- If the strategy is used for portfolio management, it can be ignored
|
- If the strategy is used for portfolio management, it can be ignored
|
||||||
level_infra : LevelInfrastructure, optional
|
level_infra : LevelInfrastructure, optional
|
||||||
@@ -41,9 +45,10 @@ class BaseStrategy:
|
|||||||
trade_exchange : Exchange
|
trade_exchange : Exchange
|
||||||
exchange that provides market info, used to deal order and generate report
|
exchange that provides market info, used to deal order and generate report
|
||||||
- If `trade_exchange` is None, self.trade_exchange will be set with common_infra
|
- If `trade_exchange` is None, self.trade_exchange will be set with common_infra
|
||||||
- It allowes different trade_exchanges is used in different executions.
|
- It allows different trade_exchanges is used in different executions.
|
||||||
- For example:
|
- For example:
|
||||||
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is recommended because it run faster.
|
- In daily execution, both daily exchange and minutely are usable, but the daily exchange is
|
||||||
|
recommended because it run faster.
|
||||||
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
- In minutely execution, the daily exchange is not usable, only the minutely exchange is recommended.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -63,13 +68,13 @@ class BaseStrategy:
|
|||||||
"""get trade exchange in a prioritized order"""
|
"""get trade exchange in a prioritized order"""
|
||||||
return getattr(self, "_trade_exchange", None) or self.common_infra.get("trade_exchange")
|
return getattr(self, "_trade_exchange", None) or self.common_infra.get("trade_exchange")
|
||||||
|
|
||||||
def reset_level_infra(self, level_infra: LevelInfrastructure):
|
def reset_level_infra(self, level_infra: LevelInfrastructure) -> None:
|
||||||
if not hasattr(self, "level_infra"):
|
if not hasattr(self, "level_infra"):
|
||||||
self.level_infra = level_infra
|
self.level_infra = level_infra
|
||||||
else:
|
else:
|
||||||
self.level_infra.update(level_infra)
|
self.level_infra.update(level_infra)
|
||||||
|
|
||||||
def reset_common_infra(self, common_infra: CommonInfrastructure):
|
def reset_common_infra(self, common_infra: CommonInfrastructure) -> None:
|
||||||
if not hasattr(self, "common_infra"):
|
if not hasattr(self, "common_infra"):
|
||||||
self.common_infra: CommonInfrastructure = common_infra
|
self.common_infra: CommonInfrastructure = common_infra
|
||||||
else:
|
else:
|
||||||
@@ -79,9 +84,9 @@ class BaseStrategy:
|
|||||||
self,
|
self,
|
||||||
level_infra: LevelInfrastructure = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
outer_trade_decision=None,
|
outer_trade_decision: BaseTradeDecision = None,
|
||||||
**kwargs,
|
**kwargs, # TODO: remove this?
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
- reset `level_infra`, used to reset trade calendar, .etc
|
- reset `level_infra`, used to reset trade calendar, .etc
|
||||||
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
|
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
|
||||||
@@ -89,18 +94,20 @@ class BaseStrategy:
|
|||||||
|
|
||||||
**NOTE**:
|
**NOTE**:
|
||||||
split this function into `reset` and `_reset` will make following cases more convenient
|
split this function into `reset` and `_reset` will make following cases more convenient
|
||||||
1. Users want to initialize his strategy by overriding `reset`, but they don't want to affect the `_reset` called
|
1. Users want to initialize his strategy by overriding `reset`, but they don't want to affect the `_reset`
|
||||||
when initialization
|
called when initialization
|
||||||
"""
|
"""
|
||||||
self._reset(
|
self._reset(
|
||||||
level_infra=level_infra, common_infra=common_infra, outer_trade_decision=outer_trade_decision, **kwargs
|
level_infra=level_infra,
|
||||||
|
common_infra=common_infra,
|
||||||
|
outer_trade_decision=outer_trade_decision,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _reset(
|
def _reset(
|
||||||
self,
|
self,
|
||||||
level_infra: LevelInfrastructure = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
outer_trade_decision=None,
|
outer_trade_decision: BaseTradeDecision = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Please refer to the docs of `reset`
|
Please refer to the docs of `reset`
|
||||||
@@ -114,7 +121,8 @@ class BaseStrategy:
|
|||||||
if outer_trade_decision is not None:
|
if outer_trade_decision is not None:
|
||||||
self.outer_trade_decision = outer_trade_decision
|
self.outer_trade_decision = outer_trade_decision
|
||||||
|
|
||||||
def generate_trade_decision(self, execute_result=None):
|
@abstractmethod
|
||||||
|
def generate_trade_decision(self, execute_result: list = None) -> BaseTradeDecision:
|
||||||
"""Generate trade decision in each trading bar
|
"""Generate trade decision in each trading bar
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -125,9 +133,11 @@ class BaseStrategy:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError("generate_trade_decision is not implemented!")
|
raise NotImplementedError("generate_trade_decision is not implemented!")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def update_trade_decision(
|
def update_trade_decision(
|
||||||
self, trade_decision: BaseTradeDecision, trade_calendar: TradeCalendarManager
|
trade_decision: BaseTradeDecision,
|
||||||
) -> Union[BaseTradeDecision, None]:
|
trade_calendar: TradeCalendarManager,
|
||||||
|
) -> Optional[BaseTradeDecision]:
|
||||||
"""
|
"""
|
||||||
update trade decision in each step of inner execution, this method enable all order
|
update trade decision in each step of inner execution, this method enable all order
|
||||||
|
|
||||||
@@ -145,7 +155,8 @@ class BaseStrategy:
|
|||||||
# default to return None, which indicates that the trade decision is not changed
|
# default to return None, which indicates that the trade decision is not changed
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def alter_outer_trade_decision(self, outer_trade_decision: BaseTradeDecision):
|
# FIXME: do not define this method as an abstract one since it is never implemented
|
||||||
|
def alter_outer_trade_decision(self, outer_trade_decision: BaseTradeDecision) -> BaseTradeDecision:
|
||||||
"""
|
"""
|
||||||
A method for updating the outer_trade_decision.
|
A method for updating the outer_trade_decision.
|
||||||
The outer strategy may change its decision during updating.
|
The outer strategy may change its decision during updating.
|
||||||
@@ -154,6 +165,10 @@ class BaseStrategy:
|
|||||||
----------
|
----------
|
||||||
outer_trade_decision : BaseTradeDecision
|
outer_trade_decision : BaseTradeDecision
|
||||||
the decision updated by the outer strategy
|
the decision updated by the outer strategy
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
BaseTradeDecision
|
||||||
"""
|
"""
|
||||||
# default to reset the decision directly
|
# default to reset the decision directly
|
||||||
# NOTE: normally, user should do something to the strategy due to the change of outer decision
|
# NOTE: normally, user should do something to the strategy due to the change of outer decision
|
||||||
@@ -200,7 +215,7 @@ class RLStrategy(BaseStrategy):
|
|||||||
level_infra: LevelInfrastructure = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -223,7 +238,7 @@ class RLIntStrategy(RLStrategy):
|
|||||||
level_infra: LevelInfrastructure = None,
|
level_infra: LevelInfrastructure = None,
|
||||||
common_infra: CommonInfrastructure = None,
|
common_infra: CommonInfrastructure = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -242,7 +257,7 @@ class RLIntStrategy(RLStrategy):
|
|||||||
self.state_interpreter = init_instance_by_config(state_interpreter, accept_types=StateInterpreter)
|
self.state_interpreter = init_instance_by_config(state_interpreter, accept_types=StateInterpreter)
|
||||||
self.action_interpreter = init_instance_by_config(action_interpreter, accept_types=ActionInterpreter)
|
self.action_interpreter = init_instance_by_config(action_interpreter, accept_types=ActionInterpreter)
|
||||||
|
|
||||||
def generate_trade_decision(self, execute_result=None):
|
def generate_trade_decision(self, execute_result: list = None) -> BaseTradeDecision:
|
||||||
_interpret_state = self.state_interpreter.interpret(execute_result=execute_result)
|
_interpret_state = self.state_interpreter.interpret(execute_result=execute_result)
|
||||||
_action = self.policy.step(_interpret_state)
|
_action = self.policy.step(_interpret_state)
|
||||||
_trade_decision = self.action_interpreter.interpret(action=_action)
|
_trade_decision = self.action_interpreter.interpret(action=_action)
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ get_cls_kwargs = get_callable_kwargs # NOTE: this is for compatibility for the
|
|||||||
|
|
||||||
|
|
||||||
def init_instance_by_config(
|
def init_instance_by_config(
|
||||||
config: Union[str, dict, object, Path],
|
config: Union[str, dict, object, Path], # TODO: use a user-defined type to replace this Union.
|
||||||
default_module=None,
|
default_module=None,
|
||||||
accept_types: Union[type, Tuple[type]] = (),
|
accept_types: Union[type, Tuple[type]] = (),
|
||||||
try_kwargs: Dict = {},
|
try_kwargs: Dict = {},
|
||||||
@@ -1063,4 +1063,5 @@ __all__ = [
|
|||||||
"unpack_archive_with_buffer",
|
"unpack_archive_with_buffer",
|
||||||
"get_tmp_file_with_buffer",
|
"get_tmp_file_with_buffer",
|
||||||
"set_log_with_config",
|
"set_log_with_config",
|
||||||
|
"init_instance_by_config",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from qlib.backtest import backtest, decision
|
from qlib.backtest import backtest
|
||||||
from qlib.tests import TestAutoData
|
from qlib.tests import TestAutoData
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -52,13 +52,12 @@ class FileStrTest(TestAutoData):
|
|||||||
factor = df["$factor"].item()
|
factor = df["$factor"].item()
|
||||||
price_unit = price / factor * 100
|
price_unit = price / factor * 100
|
||||||
dealt_num_for_1000 = (account_money // price_unit) * (100 / factor)
|
dealt_num_for_1000 = (account_money // price_unit) * (100 / factor)
|
||||||
|
print(price, factor, price_unit, dealt_num_for_1000)
|
||||||
|
|
||||||
# 2) generate orders
|
# 2) generate orders
|
||||||
orders = self._gen_orders(dealt_num_for_1000)
|
orders = self._gen_orders(dealt_num_for_1000)
|
||||||
print(orders)
|
|
||||||
orders.to_csv(self.EXAMPLE_FILE)
|
orders.to_csv(self.EXAMPLE_FILE)
|
||||||
|
print(orders)
|
||||||
orders = pd.read_csv(self.EXAMPLE_FILE, index_col=["datetime", "instrument"])
|
|
||||||
|
|
||||||
# 3) run the strategy
|
# 3) run the strategy
|
||||||
strategy_config = {
|
strategy_config = {
|
||||||
@@ -101,7 +100,11 @@ class FileStrTest(TestAutoData):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
report_dict, indicator_dict = backtest(executor=executor_config, strategy=strategy_config, **backtest_config)
|
report_dict, indicator_dict = backtest(
|
||||||
|
executor=executor_config,
|
||||||
|
strategy=strategy_config,
|
||||||
|
**backtest_config,
|
||||||
|
)
|
||||||
|
|
||||||
# ffr valid
|
# ffr valid
|
||||||
ffr_dict = indicator_dict["1day"]["ffr"].to_dict()
|
ffr_dict = indicator_dict["1day"]["ffr"].to_dict()
|
||||||
|
|||||||
Reference in New Issue
Block a user