量化交易之回测篇 - pos计算移至策略模型、重设计股票手续费计算方式

该代码实现了一个回测引擎的核心功能,包括股票池和历史数据管理、策略加载、回测执行及结果分析。针对股票策略,它加载股票池和历史数据,根据策略组合类型启动回测,计算并保存回测结果,如收益、夏普比率等,并绘制日期-净值图。同时,它还处理手续费和滑点计算,更新每日盈亏。整个过程涉及数据读取、策略执行和结果汇总。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值