mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-12 23:36:54 +08:00
fix comments
This commit is contained in:
@@ -5,13 +5,12 @@ from .account import Account
|
||||
from .exchange import Exchange
|
||||
from .executor import BaseExecutor
|
||||
from .backtest import backtest as backtest_func
|
||||
|
||||
from .backtest import collect_data as data_generator
|
||||
|
||||
from ...strategy.base import BaseStrategy
|
||||
from ...utils import init_instance_by_config
|
||||
from ...log import get_module_logger
|
||||
from ...config import C
|
||||
from .faculty import common_faculty
|
||||
|
||||
|
||||
logger = get_module_logger("backtest caller")
|
||||
@@ -89,8 +88,9 @@ def get_exchange(
|
||||
return init_instance_by_config(exchange, accept_types=Exchange)
|
||||
|
||||
|
||||
def backtest(start_time, end_time, strategy, env, benchmark="SH000300", account=1e9, exchange_kwargs={}):
|
||||
|
||||
def get_strategy_executor(
|
||||
start_time, end_time, strategy, executor, benchmark="SH000300", account=1e9, exchange_kwargs={}
|
||||
):
|
||||
trade_account = Account(
|
||||
init_cash=account,
|
||||
benchmark_config={
|
||||
@@ -101,14 +101,32 @@ def backtest(start_time, end_time, strategy, env, benchmark="SH000300", account=
|
||||
)
|
||||
trade_exchange = get_exchange(**exchange_kwargs)
|
||||
|
||||
common_faculty.update(
|
||||
trade_account=trade_account,
|
||||
trade_exchange=trade_exchange,
|
||||
common_infra = {
|
||||
"trade_account": trade_account,
|
||||
"trade_exchange": trade_exchange,
|
||||
}
|
||||
|
||||
trade_strategy = init_instance_by_config(strategy, accept_types=BaseStrategy, common_infra=common_infra)
|
||||
trade_executor = init_instance_by_config(executor, accept_types=BaseExecutor, common_infra=common_infra)
|
||||
|
||||
return trade_strategy, trade_executor
|
||||
|
||||
|
||||
def backtest(start_time, end_time, strategy, executor, benchmark="SH000300", account=1e9, exchange_kwargs={}):
|
||||
|
||||
trade_strategy, trade_executor = get_strategy_executor(
|
||||
start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs
|
||||
)
|
||||
|
||||
trade_strategy = init_instance_by_config(strategy, accept_types=BaseStrategy)
|
||||
trade_env = init_instance_by_config(env, accept_types=BaseExecutor)
|
||||
|
||||
report_dict = backtest_func(start_time, end_time, trade_strategy, trade_env)
|
||||
report_dict = backtest_func(start_time, end_time, trade_strategy, trade_executor)
|
||||
|
||||
return report_dict
|
||||
|
||||
|
||||
def collect_data(start_time, end_time, strategy, executor, benchmark="SH000300", account=1e9, exchange_kwargs={}):
|
||||
|
||||
trade_strategy, trade_executor = get_strategy_executor(
|
||||
start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs
|
||||
)
|
||||
report_dict = yield from data_generator(start_time, end_time, trade_strategy, trade_executor)
|
||||
|
||||
return report_dict
|
||||
|
||||
@@ -9,8 +9,6 @@ import pandas as pd
|
||||
from .position import Position
|
||||
from .report import Report
|
||||
from .order import Order
|
||||
from ...data import D
|
||||
from ...utils.sample import parse_freq, sample_feature
|
||||
|
||||
|
||||
"""
|
||||
@@ -34,85 +32,14 @@ class Account:
|
||||
self.init_vars(init_cash, freq, benchmark_config)
|
||||
|
||||
def init_vars(self, init_cash, freq: str, benchmark_config: dict):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
freq : str
|
||||
frequency of trading bar, used for updating hold count of trading bar
|
||||
benchmark_config : dict
|
||||
config of benchmark, may including the following arguments:
|
||||
- benchmark : Union[str, list, pd.Series]
|
||||
- If `benchmark` is pd.Series, `index` is trading date; the value T is the change from T-1 to T.
|
||||
example:
|
||||
print(D.features(D.instruments('csi500'), ['$close/Ref($close, 1)-1'])['$close/Ref($close, 1)-1'].head())
|
||||
2017-01-04 0.011693
|
||||
2017-01-05 0.000721
|
||||
2017-01-06 -0.004322
|
||||
2017-01-09 0.006874
|
||||
2017-01-10 -0.003350
|
||||
- If `benchmark` is list, will use the daily average change of the stock pool in the list as the 'bench'.
|
||||
- If `benchmark` is str, will use the daily change as the 'bench'.
|
||||
benchmark code, default is SH000300 CSI300
|
||||
- start_time : Union[str, pd.Timestamp], optional
|
||||
- If `benchmark` is pd.Series, it will be ignored
|
||||
- Else, it represent start time of benchmark, by default None
|
||||
- end_time : Union[str, pd.Timestamp], optional
|
||||
- If `benchmark` is pd.Series, it will be ignored
|
||||
- Else, it represent end time of benchmark, by default None
|
||||
|
||||
"""
|
||||
# init cash
|
||||
self.init_cash = init_cash
|
||||
self.freq = freq
|
||||
self.benchmark_config = benchmark_config
|
||||
self.bench = self._cal_benchmark(benchmark_config, freq)
|
||||
self.current = Position(cash=init_cash)
|
||||
self._reset_report()
|
||||
self.reset(freq=freq, benchmark_config=benchmark_config, init_report=True)
|
||||
|
||||
def _cal_benchmark(self, benchmark_config, freq):
|
||||
benchmark = benchmark_config.get("benchmark", "SH000300")
|
||||
if isinstance(benchmark, pd.Series):
|
||||
return benchmark
|
||||
else:
|
||||
start_time = benchmark_config.get("start_time", None)
|
||||
end_time = benchmark_config.get("end_time", None)
|
||||
|
||||
if freq is None:
|
||||
raise ValueError("benchmark freq can't be None!")
|
||||
_codes = benchmark if isinstance(benchmark, list) else [benchmark]
|
||||
fields = ["$close/Ref($close,1)-1"]
|
||||
try:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq=freq, disk_cache=1)
|
||||
except ValueError:
|
||||
_, norm_freq = parse_freq(freq)
|
||||
if norm_freq in ["month", "week", "day"]:
|
||||
try:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="day", disk_cache=1)
|
||||
except ValueError:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="minute", disk_cache=1)
|
||||
elif norm_freq == "minute":
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="minute", disk_cache=1)
|
||||
else:
|
||||
raise ValueError(f"benchmark freq {freq} is not supported")
|
||||
if len(_temp_result) == 0:
|
||||
raise ValueError(f"The benchmark {_codes} does not exist. Please provide the right benchmark")
|
||||
return _temp_result.groupby(level="datetime")[_temp_result.columns.tolist()[0]].mean().fillna(0)
|
||||
|
||||
def _sample_benchmark(self, bench, trade_start_time, trade_end_time):
|
||||
def cal_change(x):
|
||||
return (x + 1).prod() - 1
|
||||
|
||||
_ret = sample_feature(bench, trade_start_time, trade_end_time, method=cal_change)
|
||||
return 0 if _ret is None else _ret
|
||||
|
||||
def _reset_freq(self, freq):
|
||||
"""reset frequency"""
|
||||
if freq != self.freq:
|
||||
self.freq = freq
|
||||
self.bench = self._cal_benchmark(self.benchmark_config, self.freq)
|
||||
|
||||
def _reset_report(self):
|
||||
self.report = Report()
|
||||
def reset_report(self, freq, benchmark_config):
|
||||
self.report = Report(freq, benchmark_config)
|
||||
self.positions = {}
|
||||
self.rtn = 0
|
||||
self.ct = 0
|
||||
@@ -120,10 +47,25 @@ class Account:
|
||||
self.val = 0
|
||||
self.earning = 0
|
||||
|
||||
def reset(self, freq=None, init_report: bool = False):
|
||||
self._reset_freq(freq)
|
||||
if init_report:
|
||||
self._reset_report()
|
||||
def reset(self, freq=None, benchmark_config=None, init_report=False):
|
||||
"""reset freq and report of account
|
||||
|
||||
Parameters
|
||||
----------
|
||||
freq : str, optional
|
||||
frequency of account & report, by default None
|
||||
benchmark_config : {}, optional
|
||||
benchmark config of report, by default None
|
||||
init_report : bool, optional
|
||||
whether to initialize the report, by default False
|
||||
"""
|
||||
if freq is not None:
|
||||
self.freq = freq
|
||||
if benchmark_config is not None:
|
||||
self.benchmark_config = benchmark_config
|
||||
|
||||
if freq is not None or benchmark_config is not None or init_report:
|
||||
self.reset_report(self.freq, self.benchmark_config)
|
||||
|
||||
def get_positions(self):
|
||||
return self.positions
|
||||
@@ -131,7 +73,7 @@ class Account:
|
||||
def get_cash(self):
|
||||
return self.current.position["cash"]
|
||||
|
||||
def update_state_from_order(self, order, trade_val, cost, trade_price):
|
||||
def _update_state_from_order(self, order, trade_val, cost, trade_price):
|
||||
# update turnover
|
||||
self.to += trade_val
|
||||
# update cost
|
||||
@@ -155,7 +97,7 @@ class Account:
|
||||
# The cost will be substracted from the cash at last. So the trading logic can ignore the cost calculation
|
||||
if order.direction == Order.SELL:
|
||||
# sell stock
|
||||
self.update_state_from_order(order, trade_val, cost, trade_price)
|
||||
self._update_state_from_order(order, trade_val, cost, trade_price)
|
||||
# update current position
|
||||
# for may sell all of stock_id
|
||||
self.current.update_order(order, trade_val, cost, trade_price)
|
||||
@@ -163,15 +105,15 @@ class Account:
|
||||
# buy stock
|
||||
# deal order, then update state
|
||||
self.current.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_bar_count(self):
|
||||
self.current.add_count_all(bar=self.freq)
|
||||
|
||||
def update_bar_report(self, trade_start_time, trade_end_time, trade_exchange):
|
||||
"""
|
||||
start_time: pd.TimeStamp
|
||||
end_time: pd.TimeStamp
|
||||
trade_start_time: pd.TimeStamp
|
||||
trade_end_time: pd.TimeStamp
|
||||
quote: pd.DataFrame (code, date), collumns
|
||||
when the end of trade date
|
||||
- update rtn
|
||||
@@ -211,7 +153,8 @@ class Account:
|
||||
# judge whether the the trading is begin.
|
||||
# and don't add init account state into report, due to we don't have excess return in those days.
|
||||
self.report.update_report_record(
|
||||
trade_time=trade_start_time,
|
||||
trade_start_time=trade_start_time,
|
||||
trade_end_time=trade_end_time,
|
||||
account_value=now_account_value,
|
||||
cash=self.current.position["cash"],
|
||||
return_rate=(self.earning + self.ct) / last_account_value,
|
||||
@@ -220,7 +163,6 @@ class Account:
|
||||
turnover_rate=self.to / last_account_value,
|
||||
cost_rate=self.ct / last_account_value,
|
||||
stock_value=now_stock_value,
|
||||
bench_value=self._sample_benchmark(self.bench, trade_start_time, trade_end_time),
|
||||
)
|
||||
# set now_account_value to position
|
||||
self.current.position["now_account_value"] = now_account_value
|
||||
@@ -234,18 +176,3 @@ class Account:
|
||||
self.rtn = 0
|
||||
self.ct = 0
|
||||
self.to = 0
|
||||
|
||||
def load_account(self, account_path):
|
||||
report = Report()
|
||||
position = Position()
|
||||
report.load_report(account_path / "report.csv")
|
||||
position.load_position(account_path / "position.xlsx")
|
||||
|
||||
# assign values
|
||||
self.init_vars(position.init_cash)
|
||||
self.current = position
|
||||
self.report = report
|
||||
|
||||
def save_account(self, account_path):
|
||||
self.current.save_position(account_path / "position.xlsx")
|
||||
self.report.save_report(account_path / "report.csv")
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
def backtest(start_time, end_time, trade_strategy, trade_env):
|
||||
def backtest(start_time, end_time, trade_strategy, trade_executor):
|
||||
|
||||
trade_env.reset(start_time=start_time, end_time=end_time)
|
||||
trade_strategy.reset(start_time=start_time, end_time=end_time)
|
||||
trade_executor.reset(start_time=start_time, end_time=end_time)
|
||||
level_infra = trade_executor.get_level_infra()
|
||||
trade_strategy.reset(level_infra=level_infra)
|
||||
|
||||
_execute_state = trade_env.get_init_state()
|
||||
while not trade_env.finished():
|
||||
_order_list = trade_strategy.generate_order_list(_execute_state)
|
||||
_execute_state = trade_env.execute(_order_list)
|
||||
sub_execute_state = trade_executor.get_init_state()
|
||||
while not trade_executor.finished():
|
||||
sub_trade_decision = trade_strategy.generate_trade_decision(sub_execute_state)
|
||||
sub_execute_state = trade_executor.execute(sub_trade_decision)
|
||||
|
||||
return trade_env.get_report()
|
||||
return trade_executor.get_report()
|
||||
|
||||
|
||||
def collect_data(start_time, end_time, trade_strategy, trade_executor):
|
||||
|
||||
trade_executor.reset(start_time=start_time, end_time=end_time)
|
||||
level_infra = trade_executor.get_level_infra()
|
||||
trade_strategy.reset(level_infra=level_infra)
|
||||
|
||||
sub_execute_state = trade_executor.get_init_state()
|
||||
while not trade_executor.finished():
|
||||
sub_trade_decision = trade_strategy.generate_trade_decision(sub_execute_state)
|
||||
sub_execute_state = yield from trade_executor.collect_data(sub_trade_decision)
|
||||
|
||||
return trade_executor.get_report()
|
||||
|
||||
@@ -11,7 +11,7 @@ import pandas as pd
|
||||
from ...data.data import D
|
||||
from ...data.dataset.utils import get_level_index
|
||||
from ...config import C, REG_CN
|
||||
from ...utils.sample import sample_feature
|
||||
from ...utils.resam import resam_ts_data
|
||||
from ...log import get_module_logger
|
||||
from .order import Order
|
||||
|
||||
@@ -34,8 +34,9 @@ class Exchange:
|
||||
):
|
||||
"""__init__
|
||||
|
||||
:param start_time: start time for backtest
|
||||
:param end_time: end time for backtest
|
||||
:param freq: frequency of data
|
||||
:param start_time: closed start time for backtest
|
||||
:param end_time: closed end time for backtest
|
||||
:param codes: list stock_id list or a string of instruments(i.e. all, csi500, sse50)
|
||||
:param deal_price: str, 'close', 'open', 'vwap'
|
||||
:param subscribe_fields: list, subscribe fields
|
||||
@@ -91,7 +92,7 @@ class Exchange:
|
||||
# $factor is for rounding to the trading unit
|
||||
# $change is for calculating the limit of the stock
|
||||
|
||||
necessary_fields = {self.deal_price, "$close", "$change", "$factor"}
|
||||
necessary_fields = {self.deal_price, "$close", "$change", "$factor", "$volume"}
|
||||
subscribe_fields = list(necessary_fields | set(subscribe_fields))
|
||||
all_fields = list(necessary_fields | set(subscribe_fields))
|
||||
self.all_fields = all_fields
|
||||
@@ -167,12 +168,12 @@ class Exchange:
|
||||
trade_date
|
||||
is limtited
|
||||
"""
|
||||
return sample_feature(self.quote[stock_id], start_time, end_time, fields="limit", method="all").iloc[0]
|
||||
return resam_ts_data(self.quote[stock_id]["limit"], start_time, end_time, method="all").iloc[0]
|
||||
|
||||
def check_stock_suspended(self, stock_id, start_time, end_time):
|
||||
# is suspended
|
||||
if stock_id in self.quote:
|
||||
return sample_feature(self.quote[stock_id], start_time, end_time, method=None) is None
|
||||
return resam_ts_data(self.quote[stock_id], start_time, end_time, method=None) is None
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -230,15 +231,16 @@ class Exchange:
|
||||
return trade_val, trade_cost, trade_price
|
||||
|
||||
def get_quote_info(self, stock_id, start_time, end_time):
|
||||
return sample_feature(self.quote[stock_id], start_time, end_time, method="last").iloc[0]
|
||||
return resam_ts_data(self.quote[stock_id], start_time, end_time, method="last").iloc[0]
|
||||
|
||||
def get_close(self, stock_id, start_time, end_time):
|
||||
return sample_feature(self.quote[stock_id], start_time, end_time, fields="$close", method="last").iloc[0]
|
||||
return resam_ts_data(self.quote[stock_id]["$close"], start_time, end_time, method="last").iloc[0]
|
||||
|
||||
def get_volume(self, stock_id, start_time, end_time):
|
||||
return resam_ts_data(self.quote[stock_id]["$volume"], start_time, end_time, method="sum").iloc[0]
|
||||
|
||||
def get_deal_price(self, stock_id, start_time, end_time):
|
||||
deal_price = sample_feature(
|
||||
self.quote[stock_id], start_time, end_time, fields=self.deal_price, method="last"
|
||||
).iloc[0]
|
||||
deal_price = resam_ts_data(self.quote[stock_id][self.deal_price], start_time, end_time, method="last").iloc[0]
|
||||
if np.isclose(deal_price, 0.0) or np.isnan(deal_price):
|
||||
self.logger.warning(
|
||||
f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {self.deal_price}): {deal_price}!!!"
|
||||
@@ -248,7 +250,7 @@ class Exchange:
|
||||
return deal_price
|
||||
|
||||
def get_factor(self, stock_id, start_time, end_time):
|
||||
return sample_feature(self.quote[stock_id], start_time, end_time, fields="$factor", method="last").iloc[0]
|
||||
return resam_ts_data(self.quote[stock_id]["$factor"], start_time, end_time, method="last").iloc[0]
|
||||
|
||||
def generate_amount_position_from_weight_position(self, weight_position, cash, start_time, end_time):
|
||||
"""
|
||||
|
||||
@@ -2,88 +2,18 @@ import copy
|
||||
import warnings
|
||||
import pandas as pd
|
||||
from typing import Union
|
||||
from ...data.data import Cal
|
||||
|
||||
from ...utils import init_instance_by_config
|
||||
from ...utils.sample import get_sample_freq_calendar, parse_freq
|
||||
from ...utils.resam import parse_freq
|
||||
|
||||
|
||||
from .order import Order
|
||||
from .account import Account
|
||||
from .exchange import Exchange
|
||||
from .faculty import common_faculty
|
||||
from .utils import TradeCalendarManager
|
||||
|
||||
|
||||
class BaseTradeCalendar:
|
||||
"""
|
||||
Base class providing trading calendar
|
||||
- BaseStrategy and BaseExecutor should inherited from this class
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, step_bar: str, start_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
step_bar : str
|
||||
frequency of each trading step bar
|
||||
start_time : Union[str, pd.Timestamp], optional
|
||||
start time of trading, by default None
|
||||
If `start_time` is None, it must be reset before trading.
|
||||
end_time : Union[str, pd.Timestamp], optional
|
||||
end time of trading, by default None
|
||||
If `end_time` is None, it must be reset before trading.
|
||||
"""
|
||||
|
||||
self.step_bar = step_bar
|
||||
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.reset(start_time=start_time, end_time=end_time)
|
||||
|
||||
def _reset_trade_calendar(self, start_time, end_time):
|
||||
"""reset trade calendar"""
|
||||
if start_time and end_time:
|
||||
_calendar, freq, freq_sam = get_sample_freq_calendar(freq=self.step_bar)
|
||||
self.calendar = _calendar
|
||||
_, _, _start_index, _end_index = Cal.locate_index(
|
||||
self.start_time, self.end_time, freq=freq, freq_sam=freq_sam
|
||||
)
|
||||
self.start_index = _start_index
|
||||
self.end_index = _end_index
|
||||
self.trade_len = _end_index - _start_index + 1
|
||||
self.trade_index = 0
|
||||
else:
|
||||
raise ValueError("failed to reset trade calendar, param `start_time` or `end_time` is None.")
|
||||
|
||||
def reset(self, start_time=None, end_time=None):
|
||||
"""
|
||||
Reset start\end time of trading, and reset trading calendar
|
||||
"""
|
||||
|
||||
if start_time:
|
||||
self.start_time = pd.Timestamp(start_time)
|
||||
if end_time:
|
||||
self.end_time = pd.Timestamp(end_time)
|
||||
if self.start_time and self.end_time and (start_time or end_time):
|
||||
self._reset_trade_calendar(start_time=self.start_time, end_time=self.end_time)
|
||||
|
||||
def _get_calendar_time(self, trade_index=1, shift=0):
|
||||
trade_index = trade_index - shift
|
||||
calendar_index = self.start_index + trade_index
|
||||
return self.calendar[calendar_index - 1], self.calendar[calendar_index] - pd.Timedelta(seconds=1)
|
||||
|
||||
def finished(self):
|
||||
return self.trade_index >= self.trade_len
|
||||
|
||||
def step(self):
|
||||
if self.finished():
|
||||
raise RuntimeError(f"this env has completed its task, please reset it if you want to call it!")
|
||||
# trade count += 1
|
||||
self.trade_index = self.trade_index + 1
|
||||
|
||||
|
||||
class BaseExecutor(BaseTradeCalendar):
|
||||
class BaseExecutor:
|
||||
"""Base executor for trading"""
|
||||
|
||||
def __init__(
|
||||
@@ -91,48 +21,97 @@ class BaseExecutor(BaseTradeCalendar):
|
||||
step_bar: str,
|
||||
start_time: Union[str, pd.Timestamp] = None,
|
||||
end_time: Union[str, pd.Timestamp] = None,
|
||||
trade_account: Account = None,
|
||||
generate_report: bool = False,
|
||||
verbose: bool = False,
|
||||
track_data: bool = False,
|
||||
common_infra: dict = {},
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
trade_account : Account, optional
|
||||
trade account for trading, by default None
|
||||
- If `trade_account` is None, self.trade_account will be set with common_faculty
|
||||
generate_report : bool, optional
|
||||
whether to generate report, by default False
|
||||
verbose : bool, optional
|
||||
whether to print trading info, by default False
|
||||
track_data : bool, optional
|
||||
whether to generate order_list, will be used when making data for multi-level training
|
||||
- If `self.track_data` is true, when making data for training, the input `order_list` of `execute` will be generated by `get_data`
|
||||
- Else, `order_list` will not be generated
|
||||
whether to generate trade_decision, will be used when making data for multi-level training
|
||||
- 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
|
||||
common_infra : dict, optional:
|
||||
common infrastructure for backtesting, may including:
|
||||
- trade_account : Account, optional
|
||||
trade account for trading
|
||||
- trade_exchange : Exchange, optional
|
||||
exchange that provides market info
|
||||
|
||||
"""
|
||||
super(BaseExecutor, self).__init__(step_bar=step_bar, start_time=start_time, end_time=end_time, **kwargs)
|
||||
self.trade_account = copy.copy(common_faculty.trade_account if trade_account is None else trade_account)
|
||||
self.trade_account.reset(freq=self.step_bar, init_report=True)
|
||||
self.step_bar = step_bar
|
||||
self.generate_report = generate_report
|
||||
self.verbose = verbose
|
||||
self.track_data = track_data
|
||||
self.reset(start_time=start_time, end_time=end_time, track_data=track_data, common_infra=common_infra)
|
||||
|
||||
def reset(self, track_data: bool = None, **kwargs):
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
Reset `track_data`, will be used when making data for multi-level training
|
||||
reset infrastructure for trading
|
||||
- reset trade_account
|
||||
"""
|
||||
super(BaseExecutor, self).reset(**kwargs)
|
||||
if not hasattr(self, "common_infra"):
|
||||
self.common_infra = common_infra
|
||||
else:
|
||||
self.common_infra.update(common_infra)
|
||||
|
||||
if "trade_account" in common_infra:
|
||||
self.trade_account = copy.copy(common_infra.get("trade_account"))
|
||||
self.trade_account.reset(freq=self.step_bar, init_report=True)
|
||||
|
||||
def reset(self, track_data: bool = None, common_infra: dict = None, **kwargs):
|
||||
"""
|
||||
- reset `start_time` and `end_time`, used in trade calendar
|
||||
- reset `track_data`, used when making data for multi-level training
|
||||
- reset `common_infra`, used to reset `trade_account`, `trade_exchange`, .etc
|
||||
"""
|
||||
|
||||
if track_data is not None:
|
||||
self.track_data = track_data
|
||||
|
||||
if common_infra is not None:
|
||||
self.reset_common_infra(common_infra)
|
||||
|
||||
if "start_time" in kwargs or "end_time" in kwargs:
|
||||
start_time = kwargs.get("start_time")
|
||||
end_time = kwargs.get("end_time")
|
||||
self.trade_calendar = TradeCalendarManager(step_bar=self.step_bar, start_time=start_time, end_time=end_time)
|
||||
|
||||
def get_level_infra(self):
|
||||
return {"trade_calendar": self.trade_calendar}
|
||||
|
||||
def finished(self):
|
||||
return self.trade_calendar.finished()
|
||||
|
||||
def execute(self, trade_decision):
|
||||
"""execute the trade decision and return the executed result
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trade_decision : object
|
||||
|
||||
Returns
|
||||
----------
|
||||
executed state : List[Tuple[Order, float, float, float]]
|
||||
- Each element in the list represents (order, trade value, trade cost, trade price)
|
||||
"""
|
||||
raise NotImplementedError("execute is not implemented!")
|
||||
|
||||
def collect_data(self, trade_decision):
|
||||
if self.track_data:
|
||||
yield trade_decision
|
||||
return self.execute(trade_decision)
|
||||
|
||||
def get_init_state(self):
|
||||
raise NotImplementedError("get_init_state in not implemeted!")
|
||||
|
||||
def execute(self, **kwargs):
|
||||
raise NotImplementedError("execute is not implemented!")
|
||||
|
||||
def get_trade_account(self):
|
||||
raise NotImplementedError("get_trade_account is not implemented!")
|
||||
|
||||
@@ -146,56 +125,75 @@ class SplitExecutor(BaseExecutor):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar: str,
|
||||
sub_env: Union[BaseExecutor, dict],
|
||||
sub_executor: Union[BaseExecutor, dict],
|
||||
sub_strategy: Union[BaseStrategy, dict],
|
||||
start_time: Union[str, pd.Timestamp] = None,
|
||||
end_time: Union[str, pd.Timestamp] = None,
|
||||
trade_account: Account = None,
|
||||
trade_exchange: Exchange = None,
|
||||
generate_report: bool = False,
|
||||
verbose: bool = False,
|
||||
track_data: bool = False,
|
||||
common_infra: dict = {},
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
sub_env : BaseExecutor
|
||||
sub_executor : BaseExecutor
|
||||
trading env in each trading bar.
|
||||
sub_strategy : BaseStrategy
|
||||
trading strategy in each trading bar
|
||||
trade_exchange : Exchange
|
||||
exchange that provides market info
|
||||
- If `trade_exchange` is None, self.trade_exchange will be set with common_faculty
|
||||
exchange that provides market info, used to generate report
|
||||
- If generate_report is None, trade_exchange will be ignored
|
||||
- Else If `trade_exchange` is None, self.trade_exchange will be set with common_infra
|
||||
"""
|
||||
self.sub_executor = init_instance_by_config(sub_executor, common_infra=common_infra, accept_types=BaseExecutor)
|
||||
self.sub_strategy = init_instance_by_config(
|
||||
sub_strategy, common_infra=common_infra, accept_types=self.BaseStrategy
|
||||
)
|
||||
|
||||
super(SplitExecutor, self).__init__(
|
||||
step_bar=step_bar,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
trade_account=trade_account,
|
||||
generate_report=generate_report,
|
||||
verbose=verbose,
|
||||
track_data=track_data,
|
||||
common_infra=common_infra,
|
||||
**kwargs,
|
||||
)
|
||||
if generate_report:
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
self.sub_env = init_instance_by_config(sub_env, accept_types=BaseExecutor)
|
||||
self.sub_strategy = init_instance_by_config(sub_strategy, accept_types=self.BaseStrategy)
|
||||
|
||||
if generate_report and trade_exchange is not None:
|
||||
self.trade_exchange = trade_exchange
|
||||
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
reset infrastructure for trading
|
||||
- reset trade_exchange
|
||||
- reset substrategy and subexecutor common infra
|
||||
"""
|
||||
super(SplitExecutor, self).reset_common_infra(common_infra)
|
||||
|
||||
if self.generate_report and "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
self.sub_executor.reset_common_infra(common_infra)
|
||||
self.sub_strategy.reset_common_infra(common_infra)
|
||||
|
||||
def get_init_state(self):
|
||||
init_state = {"current": self.trade_account.current}
|
||||
return init_state
|
||||
return []
|
||||
|
||||
def _init_sub_trading(self, order_list):
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
self.sub_env.reset(start_time=trade_start_time, end_time=trade_end_time)
|
||||
self.sub_strategy.reset(start_time=trade_start_time, end_time=trade_end_time, trade_order_list=order_list)
|
||||
sub_execute_state = self.sub_env.get_init_state()
|
||||
return sub_execute_state
|
||||
def _init_sub_trading(self, trade_decision):
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
self.sub_executor.reset(start_time=trade_start_time, end_time=trade_end_time)
|
||||
sub_level_infra = self.sub_executor.get_level_infra()
|
||||
self.sub_strategy.reset(level_infra=sub_level_infra, rely_trade_decision=trade_decision)
|
||||
|
||||
def _update_trade_account(self):
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
self.trade_account.update_bar_count()
|
||||
if self.generate_report:
|
||||
self.trade_account.update_bar_report(
|
||||
@@ -204,30 +202,38 @@ class SplitExecutor(BaseExecutor):
|
||||
trade_exchange=self.trade_exchange,
|
||||
)
|
||||
|
||||
def execute(self, order_list):
|
||||
super(SplitExecutor, self).step()
|
||||
self._init_sub_trading(order_list)
|
||||
sub_execute_state = self.sub_env.get_init_state()
|
||||
while not self.sub_env.finished():
|
||||
_order_list = self.sub_strategy.generate_order_list(sub_execute_state)
|
||||
sub_execute_state = self.sub_env.execute(order_list=_order_list)
|
||||
self._update_trade_account()
|
||||
return {"current": self.trade_account.current}
|
||||
def execute(self, trade_decision):
|
||||
self.trade_calendar.step()
|
||||
self._init_sub_trading(trade_decision)
|
||||
execute_state = []
|
||||
sub_execute_state = self.sub_executor.get_init_state()
|
||||
while not self.sub_executor.finished():
|
||||
sub_trade_decison = self.sub_strategy.generate_trade_decision(sub_execute_state)
|
||||
sub_execute_state = self.sub_executor.execute(trade_decision=sub_trade_decison)
|
||||
execute_state.extend(sub_execute_state)
|
||||
if hasattr(self, "trade_account"):
|
||||
self._update_trade_account()
|
||||
|
||||
def get_data(self, order_list):
|
||||
return execute_state
|
||||
|
||||
def collect_data(self, trade_decision):
|
||||
if self.track_data:
|
||||
yield order_list
|
||||
super(SplitExecutor, self).step()
|
||||
self._init_sub_trading(order_list)
|
||||
sub_execute_state = self.sub_env.get_init_state()
|
||||
while not self.sub_env.finished():
|
||||
_order_list = self.sub_strategy.generate_order_list(sub_execute_state)
|
||||
sub_execute_state = yield from self.sub_env.get_data(order_list=_order_list)
|
||||
self._update_trade_account()
|
||||
return {"current": self.trade_account.current}
|
||||
yield trade_decision
|
||||
self.trade_calendar.step()
|
||||
self._init_sub_trading(trade_decision)
|
||||
execute_state = []
|
||||
sub_execute_state = self.sub_executor.get_init_state()
|
||||
while not self.sub_executor.finished():
|
||||
sub_trade_decison = self.sub_strategy.generate_trade_decision(sub_execute_state)
|
||||
sub_execute_state = yield from self.sub_executor.collect_data(trade_decision=sub_trade_decison)
|
||||
execute_state.extend(sub_execute_state)
|
||||
if hasattr(self, "trade_account"):
|
||||
self._update_trade_account()
|
||||
|
||||
return execute_state
|
||||
|
||||
def get_report(self):
|
||||
sub_env_report_dict = self.sub_env.get_report()
|
||||
sub_env_report_dict = self.sub_executor.get_report()
|
||||
if self.generate_report:
|
||||
_report = self.trade_account.report.generate_report_dataframe()
|
||||
_positions = self.trade_account.get_positions()
|
||||
@@ -242,46 +248,57 @@ class SimulatorExecutor(BaseExecutor):
|
||||
step_bar: str,
|
||||
start_time: Union[str, pd.Timestamp] = None,
|
||||
end_time: Union[str, pd.Timestamp] = None,
|
||||
trade_account: Account = None,
|
||||
trade_exchange: Exchange = None,
|
||||
generate_report: bool = False,
|
||||
verbose: bool = False,
|
||||
track_data: bool = False,
|
||||
common_infra: dict = {},
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
trade_exchange : Exchange
|
||||
exchange that provides market info
|
||||
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
|
||||
"""
|
||||
super(SimulatorExecutor, self).__init__(
|
||||
step_bar=step_bar,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
trade_account=trade_account,
|
||||
generate_report=generate_report,
|
||||
verbose=verbose,
|
||||
track_data=track_data,
|
||||
common_infra=common_infra,
|
||||
**kwargs,
|
||||
)
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
if trade_exchange is not None:
|
||||
self.trade_exchange = trade_exchange
|
||||
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
reset infrastructure for trading
|
||||
- reset trade_exchange
|
||||
"""
|
||||
super(SimulatorExecutor, self).reset_common_infra(common_infra)
|
||||
if "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
def get_init_state(self):
|
||||
init_state = {"current": self.trade_account.current, "trade_info": []}
|
||||
return init_state
|
||||
return []
|
||||
|
||||
def execute(self, order_list):
|
||||
super(SimulatorExecutor, self).step()
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
trade_info = []
|
||||
for order in order_list:
|
||||
def execute(self, trade_decision):
|
||||
self.trade_calendar.step()
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
execute_state = []
|
||||
for order in trade_decision:
|
||||
if self.trade_exchange.check_order(order) is True:
|
||||
# execute the order
|
||||
trade_val, trade_cost, trade_price = self.trade_exchange.deal_order(
|
||||
order, trade_account=self.trade_account
|
||||
)
|
||||
trade_info.append((order, trade_val, trade_cost, trade_price))
|
||||
execute_state.append((order, trade_val, trade_cost, trade_price))
|
||||
if self.verbose:
|
||||
if order.direction == Order.SELL: # sell
|
||||
print(
|
||||
@@ -323,7 +340,7 @@ class SimulatorExecutor(BaseExecutor):
|
||||
trade_exchange=self.trade_exchange,
|
||||
)
|
||||
|
||||
return {"current": self.trade_account.current, "trade_info": trade_info}
|
||||
return execute_state
|
||||
|
||||
def get_report(self):
|
||||
if self.generate_report:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
class Faculty:
|
||||
def __init__(self):
|
||||
self.__dict__["_faculty"] = dict()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__dict__["_faculty"][key]
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self.__dict__["_faculty"]:
|
||||
return self.__dict__["_faculty"][attr]
|
||||
|
||||
raise AttributeError(f"No such {attr} in self._faculty")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__dict__["_faculty"][key] = value
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self.__dict__["_faculty"][attr] = value
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
self.__dict__["_faculty"].update(*args, **kwargs)
|
||||
|
||||
|
||||
common_faculty = Faculty()
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import copy
|
||||
import pathlib
|
||||
|
||||
@@ -3,16 +3,51 @@
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from logging import warning
|
||||
import pandas as pd
|
||||
import pathlib
|
||||
import warnings
|
||||
|
||||
from pandas.core.frame import DataFrame
|
||||
|
||||
from ...utils.resam import parse_freq, resam_ts_data
|
||||
from ...data import D
|
||||
|
||||
|
||||
class Report:
|
||||
# daily report of the account
|
||||
# contain those followings: returns, costs turnovers, accounts, cash, bench, value
|
||||
# update report
|
||||
def __init__(self):
|
||||
def __init__(self, freq: str = "day", benchmark_config: dict = {}):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
freq : str
|
||||
frequency of trading bar, used for updating hold count of trading bar
|
||||
benchmark_config : dict
|
||||
config of benchmark, may including the following arguments:
|
||||
- benchmark : Union[str, list, pd.Series]
|
||||
- If `benchmark` is pd.Series, `index` is trading date; the value T is the change from T-1 to T.
|
||||
example:
|
||||
print(D.features(D.instruments('csi500'), ['$close/Ref($close, 1)-1'])['$close/Ref($close, 1)-1'].head())
|
||||
2017-01-04 0.011693
|
||||
2017-01-05 0.000721
|
||||
2017-01-06 -0.004322
|
||||
2017-01-09 0.006874
|
||||
2017-01-10 -0.003350
|
||||
- If `benchmark` is list, will use the daily average change of the stock pool in the list as the 'bench'.
|
||||
- If `benchmark` is str, will use the daily change as the 'bench'.
|
||||
benchmark code, default is SH000300 CSI300
|
||||
- start_time : Union[str, pd.Timestamp], optional
|
||||
- If `benchmark` is pd.Series, it will be ignored
|
||||
- Else, it represent start time of benchmark, by default None
|
||||
- end_time : Union[str, pd.Timestamp], optional
|
||||
- If `benchmark` is pd.Series, it will be ignored
|
||||
- Else, it represent end time of benchmark, by default None
|
||||
|
||||
"""
|
||||
self.init_vars()
|
||||
self.init_bench(freq=freq, benchmark_config=benchmark_config)
|
||||
|
||||
def init_vars(self):
|
||||
self.accounts = OrderedDict() # account postion value for each trade date
|
||||
@@ -24,6 +59,49 @@ class Report:
|
||||
self.benches = OrderedDict()
|
||||
self.latest_report_time = None # pd.TimeStamp
|
||||
|
||||
def init_bench(self, freq=None, benchmark_config=None):
|
||||
if freq is not None:
|
||||
self.freq = freq
|
||||
if benchmark_config is not None:
|
||||
self.benchmark_config = benchmark_config
|
||||
self.bench = self._cal_benchmark(self.benchmark_config, self.freq)
|
||||
|
||||
def _cal_benchmark(self, benchmark_config, freq):
|
||||
benchmark = benchmark_config.get("benchmark", "SH000300")
|
||||
if isinstance(benchmark, pd.Series):
|
||||
return benchmark
|
||||
else:
|
||||
start_time = benchmark_config.get("start_time", None)
|
||||
end_time = benchmark_config.get("end_time", None)
|
||||
|
||||
if freq is None:
|
||||
raise ValueError("benchmark freq can't be None!")
|
||||
_codes = benchmark if isinstance(benchmark, list) else [benchmark]
|
||||
fields = ["$close/Ref($close,1)-1"]
|
||||
try:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq=freq, disk_cache=1)
|
||||
except ValueError:
|
||||
_, norm_freq = parse_freq(freq)
|
||||
if norm_freq in ["month", "week", "day"]:
|
||||
try:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="day", disk_cache=1)
|
||||
except ValueError:
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="minute", disk_cache=1)
|
||||
elif norm_freq == "minute":
|
||||
_temp_result = D.features(_codes, fields, start_time, end_time, freq="minute", disk_cache=1)
|
||||
else:
|
||||
raise ValueError(f"benchmark freq {freq} is not supported")
|
||||
if len(_temp_result) == 0:
|
||||
raise ValueError(f"The benchmark {_codes} does not exist. Please provide the right benchmark")
|
||||
return _temp_result.groupby(level="datetime")[_temp_result.columns.tolist()[0]].mean().fillna(0)
|
||||
|
||||
def _sample_benchmark(self, bench, trade_start_time, trade_end_time):
|
||||
def cal_change(x):
|
||||
return (x + 1).prod() - 1
|
||||
|
||||
_ret = resam_ts_data(bench, trade_start_time, trade_end_time, method=cal_change)
|
||||
return 0.0 if _ret is None else _ret
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.accounts) == 0
|
||||
|
||||
@@ -35,30 +113,39 @@ class Report:
|
||||
|
||||
def update_report_record(
|
||||
self,
|
||||
trade_time=None,
|
||||
trade_start_time=None,
|
||||
trade_end_time=None,
|
||||
account_value=None,
|
||||
cash=None,
|
||||
return_rate=None,
|
||||
turnover_rate=None,
|
||||
cost_rate=None,
|
||||
stock_value=None,
|
||||
bench_value=None,
|
||||
):
|
||||
# check data
|
||||
if None in [trade_time, account_value, cash, return_rate, turnover_rate, cost_rate, stock_value, bench_value]:
|
||||
if None in [
|
||||
trade_start_time,
|
||||
trade_end_time,
|
||||
account_value,
|
||||
cash,
|
||||
return_rate,
|
||||
turnover_rate,
|
||||
cost_rate,
|
||||
stock_value,
|
||||
]:
|
||||
raise ValueError(
|
||||
"None in [trade_date, account_value, cash, return_rate, turnover_rate, cost_rate, stock_value, bench_value]"
|
||||
"None in [trade_start_time, trade_end_time, account_value, cash, return_rate, turnover_rate, cost_rate, stock_value]"
|
||||
)
|
||||
# update report data
|
||||
self.accounts[trade_time] = account_value
|
||||
self.returns[trade_time] = return_rate
|
||||
self.turnovers[trade_time] = turnover_rate
|
||||
self.costs[trade_time] = cost_rate
|
||||
self.values[trade_time] = stock_value
|
||||
self.cashes[trade_time] = cash
|
||||
self.benches[trade_time] = bench_value
|
||||
self.accounts[trade_start_time] = account_value
|
||||
self.returns[trade_start_time] = return_rate
|
||||
self.turnovers[trade_start_time] = turnover_rate
|
||||
self.costs[trade_start_time] = cost_rate
|
||||
self.values[trade_start_time] = stock_value
|
||||
self.cashes[trade_start_time] = cash
|
||||
self.benches[trade_start_time] = self._sample_benchmark(self.bench, trade_start_time, trade_end_time)
|
||||
# update latest_report_date
|
||||
self.latest_report_time = trade_time
|
||||
self.latest_report_time = trade_start_time
|
||||
# finish daily report update
|
||||
|
||||
def generate_report_dataframe(self):
|
||||
|
||||
67
qlib/contrib/backtest/utils.py
Normal file
67
qlib/contrib/backtest/utils.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import pandas as pd
|
||||
from typing import Union
|
||||
|
||||
from ...utils.resam import get_resam_calendar
|
||||
from ...data.data import Cal
|
||||
|
||||
|
||||
class TradeCalendarManager:
|
||||
"""
|
||||
Manager for trading calendar
|
||||
- BaseStrategy and BaseExecutor will use it
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, step_bar: str, start_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
step_bar : str
|
||||
frequency of each trading calendar
|
||||
start_time : Union[str, pd.Timestamp], optional
|
||||
closed start of the trading calendar, by default None
|
||||
If `start_time` is None, it must be reset before trading.
|
||||
end_time : Union[str, pd.Timestamp], optional
|
||||
closed end of the trade time range, by default None
|
||||
If `end_time` is None, it must be reset before trading.
|
||||
"""
|
||||
self.step_bar = step_bar
|
||||
self.start_time = pd.Timestamp(start_time) if start_time else None
|
||||
self.end_time = pd.Timestamp(start_time) if start_time else None
|
||||
self._init_trade_calendar(step_bar=step_bar, start_time=start_time, end_time=end_time)
|
||||
|
||||
def _init_trade_calendar(self, step_bar, start_time, end_time):
|
||||
"""reset trade calendar"""
|
||||
_calendar, freq, freq_sam = get_resam_calendar(freq=step_bar)
|
||||
self.calendar = _calendar
|
||||
_, _, _start_index, _end_index = Cal.locate_index(start_time, end_time, freq=freq, freq_sam=freq_sam)
|
||||
self.start_index = _start_index
|
||||
self.end_index = _end_index
|
||||
self.trade_len = _end_index - _start_index + 1
|
||||
self.trade_index = 0
|
||||
|
||||
def finished(self):
|
||||
return self.trade_index >= self.trade_len
|
||||
|
||||
def step(self):
|
||||
if self.finished():
|
||||
raise RuntimeError(f"The calendar is finished, please reset it if you want to call it!")
|
||||
self.trade_index = self.trade_index + 1
|
||||
|
||||
def get_step_bar(self):
|
||||
return self.step_bar
|
||||
|
||||
def get_trade_len(self):
|
||||
return self.trade_len
|
||||
|
||||
def get_trade_index(self):
|
||||
return self.trade_index
|
||||
|
||||
def get_calendar_time(self, trade_index=1, shift=0):
|
||||
trade_index = trade_index - shift
|
||||
calendar_index = self.start_index + trade_index
|
||||
return self.calendar[calendar_index - 1], self.calendar[calendar_index] - pd.Timedelta(seconds=1)
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from logging import warn
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -10,7 +11,7 @@ import warnings
|
||||
from ..log import get_module_logger
|
||||
from .backtest import get_exchange, backtest as backtest_func
|
||||
from ..utils import get_date_range
|
||||
from ..utils.sample import parse_freq
|
||||
from ..utils.resam import parse_freq
|
||||
|
||||
from ..data import D
|
||||
from ..config import C
|
||||
@@ -20,7 +21,7 @@ from ..data.dataset.utils import get_level_index
|
||||
logger = get_module_logger("Evaluate")
|
||||
|
||||
|
||||
def risk_analysis(r, N: int = None, freq: str = None):
|
||||
def risk_analysis(r, N: int = None, freq: str = "day"):
|
||||
"""Risk Analysis
|
||||
|
||||
Parameters
|
||||
@@ -36,8 +37,8 @@ def risk_analysis(r, N: int = None, freq: str = None):
|
||||
def cal_risk_analysis_scaler(freq):
|
||||
_count, _freq = parse_freq(freq)
|
||||
_freq_scaler = {
|
||||
"minute": 240 * 250,
|
||||
"day": 250,
|
||||
"minute": 240 * 252,
|
||||
"day": 252,
|
||||
"week": 50,
|
||||
"month": 12,
|
||||
}
|
||||
@@ -45,6 +46,8 @@ def risk_analysis(r, N: int = None, freq: str = None):
|
||||
|
||||
if N is None and freq is None:
|
||||
raise ValueError("at least one of `N` and `freq` should exist")
|
||||
if N is not None and freq is not None:
|
||||
warnings.warn("risk_analysis freq will be ignored")
|
||||
if N is None:
|
||||
N = cal_risk_analysis_scaler(freq)
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class Operator:
|
||||
user.strategy.update(score_series, pred_date, trade_date)
|
||||
|
||||
# generate and save order list
|
||||
order_list = user.strategy.generate_order_list(
|
||||
order_list = user.strategy.generate_trade_decision(
|
||||
score_series=score_series,
|
||||
current=user.account.current,
|
||||
trade_exchange=trade_exchange,
|
||||
@@ -208,7 +208,7 @@ class Operator:
|
||||
self.logger.info("Update account state {} for {}".format(trade_date, user_id))
|
||||
|
||||
def simulate(self, id, config, exchange_config, start, end, path, bench="SH000905"):
|
||||
"""Run the ( generate_order_list -> execute_order_list -> update_account) process everyday
|
||||
"""Run the ( generate_trade_decision -> execute_order_list -> update_account) process everyday
|
||||
from start date to end date.
|
||||
|
||||
Parameters
|
||||
@@ -256,7 +256,7 @@ class Operator:
|
||||
user.strategy.update(score_series, pred_date, trade_date)
|
||||
|
||||
# 3. generate and save order list
|
||||
order_list = user.strategy.generate_order_list(
|
||||
order_list = user.strategy.generate_trade_decision(
|
||||
score_series=score_series,
|
||||
current=user.account.current,
|
||||
trade_exchange=trade_exchange,
|
||||
|
||||
@@ -10,17 +10,15 @@ import copy
|
||||
class SoftTopkStrategy(WeightStrategyBase):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
model,
|
||||
dataset,
|
||||
topk,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
order_generator_cls_or_obj=OrderGenWInteract,
|
||||
trade_exchange=None,
|
||||
max_sold_weight=1.0,
|
||||
risk_degree=0.95,
|
||||
buy_method="first_fill",
|
||||
level_infra={},
|
||||
common_infra={},
|
||||
**kwargs,
|
||||
):
|
||||
"""Parameter
|
||||
@@ -33,7 +31,7 @@ class SoftTopkStrategy(WeightStrategyBase):
|
||||
average_fill: assign the weight to the stocks rank high averagely.
|
||||
"""
|
||||
super(SoftTopkStrategy, self).__init__(
|
||||
step_bar, model, dataset, start_time, end_time, order_generator_cls_or_obj, trade_exchange
|
||||
model, dataset, order_generator_cls_or_obj, level_infra, common_infra, **kwargs
|
||||
)
|
||||
self.topk = topk
|
||||
self.max_sold_weight = max_sold_weight
|
||||
|
||||
@@ -3,29 +3,26 @@ import warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from ...utils.sample import sample_feature
|
||||
from ...utils.resam import resam_ts_data
|
||||
from ...strategy.base import ModelStrategy
|
||||
from ..backtest.order import Order
|
||||
from ..backtest.faculty import common_faculty
|
||||
from .order_generator import OrderGenWInteract
|
||||
|
||||
|
||||
class TopkDropoutStrategy(ModelStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
model,
|
||||
dataset,
|
||||
topk,
|
||||
n_drop,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_exchange=None,
|
||||
method_sell="bottom",
|
||||
method_buy="top",
|
||||
risk_degree=0.95,
|
||||
hold_thresh=1,
|
||||
only_tradable=False,
|
||||
level_infra={},
|
||||
common_infra={},
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -51,8 +48,9 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
else:
|
||||
strategy will make decision with the tradable state of the stock info and avoid buy and sell them.
|
||||
"""
|
||||
super(TopkDropoutStrategy, self).__init__(step_bar, model, dataset, start_time, end_time, **kwargs)
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
super(TopkDropoutStrategy, self).__init__(
|
||||
model, dataset, level_infra=level_infra, common_infra=common_infra, **kwargs
|
||||
)
|
||||
self.topk = topk
|
||||
self.n_drop = n_drop
|
||||
self.method_sell = method_sell
|
||||
@@ -61,6 +59,20 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
self.hold_thresh = hold_thresh
|
||||
self.only_tradable = only_tradable
|
||||
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
common_infra : dict, optional
|
||||
common infrastructure for backtesting, by default None
|
||||
- It should include `trade_account`, used to get position
|
||||
- It should include `trade_exchange`, used to provide market info
|
||||
"""
|
||||
super(TopkDropoutStrategy, self).reset_common_infra(common_infra)
|
||||
|
||||
if "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
def get_risk_degree(self, trade_index=None):
|
||||
"""get_risk_degree
|
||||
Return the proportion of your total value you will used in investment.
|
||||
@@ -69,11 +81,11 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
# It will use 95% amoutn of your total value by default
|
||||
return self.risk_degree
|
||||
|
||||
def generate_order_list(self, execute_state):
|
||||
super(TopkDropoutStrategy, self).step()
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
pred_start_time, pred_end_time = self._get_calendar_time(self.trade_index, shift=1)
|
||||
pred_score = sample_feature(self.pred_scores, start_time=pred_start_time, end_time=pred_end_time, method="last")
|
||||
def generate_trade_decision(self, execute_state):
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
pred_start_time, pred_end_time = self.trade_calendar.get_calendar_time(trade_index, shift=1)
|
||||
pred_score = resam_ts_data(self.pred_scores, start_time=pred_start_time, end_time=pred_end_time, method="last")
|
||||
if pred_score is None:
|
||||
return []
|
||||
if self.only_tradable:
|
||||
@@ -115,8 +127,7 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
def filter_stock(l):
|
||||
return l
|
||||
|
||||
current = execute_state.get("current")
|
||||
current_temp = copy.deepcopy(current)
|
||||
current_temp = copy.deepcopy(self.trade_position)
|
||||
# generate order list for this adjust date
|
||||
sell_order_list = []
|
||||
buy_order_list = []
|
||||
@@ -168,7 +179,8 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
continue
|
||||
if code in sell:
|
||||
# check hold limit
|
||||
if current_temp.get_stock_count(code, bar=self.step_bar) < self.hold_thresh:
|
||||
step_bar = self.trade_calendar.get_step_bar()
|
||||
if current_temp.get_stock_count(code, bar=step_bar) < self.hold_thresh:
|
||||
continue
|
||||
# sell order
|
||||
sell_amount = current_temp.get_stock_amount(code=code)
|
||||
@@ -228,22 +240,35 @@ class TopkDropoutStrategy(ModelStrategy):
|
||||
class WeightStrategyBase(ModelStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
model,
|
||||
dataset,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
order_generator_cls_or_obj=OrderGenWInteract,
|
||||
trade_exchange=None,
|
||||
level_infra={},
|
||||
common_infra={},
|
||||
**kwargs,
|
||||
):
|
||||
super(WeightStrategyBase, self).__init__(step_bar, model, dataset, start_time, end_time, **kwargs)
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
super(WeightStrategyBase, self).__init__(
|
||||
model, dataset, level_infra=level_infra, common_infra=common_infra, **kwargs
|
||||
)
|
||||
if isinstance(order_generator_cls_or_obj, type):
|
||||
self.order_generator = order_generator_cls_or_obj()
|
||||
else:
|
||||
self.order_generator = order_generator_cls_or_obj
|
||||
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
common_infra : dict, optional
|
||||
common infrastructure for backtesting, by default None
|
||||
- It should include `trade_account`, used to get position
|
||||
- It should include `trade_exchange`, used to provide market info
|
||||
"""
|
||||
super(WeightStrategyBase, self).reset_common_infra(common_infra)
|
||||
|
||||
if "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
def get_risk_degree(self, trade_index=None):
|
||||
"""get_risk_degree
|
||||
Return the proportion of your total value you will used in investment.
|
||||
@@ -267,7 +292,7 @@ class WeightStrategyBase(ModelStrategy):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_order_list(self, execute_state):
|
||||
def generate_trade_decision(self, execute_state):
|
||||
"""
|
||||
Parameters
|
||||
-----------
|
||||
@@ -280,23 +305,22 @@ class WeightStrategyBase(ModelStrategy):
|
||||
trade_date : pd.Timestamp
|
||||
date.
|
||||
"""
|
||||
# generate_order_list
|
||||
# generate_trade_decision
|
||||
# generate_target_weight_position() and generate_order_list_from_target_weight_position() to generate order_list
|
||||
super(WeightStrategyBase, self).step()
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
pred_start_time, pred_end_time = self._get_calendar_time(self.trade_index, shift=1)
|
||||
pred_score = sample_feature(self.pred_scores, start_time=pred_start_time, end_time=pred_end_time, method="last")
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
pred_start_time, pred_end_time = self.trade_calendar.get_calendar_time(trade_index, shift=1)
|
||||
pred_score = resam_ts_data(self.pred_scores, start_time=pred_start_time, end_time=pred_end_time, method="last")
|
||||
if pred_score is None:
|
||||
return []
|
||||
current = execute_state.get("current")
|
||||
current_temp = copy.deepcopy(current)
|
||||
current_temp = copy.deepcopy(self.trade_position)
|
||||
target_weight_position = self.generate_target_weight_position(
|
||||
score=pred_score, current=current_temp, trade_start_time=trade_start_time, trade_end_time=trade_end_time
|
||||
)
|
||||
order_list = self.order_generator.generate_order_list_from_target_weight_position(
|
||||
current=current_temp,
|
||||
trade_exchange=self.trade_exchange,
|
||||
risk_degree=self.get_risk_degree(self.trade_index),
|
||||
risk_degree=self.get_risk_degree(trade_index),
|
||||
target_weight_position=target_weight_position,
|
||||
pred_start_time=pred_start_time,
|
||||
pred_end_time=pred_end_time,
|
||||
|
||||
@@ -1,80 +1,77 @@
|
||||
import copy
|
||||
import warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Union
|
||||
|
||||
from ...utils.sample import sample_feature
|
||||
from ...utils.resam import resam_ts_data
|
||||
from ...data.data import D
|
||||
from ...data.dataset.utils import convert_index_format
|
||||
from ...strategy.base import RuleStrategy, OrderEnhancement
|
||||
from ...strategy.base import RuleStrategy
|
||||
from ..backtest.order import Order
|
||||
from ..backtest.faculty import common_faculty
|
||||
|
||||
|
||||
class TWAPStrategy(RuleStrategy, OrderEnhancement):
|
||||
class TWAPStrategy(RuleStrategy):
|
||||
"""TWAP Strategy for trading"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_exchange=None,
|
||||
trade_order_list=[],
|
||||
**kwargs,
|
||||
):
|
||||
def reset_common_infra(self, common_infra):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
trade_exchange : Exchange, optional
|
||||
exchange that provides market info, by default None
|
||||
- If `trade_exchange` is None, self.trade_exchange will be set with common_faculty
|
||||
trade_order_list : list, optional
|
||||
order list to trade, which the strategy will trade in [start_time , end_time] , by default []
|
||||
common_infra : dict, optional
|
||||
common infrastructure for backtesting, by default None
|
||||
- It should include `trade_account`, used to get position
|
||||
- It should include `trade_exchange`, used to provide market info
|
||||
"""
|
||||
super(TWAPStrategy, self).__init__(step_bar, start_time, end_time, **kwargs)
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
self.trade_order_list = trade_order_list
|
||||
super(TWAPStrategy, self).reset_common_infra(common_infra)
|
||||
if common_infra is not None:
|
||||
if "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
def reset(self, trade_order_list: list = None, **kwargs):
|
||||
super(TWAPStrategy, self).reset(**kwargs)
|
||||
OrderEnhancement.reset(self, trade_order_list=trade_order_list)
|
||||
if trade_order_list is not None:
|
||||
def reset(self, rely_trade_decision: object = None, **kwargs):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
rely_trade_decision : object, optional
|
||||
"""
|
||||
|
||||
super(TWAPStrategy, self).reset(rely_trade_decision=rely_trade_decision, common_infra=common_infra, **kwargs)
|
||||
if rely_trade_decision is not None:
|
||||
self.trade_amount = {}
|
||||
for order in self.trade_order_list:
|
||||
for order in rely_trade_decision:
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount
|
||||
|
||||
def generate_order_list(self, execute_state):
|
||||
super(TWAPStrategy, self).step()
|
||||
trade_info = execute_state.get("trade_info")
|
||||
def generate_trade_decision(self, execute_state):
|
||||
|
||||
# update the order amount
|
||||
trade_info = execute_state
|
||||
for order, _, _, _ in trade_info:
|
||||
self.trade_amount[(order.stock_id, order.direction)] -= order.deal_amount
|
||||
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_len = self.trade_calendar.get_trade_len()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
order_list = []
|
||||
for order in self.trade_order_list:
|
||||
for order in self.rely_trade_decision:
|
||||
if not self.trade_exchange.is_stock_tradable(
|
||||
stock_id=order.stock_id, start_time=trade_start_time, end_time=trade_end_time
|
||||
):
|
||||
continue
|
||||
_amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor)
|
||||
_order_amount = None
|
||||
# consider trade unit
|
||||
if _amount_trade_unit is None:
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)] / (
|
||||
self.trade_len - self.trade_index + 1
|
||||
)
|
||||
if self.trade_amount[(order.stock_id, order.direction)] >= _amount_trade_unit:
|
||||
# split the order equally
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)] / (trade_len - trade_index + 1)
|
||||
# without considering trade unit
|
||||
elif self.trade_amount[(order.stock_id, order.direction)] >= _amount_trade_unit:
|
||||
# split the order equally
|
||||
# floor((trade_unit_cnt + trade_len - trade_index) / (trade_len - trade_index + 1)) == ceil(trade_unit_cnt / (trade_len - trade_index + 1))
|
||||
trade_unit_cnt = int(self.trade_amount[(order.stock_id, order.direction)] // _amount_trade_unit)
|
||||
_order_amount = (
|
||||
(trade_unit_cnt + self.trade_len - self.trade_index)
|
||||
// (self.trade_len - self.trade_index + 1)
|
||||
* _amount_trade_unit
|
||||
(trade_unit_cnt + trade_len - trade_index) // (trade_len - trade_index + 1) * _amount_trade_unit
|
||||
)
|
||||
|
||||
if order.direction == order.SELL:
|
||||
# sell all amount at last
|
||||
if self.trade_amount[(order.stock_id, order.direction)] > 1e-5 and (
|
||||
_order_amount is None or self.trade_index == self.trade_len
|
||||
_order_amount is None or trade_index == trade_len
|
||||
):
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)]
|
||||
|
||||
@@ -92,7 +89,7 @@ class TWAPStrategy(RuleStrategy, OrderEnhancement):
|
||||
return order_list
|
||||
|
||||
|
||||
class SBBStrategyBase(RuleStrategy, OrderEnhancement):
|
||||
class SBBStrategyBase(RuleStrategy):
|
||||
"""
|
||||
(S)elect the (B)etter one among every two adjacent trading (B)ars to sell or buy.
|
||||
"""
|
||||
@@ -101,81 +98,80 @@ class SBBStrategyBase(RuleStrategy, OrderEnhancement):
|
||||
TREND_SHORT = 1
|
||||
TREND_LONG = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_exchange=None,
|
||||
trade_order_list=[],
|
||||
**kwargs,
|
||||
):
|
||||
def reset_common_infra(self, common_infra):
|
||||
super(SBBStrategyBase, self).reset_common_infra(common_infra)
|
||||
if common_infra is not None:
|
||||
if "trade_exchange" in common_infra:
|
||||
self.trade_exchange = common_infra.get("trade_exchange")
|
||||
|
||||
def reset(self, rely_trade_decision=None, **kwargs):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
trade_exchange : Exchange, optional
|
||||
exchange that provides market info, by default None
|
||||
- If `trade_exchange` is None, self.trade_exchange will be set with common_faculty
|
||||
trade_order_list : list, optional
|
||||
order list to trade, which the strategy will trade in [start_time , end_time] , by default []
|
||||
rely_trade_decision : object, optional
|
||||
common_infra : None, optional
|
||||
common infrastructure for backtesting, by default None
|
||||
- It should include `trade_account`, used to get position
|
||||
- It should include `trade_exchange`, used to provide market info
|
||||
"""
|
||||
super(SBBStrategyBase, self).__init__(step_bar, start_time, end_time, **kwargs)
|
||||
self.trade_exchange = common_faculty.trade_exchange if trade_exchange is None else trade_exchange
|
||||
self.trade_order_list = trade_order_list
|
||||
|
||||
def reset(self, trade_order_list=None, **kwargs):
|
||||
super(SBBStrategyBase, self).reset(**kwargs)
|
||||
OrderEnhancement.reset(self, trade_order_list=trade_order_list)
|
||||
if trade_order_list is not None:
|
||||
super(SBBStrategyBase, self).reset(rely_trade_decision=rely_trade_decision, **kwargs)
|
||||
if rely_trade_decision is not None:
|
||||
self.trade_trend = {}
|
||||
self.trade_amount = {}
|
||||
for order in self.trade_order_list:
|
||||
# init the trade amount of order and predicted trade trend
|
||||
for order in rely_trade_decision:
|
||||
self.trade_trend[(order.stock_id, order.direction)] = self.TREND_MID
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount
|
||||
|
||||
def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None):
|
||||
raise NotImplementedError("pred_price_trend method is not implemented!")
|
||||
|
||||
def generate_order_list(self, execute_state):
|
||||
super(SBBStrategyBase, self).step()
|
||||
def generate_trade_decision(self, execute_state):
|
||||
|
||||
trade_info = execute_state.get("trade_info")
|
||||
# update the order amount
|
||||
trade_info = execute_state
|
||||
for order, _, _, _ in trade_info:
|
||||
self.trade_amount[(order.stock_id, order.direction)] -= order.deal_amount
|
||||
|
||||
trade_start_time, trade_end_time = self._get_calendar_time(self.trade_index)
|
||||
pred_start_time, pred_end_time = self._get_calendar_time(self.trade_index, shift=1)
|
||||
trade_index = self.trade_calendar.get_trade_index()
|
||||
trade_len = self.trade_calendar.get_trade_len()
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_calendar_time(trade_index)
|
||||
pred_start_time, pred_end_time = self.trade_calendar.get_calendar_time(trade_index, shift=1)
|
||||
order_list = []
|
||||
for order in self.trade_order_list:
|
||||
if self.trade_index % 2 == 1:
|
||||
# for each order in in self.rely_trade_decision
|
||||
for order in self.rely_trade_decision:
|
||||
# predict the price trend
|
||||
if trade_index % 2 == 1:
|
||||
_pred_trend = self._pred_price_trend(order.stock_id, pred_start_time, pred_end_time)
|
||||
else:
|
||||
_pred_trend = self.trade_trend[(order.stock_id, order.direction)]
|
||||
|
||||
# if not tradable, continue
|
||||
if not self.trade_exchange.is_stock_tradable(
|
||||
stock_id=order.stock_id, start_time=trade_start_time, end_time=trade_end_time
|
||||
):
|
||||
if self.trade_index % 2 == 1:
|
||||
if trade_index % 2 == 1:
|
||||
self.trade_trend[(order.stock_id, order.direction)] = _pred_trend
|
||||
continue
|
||||
|
||||
# get amount of one trade unit
|
||||
_amount_trade_unit = self.trade_exchange.get_amount_of_trade_unit(order.factor)
|
||||
if _pred_trend == self.TREND_MID:
|
||||
_order_amount = None
|
||||
# considering trade unit
|
||||
if _amount_trade_unit is None:
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)] / (
|
||||
self.trade_len - self.trade_index + 1
|
||||
)
|
||||
# split the order equally
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)] / (trade_len - trade_index + 1)
|
||||
# without considering trade unit
|
||||
elif self.trade_amount[(order.stock_id, order.direction)] >= _amount_trade_unit:
|
||||
# cal how many trade unit
|
||||
trade_unit_cnt = int(self.trade_amount[(order.stock_id, order.direction)] // _amount_trade_unit)
|
||||
# split the order equally
|
||||
# floor((trade_unit_cnt + trade_len - trade_index) / (trade_len - trade_index + 1)) == ceil(trade_unit_cnt / (trade_len - trade_index + 1))
|
||||
_order_amount = (
|
||||
(trade_unit_cnt + self.trade_len - self.trade_index)
|
||||
// (self.trade_len - self.trade_index + 1)
|
||||
* _amount_trade_unit
|
||||
(trade_unit_cnt + trade_len - trade_index) // (trade_len - trade_index + 1) * _amount_trade_unit
|
||||
)
|
||||
if order.direction == order.SELL:
|
||||
# sell all amount at last
|
||||
if self.trade_amount[(order.stock_id, order.direction)] > 1e-5 and (
|
||||
_order_amount is None or self.trade_index == self.trade_len
|
||||
_order_amount is None or trade_index == trade_len
|
||||
):
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)]
|
||||
|
||||
@@ -185,36 +181,43 @@ class SBBStrategyBase(RuleStrategy, OrderEnhancement):
|
||||
amount=_order_amount,
|
||||
start_time=trade_start_time,
|
||||
end_time=trade_end_time,
|
||||
direction=order.direction, # 1 for buy
|
||||
direction=order.direction,
|
||||
factor=order.factor,
|
||||
)
|
||||
order_list.append(_order)
|
||||
# print("DEBUG AMOUNT", _order_amount, self.trade_amount[(order.stock_id, order.direction)], _amount_trade_unit)
|
||||
|
||||
else:
|
||||
_order_amount = None
|
||||
# considering trade unit
|
||||
if _amount_trade_unit is None:
|
||||
# N trade day last, split the order into N + 1 parts, and trade 2 parts
|
||||
_order_amount = (
|
||||
2
|
||||
* self.trade_amount[(order.stock_id, order.direction)]
|
||||
/ (self.trade_len - self.trade_index + 2)
|
||||
2 * self.trade_amount[(order.stock_id, order.direction)] / (trade_len - trade_index + 2)
|
||||
)
|
||||
# without considering trade unit
|
||||
elif self.trade_amount[(order.stock_id, order.direction)] >= _amount_trade_unit:
|
||||
# cal how many trade unit
|
||||
trade_unit_cnt = int(self.trade_amount[(order.stock_id, order.direction)] // _amount_trade_unit)
|
||||
# N trade day last, split the order into N + 1 parts, and trade 2 parts
|
||||
_order_amount = (
|
||||
(trade_unit_cnt + self.trade_len - self.trade_index + 1)
|
||||
// (self.trade_len - self.trade_index + 2)
|
||||
(trade_unit_cnt + trade_len - trade_index + 1)
|
||||
// (trade_len - trade_index + 2)
|
||||
* 2
|
||||
* _amount_trade_unit
|
||||
)
|
||||
if order.direction == order.SELL:
|
||||
# sell all amount at last
|
||||
if self.trade_amount[(order.stock_id, order.direction)] >= 1e-5 and (
|
||||
_order_amount is None or self.trade_index == self.trade_len
|
||||
_order_amount is None or trade_index == trade_len
|
||||
):
|
||||
_order_amount = self.trade_amount[(order.stock_id, order.direction)]
|
||||
|
||||
if _order_amount:
|
||||
_order_amount = min(_order_amount, self.trade_amount[(order.stock_id, order.direction)])
|
||||
if self.trade_index % 2 == 1:
|
||||
if trade_index % 2 == 1:
|
||||
# in the first of two adjacent bar
|
||||
# if look short on the price, sell the stock more
|
||||
# if look long on the price, sell the stock more
|
||||
if (
|
||||
_pred_trend == self.TREND_SHORT
|
||||
and order.direction == order.SELL
|
||||
@@ -231,6 +234,9 @@ class SBBStrategyBase(RuleStrategy, OrderEnhancement):
|
||||
)
|
||||
order_list.append(_order)
|
||||
else:
|
||||
# in the second of two adjacent bar
|
||||
# if look short on the price, buy the stock more
|
||||
# if look long on the price, sell the stock more
|
||||
if (
|
||||
_pred_trend == self.TREND_SHORT
|
||||
and order.direction == order.BUY
|
||||
@@ -246,8 +252,8 @@ class SBBStrategyBase(RuleStrategy, OrderEnhancement):
|
||||
factor=order.factor,
|
||||
)
|
||||
order_list.append(_order)
|
||||
# print("DEBUG AMOUNT", _order_amount, self.trade_amount[(order.stock_id, order.direction)], _amount_trade_unit)
|
||||
if self.trade_index % 2 == 1:
|
||||
|
||||
if trade_index % 2 == 1:
|
||||
self.trade_trend[(order.stock_id, order.direction)] = _pred_trend
|
||||
|
||||
return order_list
|
||||
@@ -260,13 +266,11 @@ class SBBStrategyEMA(SBBStrategyBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_exchange=None,
|
||||
trade_order_list=[],
|
||||
rely_trade_decision=[],
|
||||
instruments="csi300",
|
||||
freq="day",
|
||||
level_infra={},
|
||||
common_infra={},
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -278,47 +282,49 @@ class SBBStrategyEMA(SBBStrategyBase):
|
||||
freq of EMA signal, by default "day"
|
||||
Note: `freq` may be different from `steb_bar`
|
||||
"""
|
||||
super(SBBStrategyEMA, self).__init__(step_bar, start_time, end_time, trade_exchange, trade_order_list, **kwargs)
|
||||
if instruments is None:
|
||||
warnings.warn("`instruments` is not set, will load all stocks")
|
||||
self.instruments = "all"
|
||||
if isinstance(instruments, str):
|
||||
self.instruments = D.instruments(instruments)
|
||||
self.freq = freq
|
||||
super(SBBStrategyEMA, self).__init__(rely_trade_decision, level_infra, common_infra, **kwargs)
|
||||
|
||||
def reset(self, start_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None, **kwargs):
|
||||
def _reset_signal(self):
|
||||
trade_len = self.trade_calendar.get_trade_len()
|
||||
fields = ["EMA($close, 10)-EMA($close, 20)"]
|
||||
signal_start_time, _ = self.trade_calendar.get_calendar_time(trade_index=1, shift=1)
|
||||
_, signal_end_time = self.trade_calendar.get_calendar_time(trade_index=trade_len, shift=1)
|
||||
signal_df = D.features(
|
||||
self.instruments, fields, start_time=signal_start_time, end_time=signal_end_time, freq=self.freq
|
||||
)
|
||||
signal_df = convert_index_format(signal_df)
|
||||
signal_df.columns = ["signal"]
|
||||
self.signal = {}
|
||||
for stock_id, stock_val in signal_df.groupby(level="instrument"):
|
||||
self.signal[stock_id] = stock_val
|
||||
|
||||
def reset_level_infra(self, level_infra):
|
||||
"""
|
||||
Reset EMA signal for trading
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start_time : Union[str, pd.Timestamp], optional
|
||||
start time for trading, also used to calculate the start time of EMA signal, by default None
|
||||
|
||||
end_time : Union[str, pd.Timestamp], optional
|
||||
end time for trading, also used to calculate the end time of EMA signal, by default None
|
||||
reset level-shared infra
|
||||
- After reset the trade_calendar, the signal will be changed
|
||||
"""
|
||||
super(SBBStrategyEMA, self).reset(start_time=start_time, end_time=end_time, **kwargs)
|
||||
if self.start_time and self.end_time and (start_time or end_time):
|
||||
fields = ["EMA($close, 10)-EMA($close, 20)"]
|
||||
signal_start_time, _ = self._get_calendar_time(trade_index=1, shift=1)
|
||||
_, signal_end_time = self._get_calendar_time(trade_index=self.trade_len, shift=1)
|
||||
signal_df = D.features(
|
||||
self.instruments, fields, start_time=signal_start_time, end_time=signal_end_time, freq=self.freq
|
||||
)
|
||||
signal_df = convert_index_format(signal_df)
|
||||
signal_df.columns = ["signal"]
|
||||
self.signal = {}
|
||||
for stock_id, stock_val in signal_df.groupby(level="instrument"):
|
||||
self.signal[stock_id] = stock_val
|
||||
if not hasattr(self, "level_infra"):
|
||||
self.level_infra = level_infra
|
||||
else:
|
||||
self.level_infra.update(level_infra)
|
||||
|
||||
if "trade_calendar" in level_infra:
|
||||
self.trade_calendar = level_infra.get("trade_calendar")
|
||||
self._reset_signal()
|
||||
|
||||
def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None):
|
||||
|
||||
if stock_id not in self.signal:
|
||||
return self.TREND_MID
|
||||
else:
|
||||
_sample_signal = sample_feature(
|
||||
self.signal[stock_id], pred_start_time, pred_end_time, fields="signal", method="last"
|
||||
_sample_signal = resam_ts_data(
|
||||
self.signal[stock_id]["signal"], pred_start_time, pred_end_time, method="last"
|
||||
)
|
||||
if _sample_signal is None or _sample_signal.iloc[0] == 0:
|
||||
return self.TREND_MID
|
||||
|
||||
Reference in New Issue
Block a user