量化交易策略回测实践¶

本教程将使用Backtrader框架实现几种常见的量化交易策略,并进行回测和参数优化。我们将使用生成的模拟数据代替真实股票数据,确保代码在任何环境下都能顺利运行。

目录¶

  1. 环境准备与数据生成
  2. 数据可视化与探索
  3. 定义交易策略
  4. 执行回测
  5. 策略参数优化
  6. 投资组合回测
  7. 结论与总结
In [1]:
# %pip install backtrader
In [2]:
import backtrader as bt  
import numpy as np  
import pandas as pd  
import matplotlib.pyplot as plt  
import datetime  
Duplicate key in file WindowsPath('d:/Users/wukek/anaconda3/Lib/site-packages/matplotlib/mpl-data/matplotlibrc'), line 412 ('axes.unicode_minus: True  # use Unicode for the minus symbol rather than hyphen.  See')
In [3]:
# 1. Tushare股票数据  
import tushare as ts
import pandas as pd
import numpy as np

def get_tushare_token(file_path='tushare_token.txt'):
    """
    从文件读取 Tushare Token
    """
    try:
        with open(file_path, 'r') as file:
            token = file.readline().strip()
            return token
    except FileNotFoundError:
        print(f"Error: Token文件 {file_path} 未找到")
        return None
    except Exception as e:
        print(f"读取Token时发生错误: {e}")
        return None

def fetch_stock_data(tickers, start_date, end_date):
    """
    从Tushare获取真实股票数据
    
    参数:
        tickers: 股票代码列表(使用完整代码如'600036.SH')
        start_date: 开始日期
        end_date: 结束日期
        
    返回:
        字典,键为股票代码,值为DataFrame数据
    """
    # 获取 Token
    token = get_tushare_token()
    if not token:
        raise ValueError("无法获取 Tushare Token")
    
    # 初始化 Pro API
    pro = ts.pro_api(token)
    
    # 存储股票数据的字典
    stock_data = {}
    
    for ticker in tickers:
        try:
            # 获取日线行情数据(前复权)
            df = pro.daily(
                ts_code=ticker, 
                start_date=start_date.replace('-', ''), 
                end_date=end_date.replace('-', '')
            )
            
            # 数据预处理
            if df is None or df.empty:
                print(f"未获取到 {ticker} 的数据")
                continue
            
            # 转换日期
            df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
            df = df.sort_values('trade_date').set_index('trade_date')
            
            # 重命名列以完全匹配合成数据格式
            df_processed = pd.DataFrame({
                'Open': df['open'],
                'High': df['high'],
                'Low': df['low'],
                'Close': df['close'],
                'Volume': df['vol']
            })
            
            # 添加到股票数据字典
            stock_data[ticker] = df_processed
        
        except Exception as e:
            print(f"获取 {ticker} 数据时发生错误: {e}")
    
    return stock_data

def preprocess_stock_data(stock_data):
    """
    进一步处理股票数据,确保数据质量
    
    参数:
        stock_data: 原始股票数据字典
    
    返回:
        处理后的股票数据字典
    """
    processed_data = {}
    
    for ticker, df in stock_data.items():
        # 删除缺失值
        df = df.dropna()
        
        # 检查数据长度
        if len(df) == 0:
            print(f"警告:{ticker} 没有有效数据")
            continue
        
        # 异常值处理
        Q1 = df['Close'].quantile(0.25)
        Q3 = df['Close'].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        df = df[(df['Close'] >= lower_bound) & (df['Close'] <= upper_bound)]
        
        # 确保列顺序和命名完全一致
        df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
        
        processed_data[ticker] = df
    
    return processed_data

# def main():
#     # 选择要获取数据的股票
#     tickers = ['600036.SH', '000001.SZ', '601318.SH']  # 招商银行、平安银行、中国平安
#     start_date = '2020-01-01'
#     end_date = '2022-12-31'
    
#     # 获取股票数据
#     stock_data = fetch_stock_data(tickers, start_date, end_date)
    
#     # 数据预处理
#     processed_stock_data = preprocess_stock_data(stock_data)
    
#     # 数据验证与分析
#     for ticker, df in processed_stock_data.items():
#         print(f"\n{ticker} 数据统计:")
#         print(df.describe())
        
#         # 额外分析
#         print("\n价格变化:")
#         print(f"起始价格: {df['Close'].iloc[0]:.2f}")
#         print(f"结束价格: {df['Close'].iloc[-1]:.2f}")
#         print(f"总体变化: {(df['Close'].iloc[-1] / df['Close'].iloc[0] - 1) * 100:.2f}%")
        
#         # 验证列名
#         print("\n列名验证:")
#         print(df.columns.tolist())
    
#     return processed_stock_data

# if __name__ == "__main__":
#     stock_data = main()

# 选择要获取数据的股票
tickers = ['600036.SH', '000001.SZ', '601318.SH']  # 招商银行、平安银行、中国平安
start_date = '2020-01-01'
end_date = '2022-12-31'
# end_date = '2020-12-31'
    
# 获取股票数据
stock_data = fetch_stock_data(tickers, start_date, end_date)

# 数据预处理
processed_stock_data = preprocess_stock_data(stock_data)

# 数据验证与分析
for ticker, df in processed_stock_data.items():
    print(f"\n{ticker} 数据统计:")
    print(df.describe())
    
    # 额外分析
    print("\n价格变化:")
    print(f"起始价格: {df['Close'].iloc[0]:.2f}")
    print(f"结束价格: {df['Close'].iloc[-1]:.2f}")
    print(f"总体变化: {(df['Close'].iloc[-1] / df['Close'].iloc[0] - 1) * 100:.2f}%")
    
    # 验证列名
    print("\n列名验证:")
    print(df.columns.tolist())
    
stock_data = processed_stock_data
600036.SH 数据统计:
             Open        High         Low       Close        Volume
count  728.000000  728.000000  728.000000  728.000000  7.280000e+02
mean    42.750920   43.345838   42.164135   42.752459  6.789903e+05
std      8.066488    8.205544    7.898203    8.051716  3.417884e+05
min     26.900000   27.130000   26.300000   26.820000  2.156602e+05
25%     35.395000   35.837500   35.045000   35.397500  4.539281e+05
50%     40.935000   41.690000   40.415000   41.055000  5.852150e+05
75%     50.877500   51.435000   50.042500   50.900000  8.042732e+05
max     58.210000   58.920000   57.610000   58.500000  2.504770e+06

价格变化:
起始价格: 38.88
结束价格: 37.26
总体变化: -4.17%

列名验证:
['Open', 'High', 'Low', 'Close', 'Volume']

000001.SZ 数据统计:
             Open        High         Low       Close        Volume
count  728.000000  728.000000  728.000000  728.000000  7.280000e+02
mean    16.570577   16.832555   16.314890   16.581470  1.089831e+06
std      3.570154    3.660223    3.471243    3.576886  5.388167e+05
min     10.330000   10.450000   10.220000   10.340000  3.439356e+05
25%     13.640000   13.745000   13.417500   13.585000  7.339923e+05
50%     15.800000   16.050000   15.625000   15.820000  9.635799e+05
75%     19.000000   19.322500   18.620000   19.022500  1.298082e+06
max     24.910000   25.310000   24.520000   25.010000  4.749276e+06

价格变化:
起始价格: 16.87
结束价格: 13.16
总体变化: -21.99%

列名验证:
['Open', 'High', 'Low', 'Close', 'Volume']

601318.SH 数据统计:
             Open        High         Low       Close        Volume
count  728.000000  728.000000  728.000000  728.000000  7.280000e+02
mean    62.862514   63.559904   62.192418   62.832857  6.668724e+05
std     16.305443   16.463620   16.085748   16.260773  3.261314e+05
min     36.390000   36.670000   35.900000   36.150000  2.251350e+05
25%     47.485000   48.027500   46.730000   47.462500  4.369186e+05
50%     62.555000   63.275000   61.970000   62.335000  5.840238e+05
75%     78.445000   79.282500   77.632500   78.197500  8.167294e+05
max     93.380000   94.620000   92.090000   93.380000  2.806695e+06

价格变化:
起始价格: 86.12
结束价格: 47.00
总体变化: -45.42%

列名验证:
['Open', 'High', 'Low', 'Close', 'Volume']
In [4]:
# 2. 数据可视化  
def plot_stock_prices(stock_data, window=20):  
    """  
    绘制股票价格走势图和移动平均线  
    
    参数:  
        stock_data: 字典,键为股票代码,值为DataFrame数据  
        window: 移动平均窗口大小  
    """  
    plt.figure(figsize=(15, 10))  
    
    for i, (ticker, data) in enumerate(stock_data.items()):  
        plt.subplot(len(stock_data), 1, i+1)  
        
        # 计算移动平均线  
        ma = data['Close'].rolling(window=window).mean()  
        
        # 绘制收盘价和移动平均线  
        plt.plot(data.index, data['Close'], label=f'{ticker} 收盘价')  
        plt.plot(data.index, ma, label=f'{window}日移动平均线', linestyle='--')  
        
        plt.title(f'{ticker} 股价走势')  
        plt.ylabel('价格')  
        plt.legend()  
        
        # 仅在最后一个子图上显示x轴标签  
        if i == len(stock_data) - 1:  
            plt.xlabel('日期')  
    
    plt.tight_layout()  
    plt.show()  

# 绘制股票价格走势图  
plot_stock_prices(stock_data)  

def calculate_returns(stock_data):  
    """  
    计算收益率  
    
    参数:  
        stock_data: 字典,键为股票代码,值为DataFrame数据  
        
    返回:  
        dict: 字典,键为股票代码,值为包含收益率的DataFrame  
    """  
    returns_data = {}  
    
    for ticker, data in stock_data.items():  
        # 复制数据  
        df = data.copy()  
        
        # 计算日收益率  
        df['Daily_Return'] = df['Close'].pct_change()  
        
        # 计算累积收益率  
        df['Cumulative_Return'] = (1 + df['Daily_Return']).cumprod() - 1  
        
        returns_data[ticker] = df  
        
    return returns_data  

# 计算收益率  
returns_data = calculate_returns(stock_data)  

def plot_cumulative_returns(returns_data):  
    """  
    绘制累积收益率对比图  
    
    参数:  
        returns_data: 字典,键为股票代码,值为包含收益率的DataFrame  
    """  
    plt.figure(figsize=(15, 6))  
    
    for ticker, data in returns_data.items():  
        plt.plot(data.index, data['Cumulative_Return'] * 100, label=ticker)  
    
    plt.title('各股票累积收益率对比')  
    plt.xlabel('日期')  
    plt.ylabel('累积收益率 (%)')  
    plt.legend()  
    plt.grid(True)  
    plt.show()  

# 绘制累积收益率对比图  
plot_cumulative_returns(returns_data)  

def calculate_statistics(returns_data):  
    """  
    计算统计指标  
    
    参数:  
        returns_data: 字典,键为股票代码,值为包含收益率的DataFrame  
        
    返回:  
        DataFrame: 包含各股票统计指标的DataFrame  
    """  
    stats = []  
    
    for ticker, data in returns_data.items():  
        # 提取日收益率  
        daily_returns = data['Daily_Return'].dropna()  
        
        # 计算统计指标  
        avg_return = daily_returns.mean() * 252 * 100  # 年化收益率  
        volatility = daily_returns.std() * np.sqrt(252) * 100  # 年化波动率  
        sharpe = avg_return / volatility if volatility != 0 else 0  # 夏普比率  
        max_drawdown = (data['Close'] / data['Close'].cummax() - 1).min() * 100  # 最大回撤  
        
        stats.append({  
            'Ticker': ticker,  
            'Avg Return (%)': avg_return,  
            'Volatility (%)': volatility,  
            'Sharpe Ratio': sharpe,  
            'Max Drawdown (%)': max_drawdown  
        })  
    
    # 转换为DataFrame  
    stats_df = pd.DataFrame(stats).set_index('Ticker')  
    
    return stats_df  

# 计算并显示统计指标  
stats_df = calculate_statistics(returns_data)  
print(stats_df)  
No description has been provided for this image
d:\Users\wukek\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:152: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from current font.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
           Avg Return (%)  Volatility (%)  Sharpe Ratio  Max Drawdown (%)
Ticker                                                                   
600036.SH        4.116795       33.533192      0.122768        -54.153846
000001.SZ       -2.252198       35.772030     -0.062960        -58.656537
601318.SH      -17.057392       28.119877     -0.606596        -61.287214
In [5]:
# 3. 准备Backtrader数据  
def prepare_backtrader_data(stock_data):  
    """  
    将股票数据转换为Backtrader格式  
    
    参数:  
        stock_data: 字典,键为股票代码,值为DataFrame数据  
        
    返回:  
        dict: 字典,键为股票代码,值为Backtrader的数据源  
    """  
    bt_data = {}  
    
    for ticker, df in stock_data.items():  
        # 确保列名与Backtrader期望的一致  
        df_bt = df.copy()  
        df_bt.columns = [col.lower() for col in df_bt.columns]  # 将列名转为小写  
        
        # 创建Backtrader数据源  
        data = bt.feeds.PandasData(  
            dataname=df_bt,  
            # 指定日期列是索引  
            datetime=None,  
            # 指定OHLCV列  
            open=0,  
            high=1,  
            low=2,  
            close=3,  
            volume=4,  
            openinterest=-1  # 不使用  
        )  
        
        bt_data[ticker] = data  
    
    return bt_data  

# 准备Backtrader数据  
bt_data = prepare_backtrader_data(stock_data)  
In [6]:
# 4. 定义交易策略  

class MACrossKellyStrategy(bt.Strategy):
    """
    移动平均交叉策略 + 凯利准则仓位控制
    当短期移动平均线上穿长期移动平均线时买入
    当短期移动平均线下穿长期移动平均线时卖出
    使用凯利准则动态调整仓位大小
    """
    params = (
        ('short_period', 20),      # 短期移动平均线周期
        ('long_period', 50),       # 长期移动平均线周期
        ('printlog', False),       # 是否打印日志
        ('kelly_lookback', 20),    # 凯利计算的历史交易回望期
        ('max_position', 0.95),    # 最大仓位限制
        ('min_position', 0.01),    # 最小仓位限制
        ('kelly_multiplier', 0.25) # 凯利比例调整系数(保守系数)
    )

    def __init__(self):
        # 初始化移动平均线指标
        self.short_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.short_period)
        self.long_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.long_period)
        
        # 交叉信号
        self.crossover = bt.indicators.CrossOver(self.short_ma, self.long_ma)
        
        # 跟踪订单,持仓和资产
        self.order = None
        self.buyprice = None
        self.buycomm = None
        
        # 凯利准则相关变量
        self.trade_history = []  # 交易历史记录
        self.kelly_fraction = 0.1  # 初始凯利比例
        
        # 添加移动平均线到图表
        self.short_ma.plotinfo.plotname = f'SMA({self.params.short_period})'
        self.long_ma.plotinfo.plotname = f'SMA({self.params.long_period})'
        
    def log(self, txt, dt=None, doprint=False):
        """ 记录策略信息 """
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()}, {txt}')
    
    def calculate_kelly_fraction(self):
        """
        计算凯利比例
        凯利公式: f* = (bp - q) / b
        其中:
        - b = 平均盈利 / 平均亏损 (盈亏比)
        - p = 胜率
        - q = 败率 = 1 - p
        """
        if len(self.trade_history) < 5:  # 至少需要5笔交易才开始计算
            return self.kelly_fraction
        
        # 获取最近的交易记录
        recent_trades = self.trade_history[-self.params.kelly_lookback:]
        
        # 分离盈利和亏损交易
        profits = [trade for trade in recent_trades if trade > 0]
        losses = [abs(trade) for trade in recent_trades if trade < 0]
        
        if len(losses) == 0:  # 没有亏损交易
            kelly_fraction = self.params.max_position
        elif len(profits) == 0:  # 没有盈利交易
            kelly_fraction = self.params.min_position
        else:
            # 计算胜率
            win_rate = len(profits) / len(recent_trades)
            loss_rate = 1 - win_rate
            
            # 计算平均盈利和平均亏损
            avg_profit = np.mean(profits)
            avg_loss = np.mean(losses)
            
            # 计算盈亏比
            profit_loss_ratio = avg_profit / avg_loss
            
            # 凯利公式
            kelly_fraction = (profit_loss_ratio * win_rate - loss_rate) / profit_loss_ratio
            
            # 应用保守系数
            kelly_fraction *= self.params.kelly_multiplier
            
            # 限制仓位范围
            kelly_fraction = max(self.params.min_position, 
                               min(self.params.max_position, kelly_fraction))
        
        self.log(f'凯利比例计算: {kelly_fraction:.3f}, 历史交易数: {len(recent_trades)}')
        return kelly_fraction
    
    def calculate_position_size(self):
        """
        根据凯利比例计算仓位大小
        """
        # 更新凯利比例
        self.kelly_fraction = self.calculate_kelly_fraction()
        
        # 计算可用资金
        available_cash = self.broker.getcash()
        current_value = self.broker.getvalue()
        
        # 根据凯利比例计算目标仓位价值
        target_position_value = current_value * self.kelly_fraction
        
        # 计算股票数量(向下取整到最接近的100股倍数)
        current_price = self.data.close[0]
        target_shares = int(target_position_value / current_price / 100) * 100
        
        # 确保不超过可用资金
        max_affordable_shares = int(available_cash / current_price / 100) * 100
        final_shares = min(target_shares, max_affordable_shares)
        
        self.log(f'凯利仓位计算: 比例={self.kelly_fraction:.3f}, '
                f'目标股数={target_shares}, 最终股数={final_shares}')
        
        return max(100, final_shares)  # 最少买100股
            
    def notify_order(self, order):
        """ 订单状态变化通知 """
        if order.status in [order.Submitted, order.Accepted]:
            return
        
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f'买入执行, 价格: {order.executed.price:.2f}, '
                    f'数量: {order.executed.size}, '
                    f'成本: {order.executed.value:.2f}, '
                    f'手续费: {order.executed.comm:.2f}'
                )
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # 卖出
                self.log(
                    f'卖出执行, 价格: {order.executed.price:.2f}, '
                    f'数量: {order.executed.size}, '
                    f'成本: {order.executed.value:.2f}, '
                    f'手续费: {order.executed.comm:.2f}'
                )
                
            self.bar_executed = len(self)
            
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单被取消/拒绝')
            
        self.order = None
        
    def notify_trade(self, trade):
        """ 交易结果通知 """
        if not trade.isclosed:
            return
        
        # 记录交易结果到历史记录
        if abs(trade.value) >0:
            pnl_percent = trade.pnlcomm / abs(trade.value) * 100  # 计算收益率
        else:
            pnl_percent = 0.0
        self.trade_history.append(pnl_percent)
        
        self.log(f'交易完成 - 毛利润: {trade.pnl:.2f}, '
                f'净利润: {trade.pnlcomm:.2f}, '
                f'收益率: {pnl_percent:.2f}%')
        
    def next(self):
        """ 策略逻辑 """
        self.log(f'收盘价: {self.data.close[0]:.2f}, '
                f'资产价值: {self.broker.getvalue():.2f}')
        
        if self.order:
            return
            
        if not self.position:
            # 买入信号
            if self.crossover > 0:
                # 计算买入数量
                size = self.calculate_position_size()
                self.log(f'买入信号 - 价格: {self.data.close[0]:.2f}, 数量: {size}')
                self.order = self.buy(size=size)
        else:
            # 卖出信号
            if self.crossover < 0:
                self.log(f'卖出信号 - 价格: {self.data.close[0]:.2f}')
                self.order = self.close()  # 全部卖出
                
    def stop(self):
        """ 策略结束时调用 """
        self.log('MA交叉+凯利策略结束,短期={} 长期={}, 最终凯利比例={:.3f}'.format(
            self.params.short_period, self.params.long_period, 
            self.kelly_fraction), doprint=True)
        
        # 打印交易统计
        if self.trade_history:
            win_trades = [t for t in self.trade_history if t > 0]
            lose_trades = [t for t in self.trade_history if t < 0]
            
            self.log(f'交易统计: 总交易={len(self.trade_history)}, '
                    f'盈利交易={len(win_trades)}, '
                    f'亏损交易={len(lose_trades)}, '
                    f'胜率={len(win_trades)/len(self.trade_history)*100:.1f}%', 
                    doprint=True)
In [7]:
# 5. 执行回测  
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

def run_backtest(data, strategy, cash=100000.0, commission=0.001, **kwargs):  
    """  
    运行回测  
    
    参数:  
        data: Backtrader数据源  
        strategy: 策略类  
        cash: 初始资金  
        commission: 手续费率  
        **kwargs: 策略参数  
        
    返回:  
        cerebro: Backtrader引擎  
    """  
    # 创建Backtrader引擎  
    cerebro = bt.Cerebro()  
    
    # 添加数据  
    cerebro.adddata(data)  
    
    # 设置初始资金  
    cerebro.broker.setcash(cash)  
    
    # 设置手续费  
    cerebro.broker.setcommission(commission=commission)  
    
    # 添加策略,传入参数  
    cerebro.addstrategy(strategy, **kwargs)  
    
    # 添加分析器  
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.01, _name='sharpe')  
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')  
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')  
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')  
    
    # 设置标的数量计算方式  
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)  
    
    # 运行回测  
    print(f'初始投资组合价值: {cerebro.broker.getvalue():.2f}')  
    results = cerebro.run()  
    print(f'最终投资组合价值: {cerebro.broker.getvalue():.2f}')  
    
    # 打印分析结果  
    strat = results[0]  
    print('\n分析结果:')  
    print(f'夏普比率: {strat.analyzers.sharpe.get_analysis()["sharperatio"]:.3f}')  
    print(f'最大回撤: {strat.analyzers.drawdown.get_analysis()["max"]["drawdown"]:.2f}%')  
    print(f'年化收益率: {strat.analyzers.returns.get_analysis()["ravg"] * 252 * 100:.2f}%')  
    
    trade_analysis = strat.analyzers.trades.get_analysis()  
    if trade_analysis['total']['total'] > 0:  
        print(f'总交易次数: {trade_analysis["total"]["total"]}')  
        print(f'获利交易次数: {trade_analysis["won"]["total"]}')  
        print(f'亏损交易次数: {trade_analysis["lost"]["total"]}')  
        if trade_analysis['won']['total'] > 0:  
            print(f'平均获利交易收益: {trade_analysis["won"]["pnl"]["average"]:.2f}')  
        if trade_analysis['lost']['total'] > 0:  
            print(f'平均亏损交易损失: {trade_analysis["lost"]["pnl"]["average"]:.2f}')  

    # Jupyter 绘图方案
    plt.figure(figsize=(16, 10))
    figs = cerebro.plot(
        style='bar',           # K线样式
        barup='green',         # 上涨K线颜色
        bardown='red',         # 下跌K线颜色
        volume=True,           # 显示成交量
        trades=True,           # 显示交易
        figsize=(16, 10)       # 图表尺寸
    )
    # plt.tight_layout()
    plt.show()

    return cerebro  

# 运行移动平均交叉策略回测  
ticker = tickers[0]  # 选择第一支股票  
cerebro_ma = run_backtest(  
    data=bt_data[ticker],  
    strategy=MACrossKellyStrategy,  
    cash=100000.0,  
    commission=0.001,
    short_period=20,  
    long_period=50,  
    printlog=False  
)  
初始投资组合价值: 100000.00
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
最终投资组合价值: 86021.44

分析结果:
夏普比率: -0.713
最大回撤: 22.95%
年化收益率: -5.21%
总交易次数: 8
获利交易次数: 2
亏损交易次数: 5
平均获利交易收益: 975.91
平均亏损交易损失: -3768.68
d:\Users\wukek\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:152: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from current font.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
In [8]:
# 6. 策略参数优化  
def optimize_strategy(data, strategy, cash=100000.0, commission=0.001, **kwargs):  
    """  
    优化策略参数  
    
    参数:  
        data: Backtrader数据源  
        strategy: 策略类  
        cash: 初始资金  
        commission: 手续费率  
        **kwargs: 策略参数范围  
        
    返回:  
        results: 优化结果  
    """  
    # 创建Backtrader引擎  
    cerebro = bt.Cerebro(maxcpus=1)  # 使用单核处理以避免并行问题  
    
    # 添加数据  
    cerebro.adddata(data)  
    
    # 设置初始资金  
    cerebro.broker.setcash(cash)  
    
    # 设置手续费  
    cerebro.broker.setcommission(commission=commission)  
    
    # 添加策略,使用参数范围  
    cerebro.optstrategy(strategy, **kwargs)  
    
    # 添加分析器  
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.01, _name='sharpe')  
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')  
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')  
    
    # 设置标的数量计算方式  
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)  
    
    # 运行优化  
    print('开始优化...')  
    results = cerebro.run()  
    print('优化完成!')  
    
    # 收集结果  
    final_results = []  
    for run in results:  
        for strategy in run:  
            sharpe = strategy.analyzers.sharpe.get_analysis()['sharperatio']  
            drawdown = strategy.analyzers.drawdown.get_analysis()['max']['drawdown']  
            annual_return = strategy.analyzers.returns.get_analysis()['ravg'] * 252 * 100  
            
            # 获取策略参数  
            params = {}  
            for param_name, param_value in strategy.params._getitems():  
                # 排除私有属性和不需要的属性  
                if not param_name.startswith('_') and param_name != 'printlog':  
                    params[param_name] = param_value  
            
            final_results.append({  
                'params': params,  
                'sharpe': sharpe,  
                'drawdown': drawdown,  
                'annual_return': annual_return  
            })  
    
    # 按夏普比率排序  
    #final_results.sort(key=lambda x: x['sharpe'], reverse=True)  
    final_results.sort(key=lambda x: float('-inf') if x['sharpe'] is None else x['sharpe'], reverse=True)
    
    return final_results  

# 优化MA交叉策略  
ticker = tickers[0]  # 使用第一支股票  

# 定义参数范围  
params = {  
    'short_period': range(5, 31, 5),  # 短期MA周期:5, 10, 15, 20, 25, 30  
    'long_period': range(30, 101, 10),  # 长期MA周期:30, 40, 50, 60, 70, 80, 90, 100  
    'printlog': [False]  # 禁用日志输出  
}  

# 运行优化  
ma_results = optimize_strategy(  
    data=bt_data[ticker],  
    strategy=MACrossKellyStrategy,  
    cash=100000.0,  
    commission=0.001,  
    **params  
)  

# 显示前10个最佳结果  
print('\nMA交叉策略优化结果 (按夏普比率排序):')  
print('\n前10个最佳参数组合:')  
for i, result in enumerate(ma_results[:10]):  
    params_str = ', '.join([f'{k}={v}' for k, v in result['params'].items()])  
    print(f'{i+1}. {params_str} - 夏普比率: {result["sharpe"]:.3f}, '  
          f'最大回撤: {result["drawdown"]:.2f}%, 年化收益率: {result["annual_return"]:.2f}%')  
开始优化...
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=30, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=17, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=15, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=16, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=70, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=80, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=90, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=8, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=5 长期=100, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=30, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=15, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=12, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=10, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=70, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=80, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=90, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=10 长期=100, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=6, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=30, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=14, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=9, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=70, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=80, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=3, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=90, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=15 长期=100, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=3, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=30, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=15, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=11, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=70, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=80, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=90, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=3, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=20 长期=100, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=3, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=30, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=18, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=10, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=7, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=70, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=80, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=90, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=2, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=25 长期=100, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=2, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=30, 最终凯利比例=0.100
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=40, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=13, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=50, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=60, 最终凯利比例=0.950
2022-12-30, 交易统计: 总交易=5, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=70, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=80, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=4, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=90, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=2, 盈利交易=0, 亏损交易=0, 胜率=0.0%
2022-12-30, MA交叉+凯利策略结束,短期=30 长期=100, 最终凯利比例=0.100
2022-12-30, 交易统计: 总交易=2, 盈利交易=0, 亏损交易=0, 胜率=0.0%
优化完成!

MA交叉策略优化结果 (按夏普比率排序):

前10个最佳参数组合:
1. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.975, 最大回撤: 4.73%, 年化收益率: 1.57%
2. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.922, 最大回撤: 5.39%, 年化收益率: 2.14%
3. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.855, 最大回撤: 3.42%, 年化收益率: 2.90%
4. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.850, 最大回撤: 6.39%, 年化收益率: 1.90%
5. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.789, 最大回撤: 3.80%, 年化收益率: 2.78%
6. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.743, 最大回撤: 5.41%, 年化收益率: 1.66%
7. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.726, 最大回撤: 6.05%, 年化收益率: 4.33%
8. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.700, 最大回撤: 3.57%, 年化收益率: 2.62%
9. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.592, 最大回撤: 8.15%, 年化收益率: 3.38%
10. short_period=20, long_period=50, kelly_lookback=20, max_position=0.95, min_position=0.01, kelly_multiplier=0.25 - 夏普比率: 0.571, 最大回撤: 4.67%, 年化收益率: 1.50%