from copy import deepcopy
import pandas
import matplotlib.pyplot as pyplot
import math
from public_module.object import BarData
from back_tester_core.back_tester_source_data import BackTesterDataCore
from back_tester_core.tqz_constant import TQZStrategyCombinationType
from public_module.tqz_extern.tools.file_path_operator.file_path_operator import TQZFilePathOperator
from public_module.tqz_extern.tools.position_operator.position_operator import TQZJsonOperator
class BackTesterCore:
__stock_pools_map = None
__history_bars_map = None
__strategy_classes = None
__back_tester_source_result = None
@classmethod
def start_back_tester(cls, strategy_combination_type: TQZStrategyCombinationType):
if strategy_combination_type in [TQZStrategyCombinationType.STOCK]:
cls.__start_back_tester_stock()
elif strategy_combination_type in [TQZStrategyCombinationType.FUTURE]:
cls.__start_back_tester_future()
else:
assert False, f'bad strategy_combination_type: {strategy_combination_type}.'
# --- lazy load part ---
@classmethod
def stock_pools_map(cls):
if cls.__stock_pools_map is None:
cls.__stock_pools_map, cls.__history_bars_map = BackTesterDataCore.load_stockPoolsMap_and_barsMap()
return cls.__stock_pools_map
@classmethod
def history_bars_map(cls):
if cls.__history_bars_map is None:
cls.__stock_pools_map, cls.__history_bars_map = BackTesterDataCore.load_stockPoolsMap_and_barsMap()
return cls.__history_bars_map
@classmethod
def strategy_classes(cls, strategy_combination_type: TQZStrategyCombinationType):
if cls.__strategy_classes is None:
cls.__strategy_classes = BackTesterDataCore.load_strategy_classes(strategy_combination_type=strategy_combination_type)
return cls.__strategy_classes
@classmethod
def back_tester_source_result(cls) -> dict:
if cls.__back_tester_source_result is None:
back_tester_source_result: dict = {}
for stock_pool_name, stock_pool_df in cls.stock_pools_map().items():
strategy_name = stock_pool_name.split('.')[0]
strategy_class, strategy_setting = cls.strategy_classes(
TQZStrategyCombinationType.STOCK
)[strategy_name]["strategy_class"], cls.strategy_classes(
TQZStrategyCombinationType.STOCK
)[strategy_name]["strategy_setting"]
assert strategy_class is not None, f'strategy_class {strategy_class} not found.'
assert stock_pool_df is not None, f'stock_pool_df {stock_pool_df} is None.'
for ix, row in stock_pool_df.iterrows():
stock_strategy_name, stock_strategy_backTester_result = cls.__get_single_back_tester_result(
ts_symbol=stock_pool_df.iloc[ix]["stocks"],
ts_symbol_fund=stock_pool_df.iloc[ix]["stock_fund"],
strategy_name=strategy_name,
strategy_class=strategy_class,
strategy_setting=strategy_setting
)
back_tester_source_result[stock_strategy_name] = stock_strategy_backTester_result
cls.__back_tester_source_result = back_tester_source_result
return cls.__back_tester_source_result
# --- private part ---
@classmethod
def __start_back_tester_stock(cls):
# merge date | get position result.
result_dataframe, stock_position_result = cls.__init_result_dataframe_and_get_stock_position_result()
# merge strategy fund | balance
result_dataframe = cls.__merge_fund_and_balance(result_dataframe=result_dataframe)
# get net_value | sharpe_ratio.
result_dataframe, sharpe_ratio = cls.__get_result_dataframe_and_sharpe_ratio(result_dataframe=result_dataframe)
# dump back tester date-net_value picture.
cls.__dump_back_tester_dateNetValue_picture(result_dataframe=result_dataframe, strategy_combination_type=TQZStrategyCombinationType.STOCK)
# dump back_tester_result | stock_position_result.
TQZJsonOperator.tqz_write_jsonfile(
content={
"sharpe_ratio": sharpe_ratio
},
target_jsonfile=f'{cls.__get_back_tester_result_path(strategy_combination_type=TQZStrategyCombinationType.STOCK)}/back_tester_result.json'
)
TQZJsonOperator.tqz_write_jsonfile(
content=stock_position_result,
target_jsonfile=f'{cls.__get_back_tester_result_path(strategy_combination_type=TQZStrategyCombinationType.STOCK)}/stock_position_result.json'
)
@classmethod
def __dump_back_tester_dateNetValue_picture(cls, result_dataframe, strategy_combination_type: TQZStrategyCombinationType):
pyplot.figure(figsize=(15, 10)) # size
pyplot.xticks(rotation=90) # rotate
pyplot.plot(result_dataframe.index, result_dataframe['net_value'], alpha=0.9) # data
pyplot.savefig(f"{cls.__get_back_tester_result_path(strategy_combination_type=strategy_combination_type)}/date_netValue.png")
@classmethod
def __get_back_tester_result_path(cls, strategy_combination_type: TQZStrategyCombinationType) -> str:
return TQZFilePathOperator.grandfather_path(
source_path=TQZFilePathOperator.father_path(
source_path=__file__
)
) + f'/back_tester_result/{strategy_combination_type.value}'
@classmethod
def __init_result_dataframe_and_get_stock_position_result(cls):
stock_position_result: {str, float} = {}
result_dataframe = pandas.DataFrame(columns=['date'])
for stock_strategy_name, stock_strategy_context in cls.back_tester_source_result().items():
result_dataframe = pandas.merge(result_dataframe['date'], stock_strategy_context["back_tester_result_df"]['date'], how='outer')
ts_symbol = f'{stock_strategy_name.split(".")[0]}.{stock_strategy_name.split(".")[1]}'
if ts_symbol in stock_position_result.keys():
stock_position_result[ts_symbol] += stock_strategy_context["last_position"]
else:
stock_position_result[ts_symbol] = stock_strategy_context["last_position"]
result_dataframe.sort_values('date', inplace=True)
result_dataframe.reset_index(inplace=True)
del result_dataframe['index']
result_dataframe.set_index('date', inplace=True)
return result_dataframe, stock_position_result
@classmethod
def __merge_fund_and_balance(cls, result_dataframe: pandas.DataFrame):
fund_result_dataframe, balance_result_dataframe = deepcopy(result_dataframe), deepcopy(result_dataframe)
for stock_strategy_name, stock_strategy_context in cls.back_tester_source_result().items():
tmp_stock_strategy_context = stock_strategy_context["back_tester_result_df"].set_index('date', inplace=False)
fund_result_dataframe.loc[:, stock_strategy_name] = tmp_stock_strategy_context['fund']
balance_result_dataframe.loc[:, stock_strategy_name] = tmp_stock_strategy_context['balance']
result_dataframe['fund'] = fund_result_dataframe[:].sum(axis=1)
result_dataframe['balance'] = balance_result_dataframe[:].sum(axis=1)
return result_dataframe
@classmethod
def __get_result_dataframe_and_sharpe_ratio(cls, result_dataframe: pandas.DataFrame):
result_dataframe['net_value'] = round(result_dataframe['balance'] / result_dataframe['fund'], 4)
result_dataframe['net_value_flow_per_day'] = result_dataframe['net_value'] - result_dataframe['net_value'].shift(1)
yield_rate_annualized = round(((result_dataframe['net_value'].values.tolist()[-1] - 1) / len(result_dataframe)) * 250, 5)
if result_dataframe['net_value_flow_per_day'].std(ddof=0) is 0:
sharpe_ratio = 0
else:
sharpe_ratio = round(yield_rate_annualized / result_dataframe['net_value_flow_per_day'].std(ddof=0) / math.sqrt(250), 4)
del result_dataframe['net_value_flow_per_day']
return result_dataframe, sharpe_ratio
@classmethod
def __get_single_back_tester_result(cls, ts_symbol: str, ts_symbol_fund: float, strategy_name: str, strategy_class, strategy_setting):
stock_strategy_name = f'{ts_symbol}.{strategy_name}'
stock_strategy = strategy_class(None, stock_strategy_name, ts_symbol, strategy_setting)
per_result_df = pandas.DataFrame(columns={'date', 'open_price', 'close_price', 'position'})
stock_strategy.on_init()
stock_strategy.on_start()
stock_strategy.strategy_fund = ts_symbol_fund
for bar in cls.history_bars_map()[ts_symbol]:
stock_strategy.on_bar(bar=bar)
per_result_df = cls.__update_per_result_dataframe(per_result_df=per_result_df, bar=bar, last_pos=stock_strategy.pos)
stock_strategy.on_stop()
print("per_result_df: " + str(per_result_df))
exit()
return stock_strategy_name, {
"back_tester_result_df": cls.__reset_result_dataframe(per_result_df=per_result_df, ts_symbol_fund=ts_symbol_fund),
"last_position": cls.__recompulate_last_position(last_pos=per_result_df.loc[len(per_result_df)-1, "position"])
}
@classmethod
def __update_per_result_dataframe(cls, per_result_df: pandas.DataFrame, bar: BarData, last_pos: float):
"""
:param per_result_df: source dataframe
:param bar: use last bar to do back tester
:param last_pos: pos before bar update
:return: result dataframe after update
"""
current_row = len(per_result_df)
per_result_df.loc[current_row, 'date'] = bar.datetime
per_result_df.loc[current_row, 'open_price'] = bar.open_price
per_result_df.loc[current_row, 'close_price'] = bar.close_price
per_result_df.loc[current_row, 'position'] = cls.__recompulate_last_position(last_pos=last_pos)
if current_row is 0:
per_result_df.loc[current_row, 'cprec_diff'] = 0
else:
per_result_df.loc[current_row, 'cprec_diff'] = per_result_df.loc[current_row, 'close_price'] - per_result_df.loc[current_row - 1, 'close_price']
per_result_df['position_diff'] = abs(per_result_df['position'] - per_result_df['position'].shift(1))
per_result_df.fillna(0, inplace=True)
back_tester_type = TQZStrategyCombinationType.STOCK
# fee part
fee_ratio = BackTesterDataCore.get_fee_ratio(strategy_combination_type=back_tester_type)
per_result_df['fee_balance'] = fee_ratio * per_result_df['position_diff'] * bar.close_price * -1
# slippage part
min_tick_flow = BackTesterDataCore.get_tick_value(strategy_combination_type=back_tester_type)
slippage = BackTesterDataCore.get_slippage(strategy_combination_type=back_tester_type)
per_result_df['slippage_balance'] = min_tick_flow * slippage * per_result_df['position_diff'] * -1
per_result_df.loc[current_row, 'pnl_balance'] = (per_result_df['position'].shift(1) * per_result_df['cprec_diff']).sum() + per_result_df['fee_balance'].sum() + per_result_df['slippage_balance'].sum()
return per_result_df
@classmethod
def __reset_result_dataframe(cls, per_result_df: pandas.DataFrame, ts_symbol_fund: float):
if len(per_result_df) is 0:
return pandas.DataFrame(columns=['date', 'fund', 'balance'])
else:
return pandas.DataFrame({
'date': per_result_df['date'],
'fund': ts_symbol_fund,
'balance': (ts_symbol_fund + per_result_df['pnl_balance'] + per_result_df['fee_balance'] + per_result_df['slippage_balance'])
})
@classmethod
def __recompulate_last_position(cls, last_pos: float):
if last_pos > 0:
last_position = math.floor(last_pos) # all position
elif last_pos < 0:
last_position = math.ceil(last_pos) # all position
else:
last_position = 0
return last_position
# --- future part ---
@classmethod
def __start_back_tester_future(cls):
pass
if __name__ == '__main__':
BackTesterCore.start_back_tester(strategy_combination_type=TQZStrategyCombinationType.STOCK)