1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-15 08:46:56 +08:00

fix account bug & update indicator_analysis & fix some comments

This commit is contained in:
bxdd
2021-06-22 02:42:09 +08:00
parent 9e45528165
commit 4ac6e6e246
9 changed files with 524 additions and 302 deletions

View File

@@ -17,7 +17,6 @@ class NestedDecisonExecutionWorkflow:
market = "csi300" market = "csi300"
benchmark = "SH000300" benchmark = "SH000300"
data_handler_config = { data_handler_config = {
"start_time": "2008-01-01", "start_time": "2008-01-01",
"end_time": "2021-05-28", "end_time": "2021-05-28",
@@ -67,28 +66,19 @@ class NestedDecisonExecutionWorkflow:
"kwargs": { "kwargs": {
"time_per_step": "week", "time_per_step": "week",
"inner_executor": { "inner_executor": {
"class": "NestedExecutor", "class": "SimulatorExecutor",
"module_path": "qlib.backtest.executor", "module_path": "qlib.backtest.executor",
"kwargs": { "kwargs": {
"time_per_step": "day", "time_per_step": "day",
"inner_executor": { "generate_report": True,
"class": "SimulatorExecutor", "verbose": True,
"module_path": "qlib.backtest.executor", "indicator_config": {
"kwargs": { "show_indicator": True,
"time_per_step": "15min",
"generate_report": True,
"verbose": True,
},
}, },
"inner_strategy": {
"class": "TWAPStrategy",
"module_path": "qlib.contrib.strategy.rule_strategy",
},
"show_indicator": True,
}, },
}, },
"inner_strategy": { "inner_strategy": {
"class": "VAStrategy", "class": "SBBStrategyEMA",
"module_path": "qlib.contrib.strategy.rule_strategy", "module_path": "qlib.contrib.strategy.rule_strategy",
"kwargs": { "kwargs": {
"freq": "day", "freq": "day",
@@ -96,7 +86,10 @@ class NestedDecisonExecutionWorkflow:
}, },
}, },
"track_data": True, "track_data": True,
"show_indicator": True, "generate_report": True,
"indicator_config": {
"show_indicator": True,
},
}, },
}, },
"backtest": { "backtest": {
@@ -105,7 +98,7 @@ class NestedDecisonExecutionWorkflow:
"account": 100000000, "account": 100000000,
"benchmark": benchmark, "benchmark": benchmark,
"exchange_kwargs": { "exchange_kwargs": {
"freq": "1min", "freq": "day",
"limit_threshold": 0.095, "limit_threshold": 0.095,
"deal_price": "close", "deal_price": "close",
"open_cost": 0.0005, "open_cost": 0.0005,
@@ -124,7 +117,7 @@ class NestedDecisonExecutionWorkflow:
GetData().qlib_data( GetData().qlib_data(
target_dir=provider_uri_1min, interval="1min", region=REG_CN, version="v2", exists_skip=True target_dir=provider_uri_1min, interval="1min", region=REG_CN, version="v2", exists_skip=True
) )
provider_uri_day = "/data/csdesign/qlib"
provider_uri_map = {"1min": provider_uri_1min, "day": provider_uri_day} provider_uri_map = {"1min": provider_uri_1min, "day": provider_uri_day}
client_config = { client_config = {
"calendar_provider": { "calendar_provider": {
@@ -179,12 +172,25 @@ class NestedDecisonExecutionWorkflow:
}, },
} }
self.port_analysis_config["strategy"] = strategy_config self.port_analysis_config["strategy"] = strategy_config
self.port_analysis_config["backtest"]["benchmark"] = D.list_instruments(
instruments=D.instruments(market=self.market), as_list=True
)
with R.start(experiment_name="backtest"): with R.start(experiment_name="backtest"):
recorder = R.get_recorder() recorder = R.get_recorder()
par = PortAnaRecord(recorder, self.port_analysis_config, "15minute") par = PortAnaRecord(
recorder,
self.port_analysis_config,
risk_analysis_freq=["week", "day"],
indicator_analysis_freq=["week", "day"],
indicator_analysis_method="value_weighted",
)
par.generate() par.generate()
# report_normal_df = recorder.load_object("portfolio_analysis/report_normal_1day.pkl")
# from qlib.contrib.report import analysis_position
# analysis_position.report_graph(report_normal_df)
def collect_data(self): def collect_data(self):
self._init_qlib() self._init_qlib()
model = init_instance_by_config(self.task["model"]) model = init_instance_by_config(self.task["model"])
@@ -192,6 +198,7 @@ class NestedDecisonExecutionWorkflow:
self._train_model(model, dataset) self._train_model(model, dataset)
executor_config = self.port_analysis_config["executor"] executor_config = self.port_analysis_config["executor"]
backtest_config = self.port_analysis_config["backtest"] backtest_config = self.port_analysis_config["backtest"]
backtest_config["benchmark"] = D.list_instruments(instruments=D.instruments(market=self.market), as_list=True)
strategy_config = { strategy_config = {
"class": "TopkDropoutStrategy", "class": "TopkDropoutStrategy",
"module_path": "qlib.contrib.strategy.model_strategy", "module_path": "qlib.contrib.strategy.model_strategy",

View File

@@ -116,9 +116,9 @@ def backtest(start_time, end_time, strategy, executor, benchmark="SH000300", acc
trade_strategy, trade_executor = get_strategy_executor( trade_strategy, trade_executor = get_strategy_executor(
start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs
) )
report_dict = backtest_loop(start_time, end_time, trade_strategy, trade_executor) report_dict, indicator_dict = backtest_loop(start_time, end_time, trade_strategy, trade_executor)
return report_dict return report_dict, indicator_dict
def collect_data(start_time, end_time, strategy, executor, benchmark="SH000300", account=1e9, exchange_kwargs={}): def collect_data(start_time, end_time, strategy, executor, benchmark="SH000300", account=1e9, exchange_kwargs={}):
@@ -126,6 +126,4 @@ def collect_data(start_time, end_time, strategy, executor, benchmark="SH000300",
trade_strategy, trade_executor = get_strategy_executor( trade_strategy, trade_executor = get_strategy_executor(
start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs start_time, end_time, strategy, executor, benchmark, account, exchange_kwargs
) )
report_dict = yield from collect_data_loop(start_time, end_time, trade_strategy, trade_executor) yield from collect_data_loop(start_time, end_time, trade_strategy, trade_executor)
return report_dict

View File

@@ -9,7 +9,7 @@ import pandas as pd
from .position import Position from .position import Position
from .report import Report, Indicator from .report import Report, Indicator
from .order import Order from .order import Order
from .exchange import Exchange
""" """
rtn & earning in the Account rtn & earning in the Account
@@ -25,10 +25,42 @@ rtn & earning in the Account
while earning is the difference of two position value, so it considers cost, it is the true return rate while earning is the difference of two position value, so it considers cost, it is the true return rate
in the specific accomplishment for rtn, it does not consider cost, in other words, rtn - cost = earning in the specific accomplishment for rtn, it does not consider cost, in other words, rtn - cost = earning
Now rtn has been removed in the hierarchical backtest implemention.
""" """
class AccumulatedInfo:
"""accumulated trading info, including accumulated return\cost\turnover"""
def __init__(self):
self.reset()
def reset(self):
self.rtn = 0 # accumulated return, do not consider cost
self.cost = 0 # accumulated cost
self.to = 0 # accumulated turnover
def add_return_value(self, value):
self.rtn += value
def add_cost(self, value):
self.cost += value
def add_turnover(self, value):
self.to += value
@property
def get_return(self):
return self.rtn
@property
def get_cost(self):
return self.cost
@property
def get_turnover(self):
return self.to
class Account: class Account:
def __init__(self, init_cash, freq: str = "day", benchmark_config: dict = {}): def __init__(self, init_cash, freq: str = "day", benchmark_config: dict = {}):
self.init_vars(init_cash, freq, benchmark_config) self.init_vars(init_cash, freq, benchmark_config)
@@ -38,17 +70,13 @@ class Account:
# init cash # init cash
self.init_cash = init_cash self.init_cash = init_cash
self.current = Position(cash=init_cash) self.current = Position(cash=init_cash)
self.accum_info = AccumulatedInfo()
self.reset(freq=freq, benchmark_config=benchmark_config, init_report=True) self.reset(freq=freq, benchmark_config=benchmark_config, init_report=True)
def reset_report(self, freq, benchmark_config): def reset_report(self, freq, benchmark_config):
self.report = Report(freq, benchmark_config) self.report = Report(freq, benchmark_config)
self.indicator = Indicator() self.indicator = Indicator()
self.positions = {} self.positions = {}
self.rtn = 0
self.ct = 0
self.to = 0
self.val = 0
self.earning = 0
def reset(self, freq=None, benchmark_config=None, init_report=False): def reset(self, freq=None, benchmark_config=None, init_report=False):
"""reset freq and report of account """reset freq and report of account
@@ -78,21 +106,22 @@ class Account:
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 # update turnover
self.to += trade_val self.accum_info.add_turnover(trade_val)
# update cost # update cost
self.ct += cost self.accum_info.add_cost(cost)
# update return
# update self.rtn from order # update return from order
trade_amount = trade_val / trade_price trade_amount = trade_val / trade_price
if order.direction == Order.SELL: # 0 for sell if order.direction == Order.SELL: # 0 for sell
# when sell stock, get profit from price change # when sell stock, get profit from price change
profit = trade_val - self.current.get_stock_price(order.stock_id) * trade_amount profit = trade_val - self.current.get_stock_price(order.stock_id) * trade_amount
self.rtn += profit # note here do not consider cost self.accum_info.add_return_value(profit) # note here do not consider cost
elif order.direction == Order.BUY: # 1 for buy elif order.direction == Order.BUY: # 1 for buy
# when buy stock, we get return for the rtn computing method # when buy stock, we get return for the rtn computing method
# profit in buy order is to make self.rtn is consistent with self.earning at the end of date # profit in buy order is to make rtn is consistent with earning at the end of bar
profit = self.current.get_stock_price(order.stock_id) * trade_amount - trade_val profit = self.current.get_stock_price(order.stock_id) * trade_amount - trade_val
self.rtn += profit self.accum_info.add_return_value(profit) # note here do not consider cost
def update_order(self, order, trade_val, cost, trade_price): def update_order(self, order, trade_val, cost, trade_price):
# if stock is sold out, no stock price information in Position, then we should update account first, then update current position # if stock is sold out, no stock price information in Position, then we should update account first, then update current position
@@ -111,23 +140,12 @@ class Account:
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): def update_bar_count(self):
"""at the end of the trading bar, update holding bar, count of stock"""
# update holding day count
self.current.add_count_all(bar=self.freq) self.current.add_count_all(bar=self.freq)
def update_bar_report(self, trade_start_time, trade_end_time, trade_exchange): def update_current(self, trade_start_time, trade_end_time, trade_exchange):
""" """update current to make rtn consistent with earning at the end of bar"""
trade_start_time: pd.TimeStamp
trade_end_time: pd.TimeStamp
quote: pd.DataFrame (code, date), collumns
when the end of trade date
- update rtn
- update price for each asset
- update value for this account
- update earning (2nd view of return )
- update holding day, count of stock
- update position hitory
- update report
:return: None
"""
# update price for stock in the position and the profit from changed_price # update price for stock in the position and the profit from changed_price
stock_list = self.current.get_stock_list() stock_list = self.current.get_stock_list()
for code in stock_list: for code in stock_list:
@@ -136,22 +154,28 @@ class Account:
continue continue
bar_close = trade_exchange.get_close(code, trade_start_time, trade_end_time) bar_close = trade_exchange.get_close(code, trade_start_time, trade_end_time)
self.current.update_stock_price(stock_id=code, price=bar_close) self.current.update_stock_price(stock_id=code, price=bar_close)
# update holding day count
# update value def update_report(self, trade_start_time, trade_end_time):
self.val = self.current.calculate_value() """update position history, report"""
# update earning # calculate earning
# account_value - last_account_value # account_value - last_account_value
# for the first trade date, account_value - init_cash # for the first trade date, account_value - init_cash
# self.report.is_empty() to judge is_first_trade_date # self.report.is_empty() to judge is_first_trade_date
# get last_account_value, now_account_value, now_stock_value # get last_account_value, last_total_cost, last_total_turnover
if self.report.is_empty(): if self.report.is_empty():
last_account_value = self.init_cash last_account_value = self.init_cash
last_total_cost = 0
last_total_turnover = 0
else: else:
last_account_value = self.report.get_latest_account_value() last_account_value = self.report.get_latest_account_value()
last_total_cost = self.report.get_latest_total_cost()
last_total_turnover = self.report.get_latest_total_turnover()
# get now_account_value, now_stock_value, now_earning, now_cost, now_turnover
now_account_value = self.current.calculate_value() now_account_value = self.current.calculate_value()
now_stock_value = self.current.calculate_stock_value() now_stock_value = self.current.calculate_stock_value()
self.earning = now_account_value - last_account_value now_earning = now_account_value - last_account_value
now_cost = self.accum_info.get_cost - last_total_cost
now_turnover = self.accum_info.get_turnover - last_total_turnover
# update report for today # update report for today
# judge whether the the trading is begin. # 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. # and don't add init account state into report, due to we don't have excess return in those days.
@@ -160,11 +184,13 @@ class Account:
trade_end_time=trade_end_time, trade_end_time=trade_end_time,
account_value=now_account_value, account_value=now_account_value,
cash=self.current.position["cash"], cash=self.current.position["cash"],
return_rate=(self.earning + self.ct) / last_account_value, return_rate=(now_earning + now_cost) / last_account_value,
# here use earning to calculate return, position's view, earning consider cost, true return # 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 # in order to make same definition with original backtest in evaluate.py
turnover_rate=self.to / last_account_value, total_turnover=self.accum_info.get_turnover,
cost_rate=self.ct / last_account_value, turnover_rate=now_turnover / last_account_value,
total_cost=self.accum_info.get_cost,
cost_rate=now_cost / last_account_value,
stock_value=now_stock_value, stock_value=now_stock_value,
) )
# set now_account_value to position # set now_account_value to position
@@ -174,8 +200,60 @@ class Account:
# note use deepcopy # note use deepcopy
self.positions[trade_start_time] = copy.deepcopy(self.current) self.positions[trade_start_time] = copy.deepcopy(self.current)
# finish today's updation def update_bar_end(
# reset the bar variables self,
self.rtn = 0 trade_start_time: pd.Timestamp,
self.ct = 0 trade_end_time: pd.Timestamp,
self.to = 0 trade_exchange: Exchange,
atomic: bool,
generate_report: bool = False,
trade_info: list = None,
inner_order_indicators: Indicator = None,
indicator_config: dict = {},
):
"""update account at each trading bar step
Parameters
----------
trade_start_time : pd.Timestamp
closed start time of step
trade_end_time : pd.Timestamp
closed end time of step
trade_exchange : Exchange
trading exchange, used to update current
atomic : bool
whether the trading executor is atomic, which means there is no higher-frequency trading executor inside it
- if atomic is True, calculate the indicators with trade_info
- else, aggregate indicators with inner indicators
generate_report : bool, optional
whether to generate report, by default False
trade_info : List[(Order, float, float, float)], optional
trading information, by default None
- necessary if atomic is True
- list of tuple(order, trade_val, trade_cost, trade_price)
inner_order_indicators : Indicator, optional
indicators of inner executor, by default None
- necessary if atomic is False
- used to aggregate outer indicators
indicator_config : dict, optional
config of calculating indicators, by default {}
"""
if atomic is True and trade_info is None:
raise ValueError("trade_info is necessary in atomic executor")
elif atomic is False and inner_order_indicators is None:
raise ValueError("inner_order_indicators is necessary in unatomic executor")
self.update_bar_count()
self.update_current(trade_start_time, trade_end_time, trade_exchange)
if generate_report:
self.update_report(trade_start_time, trade_end_time)
self.indicator.clear()
if atomic:
self.indicator.update_order_indicators(trade_start_time, trade_end_time, trade_info, trade_exchange)
else:
self.indicator.agg_order_indicators(inner_order_indicators, indicator_config)
self.indicator.cal_trade_indicators(trade_start_time, self.freq, indicator_config)
self.indicator.record(trade_start_time)

View File

@@ -1,10 +1,25 @@
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT License. # Licensed under the MIT License.
from ..utils.resam import parse_freq
def backtest_loop(start_time, end_time, trade_strategy, trade_executor): def backtest_loop(start_time, end_time, trade_strategy, trade_executor):
"""backtest funciton for the interaction of the outermost strategy and executor in the nested decison execution """backtest funciton for the interaction of the outermost strategy and executor in the nested decison execution
Returns
-------
report: Report
it records the trading report information
"""
return_value = {}
for _decison in collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value):
pass
return return_value.get("report"), return_value.get("indicator")
def collect_data_loop(start_time, end_time, trade_strategy, trade_executor, return_value: dict = None):
"""Generator for collecting the trade decision data for rl training
Parameters Parameters
---------- ----------
start_time : pd.Timestamp|str start_time : pd.Timestamp|str
@@ -15,26 +30,8 @@ def backtest_loop(start_time, end_time, trade_strategy, trade_executor):
the outermost portfolio strategy the outermost portfolio strategy
trade_executor : BaseExecutor trade_executor : BaseExecutor
the outermost executor the outermost executor
return_value : dict
Returns used for backtest_loop
-------
report: Report
it records the trading report information
"""
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_result = None
while not trade_executor.finished():
_trade_decision = trade_strategy.generate_trade_decision(_execute_result)
_execute_result = trade_executor.execute(_trade_decision)
return trade_executor.get_report()
def collect_data_loop(start_time, end_time, trade_strategy, trade_executor):
"""Generator for collecting the trade decision data for rl training
Yields Yields
------- -------
@@ -49,3 +46,19 @@ def collect_data_loop(start_time, end_time, trade_strategy, trade_executor):
while not trade_executor.finished(): while not trade_executor.finished():
_trade_decision = trade_strategy.generate_trade_decision(_execute_result) _trade_decision = trade_strategy.generate_trade_decision(_execute_result)
_execute_result = yield from trade_executor.collect_data(_trade_decision) _execute_result = yield from trade_executor.collect_data(_trade_decision)
if return_value is not None:
all_executors = trade_executor.get_all_executors()
all_reports = {
"{}{}".format(*parse_freq(_executor.time_per_step)): _executor.get_report()
for _executor in all_executors
if _executor.generate_report
}
all_indicators = {
"{}{}".format(
*parse_freq(_executor.time_per_step)
): _executor.get_trade_indicator().generate_trade_indicators_dataframe()
for _executor in all_executors
}
return_value.update({"report": all_reports, "indicator": all_indicators})

View File

@@ -48,14 +48,17 @@ class Exchange:
:param trade_unit: trade unit, 100 for China A market :param trade_unit: trade unit, 100 for China A market
:param min_cost: min cost, default 5 :param min_cost: min cost, default 5
:param extra_quote: pandas, dataframe consists of :param extra_quote: pandas, dataframe consists of
columns: like ['$vwap', '$close', '$factor', 'limit']. columns: like ['$vwap', '$close', '$volume', '$factor', 'limit_sell', 'limit_buy'].
The limit indicates that the etf is tradable on a specific day. The limit indicates that the etf is tradable on a specific day.
Necessary fields: Necessary fields:
$close is for calculating the total value at end of each day. $close is for calculating the total value at end of each day.
Optional fields: Optional fields:
$volume is only necessary when we limit the trade amount or caculate PA(vwap) indicator
$vwap is only necessary when we use the $vwap price as the deal price $vwap is only necessary when we use the $vwap price as the deal price
$factor is for rounding to the trading unit $factor is for rounding to the trading unit
limit will be set to False by default(False indicates we can buy this limit_sell will be set to False by default(False indicates we can sell this
target on this day).
limit_buy will be set to False by default(False indicates we can buy this
target on this day). target on this day).
index: MultipleIndex(instrument, pd.Datetime) index: MultipleIndex(instrument, pd.Datetime)
""" """

View File

@@ -20,7 +20,7 @@ class BaseExecutor:
time_per_step: str, time_per_step: str,
start_time: Union[str, pd.Timestamp] = None, start_time: Union[str, pd.Timestamp] = None,
end_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None,
show_indicator: bool = False, indicator_config: dict = {},
generate_report: bool = False, generate_report: bool = False,
verbose: bool = False, verbose: bool = False,
track_data: bool = False, track_data: bool = False,
@@ -33,7 +33,40 @@ class BaseExecutor:
time_per_step : str time_per_step : str
trade time per trading step, used for genreate the trade calendar trade time per trading step, used for genreate the trade calendar
show_indicator: bool, optional show_indicator: bool, optional
whether to show indicators, such as FFR/PA/POS, .etc whether to show indicators, :
- 'pa', the price advantage
- 'pos', the positive rate
- 'ffr', the fulfill rate
indicator_config: dict, optional
config for calculating trade indicator, including the following fields:
- 'show_indicator': whether to show indicators, optional, default by False. The indicators includes
- 'pa', the price advantage
- 'pos', the positive rate
- 'ffr', the fulfill rate
- 'pa_config': config for calculating price advantage(pa), optional
- 'base_price': the based price than which the trading price is advanced, Optional, default by 'twap'
- If 'base_price' is 'twap', the based price is the time weighted average price
- If 'base_price' is 'vwap', the based price is the volume weighted average price
- 'weight_method': weighted method when calculating total trading pa by different orders' pa in each step, optional, default by 'mean'
- If 'weight_method' is 'mean', calculating mean value of different orders' pa
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different orders' pa
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different orders' pa
- 'ffr_config': config for calculating fulfill rate(ffr), optional
- 'weight_method': weighted method when calculating total trading ffr by different orders' ffr in each step, optional, default by 'mean'
- If 'weight_method' is 'mean', calculating mean value of different orders' ffr
- If 'weight_method' is 'amount_weighted', calculating amount weighted average value of different orders' ffr
- If 'weight_method' is 'value_weighted', calculating value weighted average value of different orders' ffr
Example:
{
'show_indicator': True,
'pa_config': {
'base_value': 'twap',
'weight_method': 'value_weighted',
},
'ffr_config':{
'weight_method': 'value_weighted',
}
}
generate_report : bool, optional generate_report : bool, optional
whether to generate report, by default False whether to generate report, by default False
verbose : bool, optional verbose : bool, optional
@@ -51,7 +84,7 @@ class BaseExecutor:
""" """
self.time_per_step = time_per_step self.time_per_step = time_per_step
self.show_indicator = show_indicator self.indicator_config = indicator_config
self.generate_report = generate_report self.generate_report = generate_report
self.verbose = verbose self.verbose = verbose
self.track_data = track_data self.track_data = track_data
@@ -132,18 +165,20 @@ class BaseExecutor:
yield trade_decision yield trade_decision
return self.execute(trade_decision) return self.execute(trade_decision)
def get_trade_account(self):
raise NotImplementedError("get_trade_account is not implemented!")
def get_report(self): def get_report(self):
raise NotImplementedError("get_report is not implemented!") if self.generate_report:
_report = self.trade_account.report.generate_report_dataframe()
_positions = self.trade_account.get_positions()
return _report, _positions
else:
raise ValueError("generate_report should be True if you want to generate report")
def get_all_executors(self): def get_all_executors(self):
"""Return all executors""" """Return all executors"""
return [self] return [self]
def get_trade_indicator(self): def get_trade_indicator(self):
return self.trade_account.indicator.trade_indicator return self.trade_account.indicator
class NestedExecutor(BaseExecutor): class NestedExecutor(BaseExecutor):
@@ -159,7 +194,7 @@ class NestedExecutor(BaseExecutor):
inner_strategy: Union[BaseStrategy, dict], inner_strategy: Union[BaseStrategy, dict],
start_time: Union[str, pd.Timestamp] = None, start_time: Union[str, pd.Timestamp] = None,
end_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None,
show_indicator: bool = False, indicator_config: dict = {},
generate_report: bool = False, generate_report: bool = False,
verbose: bool = False, verbose: bool = False,
track_data: bool = False, track_data: bool = False,
@@ -190,7 +225,7 @@ class NestedExecutor(BaseExecutor):
time_per_step=time_per_step, time_per_step=time_per_step,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
show_indicator=show_indicator, indicator_config=indicator_config,
generate_report=generate_report, generate_report=generate_report,
verbose=verbose, verbose=verbose,
track_data=track_data, track_data=track_data,
@@ -198,7 +233,7 @@ class NestedExecutor(BaseExecutor):
**kwargs, **kwargs,
) )
if generate_report and trade_exchange is not None: if trade_exchange is not None:
self.trade_exchange = trade_exchange self.trade_exchange = trade_exchange
def reset_common_infra(self, common_infra): def reset_common_infra(self, common_infra):
@@ -209,7 +244,7 @@ class NestedExecutor(BaseExecutor):
""" """
super(NestedExecutor, self).reset_common_infra(common_infra) super(NestedExecutor, self).reset_common_infra(common_infra)
if self.generate_report and common_infra.has("trade_exchange"): if common_infra.has("trade_exchange"):
self.trade_exchange = common_infra.get("trade_exchange") self.trade_exchange = common_infra.get("trade_exchange")
self.inner_executor.reset_common_infra(common_infra) self.inner_executor.reset_common_infra(common_infra)
@@ -222,66 +257,43 @@ class NestedExecutor(BaseExecutor):
sub_level_infra = self.inner_executor.get_level_infra() sub_level_infra = self.inner_executor.get_level_infra()
self.inner_strategy.reset(level_infra=sub_level_infra, outer_trade_decision=trade_decision) self.inner_strategy.reset(level_infra=sub_level_infra, outer_trade_decision=trade_decision)
def _update_trade_account(self, inner_indicators):
trade_step = self.trade_calendar.get_trade_step()
trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
self.trade_account.update_bar_count()
if self.generate_report:
self.trade_account.update_bar_report(
trade_start_time=trade_start_time,
trade_end_time=trade_end_time,
trade_exchange=self.trade_exchange,
)
self.trade_account.indicator.clear()
self.trade_account.indicator.agg_report_info(inner_indicators=inner_indicators)
self.trade_account.indicator.agg_FFR()
self.trade_account.indicator.agg_PA(inner_indicators=inner_indicators)
if self.show_indicator:
FFR_value = self.trade_account.indicator.get_statistics_FFR(method="value_weighted")
PA_value = self.trade_account.indicator.get_statistics_PA(method="value_weighted")
POS_values = self.trade_account.indicator.get_statistics_POS()
print(
"[Indicator({}) {:%Y-%m-%d}]: FFR: {}, PA: {}, POS: {}".format(
self.time_per_step, trade_start_time, FFR_value, PA_value, POS_values
)
)
def execute(self, trade_decision): def execute(self, trade_decision):
for _data in self.collect_data(trade_decision): return_value = {}
for _decison in self.collect_data(trade_decision, return_value):
pass pass
return self._execute_result return return_value.get("execute_result")
def collect_data(self, trade_decision): def collect_data(self, trade_decision, return_value=None):
if self.track_data: if self.track_data:
yield trade_decision yield trade_decision
self._init_sub_trading(trade_decision) self._init_sub_trading(trade_decision)
execute_result = [] execute_result = []
inner_indicators = [] inner_order_indicators = []
_inner_execute_result = None _inner_execute_result = None
while not self.inner_executor.finished(): while not self.inner_executor.finished():
_inner_trade_decision = self.inner_strategy.generate_trade_decision(_inner_execute_result) _inner_trade_decision = self.inner_strategy.generate_trade_decision(_inner_execute_result)
_inner_execute_result = yield from self.inner_executor.collect_data(trade_decision=_inner_trade_decision) _inner_execute_result = yield from self.inner_executor.collect_data(trade_decision=_inner_trade_decision)
execute_result.extend(_inner_execute_result) execute_result.extend(_inner_execute_result)
inner_indicators.append(self.inner_executor.get_trade_indicator()) inner_order_indicators.append(self.inner_executor.get_trade_indicator().get_order_indicator)
if hasattr(self, "trade_account"): if hasattr(self, "trade_account"):
self._update_trade_account(inner_indicators=inner_indicators) trade_step = self.trade_calendar.get_trade_step()
trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
self.trade_account.update_bar_end(
trade_start_time,
trade_end_time,
self.trade_exchange,
atomic=False,
generate_report=self.generate_report,
inner_order_indicators=inner_order_indicators,
indicator_config=self.indicator_config,
)
self.trade_calendar.step() self.trade_calendar.step()
self._execute_result = execute_result if return_value is not None:
return_value.update({"execute_result": execute_result})
return execute_result return execute_result
def get_report(self):
sub_env_report_dict = self.inner_executor.get_report()
if self.generate_report:
_report = self.trade_account.report.generate_report_dataframe()
_positions = self.trade_account.get_positions()
_count, _freq = parse_freq(self.time_per_step)
sub_env_report_dict.update({f"{_count}{_freq}": (_report, _positions)})
return sub_env_report_dict
def get_all_executors(self): def get_all_executors(self):
"""Return all executors, including self and inner_executor.get_all_executors()""" """Return all executors, including self and inner_executor.get_all_executors()"""
return [self, *self.inner_executor.get_all_executors()] return [self, *self.inner_executor.get_all_executors()]
@@ -295,7 +307,7 @@ class SimulatorExecutor(BaseExecutor):
time_per_step: str, time_per_step: str,
start_time: Union[str, pd.Timestamp] = None, start_time: Union[str, pd.Timestamp] = None,
end_time: Union[str, pd.Timestamp] = None, end_time: Union[str, pd.Timestamp] = None,
show_indicator: bool = False, indicator_config: dict = {},
generate_report: bool = False, generate_report: bool = False,
verbose: bool = False, verbose: bool = False,
track_data: bool = False, track_data: bool = False,
@@ -314,7 +326,7 @@ class SimulatorExecutor(BaseExecutor):
time_per_step=time_per_step, time_per_step=time_per_step,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
show_indicator=show_indicator, indicator_config=indicator_config,
generate_report=generate_report, generate_report=generate_report,
verbose=verbose, verbose=verbose,
track_data=track_data, track_data=track_data,
@@ -377,41 +389,14 @@ class SimulatorExecutor(BaseExecutor):
# do nothing # do nothing
pass pass
self.trade_account.update_bar_count() self.trade_account.update_bar_end(
trade_start_time,
if self.generate_report: trade_end_time,
self.trade_account.update_bar_report( self.trade_exchange,
trade_start_time=trade_start_time, atomic=True,
trade_end_time=trade_end_time, generate_report=self.generate_report,
trade_exchange=self.trade_exchange, trade_info=execute_result,
) indicator_config=self.indicator_config,
self.trade_account.indicator.clear()
self.trade_account.indicator.update_trade_info(trade_info=execute_result)
self.trade_account.indicator.update_FFR()
self.trade_account.indicator.update_PA(
freq=self.time_per_step, trade_start_time=trade_start_time, trade_end_time=trade_end_time
) )
self.trade_account.indicator.record(trade_start_time=trade_start_time)
if self.show_indicator:
FFR_value = self.trade_account.indicator.get_statistics_FFR(method="value_weighted")
PA_value = self.trade_account.indicator.get_statistics_PA(method="value_weighted")
POS_values = self.trade_account.indicator.get_statistics_POS()
print(
"[Indicator({}) {:%Y-%m-%d %H:%M:%S}]: FFR: {}, PA: {}, POS: {}".format(
self.time_per_step, trade_start_time, FFR_value, PA_value, POS_values
)
)
self.trade_calendar.step() self.trade_calendar.step()
return execute_result return execute_result
def get_report(self):
if self.generate_report:
_report = self.trade_account.report.generate_report_dataframe()
_positions = self.trade_account.get_positions()
_count, _freq = parse_freq(self.time_per_step)
return {f"{_count}{_freq}": (_report, _positions)}
else:
return {}

View File

@@ -52,11 +52,13 @@ class Report:
self.init_bench(freq=freq, benchmark_config=benchmark_config) self.init_bench(freq=freq, benchmark_config=benchmark_config)
def init_vars(self): def init_vars(self):
self.accounts = OrderedDict() # account postion value for each trade date self.accounts = OrderedDict() # account postion value for each trade time
self.returns = OrderedDict() # daily return rate for each trade date self.returns = OrderedDict() # daily return rate for each trade time
self.turnovers = OrderedDict() # turnover for each trade date self.total_turnovers = OrderedDict() # total turnover for each trade time
self.costs = OrderedDict() # trade cost for each trade date self.turnovers = OrderedDict() # turnover for each trade time
self.values = OrderedDict() # value for each trade date self.total_costs = OrderedDict() # total trade cost for each trade time
self.costs = OrderedDict() # trade cost rate for each trade time
self.values = OrderedDict() # value for each trade time
self.cashes = OrderedDict() self.cashes = OrderedDict()
self.benches = OrderedDict() self.benches = OrderedDict()
self.latest_report_time = None # pd.TimeStamp self.latest_report_time = None # pd.TimeStamp
@@ -87,10 +89,10 @@ class Report:
def _sample_benchmark(self, bench, trade_start_time, trade_end_time): def _sample_benchmark(self, bench, trade_start_time, trade_end_time):
def cal_change(x): def cal_change(x):
return (x + 1).prod() - 1 return (x + 1).prod()
_ret = resam_ts_data(bench, trade_start_time, trade_end_time, method=cal_change) _ret = resam_ts_data(bench, trade_start_time, trade_end_time, method=cal_change)
return 0.0 if _ret is None else _ret return 0.0 if _ret is None else _ret - 1
def is_empty(self): def is_empty(self):
return len(self.accounts) == 0 return len(self.accounts) == 0
@@ -101,6 +103,12 @@ class Report:
def get_latest_account_value(self): def get_latest_account_value(self):
return self.accounts[self.latest_report_time] return self.accounts[self.latest_report_time]
def get_latest_total_cost(self):
return self.total_costs[self.latest_report_time]
def get_latest_total_turnover(self):
return self.total_turnovers[self.latest_report_time]
def update_report_record( def update_report_record(
self, self,
trade_start_time=None, trade_start_time=None,
@@ -108,7 +116,9 @@ class Report:
account_value=None, account_value=None,
cash=None, cash=None,
return_rate=None, return_rate=None,
total_turnover=None,
turnover_rate=None, turnover_rate=None,
total_cost=None,
cost_rate=None, cost_rate=None,
stock_value=None, stock_value=None,
bench_value=None, bench_value=None,
@@ -119,12 +129,14 @@ class Report:
account_value, account_value,
cash, cash,
return_rate, return_rate,
total_turnover,
turnover_rate, turnover_rate,
total_cost,
cost_rate, cost_rate,
stock_value, stock_value,
]: ]:
raise ValueError( raise ValueError(
"None in [trade_start_time, account_value, cash, return_rate, turnover_rate, cost_rate, stock_value]" "None in [trade_start_time, account_value, cash, return_rate, total_turnover, turnover_rate, total_cost, cost_rate, stock_value]"
) )
if trade_end_time is None and bench_value is None: if trade_end_time is None and bench_value is None:
@@ -135,20 +147,24 @@ class Report:
# update report data # update report data
self.accounts[trade_start_time] = account_value self.accounts[trade_start_time] = account_value
self.returns[trade_start_time] = return_rate self.returns[trade_start_time] = return_rate
self.total_turnovers[trade_start_time] = total_turnover
self.turnovers[trade_start_time] = turnover_rate self.turnovers[trade_start_time] = turnover_rate
self.total_costs[trade_start_time] = total_cost
self.costs[trade_start_time] = cost_rate self.costs[trade_start_time] = cost_rate
self.values[trade_start_time] = stock_value self.values[trade_start_time] = stock_value
self.cashes[trade_start_time] = cash self.cashes[trade_start_time] = cash
self.benches[trade_start_time] = bench_value self.benches[trade_start_time] = bench_value
# update latest_report_date # update latest_report_date
self.latest_report_time = trade_start_time self.latest_report_time = trade_start_time
# finish daily report update # finish report update in each step
def generate_report_dataframe(self): def generate_report_dataframe(self):
report = pd.DataFrame() report = pd.DataFrame()
report["account"] = pd.Series(self.accounts) report["account"] = pd.Series(self.accounts)
report["return"] = pd.Series(self.returns) report["return"] = pd.Series(self.returns)
report["total_turnover"] = pd.Series(self.total_turnovers)
report["turnover"] = pd.Series(self.turnovers) report["turnover"] = pd.Series(self.turnovers)
report["total_cost"] = pd.Series(self.total_costs)
report["cost"] = pd.Series(self.costs) report["cost"] = pd.Series(self.costs)
report["value"] = pd.Series(self.values) report["value"] = pd.Series(self.values)
report["cash"] = pd.Series(self.cashes) report["cash"] = pd.Series(self.cashes)
@@ -163,7 +179,7 @@ class Report:
def load_report(self, path): def load_report(self, path):
"""load report from a file """load report from a file
should have format like should have format like
columns = ['account', 'return', 'turnover', 'cost', 'value', 'cash', 'bench'] columns = ['account', 'return', 'total_turnover', 'turnover', 'cost', 'total_cost', 'value', 'cash', 'bench']
:param :param
path: str/ pathlib.Path() path: str/ pathlib.Path()
""" """
@@ -179,7 +195,9 @@ class Report:
account_value=r.loc[trade_start_time]["account"], account_value=r.loc[trade_start_time]["account"],
cash=r.loc[trade_start_time]["cash"], cash=r.loc[trade_start_time]["cash"],
return_rate=r.loc[trade_start_time]["return"], return_rate=r.loc[trade_start_time]["return"],
total_turnover=r.loc[trade_start_time]["total_turnover"],
turnover_rate=r.loc[trade_start_time]["turnover"], turnover_rate=r.loc[trade_start_time]["turnover"],
total_cost=r.loc[trade_start_time]["total_cost"],
cost_rate=r.loc[trade_start_time]["cost"], cost_rate=r.loc[trade_start_time]["cost"],
stock_value=r.loc[trade_start_time]["value"], stock_value=r.loc[trade_start_time]["value"],
bench_value=r.loc[trade_start_time]["bench"], bench_value=r.loc[trade_start_time]["bench"],
@@ -188,147 +206,184 @@ class Report:
class Indicator: class Indicator:
def __init__(self): def __init__(self):
self.indicator_his = dict() self.order_indicator_his = OrderedDict()
self.trade_indicator = dict() self.order_indicator = OrderedDict()
self.trade_indicator_his = OrderedDict()
def __getitem__(self, key): self.trade_indicator = OrderedDict()
return self.trade_indicator[key]
def __setitem__(self, key, value):
self.trade_indicator[key] = value
def __contains__(self, key):
return key in self.trade_indicator
def clear(self): def clear(self):
self.trade_indicator = dict() self.order_indicator = OrderedDict()
self.trade_indicator = OrderedDict()
def record(self, trade_start_time): def record(self, trade_start_time):
self.indicator_his[trade_start_time] = pd.DataFrame(self.trade_indicator) self.order_indicator_his[trade_start_time] = self.order_indicator
self.trade_indicator_his[trade_start_time] = self.trade_indicator
def update_trade_info(self, trade_info: list): def _update_order_trade_info(self, trade_info: list):
amount = dict() amount = dict()
deal_amount = dict() deal_amount = dict()
trade_price = dict() trade_price = dict()
trade_value = dict()
trade_cost = dict() trade_cost = dict()
for order, _trade_val, _trade_cost, _trade_price in trade_info: for order, _trade_val, _trade_cost, _trade_price in trade_info:
amount[order.stock_id] = order.amount * (order.direction * 2 - 1) amount[order.stock_id] = order.amount * (order.direction * 2 - 1)
deal_amount[order.stock_id] = order.deal_amount * (order.direction * 2 - 1) deal_amount[order.stock_id] = order.deal_amount * (order.direction * 2 - 1)
trade_price[order.stock_id] = _trade_price trade_price[order.stock_id] = _trade_price
trade_value[order.stock_id] = _trade_val * (order.direction * 2 - 1)
trade_cost[order.stock_id] = _trade_cost trade_cost[order.stock_id] = _trade_cost
self["amount"] = pd.Series(amount) self.order_indicator["amount"] = pd.Series(amount)
self["deal_amount"] = pd.Series(deal_amount) self.order_indicator["deal_amount"] = pd.Series(deal_amount)
self["trade_price"] = pd.Series(trade_price) self.order_indicator["trade_price"] = pd.Series(trade_price)
self["trade_cost"] = pd.Series(trade_cost) self.order_indicator["trade_value"] = pd.Series(trade_value)
self.order_indicator["trade_cost"] = pd.Series(trade_cost)
def update_FFR(self): def _update_order_fulfill_rate(self):
self["fulfill_rate"] = self["deal_amount"] / self["amount"] self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"]
def update_PA(self, freq, trade_start_time, trade_end_time, base_price="twap"): def _update_order_price_advantage(self, trade_exchange, trade_start_time, trade_end_time):
base_price = base_price.lower() self.order_indicator["base_price"] = self.order_indicator["trade_price"]
instruments = list(self.order_indicator["base_price"].index)
self.order_indicator["volume"] = pd.Series(
[
trade_exchange.get_volume(stock_id=inst, start_time=trade_start_time, end_time=trade_end_time)
for inst in instruments
],
index=instruments,
)
self.order_indicator["pa"] = (
self.order_indicator["trade_price"] - self.order_indicator["base_price"]
) / self.order_indicator["base_price"]
instruments = list(self["amount"].index) def _agg_order_trade_info(self, inner_order_indicators):
if base_price == "twap":
# too slow
# price_info, _ = get_higher_freq_feature(instruments, fields=["$close"], start_time=trade_start_time, end_time=trade_end_time, freq=freq)
# price_info = price_info.astype(float)
# self["base_price"] = price_info["$close"].groupby(level="instrument").mean()
self["base_price"] = self["trade_price"]
elif base_price == "vwap":
# too slow
price_info, _ = get_higher_freq_feature(
instruments,
fields=["$close", "$volume"],
start_time=trade_start_time,
end_time=trade_end_time,
freq=freq,
)
price_info = price_info.astype(float)
self["base_price"] = price_info.groupby(level="instrument").apply(
lambda x: (x["$close"] * x["$volume"]).sum() / x["$volume"].sum()
)
self["volume"] = price_info["$volume"].groupby(level="instrument").sum()
else:
raise ValueError(f"base_price {base_price} is not supported!")
self["pa"] = (self["trade_price"] - self["base_price"]) / self["base_price"]
def agg_report_info(self, inner_indicators):
amount = pd.Series() amount = pd.Series()
deal_amount = pd.Series() deal_amount = pd.Series()
trade_price = pd.Series() trade_price = pd.Series()
trade_value = pd.Series()
trade_cost = pd.Series() trade_cost = pd.Series()
for inner_indicator in inner_indicators: for _order_indicator in inner_order_indicators:
amount = amount.add(inner_indicator["amount"], fill_value=0) amount = amount.add(_order_indicator["amount"], fill_value=0)
deal_amount = deal_amount.add(inner_indicator["deal_amount"], fill_value=0) deal_amount = deal_amount.add(_order_indicator["deal_amount"], fill_value=0)
trade_price = trade_price.add(inner_indicator["trade_price"] * inner_indicator["deal_amount"], fill_value=0) trade_price = trade_price.add(
trade_cost = trade_cost.add(inner_indicator["trade_cost"], fill_value=0) _order_indicator["trade_price"] * _order_indicator["deal_amount"], fill_value=0
)
trade_value = trade_value.add(_order_indicator["trade_value"], fill_value=0)
trade_cost = trade_cost.add(_order_indicator["trade_cost"], fill_value=0)
self["amount"] = amount self.order_indicator["amount"] = amount
self["deal_amount"] = deal_amount self.order_indicator["deal_amount"] = deal_amount
trade_price /= self["deal_amount"] trade_price /= self.order_indicator["deal_amount"]
self["trade_price"] = trade_price self.order_indicator["trade_price"] = trade_price
self["trade_cost"] = trade_cost self.order_indicator["trade_value"] = trade_value
self.order_indicator["trade_cost"] = trade_cost
def agg_FFR(self): def _agg_order_fulfill_rate(self):
self["fulfill_rate"] = self["deal_amount"] / self["amount"] self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"]
def agg_PA(self, inner_indicators, base_price="twap"): def _agg_order_price_advantage(self, inner_order_indicators, base_price="twap"):
base_price = base_price.lower() base_price = base_price.lower()
volume = pd.Series()
for _order_indicator in inner_order_indicators:
volume = volume.add(_order_indicator["volume"], fill_value=0)
self.order_indicator["volume"] = volume
if base_price == "twap": if base_price == "twap":
base_price = pd.Series() base_price = pd.Series()
price_count = pd.Series() price_count = pd.Series()
for inner_indicator in inner_indicators: for _order_indicator in inner_order_indicators:
base_price = base_price.add(inner_indicator["base_price"], fill_value=0) base_price = base_price.add(_order_indicator["base_price"], fill_value=0)
price_count = price_count.add(pd.Series(1, index=inner_indicator["base_price"].index), fill_value=0) price_count = price_count.add(pd.Series(1, index=_order_indicator["base_price"].index), fill_value=0)
base_price /= price_count base_price /= price_count
self["base_price"] = base_price self.order_indicator["base_price"] = base_price
elif base_price == "vwap": elif base_price == "vwap":
base_price = pd.Series() base_price = pd.Series()
volume = pd.Series() for _order_indicator in inner_order_indicators:
for inner_indicator in inner_indicators: base_price = base_price.add(_order_indicator["base_price"] * _order_indicator["volume"], fill_value=0)
base_price = base_price.add(inner_indicator["base_price"] * inner_indicator["volume"], fill_value=0) base_price /= self.order_indicator["volume"]
volume = volume.add(inner_indicator["volume"], fill_value=0) self.order_indicator["base_price"] = base_price
base_price /= volume
self["base_price"] = base_price
self["volume"] = volume
else: else:
raise ValueError(f"base_price {base_price} is not supported!") raise ValueError(f"base_price {base_price} is not supported!")
self["pa"] = (self["trade_price"] - self["base_price"]) / self["base_price"] self.order_indicator["pa"] = self.order_indicator["trade_price"] / self.order_indicator["base_price"] - 1
def get_statistics_FFR(self, method="mean"): def _cal_trade_fulfill_rate(self, method="mean"):
if method == "mean": if method == "mean":
return self["fulfill_rate"].mean() return self.order_indicator["ffr"].mean()
elif method == "amount_weighted": elif method == "amount_weighted":
weights = self["deal_amount"].abs() weights = self.order_indicator["deal_amount"].abs()
return (self["fulfill_rate"] * weights).sum() / weights.sum() return (self.order_indicator["ffr"] * weights).sum() / weights.sum()
elif method == "value_weighted": elif method == "value_weighted":
weights = (self["deal_amount"] * self["trade_price"]).abs() weights = self.order_indicator["trade_value"].abs()
return (self["fulfill_rate"] * weights).sum() / weights.sum() return (self.order_indicator["ffr"] * weights).sum() / weights.sum()
else: else:
raise ValueError(f"method {method} is not supported!") raise ValueError(f"method {method} is not supported!")
def get_statistics_PA(self, method="mean"): def _cal_trade_price_advantage(self, method="mean"):
pa_order = self["pa"] * (self["amount"] < 0).astype(int)
pa_order = self.order_indicator["pa"] * (self.order_indicator["amount"] < 0).astype(int)
if method == "mean": if method == "mean":
return pa_order.mean() return pa_order.mean()
elif method == "amount_weighted": elif method == "amount_weighted":
weights = self["deal_amount"].abs() weights = self.order_indicator["deal_amount"].abs()
return (pa_order * weights).sum() / weights.sum() return (pa_order * weights).sum() / weights.sum()
elif method == "value_weighted": elif method == "value_weighted":
weights = (self["deal_amount"] * self["trade_price"]).abs() weights = self.order_indicator["trade_value"].abs()
return (pa_order * weights).sum() / weights.sum() return (pa_order * weights).sum() / weights.sum()
else: else:
raise ValueError(f"method {method} is not supported!") raise ValueError(f"method {method} is not supported!")
def get_statistics_POS(self): def _cal_trade_positive_rate(self):
pa_order = self["pa"] * (self["amount"] < 0).astype(int) pa_order = self.order_indicator["pa"] * (self.order_indicator["amount"] < 0).astype(int)
return (pa_order > 1e-8).astype(int).sum() / len(pa_order) return (pa_order > 0).astype(int).sum() / len(pa_order)
def _cal_trade_amount(self):
return self.order_indicator["deal_amount"].abs().sum()
def _cal_trade_value(self):
return self.order_indicator["trade_value"].abs().sum()
def update_order_indicators(self, trade_start_time, trade_end_time, trade_info, trade_exchange):
self._update_order_trade_info(trade_info=trade_info)
self._update_order_fulfill_rate()
self._update_order_price_advantage(trade_exchange, trade_start_time, trade_end_time)
def agg_order_indicators(self, inner_order_indicators, indicator_config={}):
self._agg_order_trade_info(inner_order_indicators)
self._agg_order_fulfill_rate()
pa_config = indicator_config.get("pa_config", {})
self._agg_order_price_advantage(inner_order_indicators, base_price=pa_config.get("base_price", "twap"))
def cal_trade_indicators(self, trade_start_time, freq, indicator_config={}):
show_indicator = indicator_config.get("show_indicator", False)
ffr_config = indicator_config.get("ffr_config", {})
pa_config = indicator_config.get("pa_config", {})
fulfill_rate = self._cal_trade_fulfill_rate(method=ffr_config.get("weight_method", "mean"))
price_advantage = self._cal_trade_price_advantage(method=pa_config.get("weight_method", "mean"))
positive_rate = self._cal_trade_positive_rate()
trade_amount = self._cal_trade_amount()
trade_value = self._cal_trade_value()
self.trade_indicator["ffr"] = fulfill_rate
self.trade_indicator["pa"] = price_advantage
self.trade_indicator["pos"] = positive_rate
self.trade_indicator["amount"] = trade_amount
self.trade_indicator["value"] = trade_value
if show_indicator:
print(
"[Indicator({}) {:%Y-%m-%d %H:%M:%S}]: FFR: {}, PA: {}, POS: {}".format(
freq, trade_start_time, fulfill_rate, price_advantage, positive_rate
)
)
@property
def get_order_indicator(self):
return self.order_indicator
@property
def get_trade_indicator(self):
return self.trade_indicator
def generate_trade_indicators_dataframe(self):
return pd.DataFrame.from_dict(self.trade_indicator_his, orient="index")

View File

@@ -11,7 +11,7 @@ import warnings
from ..log import get_module_logger from ..log import get_module_logger
from ..backtest import get_exchange, backtest as backtest_func from ..backtest import get_exchange, backtest as backtest_func
from ..utils import get_date_range from ..utils import get_date_range
from ..utils.resam import parse_freq from ..utils.resam import parse_freq, NORM_FREQ_MONTH, NORM_FREQ_WEEK, NORM_FREQ_DAY, NORM_FREQ_MINUTE
from ..data import D from ..data import D
from ..config import C from ..config import C
@@ -37,12 +37,12 @@ def risk_analysis(r, N: int = None, freq: str = "day"):
def cal_risk_analysis_scaler(freq): def cal_risk_analysis_scaler(freq):
_count, _freq = parse_freq(freq) _count, _freq = parse_freq(freq)
_freq_scaler = { _freq_scaler = {
"minute": 240 * 252, NORM_FREQ_MINUTE: 240 * 252,
"day": 252, NORM_FREQ_DAY: 252,
"week": 50, NORM_FREQ_WEEK: 50,
"month": 12, NORM_FREQ_MONTH: 12,
} }
return _count * _freq_scaler[_freq] return _freq_scaler[_freq] / _count
if N is None and freq is None: if N is None and freq is None:
raise ValueError("at least one of `N` and `freq` should exist") raise ValueError("at least one of `N` and `freq` should exist")
@@ -63,7 +63,51 @@ def risk_analysis(r, N: int = None, freq: str = "day"):
"information_ratio": information_ratio, "information_ratio": information_ratio,
"max_drawdown": max_drawdown, "max_drawdown": max_drawdown,
} }
res = pd.Series(data, index=data.keys()).to_frame("risk") res = pd.Series(data).to_frame("risk")
return res
def indicator_analysis(df, method="mean"):
"""analyze statistical time-series indicators of trading
Parameters
----------
df : pandas.DataFrame
columns: like ['pa', 'pos', 'ffr', 'amount', 'value'].
Necessary fields:
- 'pa' is the price advantage in trade indicators
- 'pos' is the positive rate in trade indicators
- 'ffr' is the fulfill rate in trade indicators
Optional fields:
- 'amount' is the total deal amount, only necessary when method is 'amount_weighted'
- 'value' is the total trade value, only necessary when method is 'value_weighted'
index: Index(datetime)
method : str, optional
statistics method, by default "mean"
- if method is 'mean', count the mean statistical value of each trade indicator
- if method is 'amount_weighted', count the amount weighted mean statistical value of each trade indicator
- if method is 'value_weighted', count the value weighted mean statistical value of each trade indicator
Returns
-------
pd.DataFrame
statistical value of each trade indicator
"""
indicators_df = df[["pa", "pos", "ffr"]]
if method == "mean":
res = indicators_df.mean()
elif method == "amount_weighted":
weights = df["amount"].abs()
res = indicators_df.mul(weights, axis=0).sum() / weights.sum()
elif method == "value_weighted":
weights = df["value"].abs()
res = indicators_df.mul(weights, axis=0).sum() / weights.sum()
else:
raise ValueError(f"indicator_analysis method {method} is not supported!")
res = res.to_frame("value")
return res return res

View File

@@ -7,7 +7,7 @@ import warnings
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
from ..contrib.evaluate import risk_analysis from ..contrib.evaluate import indicator_analysis, risk_analysis, indicator_analysis
from ..data.dataset import DatasetH from ..data.dataset import DatasetH
from ..data.dataset.handler import DataHandlerLP from ..data.dataset.handler import DataHandlerLP
@@ -294,7 +294,9 @@ class PortAnaRecord(RecordTemp):
artifact_path = "portfolio_analysis" artifact_path = "portfolio_analysis"
def __init__(self, recorder, config, risk_analysis_freq, **kwargs): def __init__(
self, recorder, config, risk_analysis_freq, indicator_analysis_freq, indicator_analysis_method=None, **kwargs
):
""" """
config["strategy"] : dict config["strategy"] : dict
define the strategy class as well as the kwargs. define the strategy class as well as the kwargs.
@@ -304,6 +306,10 @@ class PortAnaRecord(RecordTemp):
define the backtest kwargs. define the backtest kwargs.
risk_analysis_freq : str|List[str] risk_analysis_freq : str|List[str]
risk analysis freq of report risk analysis freq of report
indicator_analysis_freq : str|List[str]
indicator analysis freq of report
indicator_analysis_method : str, optional, default by None
the candidated values include 'mean', 'amount_weighted', 'value_weighted'
""" """
super().__init__(recorder=recorder, **kwargs) super().__init__(recorder=recorder, **kwargs)
@@ -312,10 +318,17 @@ class PortAnaRecord(RecordTemp):
self.backtest_config = config["backtest"] self.backtest_config = config["backtest"]
if isinstance(risk_analysis_freq, str): if isinstance(risk_analysis_freq, str):
risk_analysis_freq = [risk_analysis_freq] risk_analysis_freq = [risk_analysis_freq]
if isinstance(indicator_analysis_freq, str):
indicator_analysis_freq = [indicator_analysis_freq]
self.risk_analysis_freq = [ self.risk_analysis_freq = [
"{0}{1}".format(*parse_freq(_analysis_freq)) for _analysis_freq in risk_analysis_freq "{0}{1}".format(*parse_freq(_analysis_freq)) for _analysis_freq in risk_analysis_freq
] ]
self.report_freq = self._get_report_freq(self.executor_config) self.indicator_analysis_freq = [
"{0}{1}".format(*parse_freq(_analysis_freq)) for _analysis_freq in indicator_analysis_freq
]
self.indicator_analysis_method = indicator_analysis_method
self.all_freq = self._get_report_freq(self.executor_config)
def _get_report_freq(self, executor_config): def _get_report_freq(self, executor_config):
ret_freq = [] ret_freq = []
@@ -328,21 +341,26 @@ class PortAnaRecord(RecordTemp):
def generate(self, **kwargs): def generate(self, **kwargs):
# custom strategy and get backtest # custom strategy and get backtest
report_dict = normal_backtest( report_dict, indicator_dict = normal_backtest(
executor=self.executor_config, strategy=self.strategy_config, **self.backtest_config executor=self.executor_config, strategy=self.strategy_config, **self.backtest_config
) )
for report_freq, (report_normal, positions_normal) in report_dict.items(): for _freq, (report_normal, positions_normal) in report_dict.items():
self.recorder.save_objects( self.recorder.save_objects(
**{f"report_normal_{report_freq}.pkl": report_normal}, artifact_path=PortAnaRecord.get_path() **{f"report_normal_{_freq}.pkl": report_normal}, artifact_path=PortAnaRecord.get_path()
) )
self.recorder.save_objects( self.recorder.save_objects(
**{f"positions_normal_{report_freq}.pkl": positions_normal}, artifact_path=PortAnaRecord.get_path() **{f"positions_normal_{_freq}.pkl": positions_normal}, artifact_path=PortAnaRecord.get_path()
)
for _freq, indicators_normal in indicator_dict.items():
self.recorder.save_objects(
**{f"indicators_normal_{_freq}.pkl": indicators_normal}, artifact_path=PortAnaRecord.get_path()
) )
for _analysis_freq in self.risk_analysis_freq: for _analysis_freq in self.risk_analysis_freq:
if _analysis_freq not in report_dict: if _analysis_freq not in report_dict:
warnings.warn( warnings.warn(
f"the freq {_analysis_freq} report is not found, please set the corresponding env with `generate_report==True`" f"the freq {_analysis_freq} report is not found, please set the corresponding env with `generate_report=True`"
) )
else: else:
report_normal, _ = report_dict.get(_analysis_freq) report_normal, _ = report_dict.get(_analysis_freq)
@@ -353,25 +371,46 @@ class PortAnaRecord(RecordTemp):
analysis["excess_return_with_cost"] = risk_analysis( analysis["excess_return_with_cost"] = risk_analysis(
report_normal["return"] - report_normal["bench"] - report_normal["cost"], freq=_analysis_freq report_normal["return"] - report_normal["bench"] - report_normal["cost"], freq=_analysis_freq
) )
analysis_df = pd.concat(analysis) # type: pd.DataFrame analysis_df = pd.concat(analysis) # type: pd.DataFrame
# log metrics # log metrics
self.recorder.log_metrics(**flatten_dict(analysis_df["risk"].unstack().T.to_dict())) analysis_dict = flatten_dict(analysis_df["risk"].unstack().T.to_dict())
self.recorder.log_metrics(**{f"{_analysis_freq}.{k}": v for k, v in analysis_dict.items()})
# save results # save results
self.recorder.save_objects( self.recorder.save_objects(
**{f"port_analysis_{report_freq}.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path() **{f"port_analysis_{_analysis_freq}.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path()
) )
logger.info( logger.info(
f"Portfolio analysis record 'port_analysis_{report_freq}.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}" f"Portfolio analysis record 'port_analysis_{_analysis_freq}.pkl' has been saved as the artifact of the Experiment {self.recorder.experiment_id}"
) )
# print out results # print out results
pprint("The following are analysis results of the excess return without cost.") pprint(f"The following are analysis results of benchmark return({_analysis_freq}).")
pprint(risk_analysis(report_normal["bench"], freq=_analysis_freq))
pprint(f"The following are analysis results of the excess return without cost({_analysis_freq}).")
pprint(analysis["excess_return_without_cost"]) pprint(analysis["excess_return_without_cost"])
pprint("The following are analysis results of the excess return with cost.") pprint(f"The following are analysis results of the excess return with cost({_analysis_freq}).")
pprint(analysis["excess_return_with_cost"]) pprint(analysis["excess_return_with_cost"])
for _analysis_freq in self.indicator_analysis_freq:
indicators_normal = indicator_dict.get(_analysis_freq)
if self.indicator_analysis_method is None:
analysis_df = indicator_analysis(indicators_normal)
else:
analysis_df = indicator_analysis(indicators_normal, method=self.indicator_analysis_method)
# log metrics
analysis_dict = analysis_df["value"].to_dict()
self.recorder.log_metrics(**{f"{_analysis_freq}.{k}": v for k, v in analysis_dict.items()})
# save results
self.recorder.save_objects(
**{f"indicator_analysis_{_analysis_freq}.pkl": analysis_df}, artifact_path=PortAnaRecord.get_path()
)
pprint(f"The following are analysis results of indicators({_analysis_freq}).")
pprint(analysis_df)
def list(self): def list(self):
list_path = [] list_path = []
for _freq in self.report_freq: for _freq in self.all_freq:
list_path.extend( list_path.extend(
[ [
PortAnaRecord.get_path(f"report_normal_{_freq}.pkl"), PortAnaRecord.get_path(f"report_normal_{_freq}.pkl"),
@@ -380,7 +419,7 @@ class PortAnaRecord(RecordTemp):
) )
for _analysis_freq in self.risk_analysis_freq: for _analysis_freq in self.risk_analysis_freq:
if _analysis_freq in self.report_freq: if _analysis_freq in self.all_freq:
list_path.append(PortAnaRecord.get_path(f"port_analysis_{_analysis_freq}.pkl")) list_path.append(PortAnaRecord.get_path(f"port_analysis_{_analysis_freq}.pkl"))
else: else:
warnings.warn(f"{_analysis_freq} is not found") warnings.warn(f"{_analysis_freq} is not found")