基于Baostock和本地存储的HDF数据构建低价股、资金流和小市值策略回测框架

本文提供一套基于Baostock和本地存储的HDF数据构建的回测框架,用于测试低价股、资金流和小市值策略。框架支持买卖限制、增发考虑、分板块计算收益和轮动等功能,并包含数据读取类、选股策略类和回测执行类。

步骤及代码如下:

1. 数据读取类

数据读取类主要用于从Baostock下载数据,并将数据存储为HDF格式。代码如下:

import baostock as bs
import pandas as pd

class DataLoader:
    
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        
    def download_data(self, stock_list):
        '从Baostock下载数据'
        bs.login()
        for stock in stock_list:
            rs = bs.query_history_k_data_plus(stock, 'date,open,high,low,close,volume,amount',
                                             start_date=self.start_date, end_date=self.end_date,
                                             frequency='d', adjustflag='3')
            data_list = []
            while (rs.error_code == '0') & rs.next():
                data_list.append(rs.get_row_data())
            if len(data_list) > 0:
                df = pd.DataFrame(data_list, columns=rs.fields)
                df.set_index('date', inplace=True)
                df.to_hdf(stock+'.h5', key='df', mode='w')
        bs.logout()
        
    def read_data(self, stock_list=None):
        '从HDF读取数据'
        if stock_list is None:
            # 读取所有股票数据
            pass
        else:
            # 读取指定股票数据
            data = {}
            for stock in stock_list:
                df = pd.read_hdf(stock+'.h5')
                data[stock] = df
            return data

2. 选股策略类

选股策略类主要用于实现低价股、资金流和小市值策略。代码如下:

class StockPicker:
    
    def __init__(self, data):
        self.data = data
        
    def low_price_strategy(self, price):
        '低价股策略,返回价格小于等于price的股票'
        selected_stock = []
        for stock, df in self.data.items():
            if df.iloc[-1]['close'] <= price:
                selected_stock.append(stock)
        return selected_stock
    
    def capital_flow_strategy(self, n):
        '资金流策略,返回最近n天资金流入前n的股票'
        selected_stock = []
        for stock, df in self.data.items():
            net_amount = df.iloc[-n:]['amount'].apply(float).sum()
            if net_amount > 0:
                selected_stock.append((stock, net_amount))
        selected_stock = sorted(selected_stock, key=lambda x:x[1], reverse=True)[:n]
        return [s[0] for s in selected_stock]
    
    def small_cap_strategy(self, market_cap):
        '小市值策略,返回市值小于等于market_cap的股票'
        selected_stock = []
        for stock, df in self.data.items():
            if df.iloc[-1]['close'] * df.iloc[-1]['outstanding_share'] <= market_cap * 100000000:
                selected_stock.append(stock)
        return selected_stock

3. 回测执行类

回测执行类主要用于实现买卖限制、考虑增发、支持分板块计算收益、支持轮动等功能。代码如下:

class BackTester:
    
    def __init__(self, data, start_date, end_date, capital, commission_rate):
        self.data = data
        self.start_date = start_date
        self.end_date = end_date
        self.capital = capital
        self.commission_rate = commission_rate
        self.holdings = {}
        self.cash = capital
        self.stock_prices = {}
        self.stock_returns = {}
        
    def run_backtest(self, selected_stock, board=None, frequency='d', rotate=None):
        '运行回测'
        if board is None:
            # 统计所有股票的收益
            pass
        else:
            # 统计指定板块的收益
            selected_stock = [s for s in selected_stock if self.get_board(s) == board]
        
        if frequency == 'd':
            dates = pd.date_range(self.start_date, self.end_date)
        elif frequency == 'w':
            dates = pd.date_range(self.start_date, self.end_date, freq='W-FRI')
        elif frequency == 'm':
            dates = pd.date_range(self.start_date, self.end_date, freq='M')
        else:
            raise ValueError('Unsupported frequency')
        
        for date in dates:
            if rotate is not None:
                # 按月轮动
                if date.day == 1:
                    selected_stock = self.rotate(selected_stock, board, rotate)
                elif date.day > 5:
                    continue
                
            for stock in selected_stock:
                df = self.data[stock]
                if date not in df.index:
                    continue
                
                price = self.stock_prices.get(stock, None)
                if price is None:
                    price = df.loc[date]['open']
                    self.stock_prices[stock] = price
                    
                if stock in self.holdings:
                    # 卖出
                    if df.loc[date]['low'] <= price * 0.9:
                        sell_price = price * 0.9
                        sell_amount = self.holdings[stock]
                        self.cash += sell_price * sell_amount * (1 - self.commission_rate)
                        self.holdings[stock] = 0
                        self.stock_prices[stock] = None
                    elif df.loc[date]['high'] >= price * 1.1:
                        sell_price = price * 1.1
                        sell_amount = self.holdings[stock]
                        self.cash += sell_price * sell_amount * (1 - self.commission_rate)
                        self.holdings[stock] = 0
                        self.stock_prices[stock] = None
                    else:
                        pass
                else:
                    # 买入
                    if self.cash <= 0:
                        continue
                    if df.loc[date]['low'] <= price * 1.1:
                        buy_price = price * 1.1
                        buy_amount = int(self.cash / buy_price / 100) * 100
                        if buy_amount == 0:
                            continue
                        self.cash -= buy_price * buy_amount * (1 + self.commission_rate)
                        self.holdings[stock] = buy_amount
                    elif df.loc[date]['high'] >= price * 0.9:
                        buy_price = price * 0.9
                        buy_amount = int(self.cash / buy_price / 100) * 100
                        if buy_amount == 0:
                            continue
                        self.cash -= buy_price * buy_amount * (1 + self.commission_rate)
                        self.holdings[stock] = buy_amount
                    else:
                        pass
                        
            self.stock_returns[date] = self.get_portfolio_return(selected_stock, date)
                        
    def get_portfolio_return(self, selected_stock, date):
        '计算组合收益'
        total_value = self.cash
        for stock, amount in self.holdings.items():
            if amount == 0:
                continue
            df = self.data[stock]
            if date not in df.index:
                continue
            if self.stock_prices[stock] is None:
                continue
            total_value += df.loc[date]['close'] * amount
            total_value += df.loc[date]['incr_holding_shares'] * df.loc[date]['close'] * amount
        return (total_value - self.capital) / self.capital
    
    def get_board(self, stock):
        '获取股票所在板块'
        if stock.startswith('688'):
            return '科创板'
        elif stock.startswith('300'):
            return '创业板'
        elif stock.startswith('00'):
            return '深市主板'
        elif stock.startswith('60'):
            return '沪市主板'
        else:
            return None
        
    def rotate(self, selected_stock, board, rotate):
        '轮动策略'
        if board is None:
            # 所有股票轮动
            pass
        else:
            # 按板块轮动
            selected_stock = [s for s in selected_stock if self.get_board(s) == board]
            
        prev_month = (datetime.datetime.strptime(self.start_date, '%Y-%m-%d') + relativedelta(months=rotate-1)).strftime('%Y-%m-%d')
        prev_data = self.data.copy()
        for stock in selected_stock:
            prev_df = pd.read_hdf(stock+'.h5', where='date>='%s' and date<'%s'' % (prev_month, self.start_date))
            prev_data[stock] = prev_df.append(self.data[stock])
        prev_backtester = BackTester(prev_data, prev_month, self.end_date, self.capital, self.commission_rate)
        prev_backtester.run_backtest(selected_stock, board, frequency='d', rotate=None)
        selected_stock = sorted(selected_stock, key=lambda x:prev_backtester.stock_returns[x], reverse=True)
        return selected_stock[:5]
    
    def plot_returns(self):
        '绘制收益曲线'
        returns = pd.Series(self.stock_returns)
        returns.plot(figsize=(10, 6), title='Portfolio Returns')
        plt.show()

4. 示例代码

假设我们要运行低价股策略,资金流策略和小市值策略,回测时间为2020-01-01到2021-01-01,初始资金为100万,交易佣金为0.1%。首先需要从Baostock下载数据:

data_loader = DataLoader('2020-01-01', '2021-01-01')
data_loader.download_data(['000001.SH', '000300.SH', '399001.SZ', '399006.SZ'])
data = data_loader.read_data(['000001.SH', '000300.SH', '399001.SZ', '399006.SZ'])

# 运行低价股策略
stock_picker = StockPicker(data)
selected_stock = stock_picker.low_price_strategy(5)
backtester = BackTester(data, '2020-01-01', '2021-01-01', 1000000, 0.001)
backtester.run_backtest(selected_stock)
backtester.plot_returns()

# 运行资金流策略
selected_stock = stock_picker.capital_flow_strategy(10)
backtester = BackTester(data, '2020-01-01', '2021-01-01', 1000000, 0.001)
backtester.run_backtest(selected_stock)
backtester.plot_returns()

# 运行小市值策略
selected_stock = stock_picker.small_cap_strategy(10)
backtester = BackTester(data, '2020-01-01', '2021-01-01', 1000000, 0.001)
backtester.run_backtest(selected_stock)
backtester.plot_returns()

本框架提供了基本的策略回测功能,用户可以根据自身需求进行扩展。例如,添加新的选股策略、增加交易成本模型、优化回测逻辑等等。

基于Baostock和本地存储的HDF数据构建低价股、资金流和小市值策略回测框架

原文地址: https://www.cveoy.top/t/topic/nLB5 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录