# -*- coding: utf-8 -*- """ 华尔街量化回测集成模块 - TANZG 单股快速回测 """ import sys, os, warnings warnings.filterwarnings('ignore') _bt_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'backtest')) if _bt_dir not in sys.path: sys.path.insert(0, _bt_dir) import pandas as pd import numpy as np from datetime import datetime, timedelta from rules import VolumePriceRules, KCandlestickRules, ChipDistributionRules, MoneyFlowRules from scoring import evaluate_signal from indicators import calc_all_indicators def quick_backtest(kline_df: pd.DataFrame, lookback_days: int = 250) -> dict: """ 单股快速回测 - 验证华尔街模型在该股票上的历史表现 使用最近 lookback_days 天数据,信号触发后持有10天的盈亏统计 Args: kline_df: 日线K线DataFrame lookback_days: 回测使用数据天数(默认250个交易日≈1年) Returns: { 'total_signals': int, # 总信号次数 'win_count': int, # 盈利次数 'lose_count': int, # 亏损次数 'win_rate': float, # 胜率 % 'avg_profit_pct': float, # 平均盈利% 'avg_loss_pct': float, # 平均亏损% 'profit_loss_ratio': float, # 盈亏比 'hold_days': int, # 持仓天数 'conclusion': str, # 回测结论 } """ if kline_df is None or len(kline_df) < 100: return {'error': 'K线数据不足,无法回测'} df = kline_df.copy() # 标准化列名 col_map = {} for c in df.columns: cl = c.lower().strip() if cl == 'date': col_map[c] = 'date' elif cl == 'open': col_map[c] = 'open' elif cl == 'high': col_map[c] = 'high' elif cl == 'low': col_map[c] = 'low' elif cl == 'close': col_map[c] = 'close' elif cl in ('volume', 'vol'): col_map[c] = 'volume' elif cl in ('amount',): col_map[c] = 'amount' if col_map: df = df.rename(columns=col_map) for col in ['date', 'open', 'high', 'low', 'close', 'volume']: if col not in df.columns: return {'error': f'缺少必要列: {col}'} df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').reset_index(drop=True) # 资金流默认无 df['fund_net_inflow'] = 0.0 df['fund_net_inflow_ratio'] = 0.0 # 计算指标 df = calc_all_indicators(df) # 截取最近 lookback_days df = df.tail(lookback_days + 60).reset_index(drop=True) # 逐日扫描信号,持有N天后检查盈亏 hold_days = 10 signals_found = [] min_idx = 60 for i in range(min_idx, len(df) - hold_days): row = df.iloc[i] history = df.iloc[:i] if len(history) < 60: continue vp = VolumePriceRules.evaluate_all(row, history) kc = KCandlestickRules.evaluate_all(row, history) cp = ChipDistributionRules.evaluate_all(row, history) mf = MoneyFlowRules.evaluate_all(row, history) sig = evaluate_signal(vp, kc, cp, mf) if sig['signal_level'] in ('A', 'B') and not sig['veto_triggered']: # 记录买入价和持有10日后的价格 buy_price = row['close'] future_idx = i + hold_days if future_idx < len(df): sell_price = df.iloc[future_idx]['close'] profit_pct = (sell_price / buy_price - 1) * 100 signals_found.append({ 'date': str(row['date'].date()), 'score': sig['total_score'], 'level': sig['signal_level'], 'buy_price': round(float(buy_price), 2), 'sell_price': round(float(sell_price), 2), 'profit_pct': round(float(profit_pct), 2), }) if not signals_found: return { 'total_signals': 0, 'win_count': 0, 'lose_count': 0, 'win_rate': 0, 'avg_profit_pct': 0, 'avg_loss_pct': 0, 'profit_loss_ratio': 0, 'hold_days': hold_days, 'conclusion': '近一年无足够的买入信号样本,无法进行有效回测', } winning = [s for s in signals_found if s['profit_pct'] > 0] losing = [s for s in signals_found if s['profit_pct'] <= 0] win_rate = len(winning) / len(signals_found) * 100 if signals_found else 0 avg_win = np.mean([s['profit_pct'] for s in winning]) if winning else 0 avg_loss = abs(np.mean([s['profit_pct'] for s in losing])) if losing else 0 pl_ratio = avg_win / avg_loss if avg_loss > 0 else 0 # 结论 if win_rate >= 65 and pl_ratio >= 2.0: conclusion = f'量化模型在{len(signals_found)}次信号中胜率{win_rate:.1f}%,盈亏比{pl_ratio:.2f},模型对该股票有效' elif win_rate >= 55: conclusion = f'胜率{win_rate:.1f}%尚可,但盈亏比{pl_ratio:.2f}偏低,建议配合其他指标使用' elif win_rate >= 45: conclusion = f'胜率{win_rate:.1f}%,盈亏比{pl_ratio:.2f},模型对该股票区分度一般' else: conclusion = f'胜率{win_rate:.1f}%,盈亏比{pl_ratio:.2f},量化模型对该股票效果不佳,谨慎使用' return { 'total_signals': len(signals_found), 'win_count': len(winning), 'lose_count': len(losing), 'win_rate': round(win_rate, 1), 'avg_profit_pct': round(avg_win, 2), 'avg_loss_pct': round(avg_loss, 2), 'profit_loss_ratio': round(pl_ratio, 2), 'hold_days': hold_days, 'sample_signals': signals_found[-10:], # 最近10次信号 'conclusion': conclusion, }