mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-13 15:56:57 +08:00
fix bug
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from .order import Order
|
||||
from .account import Account
|
||||
from .position import Position
|
||||
from .exchange import Exchange
|
||||
from .report import Report
|
||||
from .backtest import backtest as backtest_func, get_date_range
|
||||
from .backtest import backtest as backtest_func
|
||||
|
||||
import copy
|
||||
import numpy as np
|
||||
import inspect
|
||||
from ...utils import init_instance_by_config
|
||||
@@ -17,86 +17,11 @@ from ...config import C
|
||||
logger = get_module_logger("backtest caller")
|
||||
|
||||
|
||||
def get_strategy(
|
||||
strategy=None,
|
||||
topk=50,
|
||||
margin=0.5,
|
||||
n_drop=5,
|
||||
risk_degree=0.95,
|
||||
str_type="dropout",
|
||||
adjust_dates=None,
|
||||
):
|
||||
"""get_strategy
|
||||
|
||||
There will be 3 ways to return a stratgy. Please follow the code.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
strategy : Strategy()
|
||||
strategy used in backtest.
|
||||
topk : int (Default value: 50)
|
||||
top-N stocks to buy.
|
||||
margin : int or float(Default value: 0.5)
|
||||
- if isinstance(margin, int):
|
||||
|
||||
sell_limit = margin
|
||||
|
||||
- else:
|
||||
|
||||
sell_limit = pred_in_a_day.count() * margin
|
||||
|
||||
buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit).
|
||||
sell_limit should be no less than topk.
|
||||
n_drop : int
|
||||
number of stocks to be replaced in each trading date.
|
||||
risk_degree: float
|
||||
0-1, 0.95 for example, use 95% money to trade.
|
||||
str_type: 'amount', 'weight' or 'dropout'
|
||||
strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class: Strategy
|
||||
an initialized strategy object
|
||||
"""
|
||||
|
||||
# There will be 3 ways to return a strategy.
|
||||
if strategy is None:
|
||||
# 1) create strategy with param `strategy`
|
||||
str_cls_dict = {
|
||||
"amount": "TopkAmountStrategy",
|
||||
"weight": "TopkWeightStrategy",
|
||||
"dropout": "TopkDropoutStrategy",
|
||||
}
|
||||
logger.info("Create new strategy ")
|
||||
from .. import strategy as strategy_pool
|
||||
|
||||
str_cls = getattr(strategy_pool, str_cls_dict.get(str_type))
|
||||
strategy = str_cls(
|
||||
topk=topk,
|
||||
buffer_margin=margin,
|
||||
n_drop=n_drop,
|
||||
risk_degree=risk_degree,
|
||||
adjust_dates=adjust_dates,
|
||||
)
|
||||
elif isinstance(strategy, (dict, str)):
|
||||
# 2) create strategy with init_instance_by_config
|
||||
logger.info("Create new strategy ")
|
||||
strategy = init_instance_by_config(strategy)
|
||||
|
||||
from ..strategy.strategy import BaseStrategy
|
||||
|
||||
# else: nothing happens. 3) Use the strategy directly
|
||||
if not isinstance(strategy, BaseStrategy):
|
||||
raise TypeError("Strategy not supported")
|
||||
return strategy
|
||||
|
||||
|
||||
def get_exchange(
|
||||
pred,
|
||||
exchange=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
codes = "all",
|
||||
subscribe_fields=[],
|
||||
open_cost=0.0015,
|
||||
close_cost=0.0025,
|
||||
@@ -104,7 +29,6 @@ def get_exchange(
|
||||
trade_unit=None,
|
||||
limit_threshold=None,
|
||||
deal_price=None,
|
||||
extract_codes=False,
|
||||
shift=1,
|
||||
):
|
||||
"""get_exchange
|
||||
@@ -128,9 +52,6 @@ def get_exchange(
|
||||
dealing price type: 'close', 'open', 'vwap'.
|
||||
limit_threshold : float
|
||||
limit move 0.1 (10%) for example, long and short with same limit.
|
||||
extract_codes: bool
|
||||
will we pass the codes extracted from the pred to the exchange.
|
||||
NOTE: This will be faster with offline qlib.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -149,176 +70,61 @@ def get_exchange(
|
||||
# handle exception for deal_price
|
||||
if deal_price[0] != "$":
|
||||
deal_price = "$" + deal_price
|
||||
if extract_codes:
|
||||
codes = sorted(pred.index.get_level_values("instrument").unique())
|
||||
else:
|
||||
codes = "all" # TODO: We must ensure that 'all.txt' includes all the stocks
|
||||
|
||||
dates = sorted(pred.index.get_level_values("datetime").unique())
|
||||
dates = np.append(dates, get_date_range(dates[-1], left_shift=1, right_shift=shift))
|
||||
|
||||
exchange = Exchange(
|
||||
trade_dates=dates,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
codes=codes,
|
||||
deal_price=deal_price,
|
||||
subscribe_fields=subscribe_fields,
|
||||
limit_threshold=limit_threshold,
|
||||
open_cost=open_cost,
|
||||
close_cost=close_cost,
|
||||
min_cost=min_cost,
|
||||
trade_unit=trade_unit,
|
||||
min_cost=min_cost,
|
||||
)
|
||||
return exchange
|
||||
return exchange
|
||||
else:
|
||||
return init_instance_by_config(exchange, accept_types=Exchange)
|
||||
|
||||
def init_env_instance_by_config(env):
|
||||
if isinstance(env, dict):
|
||||
env_config = copy.copy(env)
|
||||
if "kwargs" in env_config:
|
||||
env_kwargs = copy.copy(env_config["kwargs"])
|
||||
if "sub_env" in env_kwargs:
|
||||
env_kwargs["sub_env"] = init_env_instance_by_config(env_kwargs["sub_env"])
|
||||
if "sub_strategy" in env_kwargs:
|
||||
env_kwargs["sub_strategy"] = init_instance_by_config(env_kwargs["sub_strategy"])
|
||||
env_config["kwargs"] = env_kwargs
|
||||
return init_instance_by_config(env_config)
|
||||
else:
|
||||
return env
|
||||
|
||||
def get_executor(
|
||||
executor=None,
|
||||
trade_exchange=None,
|
||||
verbose=True,
|
||||
):
|
||||
"""get_executor
|
||||
def setup_exchange(root_instance, trade_exchange=None, force=False):
|
||||
if "trade_exchange" in inspect.getfullargspec(root_instance.__class__).args:
|
||||
if force:
|
||||
root_instance.reset(trade_exchange=trade_exchange)
|
||||
else:
|
||||
if not hasattr(root_instance, "trade_exchange") or root_instance.trade_exchange is None:
|
||||
root_instance.reset(trade_exchange=trade_exchange)
|
||||
if hasattr(root_instance, "sub_env"):
|
||||
setup_exchange(root_instance.sub_env, trade_exchange)
|
||||
if hasattr(root_instance, "sub_strategy"):
|
||||
setup_exchange(root_instance.sub_strategy, trade_exchange)
|
||||
|
||||
|
||||
def backtest(start_time, end_time, strategy, env, benchmark=None, account=1e9, **kwargs):
|
||||
trade_strategy = init_instance_by_config(strategy)
|
||||
trade_env = init_env_instance_by_config(env)
|
||||
|
||||
There will be 3 ways to return a executor. Please follow the code.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
executor : BaseExecutor
|
||||
executor used in backtest.
|
||||
trade_exchange : Exchange
|
||||
exchange used in executor
|
||||
verbose : bool
|
||||
whether to print log.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class: BaseExecutor
|
||||
an initialized BaseExecutor object
|
||||
"""
|
||||
|
||||
# There will be 3 ways to return a executor.
|
||||
if executor is None:
|
||||
# 1) create executor with param `executor`
|
||||
logger.info("Create new executor ")
|
||||
from ..online.executor import SimulatorExecutor
|
||||
|
||||
executor = SimulatorExecutor(trade_exchange=trade_exchange, verbose=verbose)
|
||||
elif isinstance(executor, (dict, str)):
|
||||
# 2) create executor with config
|
||||
logger.info("Create new executor ")
|
||||
executor = init_instance_by_config(executor)
|
||||
|
||||
from ..online.executor import BaseExecutor
|
||||
|
||||
# 3) Use the executor directly
|
||||
if not isinstance(executor, BaseExecutor):
|
||||
raise TypeError("Executor not supported")
|
||||
return executor
|
||||
|
||||
|
||||
# This is the API for compatibility for legacy code
|
||||
def backtest(pred, account=1e9, shift=1, benchmark="SH000905", verbose=True, return_order=False, **kwargs):
|
||||
"""This function will help you set a reasonable Exchange and provide default value for strategy
|
||||
Parameters
|
||||
----------
|
||||
|
||||
- **backtest workflow related or commmon arguments**
|
||||
|
||||
pred : pandas.DataFrame
|
||||
predict should has <datetime, instrument> index and one `score` column.
|
||||
account : float
|
||||
init account value.
|
||||
shift : int
|
||||
whether to shift prediction by one day.
|
||||
benchmark : str
|
||||
benchmark code, default is SH000905 CSI 500.
|
||||
verbose : bool
|
||||
whether to print log.
|
||||
return_order : bool
|
||||
whether to return order list
|
||||
|
||||
- **strategy related arguments**
|
||||
|
||||
strategy : Strategy()
|
||||
strategy used in backtest.
|
||||
topk : int (Default value: 50)
|
||||
top-N stocks to buy.
|
||||
margin : int or float(Default value: 0.5)
|
||||
- if isinstance(margin, int):
|
||||
|
||||
sell_limit = margin
|
||||
|
||||
- else:
|
||||
|
||||
sell_limit = pred_in_a_day.count() * margin
|
||||
|
||||
buffer margin, in single score_mode, continue holding stock if it is in nlargest(sell_limit).
|
||||
sell_limit should be no less than topk.
|
||||
n_drop : int
|
||||
number of stocks to be replaced in each trading date.
|
||||
risk_degree: float
|
||||
0-1, 0.95 for example, use 95% money to trade.
|
||||
str_type: 'amount', 'weight' or 'dropout'
|
||||
strategy type: TopkAmountStrategy ,TopkWeightStrategy or TopkDropoutStrategy.
|
||||
|
||||
- **exchange related arguments**
|
||||
|
||||
exchange: Exchange()
|
||||
pass the exchange for speeding up.
|
||||
subscribe_fields: list
|
||||
subscribe fields.
|
||||
open_cost : float
|
||||
open transaction cost. The default value is 0.002(0.2%).
|
||||
close_cost : float
|
||||
close transaction cost. The default value is 0.002(0.2%).
|
||||
min_cost : float
|
||||
min transaction cost.
|
||||
trade_unit : int
|
||||
100 for China A.
|
||||
deal_price: str
|
||||
dealing price type: 'close', 'open', 'vwap'.
|
||||
limit_threshold : float
|
||||
limit move 0.1 (10%) for example, long and short with same limit.
|
||||
extract_codes: bool
|
||||
will we pass the codes extracted from the pred to the exchange.
|
||||
|
||||
.. note:: This will be faster with offline qlib.
|
||||
|
||||
- **executor related arguments**
|
||||
|
||||
executor : BaseExecutor()
|
||||
executor used in backtest.
|
||||
verbose : bool
|
||||
whether to print log.
|
||||
|
||||
"""
|
||||
# check strategy:
|
||||
spec = inspect.getfullargspec(get_strategy)
|
||||
str_args = {k: v for k, v in kwargs.items() if k in spec.args}
|
||||
strategy = get_strategy(**str_args)
|
||||
|
||||
# init exchange:
|
||||
spec = inspect.getfullargspec(get_exchange)
|
||||
ex_args = {k: v for k, v in kwargs.items() if k in spec.args}
|
||||
trade_exchange = get_exchange(pred, **ex_args)
|
||||
exchange_args = {k: v for k, v in kwargs.items() if k in spec.args}
|
||||
trade_exchange = get_exchange(**exchange_args)
|
||||
|
||||
# init executor:
|
||||
executor = get_executor(executor=kwargs.get("executor"), trade_exchange=trade_exchange, verbose=verbose)
|
||||
setup_exchange(trade_env, trade_exchange)
|
||||
setup_exchange(trade_strategy, trade_exchange)
|
||||
|
||||
# run backtest
|
||||
report_dict = backtest_func(
|
||||
pred=pred,
|
||||
strategy=strategy,
|
||||
executor=executor,
|
||||
trade_exchange=trade_exchange,
|
||||
shift=shift,
|
||||
verbose=verbose,
|
||||
account=account,
|
||||
benchmark=benchmark,
|
||||
return_order=return_order,
|
||||
)
|
||||
# for compatibility of the old API. return the dict positions
|
||||
report_dict = backtest_func(start_time, end_time, trade_strategy, trade_env, benchmark, account)
|
||||
|
||||
positions = report_dict.get("positions")
|
||||
report_dict.update({"positions": {k: p.position for k, p in positions.items()}})
|
||||
return report_dict
|
||||
|
||||
@@ -26,10 +26,10 @@ rtn & earning in the Account
|
||||
|
||||
|
||||
class Account:
|
||||
def __init__(self, init_cash, last_trade_date=None):
|
||||
self.init_vars(init_cash, last_trade_date)
|
||||
def __init__(self, init_cash, last_trade_time=None):
|
||||
self.init_vars(init_cash, last_trade_time)
|
||||
|
||||
def init_vars(self, init_cash, last_trade_date=None):
|
||||
def init_vars(self, init_cash, last_trade_time=None):
|
||||
# init cash
|
||||
self.init_cash = init_cash
|
||||
self.current = Position(cash=init_cash)
|
||||
@@ -40,7 +40,7 @@ class Account:
|
||||
self.val = 0
|
||||
self.report = Report()
|
||||
self.earning = 0
|
||||
self.last_trade_date = last_trade_date
|
||||
self.last_trade_time = last_trade_time
|
||||
|
||||
def get_positions(self):
|
||||
return self.positions
|
||||
@@ -83,9 +83,10 @@ class Account:
|
||||
self.current.update_order(order, trade_val, cost, trade_price)
|
||||
self.update_state_from_order(order, trade_val, cost, trade_price)
|
||||
|
||||
def update_daily_end(self, today, trader):
|
||||
def update_bar_end(self, trade_start_time, trade_end_time, trade_exchange):
|
||||
"""
|
||||
today: pd.TimeStamp
|
||||
start_time: pd.TimeStamp
|
||||
end_time: pd.TimeStamp
|
||||
quote: pd.DataFrame (code, date), collumns
|
||||
when the end of trade date
|
||||
- update rtn
|
||||
@@ -102,11 +103,11 @@ class Account:
|
||||
profit = 0
|
||||
for code in stock_list:
|
||||
# if suspend, no new price to be updated, profit is 0
|
||||
if trader.check_stock_suspended(code, today):
|
||||
if trade_exchange.check_stock_suspended(code, trade_start_time, trade_end_time):
|
||||
continue
|
||||
today_close = trader.get_close(code, today)
|
||||
profit += (today_close - self.current.position[code]["price"]) * self.current.position[code]["amount"]
|
||||
self.current.update_stock_price(stock_id=code, price=today_close)
|
||||
bar_close = trade_exchange.get_close(code, trade_start_time, trade_end_time)
|
||||
profit += (bar_close - self.current.position[code]["price"]) * self.current.position[code]["amount"]
|
||||
self.current.update_stock_price(stock_id=code, price=bar_close)
|
||||
self.rtn += profit
|
||||
# update holding day count
|
||||
self.current.add_count_all()
|
||||
@@ -116,54 +117,54 @@ class Account:
|
||||
# account_value - last_account_value
|
||||
# for the first trade date, account_value - init_cash
|
||||
# self.report.is_empty() to judge is_first_trade_date
|
||||
# get last_account_value, today_account_value, today_stock_value
|
||||
# get last_account_value, now_account_value, now_stock_value
|
||||
if self.report.is_empty():
|
||||
last_account_value = self.init_cash
|
||||
else:
|
||||
last_account_value = self.report.get_latest_account_value()
|
||||
today_account_value = self.current.calculate_value()
|
||||
today_stock_value = self.current.calculate_stock_value()
|
||||
self.earning = today_account_value - last_account_value
|
||||
now_account_value = self.current.calculate_value()
|
||||
now_stock_value = self.current.calculate_stock_value()
|
||||
self.earning = now_account_value - last_account_value
|
||||
# update report for today
|
||||
# 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_date=today,
|
||||
account_value=today_account_value,
|
||||
trade_time=trade_start_time,
|
||||
account_value=now_account_value,
|
||||
cash=self.current.position["cash"],
|
||||
return_rate=(self.earning + self.ct) / last_account_value,
|
||||
# here use earning to calculate return, position's view, earning consider cost, true return
|
||||
# in order to make same definition with original backtest in evaluate.py
|
||||
turnover_rate=self.to / last_account_value,
|
||||
cost_rate=self.ct / last_account_value,
|
||||
stock_value=today_stock_value,
|
||||
stock_value=now_stock_value,
|
||||
)
|
||||
# set today_account_value to position
|
||||
self.current.position["today_account_value"] = today_account_value
|
||||
# set now_account_value to position
|
||||
self.current.position["now_account_value"] = now_account_value
|
||||
self.current.update_weight_all()
|
||||
# update positions
|
||||
# note use deepcopy
|
||||
self.positions[today] = copy.deepcopy(self.current)
|
||||
self.positions[trade_start_time] = copy.deepcopy(self.current)
|
||||
|
||||
# finish today's updation
|
||||
# reset the daily variables
|
||||
self.rtn = 0
|
||||
self.ct = 0
|
||||
self.to = 0
|
||||
self.last_trade_date = today
|
||||
self.last_trade_time = (trade_start_time, trade_end_time)
|
||||
|
||||
def load_account(self, account_path):
|
||||
report = Report()
|
||||
position = Position()
|
||||
last_trade_date = position.load_position(account_path / "position.xlsx")
|
||||
last_trade_time = position.load_position(account_path / "position.xlsx")
|
||||
report.load_report(account_path / "report.csv")
|
||||
|
||||
# assign values
|
||||
self.init_vars(position.init_cash)
|
||||
self.current = position
|
||||
self.report = report
|
||||
self.last_trade_date = last_trade_date if last_trade_date else None
|
||||
self.last_trade_time = last_trade_time
|
||||
|
||||
def save_account(self, account_path):
|
||||
self.current.save_position(account_path / "position.xlsx", self.last_trade_date)
|
||||
self.current.save_position(account_path / "position.xlsx", self.last_trade_time)
|
||||
self.report.save_report(account_path / "report.csv")
|
||||
|
||||
@@ -4,140 +4,24 @@
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from ...utils import get_date_by_shift, get_date_range
|
||||
from ...data import D
|
||||
|
||||
from .account import Account
|
||||
from ...config import C
|
||||
from ...log import get_module_logger
|
||||
from ...data.dataset.utils import get_level_index
|
||||
|
||||
LOG = get_module_logger("backtest")
|
||||
|
||||
|
||||
def backtest(pred, strategy, executor, trade_exchange, shift, verbose, account, benchmark, return_order):
|
||||
"""Parameters
|
||||
----------
|
||||
pred : pandas.DataFrame
|
||||
predict should has <datetime, instrument> index and one `score` column
|
||||
Qlib want to support multi-singal strategy in the future. So pd.Series is not used.
|
||||
strategy : Strategy()
|
||||
strategy part for backtest
|
||||
trade_exchange : Exchange()
|
||||
exchage for backtest
|
||||
shift : int
|
||||
whether to shift prediction by one day
|
||||
verbose : bool
|
||||
whether to print log
|
||||
account : float
|
||||
init account value
|
||||
benchmark : str/list/pd.Series
|
||||
`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
|
||||
|
||||
`benchmark` is list, will use the daily average change of the stock pool in the list as the 'bench'.
|
||||
`benchmark` is str, will use the daily change as the 'bench'.
|
||||
benchmark code, default is SH000905 CSI500
|
||||
"""
|
||||
# Convert format if the input format is not expected
|
||||
if get_level_index(pred, level="datetime") == 1:
|
||||
pred = pred.swaplevel().sort_index()
|
||||
if isinstance(pred, pd.Series):
|
||||
pred = pred.to_frame("score")
|
||||
def backtest(start_time, end_time, trade_strategy, trade_env, benchmark, account):
|
||||
|
||||
trade_account = Account(init_cash=account)
|
||||
_pred_dates = pred.index.get_level_values(level="datetime")
|
||||
predict_dates = D.calendar(start_time=_pred_dates.min(), end_time=_pred_dates.max())
|
||||
if isinstance(benchmark, pd.Series):
|
||||
bench = benchmark
|
||||
else:
|
||||
_codes = benchmark if isinstance(benchmark, list) else [benchmark]
|
||||
_temp_result = D.features(
|
||||
_codes,
|
||||
["$close/Ref($close,1)-1"],
|
||||
predict_dates[0],
|
||||
get_date_by_shift(predict_dates[-1], shift=shift),
|
||||
disk_cache=1,
|
||||
)
|
||||
if len(_temp_result) == 0:
|
||||
raise ValueError(f"The benchmark {_codes} does not exist. Please provide the right benchmark")
|
||||
bench = _temp_result.groupby(level="datetime")[_temp_result.columns.tolist()[0]].mean()
|
||||
trade_env.reset(start_time=start_time, end_time=end_time, trade_account=trade_account)
|
||||
trade_strategy.reset(start_time=start_time, end_time=end_time)
|
||||
|
||||
trade_dates = np.append(predict_dates[shift:], get_date_range(predict_dates[-1], left_shift=1, right_shift=shift))
|
||||
if return_order:
|
||||
multi_order_list = []
|
||||
# trading apart
|
||||
for pred_date, trade_date in zip(predict_dates, trade_dates):
|
||||
# for loop predict date and trading date
|
||||
# print
|
||||
if verbose:
|
||||
LOG.info("[I {:%Y-%m-%d}]: trade begin.".format(trade_date))
|
||||
|
||||
# 1. Load the score_series at pred_date
|
||||
try:
|
||||
score = pred.loc(axis=0)[pred_date, :] # (trade_date, stock_id) multi_index, score in pdate
|
||||
score_series = score.reset_index(level="datetime", drop=True)[
|
||||
"score"
|
||||
] # pd.Series(index:stock_id, data: score)
|
||||
except KeyError:
|
||||
LOG.warning("No score found on predict date[{:%Y-%m-%d}]".format(trade_date))
|
||||
score_series = None
|
||||
|
||||
if score_series is not None and score_series.count() > 0: # in case of the scores are all None
|
||||
# 2. Update your strategy (and model)
|
||||
strategy.update(score_series, pred_date, trade_date)
|
||||
|
||||
# 3. Generate order list
|
||||
order_list = strategy.generate_order_list(
|
||||
score_series=score_series,
|
||||
current=trade_account.current,
|
||||
trade_exchange=trade_exchange,
|
||||
pred_date=pred_date,
|
||||
trade_date=trade_date,
|
||||
)
|
||||
else:
|
||||
order_list = []
|
||||
if return_order:
|
||||
multi_order_list.append((trade_account, order_list, trade_date))
|
||||
# 4. Get result after executing order list
|
||||
# NOTE: The following operation will modify order.amount.
|
||||
# NOTE: If it is buy and the cash is insufficient, the tradable amount will be recalculated
|
||||
trade_info = executor.execute(trade_account, order_list, trade_date)
|
||||
|
||||
# 5. Update account information according to transaction
|
||||
update_account(trade_account, trade_info, trade_exchange, trade_date)
|
||||
|
||||
# generate backtest report
|
||||
trade_state = trade_env.get_init_state()
|
||||
while not trade_env.finished():
|
||||
_order_list = trade_strategy.generate_order_list(**trade_state)
|
||||
print("_order_list", _order_list)
|
||||
trade_state, trade_info = trade_env.execute(_order_list)
|
||||
|
||||
report_df = trade_account.report.generate_report_dataframe()
|
||||
report_df["bench"] = bench
|
||||
positions = trade_account.get_positions()
|
||||
|
||||
report_dict = {"report_df": report_df, "positions": positions}
|
||||
if return_order:
|
||||
report_dict.update({"order_list": multi_order_list})
|
||||
|
||||
return report_dict
|
||||
|
||||
|
||||
def update_account(trade_account, trade_info, trade_exchange, trade_date):
|
||||
"""Update the account and strategy
|
||||
Parameters
|
||||
----------
|
||||
trade_account : Account()
|
||||
trade_info : list of [Order(), float, float, float]
|
||||
(order, trade_val, trade_cost, trade_price), trade_info with out factor
|
||||
trade_exchange : Exchange()
|
||||
used to get the $close_price at trade_date to update account
|
||||
trade_date : pd.Timestamp
|
||||
"""
|
||||
# update account
|
||||
for [order, trade_val, trade_cost, trade_price] in trade_info:
|
||||
if order.deal_amount == 0:
|
||||
continue
|
||||
trade_account.update_order(order=order, trade_val=trade_val, cost=trade_cost, trade_price=trade_price)
|
||||
# at the end of trade date, update the account based the $close_price of stocks.
|
||||
trade_account.update_daily_end(today=trade_date, trader=trade_exchange)
|
||||
|
||||
203
qlib/contrib/backtest/env.py
Normal file
203
qlib/contrib/backtest/env.py
Normal file
@@ -0,0 +1,203 @@
|
||||
|
||||
|
||||
import re
|
||||
import json
|
||||
import copy
|
||||
import warnings
|
||||
import pathlib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from ...data.data import Cal
|
||||
from ...utils import get_sample_freq_calendar
|
||||
from .order import Order
|
||||
|
||||
|
||||
class TradeCalendarBase:
|
||||
|
||||
def _reset_trade_calendar(self, start_time, end_time):
|
||||
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:
|
||||
_calendar, freq, freq_sam = get_sample_freq_calendar(freq=self.step_bar)
|
||||
self.calendar = _calendar
|
||||
_start_time, _end_time, _start_index, _end_index = Cal.locate_index(self.start_time, self.end_time, freq=freq, freq_sam=freq_sam)
|
||||
_trade_calendar = self.calendar[_start_index: _end_index + 1]
|
||||
if _start_time != self.start_time:
|
||||
self.trade_calendar = np.hstack((self.start_time, _trade_calendar, self.end_time))
|
||||
self.start_index = _start_index - 1
|
||||
else:
|
||||
self.trade_calendar = np.hstack((_trade_calendar, self.end_time))
|
||||
self.start_index = _start_index
|
||||
self.end_index = _end_index
|
||||
self.trade_index = 0
|
||||
self.trade_len = len(self.trade_calendar)
|
||||
else:
|
||||
raise ValueError("failed to reset trade calendar, params `start_time` or `end_time` is None.")
|
||||
|
||||
def _get_trade_time(self, trade_index=1, shift=0):
|
||||
trade_index = trade_index - shift
|
||||
if 0 < trade_index < self.trade_len - 1:
|
||||
trade_start_time = self.trade_calendar[trade_index - 1]
|
||||
trade_end_time = self.trade_calendar[trade_index] - pd.Timedelta(seconds=1)
|
||||
return trade_start_time, trade_end_time
|
||||
elif trade_index == self.trade_len - 1:
|
||||
trade_start_time = self.trade_calendar[trade_index - 1]
|
||||
trade_end_time = self.trade_calendar[trade_index]
|
||||
return trade_start_time, trade_end_time
|
||||
else:
|
||||
raise RuntimeError("trade_index out of range")
|
||||
|
||||
def _get_calendar_time(self, trade_index=1, shift=1):
|
||||
trade_index = trade_index - shift
|
||||
calendar_index = self.start_index + trade_index
|
||||
return self.calendar[calendar_index - 1], self.calendar[calendar_index]
|
||||
|
||||
class BaseEnv(TradeCalendarBase):
|
||||
"""
|
||||
# Strategy framework document
|
||||
|
||||
class Env(BaseEnv):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_account=None,
|
||||
verbose=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.step_bar = step_bar
|
||||
self.verbose = verbose
|
||||
self.reset(start_time=start_time, end_time=end_time, trade_account=trade_account, **kwargs)
|
||||
|
||||
def _get_position(self):
|
||||
return self.trade_account.current
|
||||
|
||||
|
||||
def reset(self, start_time=None, end_time=None, trade_account=None, **kwargs):
|
||||
if start_time or end_time:
|
||||
self._reset_trade_calendar(start_time=start_time, end_time=end_time)
|
||||
if trade_account:
|
||||
self.trade_account = trade_account
|
||||
|
||||
for k, v in kwargs:
|
||||
if hasattr(self, k):
|
||||
setattr(self, k, v)
|
||||
|
||||
def get_init_state(self):
|
||||
init_state = {"current": self._get_position()}
|
||||
return init_state
|
||||
|
||||
|
||||
def execute(self, order_list=None, **kwargs):
|
||||
self.trade_index = self.trade_index + 1
|
||||
|
||||
def finished(self):
|
||||
return self.trade_index >= self.trade_len - 1
|
||||
|
||||
|
||||
class SplitEnv(BaseEnv):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
sub_env,
|
||||
sub_strategy,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_account=None,
|
||||
verbose=False,
|
||||
**kwargs
|
||||
):
|
||||
self.sub_env = sub_env
|
||||
self.sub_strategy = sub_strategy
|
||||
super(SplitEnv, self).__init__(step_bar=step_bar, start_time=start_time, end_time=end_time, trade_account=trade_account, verbose=verbose)
|
||||
|
||||
def execute(self, order_list, **kwargs):
|
||||
if self.finished():
|
||||
raise StopIteration(f"this env has completed its task, please reset it if you want to call it!")
|
||||
#if self.track:
|
||||
# yield action
|
||||
#episode_reward = 0
|
||||
super(SplitEnv, self).execute(**kwargs)
|
||||
trade_start_time, trade_end_time = self._get_trade_time(trade_index=self.trade_index)
|
||||
self.sub_env.reset(start_time=trade_start_time, end_time=trade_end_time, trade_account=self.trade_account)
|
||||
self.sub_strategy.reset(start_time=trade_start_time, end_time=trade_end_time, trade_order_list=order_list)
|
||||
trade_state = self.sub_env.get_init_state()
|
||||
while not self.sub_env.finished():
|
||||
_order_list = self.sub_strategy.generate_order_list(**trade_state)
|
||||
trade_state, trade_info = self.sub_env.execute(order_list=_order_list)
|
||||
#episode_reward += sub_reward
|
||||
_obs = {"current": self._get_position()}
|
||||
_info = {}
|
||||
return _obs, _info
|
||||
|
||||
|
||||
|
||||
class SimulatorEnv(BaseEnv):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
trade_account=None,
|
||||
trade_exchange=None,
|
||||
verbose=False,
|
||||
**kwargs,
|
||||
):
|
||||
super(SimulatorEnv, self).__init__(step_bar=step_bar, start_time=start_time, end_time=end_time, trade_account=trade_account, trade_exchange=trade_exchange, verbose=verbose, **kwargs)
|
||||
|
||||
def reset(self, trade_exchange=None, **kwargs):
|
||||
super(SimulatorEnv, self).reset(**kwargs)
|
||||
if trade_exchange:
|
||||
self.trade_exchange=trade_exchange
|
||||
|
||||
def execute(self, order_list, **kwargs):
|
||||
"""
|
||||
Return: obs, done, info
|
||||
"""
|
||||
if self.finished():
|
||||
raise StopIteration(f"this env has completed its task, please reset it if you want to call it!")
|
||||
super(SimulatorEnv, self).execute(**kwargs)
|
||||
trade_start_time, trade_end_time = self._get_trade_time(trade_index=self.trade_index)
|
||||
trade_info = []
|
||||
for order in order_list:
|
||||
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))
|
||||
if self.verbose:
|
||||
if order.direction == Order.SELL: # sell
|
||||
print(
|
||||
"[I {:%Y-%m-%d}]: sell {}, price {:.2f}, amount {}, value {:.2f}.".format(
|
||||
trade_start_time,
|
||||
order.stock_id,
|
||||
trade_price,
|
||||
order.deal_amount,
|
||||
trade_val,
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"[I {:%Y-%m-%d}]: buy {}, price {:.2f}, amount {}, value {:.2f}.".format(
|
||||
trade_start_time,
|
||||
order.stock_id,
|
||||
trade_price,
|
||||
order.deal_amount,
|
||||
trade_val,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
if self.verbose:
|
||||
print("[W {:%Y-%m-%d}]: {} wrong.".format(trade_start_time, order.stock_id))
|
||||
# do nothing
|
||||
pass
|
||||
self.trade_account.update_bar_end(trade_start_time=trade_start_time, trade_end_time=trade_end_time, trade_exchange=self.trade_exchange)
|
||||
_obs = {"current": self._get_position()}
|
||||
_info = {"trade_info": trade_info}
|
||||
return _obs, _info
|
||||
@@ -8,16 +8,19 @@ import logging
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from ...data import D
|
||||
from .order import Order
|
||||
from ...data.data import D
|
||||
from ...config import C, REG_CN
|
||||
from ...utils import sample_feature
|
||||
from ...log import get_module_logger
|
||||
from .order import Order
|
||||
|
||||
|
||||
|
||||
class Exchange:
|
||||
def __init__(
|
||||
self,
|
||||
trade_dates=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
codes="all",
|
||||
deal_price=None,
|
||||
subscribe_fields=[],
|
||||
@@ -30,7 +33,8 @@ class Exchange:
|
||||
):
|
||||
"""__init__
|
||||
|
||||
:param trade_dates: list of pd.Timestamp
|
||||
:param start_time: start time for backtest
|
||||
:param end_time: 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
|
||||
@@ -51,6 +55,8 @@ class Exchange:
|
||||
target on this day).
|
||||
index: MultipleIndex(instrument, pd.Datetime)
|
||||
"""
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
if trade_unit is None:
|
||||
trade_unit = C.trade_unit
|
||||
if limit_threshold is None:
|
||||
@@ -91,21 +97,15 @@ class Exchange:
|
||||
self.close_cost = close_cost
|
||||
self.min_cost = min_cost
|
||||
self.limit_threshold = limit_threshold
|
||||
# TODO: the quote, trade_dates, codes are not necessray.
|
||||
# It is just for performance consideration.
|
||||
if trade_dates is not None and len(trade_dates):
|
||||
start_date, end_date = trade_dates[0], trade_dates[-1]
|
||||
else:
|
||||
self.logger.warning("trade_dates have not been assigned, all dates will be loaded")
|
||||
start_date, end_date = None, None
|
||||
|
||||
|
||||
self.extra_quote = extra_quote
|
||||
self.set_quote(codes, start_date, end_date)
|
||||
self.set_quote(codes, start_time, end_time)
|
||||
|
||||
def set_quote(self, codes, start_date, end_date):
|
||||
def set_quote(self, codes, start_time, end_time):
|
||||
if len(codes) == 0:
|
||||
codes = D.instruments()
|
||||
self.quote = D.features(codes, self.all_fields, start_date, end_date, disk_cache=True).dropna(subset=["$close"])
|
||||
self.quote = D.features(codes, self.all_fields, start_time, end_time, disk_cache=True).dropna(subset=["$close"])
|
||||
self.quote.columns = self.all_fields
|
||||
|
||||
if self.quote[self.deal_price].isna().any():
|
||||
@@ -146,35 +146,37 @@ class Exchange:
|
||||
quote_df = pd.concat([quote_df, self.extra_quote], sort=False, axis=0)
|
||||
|
||||
# update quote: pd.DataFrame to dict, for search use
|
||||
self.quote = quote_df.to_dict("index")
|
||||
self.quote = quote_df
|
||||
|
||||
def _update_limit(self, buy_limit, sell_limit):
|
||||
self.quote["limit"] = ~self.quote["$change"].between(-sell_limit, buy_limit, inclusive=False)
|
||||
|
||||
def check_stock_limit(self, stock_id, trade_date):
|
||||
def check_stock_limit(self, stock_id, start_time, end_time):
|
||||
"""Parameter
|
||||
stock_id
|
||||
trade_date
|
||||
is limtited
|
||||
"""
|
||||
return self.quote[(stock_id, trade_date)]["limit"]
|
||||
return sample_feature(self.quote, stock_id, start_time, end_time, fields="limit", method="any").iloc[0]
|
||||
|
||||
|
||||
def check_stock_suspended(self, stock_id, trade_date):
|
||||
def check_stock_suspended(self, stock_id, start_time, end_time):
|
||||
# is suspended
|
||||
return (stock_id, trade_date) not in self.quote
|
||||
return sample_feature(self.quote, stock_id, start_time, end_time).empty
|
||||
|
||||
def is_stock_tradable(self, stock_id, trade_date):
|
||||
|
||||
def is_stock_tradable(self, stock_id, start_time, end_time):
|
||||
# check if stock can be traded
|
||||
# same as check in check_order
|
||||
if self.check_stock_suspended(stock_id, trade_date) or self.check_stock_limit(stock_id, trade_date):
|
||||
if self.check_stock_suspended(stock_id, start_time, end_time) or self.check_stock_limit(stock_id, start_time, end_time):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def check_order(self, order):
|
||||
# check limit and suspended
|
||||
if self.check_stock_suspended(order.stock_id, order.trade_date) or self.check_stock_limit(
|
||||
order.stock_id, order.trade_date
|
||||
if self.check_stock_suspended(order.stock_id, order.start_time, order.end_time) or self.check_stock_limit(
|
||||
order.stock_id, order.start_time, order.end_time
|
||||
):
|
||||
return False
|
||||
else:
|
||||
@@ -199,7 +201,7 @@ class Exchange:
|
||||
if trade_account is not None and position is not None:
|
||||
raise ValueError("trade_account and position can only choose one")
|
||||
|
||||
trade_price = self.get_deal_price(order.stock_id, order.trade_date)
|
||||
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time)
|
||||
trade_val, trade_cost = self._calc_trade_info_by_order(
|
||||
order, trade_account.current if trade_account else position
|
||||
)
|
||||
@@ -214,24 +216,24 @@ class Exchange:
|
||||
|
||||
return trade_val, trade_cost, trade_price
|
||||
|
||||
def get_quote_info(self, stock_id, trade_date):
|
||||
return self.quote[(stock_id, trade_date)]
|
||||
def get_quote_info(self, stock_id, start_time, end_time):
|
||||
return sample_feature(self.quote, stock_id, start_time, end_time)
|
||||
|
||||
def get_close(self, stock_id, trade_date):
|
||||
return self.quote[(stock_id, trade_date)]["$close"]
|
||||
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]
|
||||
|
||||
def get_deal_price(self, stock_id, trade_date):
|
||||
deal_price = self.quote[(stock_id, trade_date)][self.deal_price]
|
||||
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]
|
||||
if np.isclose(deal_price, 0.0) or np.isnan(deal_price):
|
||||
self.logger.warning(f"(stock_id:{stock_id}, trade_date:{trade_date}, {self.deal_price}): {deal_price}!!!")
|
||||
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {self.deal_price}): {deal_price}!!!")
|
||||
self.logger.warning(f"setting deal_price to close price")
|
||||
deal_price = self.get_close(stock_id, trade_date)
|
||||
deal_price = self.get_close(stock_id, start_time, end_time)
|
||||
return deal_price
|
||||
|
||||
def get_factor(self, stock_id, trade_date):
|
||||
return self.quote[(stock_id, trade_date)]["$factor"]
|
||||
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]
|
||||
|
||||
def generate_amount_position_from_weight_position(self, weight_position, cash, trade_date):
|
||||
def generate_amount_position_from_weight_position(self, weight_position, cash, start_time, end_time):
|
||||
"""
|
||||
The generate the target position according to the weight and the cash.
|
||||
NOTE: All the cash will assigned to the tadable stock.
|
||||
@@ -246,7 +248,7 @@ class Exchange:
|
||||
# calculate the total weight of tradable value
|
||||
tradable_weight = 0.0
|
||||
for stock_id in weight_position:
|
||||
if self.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):
|
||||
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
|
||||
if weight_position[stock_id] < 0 or weight_position[stock_id] > 1:
|
||||
raise ValueError(
|
||||
@@ -260,12 +262,12 @@ class Exchange:
|
||||
|
||||
amount_dict = {}
|
||||
for stock_id in weight_position:
|
||||
if weight_position[stock_id] > 0.0 and self.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):
|
||||
if weight_position[stock_id] > 0.0 and self.is_stock_tradable(stock_id=stock_id, start_time=start_time, end_time=end_time):
|
||||
amount_dict[stock_id] = (
|
||||
cash
|
||||
* weight_position[stock_id]
|
||||
/ tradable_weight
|
||||
// self.get_deal_price(stock_id=stock_id, trade_date=trade_date)
|
||||
// self.get_deal_price(stock_id=stock_id, start_time=start_time, end_time=end_time)
|
||||
)
|
||||
return amount_dict
|
||||
|
||||
@@ -292,7 +294,7 @@ class Exchange:
|
||||
deal_amount = self.round_amount_by_trade_unit(deal_amount, factor)
|
||||
return -deal_amount
|
||||
|
||||
def generate_order_for_target_amount_position(self, target_position, current_position, trade_date):
|
||||
def generate_order_for_target_amount_position(self, target_position, current_position, start_time, end_time):
|
||||
"""Parameter:
|
||||
target_position : dict { stock_id : amount }
|
||||
current_postion : dict { stock_id : amount}
|
||||
@@ -315,12 +317,12 @@ class Exchange:
|
||||
for stock_id in sorted_ids:
|
||||
|
||||
# Do not generate order for the nontradable stocks
|
||||
if not self.is_stock_tradable(stock_id=stock_id, trade_date=trade_date):
|
||||
if not self.is_stock_tradable(stock_id=stock_id, start_time=start_time, end_time=end_time):
|
||||
continue
|
||||
|
||||
target_amount = target_position.get(stock_id, 0)
|
||||
current_amount = current_position.get(stock_id, 0)
|
||||
factor = self.quote[(stock_id, trade_date)]["$factor"]
|
||||
factor = self.get_factor(stock_id, start_time=start_time, end_time=end_time)
|
||||
|
||||
deal_amount = self.get_real_deal_amount(current_amount, target_amount, factor)
|
||||
if deal_amount == 0:
|
||||
@@ -332,7 +334,8 @@ class Exchange:
|
||||
stock_id=stock_id,
|
||||
amount=deal_amount,
|
||||
direction=Order.BUY,
|
||||
trade_date=trade_date,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
factor=factor,
|
||||
)
|
||||
)
|
||||
@@ -343,14 +346,15 @@ class Exchange:
|
||||
stock_id=stock_id,
|
||||
amount=abs(deal_amount),
|
||||
direction=Order.SELL,
|
||||
trade_date=trade_date,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
factor=factor,
|
||||
)
|
||||
)
|
||||
# return order_list : buy + sell
|
||||
return sell_order_list + buy_order_list
|
||||
|
||||
def calculate_amount_position_value(self, amount_dict, trade_date, only_tradable=False):
|
||||
def calculate_amount_position_value(self, amount_dict, start_time, end_time, only_tradable=False):
|
||||
"""Parameter
|
||||
position : Position()
|
||||
amount_dict : {stock_id : amount}
|
||||
@@ -358,10 +362,10 @@ class Exchange:
|
||||
value = 0
|
||||
for stock_id in amount_dict:
|
||||
if (
|
||||
self.check_stock_suspended(stock_id=stock_id, trade_date=trade_date) is False
|
||||
and self.check_stock_limit(stock_id=stock_id, trade_date=trade_date) is False
|
||||
self.check_stock_suspended(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
||||
and self.check_stock_limit(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
||||
):
|
||||
value += self.get_deal_price(stock_id=stock_id, trade_date=trade_date) * amount_dict[stock_id]
|
||||
value += self.get_deal_price(stock_id=stock_id, start_time=start_time, end_time=end_time) * amount_dict[stock_id]
|
||||
return value
|
||||
|
||||
def round_amount_by_trade_unit(self, deal_amount, factor):
|
||||
@@ -384,7 +388,7 @@ class Exchange:
|
||||
:return: trade_val, trade_cost
|
||||
"""
|
||||
|
||||
trade_price = self.get_deal_price(order.stock_id, order.trade_date)
|
||||
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time)
|
||||
if order.direction == Order.SELL:
|
||||
# sell
|
||||
if position is not None:
|
||||
|
||||
15
qlib/contrib/backtest/interpreter.py
Normal file
15
qlib/contrib/backtest/interpreter.py
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
class BaseInterpreter:
|
||||
@staticmethod
|
||||
def interpret(**kwargs):
|
||||
raise NotImplementedError("interpret is not implemented!")
|
||||
|
||||
class ActionInterpreter:
|
||||
@staticmethod
|
||||
def interpret(action, **kwargs):
|
||||
return action
|
||||
|
||||
class StateInterpreter:
|
||||
@staticmethod
|
||||
def interpret(state, **kwargs):
|
||||
return state
|
||||
@@ -7,7 +7,7 @@ class Order:
|
||||
SELL = 0
|
||||
BUY = 1
|
||||
|
||||
def __init__(self, stock_id, amount, trade_date, direction, factor):
|
||||
def __init__(self, stock_id, amount, start_time, end_time, direction, factor):
|
||||
"""Parameter
|
||||
direction : Order.SELL for sell; Order.BUY for buy
|
||||
stock_id : str
|
||||
@@ -24,6 +24,7 @@ class Order:
|
||||
self.amount = amount
|
||||
# amount of successfully completed orders
|
||||
self.deal_amount = 0
|
||||
self.trade_date = trade_date
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
self.direction = direction
|
||||
self.factor = factor
|
||||
|
||||
@@ -28,13 +28,13 @@ a typical example is :{
|
||||
class Position:
|
||||
"""Position"""
|
||||
|
||||
def __init__(self, cash=0, position_dict={}, today_account_value=0):
|
||||
def __init__(self, cash=0, position_dict={}, now_account_value=0):
|
||||
# NOTE: The position dict must be copied!!!
|
||||
# Otherwise the initial value
|
||||
self.init_cash = cash
|
||||
self.position = position_dict.copy()
|
||||
self.position["cash"] = cash
|
||||
self.position["today_account_value"] = today_account_value
|
||||
self.position["now_account_value"] = now_account_value
|
||||
|
||||
def init_stock(self, stock_id, amount, price=None):
|
||||
self.position[stock_id] = {}
|
||||
@@ -82,7 +82,7 @@ class Position:
|
||||
# SELL
|
||||
self.sell_stock(order.stock_id, trade_val, cost, trade_price)
|
||||
else:
|
||||
raise NotImplementedError("do not suppotr order direction {}".format(order.direction))
|
||||
raise NotImplementedError("do not support order direction {}".format(order.direction))
|
||||
|
||||
def update_stock_price(self, stock_id, price):
|
||||
self.position[stock_id]["price"] = price
|
||||
@@ -109,7 +109,7 @@ class Position:
|
||||
return value
|
||||
|
||||
def get_stock_list(self):
|
||||
stock_list = list(set(self.position.keys()) - {"cash", "today_account_value"})
|
||||
stock_list = list(set(self.position.keys()) - {"cash", "now_account_value"})
|
||||
return stock_list
|
||||
|
||||
def get_stock_price(self, code):
|
||||
@@ -163,16 +163,17 @@ class Position:
|
||||
for stock_code, weight in weight_dict.items():
|
||||
self.update_stock_weight(stock_code, weight)
|
||||
|
||||
def save_position(self, path, last_trade_date):
|
||||
def save_position(self, path, last_trade_time):
|
||||
path = pathlib.Path(path)
|
||||
p = copy.deepcopy(self.position)
|
||||
cash = pd.Series(dtype=np.float)
|
||||
cash["init_cash"] = self.init_cash
|
||||
cash["cash"] = p["cash"]
|
||||
cash["today_account_value"] = p["today_account_value"]
|
||||
cash["last_trade_date"] = str(last_trade_date.date()) if last_trade_date else None
|
||||
cash["now_account_value"] = p["now_account_value"]
|
||||
cash["last_trade_start_time"] = str(last_trade_time[0]) if last_trade_time else None
|
||||
cash["last_trade_end_time"] = str(last_trade_time[1]) if last_trade_time else None
|
||||
del p["cash"]
|
||||
del p["today_account_value"]
|
||||
del p["now_account_value"]
|
||||
positions = pd.DataFrame.from_dict(p, orient="index")
|
||||
with pd.ExcelWriter(path) as writer:
|
||||
positions.to_excel(writer, sheet_name="position")
|
||||
@@ -189,10 +190,10 @@ class Position:
|
||||
'weight': <the security weight of total position value>,
|
||||
|
||||
sheet "cash"
|
||||
index: ['init_cash', 'cash', 'today_account_value']
|
||||
index: ['init_cash', 'cash', 'now_account_value']
|
||||
'init_cash': <inital cash when account was created>,
|
||||
'cash': <current cash in account>,
|
||||
'today_account_value': <current total account value, should equal to sum(price[stock]*amount[stock])>
|
||||
'now_account_value': <current total account value, should equal to sum(price[stock]*amount[stock])>
|
||||
"""
|
||||
path = pathlib.Path(path)
|
||||
positions = pd.read_excel(open(path, "rb"), sheet_name="position", index_col=0)
|
||||
@@ -200,14 +201,17 @@ class Position:
|
||||
positions = positions.to_dict(orient="index")
|
||||
init_cash = cash_record.loc["init_cash"].values[0]
|
||||
cash = cash_record.loc["cash"].values[0]
|
||||
today_account_value = cash_record.loc["today_account_value"].values[0]
|
||||
last_trade_date = cash_record.loc["last_trade_date"].values[0]
|
||||
now_account_value = cash_record.loc["now_account_value"].values[0]
|
||||
last_trade_start_time = cash_record.loc["last_trade_start_time"].values[0]
|
||||
last_trade_end_time = cash_record.loc["last_trade_end_time"].values[0]
|
||||
|
||||
# assign values
|
||||
self.position = {}
|
||||
self.init_cash = init_cash
|
||||
self.position = positions
|
||||
self.position["cash"] = cash
|
||||
self.position["today_account_value"] = today_account_value
|
||||
self.position["now_account_value"] = now_account_value
|
||||
|
||||
return None if pd.isna(last_trade_date) else pd.Timestamp(last_trade_date)
|
||||
last_trade_start_time = None if pd.isna(last_trade_start_time) else pd.Timestamp(last_trade_start_time)
|
||||
last_trade_end_time = None if pd.isna(last_trade_end_time) else pd.Timestamp(last_trade_end_time)
|
||||
return last_trade_start_time, last_trade_end_time
|
||||
|
||||
@@ -21,20 +21,20 @@ class Report:
|
||||
self.costs = OrderedDict() # trade cost for each trade date
|
||||
self.values = OrderedDict() # value for each trade date
|
||||
self.cashes = OrderedDict()
|
||||
self.latest_report_date = None # pd.TimeStamp
|
||||
self.latest_report_time = None # pd.TimeStamp
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.accounts) == 0
|
||||
|
||||
def get_latest_date(self):
|
||||
return self.latest_report_date
|
||||
return self.latest_report_time
|
||||
|
||||
def get_latest_account_value(self):
|
||||
return self.accounts[self.latest_report_date]
|
||||
return self.accounts[self.latest_report_time]
|
||||
|
||||
def update_report_record(
|
||||
self,
|
||||
trade_date=None,
|
||||
trade_time=None,
|
||||
account_value=None,
|
||||
cash=None,
|
||||
return_rate=None,
|
||||
@@ -44,7 +44,7 @@ class Report:
|
||||
):
|
||||
# check data
|
||||
if None in [
|
||||
trade_date,
|
||||
trade_time,
|
||||
account_value,
|
||||
cash,
|
||||
return_rate,
|
||||
@@ -56,14 +56,14 @@ class Report:
|
||||
"None in [trade_date, account_value, cash, return_rate, turnover_rate, cost_rate, stock_value]"
|
||||
)
|
||||
# update report data
|
||||
self.accounts[trade_date] = account_value
|
||||
self.returns[trade_date] = return_rate
|
||||
self.turnovers[trade_date] = turnover_rate
|
||||
self.costs[trade_date] = cost_rate
|
||||
self.values[trade_date] = stock_value
|
||||
self.cashes[trade_date] = cash
|
||||
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
|
||||
# update latest_report_date
|
||||
self.latest_report_date = trade_date
|
||||
self.latest_report_time = trade_time
|
||||
# finish daily report update
|
||||
|
||||
def generate_report_dataframe(self):
|
||||
@@ -74,7 +74,7 @@ class Report:
|
||||
report["cost"] = pd.Series(self.costs)
|
||||
report["value"] = pd.Series(self.values)
|
||||
report["cash"] = pd.Series(self.cashes)
|
||||
report.index.name = "date"
|
||||
report.index.name = "trade_time"
|
||||
return report
|
||||
|
||||
def save_report(self, path):
|
||||
@@ -94,13 +94,13 @@ class Report:
|
||||
|
||||
index = r.index
|
||||
self.init_vars()
|
||||
for date in index:
|
||||
for trade_time in index:
|
||||
self.update_report_record(
|
||||
trade_date=date,
|
||||
account_value=r.loc[date]["account"],
|
||||
cash=r.loc[date]["cash"],
|
||||
return_rate=r.loc[date]["return"],
|
||||
turnover_rate=r.loc[date]["turnover"],
|
||||
cost_rate=r.loc[date]["cost"],
|
||||
stock_value=r.loc[date]["value"],
|
||||
trade_time=trade_time,
|
||||
account_value=r.loc[trade_time]["account"],
|
||||
cash=r.loc[trade_time]["cash"],
|
||||
return_rate=r.loc[trade_time]["return"],
|
||||
turnover_rate=r.loc[trade_time]["turnover"],
|
||||
cost_rate=r.loc[trade_time]["cost"],
|
||||
stock_value=r.loc[trade_time]["value"],
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
from .dl_strategy import (
|
||||
TopkDropoutStrategy,
|
||||
BaseStrategy,
|
||||
WeightStrategyBase,
|
||||
)
|
||||
|
||||
from .rule_strategy import(
|
||||
TWAPStrategy,
|
||||
SBBEMAStrategy
|
||||
SBBStrategyBase,
|
||||
SBBStrategyEMA,
|
||||
)
|
||||
|
||||
from .cost_control import (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
from .strategy import WeightStrategyBase
|
||||
from .dl_strategy import WeightStrategyBase
|
||||
import copy
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from ...utils import sample_feature
|
||||
from ...strategy.base import DLStrategy
|
||||
from ...backtest.order import Order
|
||||
from ...strategy.base import ModelStrategy
|
||||
from ..backtest.order import Order
|
||||
from .order_generator import OrderGenWInteract
|
||||
|
||||
|
||||
class TopkDropoutStrategy(DLStrategy):
|
||||
class TopkDropoutStrategy(ModelStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
@@ -53,7 +53,7 @@ class TopkDropoutStrategy(DLStrategy):
|
||||
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)
|
||||
super(TopkDropoutStrategy, self).__init__(step_bar, model, dataset, start_time, end_time, trade_exchange=trade_exchange)
|
||||
self.topk = topk
|
||||
self.n_drop = n_drop
|
||||
self.method_sell = method_sell
|
||||
@@ -67,9 +67,10 @@ class TopkDropoutStrategy(DLStrategy):
|
||||
self.only_tradable = only_tradable
|
||||
|
||||
|
||||
def reset(trade_exchange=None, **kwargs):
|
||||
def reset(self, trade_exchange=None, **kwargs):
|
||||
super(TopkDropoutStrategy, self).reset(**kwargs)
|
||||
self.trade_exchange = trade_exchange
|
||||
if trade_exchange:
|
||||
self.trade_exchange = trade_exchange
|
||||
|
||||
def get_risk_degree(self, trade_index):
|
||||
"""get_risk_degree
|
||||
@@ -189,7 +190,7 @@ class TopkDropoutStrategy(DLStrategy):
|
||||
# update cash
|
||||
cash += trade_val - trade_cost
|
||||
# sold
|
||||
del self.stock_count[code]
|
||||
self.stock_count[code] = 0
|
||||
else:
|
||||
# no buy signal, but the stock is kept
|
||||
self.stock_count[code] += 1
|
||||
@@ -210,10 +211,10 @@ class TopkDropoutStrategy(DLStrategy):
|
||||
# value = value / (1+self.trade_exchange.open_cost) # set open_cost limit
|
||||
for code in buy:
|
||||
# check is stock suspended
|
||||
if not self.trade_exchange.is_stock_tradable(stock_id=code, trade_date=trade_date):
|
||||
if not self.trade_exchange.is_stock_tradable(stock_id=code, start_time=trade_start_time, end_time=trade_end_time):
|
||||
continue
|
||||
# buy order
|
||||
buy_price = self.trade_exchange.get_deal_price(stock_id=code, trade_date=trade_date)
|
||||
buy_price = self.trade_exchange.get_deal_price(stock_id=code, start_time=trade_start_time, end_time=trade_end_time)
|
||||
buy_amount = value / buy_price
|
||||
factor = self.trade_exchange.get_factor(stock_id=code, start_time=trade_start_time, end_time=trade_end_time)
|
||||
buy_amount = self.trade_exchange.round_amount_by_trade_unit(buy_amount, factor)
|
||||
@@ -229,8 +230,8 @@ class TopkDropoutStrategy(DLStrategy):
|
||||
self.stock_count[code] = 1
|
||||
return sell_order_list + buy_order_list
|
||||
|
||||
class WeightStrategyBase(DLStrategy):
|
||||
def __init__(self, trade_exchange, order_generator_cls_or_obj=OrderGenWInteract, start_time=None, end_time=None, **kwargs):
|
||||
class WeightStrategyBase(ModelStrategy):
|
||||
def __init__(self, step_bar, start_time=None, end_time=None, order_generator_cls_or_obj=OrderGenWInteract, trade_exchange=None, **kwargs):
|
||||
super(WeightStrategyBase, self).__init__(step_bar, start_time, end_time)
|
||||
self.trade_exchange = trade_exchange
|
||||
if isinstance(order_generator_cls_or_obj, type):
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"""
|
||||
This order generator is for strategies based on WeightStrategyBase
|
||||
"""
|
||||
from ...backtest.position import Position
|
||||
from ...backtest.exchange import Exchange
|
||||
from ..backtest.position import Position
|
||||
from ..backtest.exchange import Exchange
|
||||
import pandas as pd
|
||||
import copy
|
||||
|
||||
|
||||
@@ -4,18 +4,20 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from ...utils import sample_feature
|
||||
from ...data.data import D
|
||||
from ...strategy.base import RuleStrategy, TradingEnhancement
|
||||
from ...backtest.order import Order
|
||||
from ..backtest.order import Order
|
||||
|
||||
|
||||
class TWAPStrategy(RuleStrategy, TradingEnhancement):
|
||||
|
||||
def reset(self, trade_order_list=None, **kwargs):
|
||||
super(TWAPStrategy, self).reset(**kwargs)
|
||||
TradingEnhancement.reset(trade_order_list=trade_order_list)
|
||||
self.trade_amount = {}
|
||||
for order in self.trade_order_list:
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount // self.trade_len
|
||||
TradingEnhancement.reset(self, trade_order_list=trade_order_list)
|
||||
if trade_order_list:
|
||||
self.trade_amount = {}
|
||||
for order in self.trade_order_list:
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount // self.trade_len
|
||||
|
||||
|
||||
def generate_order_list(self, **kwargs):
|
||||
@@ -43,13 +45,15 @@ class SBBStrategyBase(RuleStrategy, TradingEnhancement):
|
||||
TREND_LONG = 2
|
||||
|
||||
def reset(self, trade_order_list=None, **kwargs):
|
||||
TradingEnhancement.reset(trade_order_list=trade_order_list)
|
||||
self.trade_amount = {}
|
||||
self.trade_delay = {}
|
||||
for order in self.trade_order_list:
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount // self.trade_len
|
||||
self.trade_trend[(order.stock_id, order.direction)] = TREND_MID
|
||||
super(SBBStrategyBase, self).reset(**kwargs)
|
||||
TradingEnhancement.reset(self, trade_order_list=trade_order_list)
|
||||
if trade_order_list:
|
||||
self.trade_amount = {}
|
||||
self.trade_trend = {}
|
||||
for order in self.trade_order_list:
|
||||
self.trade_amount[(order.stock_id, order.direction)] = order.amount // self.trade_len
|
||||
self.trade_trend[(order.stock_id, order.direction)] = self.TREND_MID
|
||||
|
||||
|
||||
def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None):
|
||||
raise NotImplementedError("pred_price_trend method is not implemented!")
|
||||
@@ -64,7 +68,7 @@ class SBBStrategyBase(RuleStrategy, TradingEnhancement):
|
||||
_pred_trend = self._pred_price_trend(order.stock_id)
|
||||
else:
|
||||
_pred_trend = self.trade_trend[(order.stock_id, order.direction)]
|
||||
if _pred_trend == TREND_MID:
|
||||
if _pred_trend == self.TREND_MID:
|
||||
_order = Order(
|
||||
stock_id=order.stock_id,
|
||||
amount=self.trade_amount[(order.stock_id, order.direction)],
|
||||
@@ -97,7 +101,7 @@ class SBBStrategyBase(RuleStrategy, TradingEnhancement):
|
||||
factor=order.factor,
|
||||
)
|
||||
order_list.append(_order)
|
||||
if self.trade_index % 2 == 1
|
||||
if self.trade_index % 2 == 1:
|
||||
self.trade_trend[(order.stock_id, order.direction)] = _pred_trend
|
||||
|
||||
return order_list
|
||||
@@ -110,8 +114,8 @@ class SBBStrategyEMA(SBBStrategyBase):
|
||||
def __init__(
|
||||
self,
|
||||
step_bar,
|
||||
start_time,
|
||||
end_time,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
instruments="csi300",
|
||||
freq="day",
|
||||
**kwargs,
|
||||
@@ -121,21 +125,23 @@ class SBBStrategyEMA(SBBStrategyBase):
|
||||
warnings.warn("`instruments` is not set, will load all stocks")
|
||||
self.instruments = "all"
|
||||
if isinstance(instruments, str):
|
||||
self.instruments = D.instruments(instruments, filter_pipe=self.filter_pipe)
|
||||
self.instruments = D.instruments(instruments)
|
||||
self.freq = freq
|
||||
|
||||
|
||||
def _reset_trade_calendar(self, start_time=None, end_time=None, _calendar=None):
|
||||
super(SBBStrategyEMA, self)._reset_trade_calendar(start_time=start_time, end_time=end_time, _calendar=_calendar)
|
||||
fields = [("EMA($close, 10) - EMA($close, 20)", "signal")]
|
||||
signal_start_time, _ = self._get_calendar_time(trade_index=self.trade_index, shift=1)
|
||||
self.signal = D.features(instruments, fields, start_time=signal_start_time, end_time=self.end_time, freq=self.freq)
|
||||
def _reset_trade_calendar(self, start_time=None, end_time=None):
|
||||
super(SBBStrategyEMA, self)._reset_trade_calendar(start_time=start_time, end_time=end_time)
|
||||
if self.start_time and self.end_time:
|
||||
fields = ["EMA($close, 10)-EMA($close, 20)"]
|
||||
signal_start_time, _ = self._get_calendar_time(trade_index=self.trade_index, shift=1)
|
||||
self.signal = D.features(self.instruments, fields, start_time=signal_start_time, end_time=self.end_time, freq=self.freq)
|
||||
self.signal.columns = ["signal"]
|
||||
|
||||
def _pred_price_trend(self, stock_id, pred_start_time=None, pred_end_time=None):
|
||||
_sample_signal = sample_feature(self.signal, stock_id, start_time=pred_start_time, end_time=pred_end_time, fields="signal", method="last")
|
||||
if _sample_signal.empty:
|
||||
return SBBStrategy.TREND_MID
|
||||
elif _sample_signal.iloc[0, 0] > 0:
|
||||
return SBBStrategy.TREND_LONG
|
||||
return self.TREND_MID
|
||||
elif _sample_signal.iloc[0] > 0:
|
||||
return self.TREND_LONG
|
||||
else:
|
||||
return SBBStrategy.TREND_SHORT
|
||||
return self.TREND_SHORT
|
||||
Reference in New Issue
Block a user