Files
quant/gogogo/result_visualizer.py
2025-11-15 17:25:43 +08:00

407 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# result_visualizer.py
import vectorbt as vbt
import pandas as pd
import matplotlib.pyplot as plt
from strategy_executor import generate_strategy, create_portfolio, create_real_stock_portfolio, debug_signal_consistency, debug_signal_logic
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def plot_results(strategy_data, portfolio):
"""绘制结果图表"""
price_ratio = strategy_data['price_ratio']
signals = strategy_data['signals']
ratio_ma = strategy_data['ratio_ma']
upper_band = strategy_data['upper_band']
lower_band = strategy_data['lower_band']
initial_cash = strategy_data['initial_cash']
# ========== 绘制基于比率的技术分析图表 ==========
print("\n绘制基于比率的技术分析图表...")
# 创建详细的技术分析图表
fig, axes = plt.subplots(4, 1, figsize=(15, 16))
# 1. 价格比率和布林带 + 交易信号
axes[0].plot(price_ratio.index, price_ratio, label='价格比率(中芯/华虹)', linewidth=1.5, color='blue')
axes[0].plot(price_ratio.index, ratio_ma, label='移动平均', linewidth=1, alpha=0.7, color='orange')
axes[0].plot(price_ratio.index, upper_band, label='上轨', linewidth=1, alpha=0.7, linestyle='--', color='red')
axes[0].plot(price_ratio.index, lower_band, label='下轨', linewidth=1, alpha=0.7, linestyle='--', color='green')
axes[0].set_title('中芯国际-华虹半导体价格比率 (配对交易标的)')
axes[0].set_ylabel('价格比率')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 标记交易信号
long_signals = signals[signals == 1]
short_signals = signals[signals == -1]
if len(long_signals) > 0:
axes[0].scatter(long_signals.index, price_ratio[long_signals.index],
color='green', marker='^', s=80, label='买入比率(做多中芯/做空华虹)', zorder=5)
if len(short_signals) > 0:
axes[0].scatter(short_signals.index, price_ratio[short_signals.index],
color='red', marker='v', s=80, label='卖空比率(做空中芯/做多华虹)', zorder=5)
# 2. 交易信号
axes[1].plot(signals.index, signals, label='交易信号', linewidth=2, color='purple', drawstyle='steps-post')
axes[1].axhline(y=1, color='green', linestyle='--', alpha=0.5, label='买入信号')
axes[1].axhline(y=-1, color='red', linestyle='--', alpha=0.5, label='卖出信号')
axes[1].axhline(y=0, color='gray', linestyle='-', alpha=0.3)
axes[1].set_title('交易信号时序')
axes[1].set_ylabel('信号')
axes[1].set_ylim(-1.5, 1.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 3. 仓位变化
positions = signals.replace({1: '做多', -1: '做空', 0: '空仓'})
axes[2].plot(positions.index, positions, label='仓位状态', linewidth=2, color='darkorange', drawstyle='steps-post')
axes[2].set_title('仓位变化')
axes[2].set_ylabel('仓位')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
# 4. 组合净值
if portfolio is not None:
portfolio_value = portfolio.value()
if len(portfolio_value) > 0:
axes[3].plot(portfolio_value.index, portfolio_value, label='组合净值', linewidth=2, color='darkblue')
# 标记初始资金线
axes[3].axhline(y=initial_cash, color='red', linestyle='--', alpha=0.7, label=f'初始资金({initial_cash})')
axes[3].set_title('基于价格比率的配对交易组合净值')
axes[3].set_ylabel('组合价值')
axes[3].set_xlabel('日期')
axes[3].legend()
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
def print_statistics(strategy_data, portfolio):
"""打印统计结果"""
price_ratio = strategy_data['price_ratio']
signals = strategy_data['signals']
print("\n=== 基于价格比率的配对交易策略表现 ===")
if portfolio is not None:
# ========== 使用vectorbt进行专业分析 ==========
print("\n=== VectorBT 专业分析(基于价格比率) ===")
try:
# 选择第一列(也是唯一的一列)
portfolio_single = portfolio['RATIO']
print(portfolio_single.stats())
# 绘制vectorbt图表
fig = portfolio_single.plot(subplots=[
'orders', # 订单
'trade_pnl', # 交易盈亏
'cum_returns', # 累积收益
'drawdowns' # 回撤
])
fig.update_layout(
title='基于价格比率的配对交易详细分析',
height=800
)
fig.show()
except Exception as e:
print(f"详细分析绘制失败: {e}")
# ========== 打印详细统计 ==========
print("\n=== 详细统计 ===")
try:
# 使用单列统计
stats = portfolio['RATIO'].stats()
def safe_get_stat(stat_dict, key, default="N/A"):
value = stat_dict.get(key, default)
if hasattr(value, 'iloc'):
return value.iloc[0] if len(value) == 1 else value
return value
print(f"开始日期: {safe_get_stat(stats, 'Start')}")
print(f"结束日期: {safe_get_stat(stats, 'End')}")
print(f"期间: {safe_get_stat(stats, 'Period')}")
print(f"总收益率: {safe_get_stat(stats, 'Total Return [%]', 'N/A')}%")
print(f"年化收益率: {safe_get_stat(stats, 'Annual Return [%]', 'N/A')}%")
print(f"年化波动率: {safe_get_stat(stats, 'Annual Volatility [%]', 'N/A')}%")
print(f"夏普比率: {safe_get_stat(stats, 'Sharpe Ratio', 'N/A')}")
print(f"最大回撤: {safe_get_stat(stats, 'Max Drawdown [%]', 'N/A')}%")
print(f"总交易次数: {safe_get_stat(stats, 'Total Trades', 'N/A')}")
print(f"胜率: {safe_get_stat(stats, 'Win Rate [%]', 'N/A')}%")
print(f"盈亏比: {safe_get_stat(stats, 'Profit Factor', 'N/A')}")
except Exception as e:
print(f"获取详细统计时出错: {e}")
# 分析每笔交易
try:
trades_df = portfolio['RATIO'].trades.records_readable
if len(trades_df) > 0:
print(f"\n交易分析:")
print(f"总交易次数: {len(trades_df)}")
if 'Duration' in trades_df.columns:
print(f"平均持仓时间: {trades_df['Duration'].mean():.1f}")
if 'PnL' in trades_df.columns:
print(f"最大单笔盈利: {trades_df['PnL'].max():.2f}")
print(f"最大单笔亏损: {trades_df['PnL'].min():.2f}")
winning_trades = trades_df[trades_df['PnL'] > 0]
losing_trades = trades_df[trades_df['PnL'] < 0]
if len(winning_trades) > 0:
print(f"平均盈利: {winning_trades['PnL'].mean():.2f}")
if len(losing_trades) > 0:
print(f"平均亏损: {losing_trades['PnL'].mean():.2f}")
except Exception as e:
print(f"分析交易时出错: {e}")
# ========== 比率数据统计摘要 ==========
print("\n=== 价格比率统计摘要 ===")
print(f"数据期间: {price_ratio.index.min()}{price_ratio.index.max()}")
print(f"数据点数: {len(price_ratio)}")
print(f"比率均值: {price_ratio.mean():.4f}")
print(f"比率标准差: {price_ratio.std():.4f}")
print(f"比率变异系数: {price_ratio.std()/price_ratio.mean():.4f}")
# 计算交易信号统计
long_count = (signals == 1).sum()
short_count = (signals == -1).sum()
total_signals = long_count + short_count
print(f"\n交易信号统计:")
print(f"做多信号次数: {long_count}")
print(f"做空信号次数: {short_count}")
print(f"总信号次数: {total_signals}")
def print_trade_orders(strategy_data, stock_portfolio):
"""打印所有交易订单信息"""
if stock_portfolio is None:
return
print("\n" + "="*60)
print("=== 详细交易订单信息 ===")
print("="*60)
try:
# 获取订单记录
orders_df = stock_portfolio.orders.records_readable
if len(orders_df) > 0:
print(f"\n总订单数量: {len(orders_df)}")
# 按时间排序
time_column = None
possible_time_columns = ['Timestamp', 'Date', 'Time']
for col in possible_time_columns:
if col in orders_df.columns:
time_column = col
break
if time_column:
orders_sorted = orders_df.sort_values(time_column)
else:
orders_sorted = orders_df
# 显示所有订单(按时间排序)
pd.set_option('display.max_rows', None)
pd.set_option('display.width', None)
print("\n【所有交易订单(按时间排序)】")
print(orders_sorted.to_string(index=False))
# 按股票分组统计
print(f"\n【按股票统计】")
for column in ['SMIC', 'HHIC']:
column_orders = orders_sorted[orders_sorted['Column'] == column]
if len(column_orders) > 0:
buy_orders = column_orders[column_orders['Side'] == 'Buy']
sell_orders = column_orders[column_orders['Side'] == 'Sell']
print(f"\n{column} 订单统计:")
print(f" 总订单数: {len(column_orders)}")
print(f" 买入订单: {len(buy_orders)}")
print(f" 卖出订单: {len(sell_orders)}")
print(f" 总交易股数: {column_orders['Size'].abs().sum():.2f}")
print(f" 总手续费: {column_orders['Fees'].sum():.2f}")
# 重置pandas显示选项
pd.reset_option('display.max_rows')
pd.reset_option('display.width')
else:
print("没有找到交易订单")
except Exception as e:
print(f"打印订单信息时出错: {e}")
import traceback
traceback.print_exc()
def print_detailed_trade_analysis(strategy_data, stock_portfolio):
"""打印详细的交易分析"""
if stock_portfolio is None:
return
print("\n" + "="*60)
print("=== 详细交易分析 ===")
print("="*60)
try:
# 获取交易记录
trades_df = stock_portfolio.trades.records_readable
if len(trades_df) > 0:
print(f"\n总交易次数: {len(trades_df)}")
# 按开仓时间排序
trades_sorted = trades_df.sort_values('Entry Timestamp')
# 显示所有交易(按时间排序)
pd.set_option('display.max_rows', None)
pd.set_option('display.width', None)
print("\n【所有交易记录(按开仓时间排序)】")
print(trades_sorted.to_string(index=False))
# 按股票分组分析
print(f"\n【按股票交易分析】")
for column in ['SMIC', 'HHIC']:
column_trades = trades_sorted[trades_sorted['Column'] == column]
if len(column_trades) > 0:
winning_trades = column_trades[column_trades['PnL'] > 0]
losing_trades = column_trades[column_trades['PnL'] < 0]
print(f"\n{column} 交易分析:")
print(f" 总交易次数: {len(column_trades)}")
print(f" 盈利交易: {len(winning_trades)}")
print(f" 亏损交易: {len(losing_trades)}")
print(f" 胜率: {len(winning_trades)/len(column_trades)*100:.1f}%")
print(f" 总盈亏: {column_trades['PnL'].sum():.2f}")
print(f" 平均每笔盈亏: {column_trades['PnL'].mean():.2f}")
print(f" 最大盈利: {column_trades['PnL'].max():.2f}")
print(f" 最大亏损: {column_trades['PnL'].min():.2f}")
if len(winning_trades) > 0:
print(f" 平均盈利: {winning_trades['PnL'].mean():.2f}")
if len(losing_trades) > 0:
print(f" 平均亏损: {losing_trades['PnL'].mean():.2f}")
# 正确的配对交易分析
print(f"\n【配对交易分析(修正)】")
# 按开仓时间分组找出同一天开仓的SMIC和HHIC交易
entry_groups = trades_sorted.groupby('Entry Timestamp').agg({
'Column': list,
'PnL': list,
'Size': list,
'Direction': list,
'Exit Timestamp': list
})
pair_trades = []
for entry_date, group_data in entry_groups.iterrows():
columns = group_data['Column']
pnls = group_data['PnL']
directions = group_data['Direction']
exit_times = group_data['Exit Timestamp']
# 检查是否同时有SMIC和HHIC的交易
if 'SMIC' in columns and 'HHIC' in columns:
smic_idx = columns.index('SMIC')
hhic_idx = columns.index('HHIC')
# 检查交易方向是否配对(一个做多,一个做空)
if directions[smic_idx] != directions[hhic_idx]:
pair_pnl = pnls[smic_idx] + pnls[hhic_idx]
pair_trades.append({
'entry_date': entry_date,
'exit_date_smic': exit_times[smic_idx],
'exit_date_hhic': exit_times[hhic_idx],
'smic_pnl': pnls[smic_idx],
'hhic_pnl': pnls[hhic_idx],
'total_pnl': pair_pnl,
'smic_direction': directions[smic_idx],
'hhic_direction': directions[hhic_idx]
})
if pair_trades:
# 按开仓时间排序配对交易
pair_trades_sorted = sorted(pair_trades, key=lambda x: x['entry_date'])
print(f"配对交易次数: {len(pair_trades_sorted)}")
total_pair_pnl = sum(trade['total_pnl'] for trade in pair_trades_sorted)
avg_pair_pnl = total_pair_pnl / len(pair_trades_sorted)
winning_pairs = [t for t in pair_trades_sorted if t['total_pnl'] > 0]
losing_pairs = [t for t in pair_trades_sorted if t['total_pnl'] < 0]
print(f"配对交易总盈亏: {total_pair_pnl:.2f}")
print(f"平均每对盈亏: {avg_pair_pnl:.2f}")
print(f"盈利配对: {len(winning_pairs)}")
print(f"亏损配对: {len(losing_pairs)}")
print(f"配对胜率: {len(winning_pairs)/len(pair_trades_sorted)*100:.1f}%")
# 显示每对交易的详细信息(按时间排序)
print(f"\n【各配对交易详情(按开仓时间排序)】")
for i, trade in enumerate(pair_trades_sorted, 1):
status = "盈利" if trade['total_pnl'] > 0 else "亏损"
print(f"{i}对 - 开仓: {trade['entry_date']}")
print(f" SMIC({trade['smic_direction']}): {trade['smic_pnl']:.2f} (平仓: {trade['exit_date_smic']})")
print(f" HHIC({trade['hhic_direction']}): {trade['hhic_pnl']:.2f} (平仓: {trade['exit_date_hhic']})")
print(f" 总盈亏: {trade['total_pnl']:.2f} [{status}]")
print()
else:
print("没有找到配对交易")
# 重置pandas显示选项
pd.reset_option('display.max_rows')
pd.reset_option('display.width')
else:
print("没有找到交易记录")
except Exception as e:
print(f"打印交易分析时出错: {e}")
import traceback
traceback.print_exc()
def main():
"""主函数"""
# 生成策略
strategy_data = generate_strategy()
# 调试信号生成逻辑
debug_signal_logic(strategy_data)
# 创建基于比率的投资组合(用于分析)
ratio_portfolio = create_portfolio(strategy_data)
# 创建基于真实股票的投资组合(用于真实收益计算)
stock_portfolio = create_real_stock_portfolio(strategy_data)
# 调试信号一致性
debug_signal_consistency(strategy_data, stock_portfolio)
# 打印基于比率的统计
print_statistics(strategy_data, ratio_portfolio)
# 打印真实股票组合的统计
print(stock_portfolio['SMIC'].stats())
print(stock_portfolio['HHIC'].stats())
# 绘制基于比率的结果
plot_results(strategy_data, ratio_portfolio)
# 打印详细交易订单信息
print_trade_orders(strategy_data, stock_portfolio)
# 打印详细交易分析
print_detailed_trade_analysis(strategy_data, stock_portfolio)
print("程序执行完成!")
if __name__ == "__main__":
main()