Download webui/akquant_wf.py from sgfw/kronos: direct link, hf CLI and curl.
- Browser
- Download file 32.9 kB
-
https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/akquant_wf.py
- Command line
-
hf download hf://spaces/sgfw/kronos/webui/akquant_wf.py
-
curl -L -o akquant_wf.py https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/akquant_wf.py
32.9 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| 蒸鱼AKQuant Walk-Forward回测验证框架 | |
| ============================================= | |
| 集成AKQuant回测理念到蒸鱼(Kronos)系统: | |
| - Walk-Forward Validation (rolling/expanding/anchored) | |
| - Pipeline防泄漏: StandardScaler在每个WF窗口内独立fit | |
| - Purged K-Fold: 训练/测试间设置embargo期防止标签泄漏 | |
| - QuantStats绩效报告: Sharpe/Sortino/最大回撤/蒙特卡洛模拟 | |
| - 方向准确率: 每个WF窗口独立统计, 汇总平均 | |
| - 衰减分析: 训练→验证→测试的准确率衰减趋势 | |
| 复用ml_engine.py的26特征和compute_single_features函数 | |
| """ | |
| import numpy as np | |
| import pandas as pd | |
| import os, json, time, warnings | |
| from datetime import datetime | |
| from copy import deepcopy | |
| warnings.filterwarnings('ignore') | |
| import importlib.util as _ilu | |
| _ml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ml_engine.py') | |
| _ml_spec = _ilu.spec_from_file_location('ml_engine', _ml_path) | |
| _ml_mod = _ilu.module_from_spec(_ml_spec) | |
| _ml_spec.loader.exec_module(_ml_mod) | |
| download_daily = _ml_mod.download_daily | |
| compute_single_features = _ml_mod.compute_single_features | |
| FEATURE_COLS = _ml_mod.FEATURE_COLS | |
| REPORT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'reports') | |
| os.makedirs(REPORT_DIR, exist_ok=True) | |
| class WalkForwardValidator: | |
| """AKQuant风格Walk-Forward回测验证器 | |
| 支持三种窗口模式: rolling/expanding/anchored | |
| 防泄漏: Pipeline(StandardScaler+Classifier)每个窗口独立fit, Purged embargo | |
| """ | |
| def __init__(self, mode='rolling', train_days=400, test_days=60, | |
| step_days=60, embargo_days=5, n_purge_splits=5): | |
| self.mode = mode | |
| self.train_days = train_days | |
| self.test_days = test_days | |
| self.step_days = step_days | |
| self.embargo_days = embargo_days | |
| self.n_purge_splits = n_purge_splits | |
| def _generate_windows(self, n_total): | |
| windows = [] | |
| min_req = 250 + self.train_days + self.embargo_days + self.test_days | |
| if n_total < min_req: | |
| return windows | |
| pos = 0 | |
| while True: | |
| ts = 250 | |
| te = ts + self.train_days | |
| if self.mode == 'expanding': | |
| te = 250 + self.train_days + pos | |
| elif self.mode == 'anchored': | |
| te = 250 + self.train_days + pos | |
| else: # rolling | |
| ts = 250 + pos | |
| te = ts + self.train_days | |
| gap_end = te + self.embargo_days | |
| test_s = gap_end | |
| test_e = test_s + self.test_days | |
| if test_e > n_total: | |
| break | |
| windows.append({'train_start': ts, 'train_end': te, | |
| 'test_start': test_s, 'test_end': min(test_e, n_total)}) | |
| pos += self.step_days | |
| return windows | |
| def _build_dataset(self, code, name=''): | |
| print(f" [WF] 下载数据 {code} {name}...") | |
| d = download_daily(code) | |
| if d is None: | |
| return None | |
| n = len(d['close']) | |
| c, h, l, v = d['close'], d['high'], d['low'], d['volume'] | |
| records = [] | |
| for i in range(250, n - 5): | |
| feat = compute_single_features( | |
| {'close': c[:i+1], 'high': h[:i+1], 'low': l[:i+1], 'volume': v[:i+1]}) | |
| if feat is None: | |
| continue | |
| if i + 5 < n: | |
| fr = (c[i+5] / c[i] - 1) * 100 | |
| if fr > 0.5: | |
| feat['label'] = 1 | |
| elif fr < -0.5: | |
| feat['label'] = 0 | |
| else: | |
| continue | |
| records.append(feat) | |
| if len(records) < 100: | |
| return None | |
| df = pd.DataFrame(records) | |
| print(f" [WF] {code} {name}: {n}天 -> {len(df)}有效样本") | |
| return df | |
| def validate(self, stock_list=None, model_type='gbdt'): | |
| if stock_list is None: | |
| stock_list = [ | |
| ('sz300896','爱美客'),('sh600519','贵州茅台'),('sz000001','平安银行'), | |
| ('sh600489','中金黄金'),('sz300601','康泰生物'),('sz300142','沃森生物'), | |
| ('sh600036','招商银行'),('sz000858','五粮液'),('sh601318','中国平安'), | |
| ('sz300750','宁德时代'),('sz002475','立讯精密'),('sh600900','长江电力'), | |
| ('sz300122','智飞生物'),('sz002625','光启技术'),('sh601012','隆基绿能'), | |
| ('sz000333','美的集团'), | |
| ] | |
| print(f"\n{'='*60}") | |
| print(f"[AKQuant WF] 蒸鱼Walk-Forward回测验证") | |
| print(f"[AKQuant WF] 模式={self.mode}, 训练={self.train_days}天, " | |
| f"测试={self.test_days}天, 步长={self.step_days}天") | |
| print(f"[AKQuant WF] 模型={model_type}, 股票数={len(stock_list)}") | |
| print(f"{'='*60}") | |
| all_data = {} | |
| for code, name in stock_list: | |
| df = self._build_dataset(code, name) | |
| if df is not None: | |
| all_data[code] = df | |
| time.sleep(0.15) | |
| if not all_data: | |
| return {'success': False, 'error': '无有效数据'} | |
| stock_results = {} | |
| for code, df in all_data.items(): | |
| result = self._validate_single(df, code, model_type) | |
| if result['success']: | |
| stock_results[code] = result | |
| print(f" [WF] {code}: {result['n_windows']}窗口, " | |
| f"准确率={result['avg_accuracy']:.1f}%") | |
| if not stock_results: | |
| return {'success': False, 'error': '所有股票WF验证失败'} | |
| summary = self._compute_summary(stock_results) | |
| decay = self._compute_decay(stock_results) | |
| ts = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| rp = os.path.join(REPORT_DIR, f'wf_report_{ts}.json') | |
| final = { | |
| 'success': True, 'timestamp': str(datetime.now()), | |
| 'config': {'mode': self.mode, 'train_days': self.train_days, | |
| 'test_days': self.test_days, 'step_days': self.step_days, | |
| 'embargo_days': self.embargo_days, 'model_type': model_type}, | |
| 'n_stocks': len(stock_results), 'stock_results': stock_results, | |
| 'summary': summary, 'decay': decay, | |
| } | |
| with open(rp, 'w', encoding='utf-8') as f: | |
| json.dump(final, f, ensure_ascii=False, indent=2, default=str) | |
| print(f"\n[AKQuant WF] 报告: {rp}") | |
| print(f"[AKQuant WF] 汇总: {summary['avg_accuracy']:.1f}%, " | |
| f"{summary['n_total_windows']}窗口, 稳定性={summary['stability']}") | |
| return final | |
| def _validate_single(self, df, code, model_type): | |
| from sklearn.ensemble import GradientBoostingClassifier | |
| from sklearn.metrics import (accuracy_score, precision_score, | |
| recall_score, f1_score) | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.preprocessing import StandardScaler | |
| n_total = len(df) | |
| windows = self._generate_windows(n_total) | |
| if len(windows) < 2: | |
| return {'success': False, 'error': f'{code}: 窗口数不足'} | |
| window_results = [] | |
| test_rets_all = [] | |
| for wi, win in enumerate(windows): | |
| train_df = df.iloc[win['train_start']:win['train_end']].copy() | |
| test_df = df.iloc[win['test_start']:win['test_end']].copy() | |
| if len(train_df) < 50 or len(test_df) < 10: | |
| continue | |
| X_train = train_df[FEATURE_COLS].values.astype(np.float64) | |
| y_train = train_df['label'].values.astype(int) | |
| X_test = test_df[FEATURE_COLS].values.astype(np.float64) | |
| y_test = test_df['label'].values.astype(int) | |
| if len(np.unique(y_train)) < 2: | |
| continue | |
| # Pipeline防泄漏: 每个窗口独立fit | |
| if model_type == 'xgboost': | |
| try: | |
| import xgboost as xgb | |
| pipe = Pipeline([ | |
| ('scaler', StandardScaler()), | |
| ('clf', xgb.XGBClassifier( | |
| n_estimators=200, max_depth=4, learning_rate=0.05, | |
| min_child_weight=10, subsample=0.8, colsample_bytree=0.8, | |
| reg_alpha=0.1, reg_lambda=1.0, random_state=42, | |
| use_label_encoder=False, eval_metric='logloss')) | |
| ]) | |
| except ImportError: | |
| pipe = Pipeline([('scaler', StandardScaler()), | |
| ('clf', GradientBoostingClassifier( | |
| n_estimators=300, max_depth=4, learning_rate=0.05, | |
| min_samples_leaf=15, random_state=42))]) | |
| else: | |
| pipe = Pipeline([('scaler', StandardScaler()), | |
| ('clf', GradientBoostingClassifier( | |
| n_estimators=300, max_depth=4, learning_rate=0.05, | |
| min_samples_leaf=15, random_state=42))]) | |
| try: | |
| pipe.fit(X_train, y_train) | |
| y_pred = pipe.predict(X_test) | |
| acc = accuracy_score(y_test, y_pred) | |
| prec = precision_score(y_test, y_pred, zero_division=0) | |
| rec = recall_score(y_test, y_pred, zero_division=0) | |
| f1 = f1_score(y_test, y_pred, zero_division=0) | |
| y_proba = pipe.predict_proba(X_test)[:, 1] | |
| wr = { | |
| 'window_id': wi+1, 'train_size': len(X_train), | |
| 'test_size': len(X_test), 'accuracy': round(acc*100, 2), | |
| 'precision': round(prec*100, 2), 'recall': round(rec*100, 2), | |
| 'f1': round(f1*100, 2), | |
| 'buy_rate': round(float(np.mean(y_pred)), 4), | |
| 'avg_buy_prob': round(float(np.mean(y_proba)), 4), | |
| } | |
| window_results.append(wr) | |
| for j in range(len(y_test)): | |
| if y_proba[j] > 0.5: | |
| test_rets_all.append(1.0 if y_test[j]==1 else -1.0) | |
| else: | |
| test_rets_all.append(1.0 if y_test[j]==0 else -1.0) | |
| except Exception: | |
| continue | |
| if not window_results: | |
| return {'success': False, 'error': f'{code}: 所有窗口训练失败'} | |
| accuracies = [w['accuracy'] for w in window_results] | |
| qs_stats = {} | |
| if test_rets_all: | |
| rets_arr = np.array(test_rets_all) | |
| cum = np.cumsum(rets_arr) | |
| peak = np.maximum.accumulate(cum) | |
| dd = (peak - cum) / (peak + 1e-8) * 100 | |
| qs_stats = { | |
| 'total_trades': len(rets_arr), | |
| 'win_rate': round(float(np.mean(rets_arr > 0)*100), 2), | |
| 'avg_return': round(float(np.mean(rets_arr)), 4), | |
| 'sharpe': round(float(np.mean(rets_arr)/(np.std(rets_arr)+1e-8)), 4), | |
| 'max_drawdown': round(float(np.max(dd)), 2), | |
| 'cum_return': round(float(np.sum(rets_arr)), 2), | |
| } | |
| return { | |
| 'success': True, 'code': code, | |
| 'n_windows': len(window_results), 'windows': window_results, | |
| 'avg_accuracy': round(float(np.mean(accuracies)), 2), | |
| 'std_accuracy': round(float(np.std(accuracies)), 2), | |
| 'min_accuracy': round(float(min(accuracies)), 2), | |
| 'max_accuracy': round(float(max(accuracies)), 2), | |
| 'quantstats': qs_stats, | |
| } | |
| def _compute_summary(self, stock_results): | |
| all_accs, all_precs, all_f1s, tw = [], [], [], 0 | |
| for sr in stock_results.values(): | |
| all_accs.append(sr['avg_accuracy']) | |
| if sr['windows']: | |
| all_precs.append(float(np.mean([w['precision'] for w in sr['windows']]))) | |
| all_f1s.append(float(np.mean([w['f1'] for w in sr['windows']]))) | |
| tw += sr['n_windows'] | |
| all_qs = [sr.get('quantstats',{}) for sr in stock_results.values() if sr.get('quantstats')] | |
| return { | |
| 'avg_accuracy': round(float(np.mean(all_accs)), 2), | |
| 'std_accuracy': round(float(np.std(all_accs)), 2), | |
| 'min_accuracy': round(float(min(all_accs)), 2), | |
| 'max_accuracy': round(float(max(all_accs)), 2), | |
| 'avg_precision': round(float(np.mean(all_precs)) if all_precs else 0, 2), | |
| 'avg_f1': round(float(np.mean(all_f1s)) if all_f1s else 0, 2), | |
| 'n_total_windows': tw, 'n_stocks': len(stock_results), | |
| 'stability': '高' if np.std(all_accs)<3 else ('中' if np.std(all_accs)<6 else '低'), | |
| 'pass_rate': round(float(sum(1 for a in all_accs if a>52)/len(all_accs)*100), 2), | |
| 'quantstats': { | |
| 'avg_win_rate': round(float(np.mean([q.get('win_rate',50) for q in all_qs])),2) if all_qs else 0, | |
| 'avg_sharpe': round(float(np.mean([q.get('sharpe',0) for q in all_qs])),4) if all_qs else 0, | |
| 'avg_max_drawdown': round(float(np.mean([q.get('max_drawdown',0) for q in all_qs])),2) if all_qs else 0, | |
| } if all_qs else {}, | |
| } | |
| def _compute_decay(self, stock_results): | |
| all_wa = [] | |
| for sr in stock_results.values(): | |
| all_wa.extend([w['accuracy'] for w in sr['windows']]) | |
| if len(all_wa) < 4: | |
| return {'trend': '数据不足', 'decay_rate': 0} | |
| x = np.arange(len(all_wa)); y = np.array(all_wa) | |
| try: | |
| slope = np.polyfit(x, y, 1)[0] | |
| half = len(all_wa)//2 | |
| fa = float(np.mean(y[:half])); sa = float(np.mean(y[half:])) | |
| trend = '稳定' if slope > -0.1 else ('轻微衰减' if slope > -0.5 else '明显衰减') | |
| return {'trend': trend, 'decay_rate': round(abs(slope),4), | |
| 'first_half_avg': round(fa,2), 'second_half_avg': round(sa,2), | |
| 'total_windows': len(all_wa)} | |
| except Exception: | |
| return {'trend': '无法计算', 'decay_rate': 0} | |
| # ============================================================ | |
| # Purged K-Fold + Embargo 验证器 | |
| # ============================================================ | |
| class PurgedKFoldValidator: | |
| """Purged K-Fold回测验证器 | |
| 在标准K-Fold基础上: 1) 训练/测试间设置gap防止标签泄漏 | |
| 2) 每个fold内Pipeline独立fit 3) 统计汇总+蒙特卡洛基线 | |
| """ | |
| def __init__(self, n_splits=5, embargo_days=5, gap_days=3): | |
| self.n_splits = n_splits | |
| self.embargo_days = embargo_days | |
| self.gap_days = gap_days | |
| def _build_dataset(self, code, name=''): | |
| print(f" [PKF] 下载数据 {code} {name}...") | |
| d = download_daily(code) | |
| if d is None: | |
| return None | |
| n = len(d['close']) | |
| c, h, l, v = d['close'], d['high'], d['low'], d['volume'] | |
| records = [] | |
| for i in range(250, n - 5): | |
| feat = compute_single_features( | |
| {'close': c[:i+1], 'high': h[:i+1], 'low': l[:i+1], 'volume': v[:i+1]}) | |
| if feat is None: | |
| continue | |
| if i + 5 < n: | |
| fr = (c[i+5] / c[i] - 1) * 100 | |
| if fr > 0.5: | |
| feat['label'] = 1 | |
| elif fr < -0.5: | |
| feat['label'] = 0 | |
| else: | |
| continue | |
| records.append(feat) | |
| if len(records) < 100: | |
| return None | |
| df = pd.DataFrame(records) | |
| print(f" [PKF] {code} {name}: {n}天 -> {len(df)}有效样本") | |
| return df | |
| def _generate_purged_splits(self, n_total): | |
| """生成Purged K-Fold索引: 每个split间有gap+embargo""" | |
| total_gap = self.gap_days + self.embargo_days | |
| fold_size = (n_total - total_gap * (self.n_splits - 1)) // self.n_splits | |
| if fold_size < 50: | |
| return [] | |
| splits = [] | |
| pos = 0 | |
| for i in range(self.n_splits): | |
| test_start = pos | |
| test_end = min(pos + fold_size, n_total) | |
| if test_end - test_start < 20: | |
| break | |
| train_end = test_start - total_gap | |
| splits.append({ | |
| 'train_start': 0, | |
| 'train_end': max(train_end, 0), | |
| 'test_start': test_start, | |
| 'test_end': test_end, | |
| }) | |
| pos = test_end + total_gap | |
| if pos >= n_total: | |
| break | |
| return splits | |
| def _validate_single(self, df, code, model_type='gbdt'): | |
| from sklearn.ensemble import GradientBoostingClassifier | |
| from sklearn.metrics import (accuracy_score, precision_score, | |
| recall_score, f1_score) | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.preprocessing import StandardScaler | |
| n_total = len(df) | |
| splits = self._generate_purged_splits(n_total) | |
| if len(splits) < 2: | |
| return {'success': False, 'error': f'{code}: 有效split数不足'} | |
| fold_results = [] | |
| test_rets_all = [] | |
| for fi, sp in enumerate(splits): | |
| if sp['train_end'] <= 0: | |
| continue | |
| train_df = df.iloc[sp['train_start']:sp['train_end']].copy() | |
| test_df = df.iloc[sp['test_start']:sp['test_end']].copy() | |
| if len(train_df) < 50 or len(test_df) < 10: | |
| continue | |
| X_train = train_df[FEATURE_COLS].values.astype(np.float64) | |
| y_train = train_df['label'].values.astype(int) | |
| X_test = test_df[FEATURE_COLS].values.astype(np.float64) | |
| y_test = test_df['label'].values.astype(int) | |
| if len(np.unique(y_train)) < 2: | |
| continue | |
| if model_type == 'xgboost': | |
| try: | |
| import xgboost as xgb | |
| pipe = Pipeline([ | |
| ('scaler', StandardScaler()), | |
| ('clf', xgb.XGBClassifier( | |
| n_estimators=200, max_depth=4, learning_rate=0.05, | |
| min_child_weight=10, subsample=0.8, colsample_bytree=0.8, | |
| reg_alpha=0.1, reg_lambda=1.0, random_state=42, | |
| use_label_encoder=False, eval_metric='logloss')) | |
| ]) | |
| except ImportError: | |
| pipe = Pipeline([('scaler', StandardScaler()), | |
| ('clf', GradientBoostingClassifier( | |
| n_estimators=300, max_depth=4, learning_rate=0.05, | |
| min_samples_leaf=15, random_state=42))]) | |
| else: | |
| pipe = Pipeline([('scaler', StandardScaler()), | |
| ('clf', GradientBoostingClassifier( | |
| n_estimators=300, max_depth=4, learning_rate=0.05, | |
| min_samples_leaf=15, random_state=42))]) | |
| try: | |
| pipe.fit(X_train, y_train) | |
| y_pred = pipe.predict(X_test) | |
| y_proba = pipe.predict_proba(X_test)[:, 1] | |
| acc = accuracy_score(y_test, y_pred) | |
| fr = { | |
| 'fold_id': fi+1, 'train_size': len(X_train), | |
| 'test_size': len(X_test), | |
| 'accuracy': round(acc*100, 2), | |
| 'precision': round(precision_score(y_test, y_pred, zero_division=0)*100, 2), | |
| 'recall': round(recall_score(y_test, y_pred, zero_division=0)*100, 2), | |
| 'f1': round(f1_score(y_test, y_pred, zero_division=0)*100, 2), | |
| 'buy_rate': round(float(np.mean(y_pred)), 4), | |
| } | |
| fold_results.append(fr) | |
| for j in range(len(y_test)): | |
| if y_proba[j] > 0.5: | |
| test_rets_all.append(1.0 if y_test[j]==1 else -1.0) | |
| else: | |
| test_rets_all.append(1.0 if y_test[j]==0 else -1.0) | |
| except Exception: | |
| continue | |
| if not fold_results: | |
| return {'success': False, 'error': f'{code}: 所有fold训练失败'} | |
| accuracies = [f['accuracy'] for f in fold_results] | |
| qs_stats = {} | |
| if test_rets_all: | |
| rets_arr = np.array(test_rets_all) | |
| cum = np.cumsum(rets_arr) | |
| peak = np.maximum.accumulate(cum) | |
| dd = (peak - cum) / (peak + 1e-8) * 100 | |
| qs_stats = { | |
| 'total_trades': len(rets_arr), | |
| 'win_rate': round(float(np.mean(rets_arr > 0)*100), 2), | |
| 'avg_return': round(float(np.mean(rets_arr)), 4), | |
| 'sharpe': round(float(np.mean(rets_arr)/(np.std(rets_arr)+1e-8)), 4), | |
| 'max_drawdown': round(float(np.max(dd)), 2), | |
| } | |
| return { | |
| 'success': True, 'code': code, | |
| 'n_folds': len(fold_results), 'folds': fold_results, | |
| 'avg_accuracy': round(float(np.mean(accuracies)), 2), | |
| 'std_accuracy': round(float(np.std(accuracies)), 2), | |
| 'quantstats': qs_stats, | |
| } | |
| def validate(self, stock_list=None, model_type='gbdt'): | |
| if stock_list is None: | |
| stock_list = [ | |
| ('sz300896','爱美客'),('sh600519','贵州茅台'),('sz000001','平安银行'), | |
| ('sh600489','中金黄金'),('sz300601','康泰生物'),('sz300142','沃森生物'), | |
| ('sh600036','招商银行'),('sz000858','五粮液'),('sh601318','中国平安'), | |
| ('sz300750','宁德时代'),('sz002475','立讯精密'),('sh600900','长江电力'), | |
| ('sz300122','智飞生物'),('sz002625','光启技术'),('sh601012','隆基绿能'), | |
| ('sz000333','美的集团'), | |
| ] | |
| print(f"\n{'='*60}") | |
| print(f"[AKQuant PKF] Purged K-Fold回测验证") | |
| print(f"[AKQuant PKF] folds={self.n_splits}, embargo={self.embargo_days}天, " | |
| f"gap={self.gap_days}天, 模型={model_type}") | |
| print(f"{'='*60}") | |
| all_data = {} | |
| for code, name in stock_list: | |
| df = self._build_dataset(code, name) | |
| if df is not None: | |
| all_data[code] = df | |
| time.sleep(0.15) | |
| if not all_data: | |
| return {'success': False, 'error': '无有效数据'} | |
| stock_results = {} | |
| for code, df in all_data.items(): | |
| result = self._validate_single(df, code, model_type) | |
| if result['success']: | |
| stock_results[code] = result | |
| print(f" [PKF] {code}: {result['n_folds']}folds, " | |
| f"准确率={result['avg_accuracy']:.1f}%") | |
| if not stock_results: | |
| return {'success': False, 'error': '所有股票PKF验证失败'} | |
| all_accs = [sr['avg_accuracy'] for sr in stock_results.values()] | |
| tf = sum(sr['n_folds'] for sr in stock_results.values()) | |
| summary = { | |
| 'avg_accuracy': round(float(np.mean(all_accs)), 2), | |
| 'std_accuracy': round(float(np.std(all_accs)), 2), | |
| 'n_stocks': len(stock_results), | |
| 'n_total_folds': tf, | |
| 'pass_rate': round(float(sum(1 for a in all_accs if a>52)/len(all_accs)*100), 2), | |
| 'stability': '高' if np.std(all_accs)<3 else ('中' if np.std(all_accs)<6 else '低'), | |
| } | |
| ts = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| rp = os.path.join(REPORT_DIR, f'pkf_report_{ts}.json') | |
| final = { | |
| 'success': True, 'timestamp': str(datetime.now()), | |
| 'config': {'n_splits': self.n_splits, 'embargo_days': self.embargo_days, | |
| 'gap_days': self.gap_days, 'model_type': model_type}, | |
| 'n_stocks': len(stock_results), 'stock_results': stock_results, | |
| 'summary': summary, | |
| } | |
| with open(rp, 'w', encoding='utf-8') as f: | |
| json.dump(final, f, ensure_ascii=False, indent=2, default=str) | |
| print(f"\n[AKQuant PKF] 报告: {rp}") | |
| print(f"[AKQuant PKF] 汇总: {summary['avg_accuracy']:.1f}%, " | |
| f"稳定性={summary['stability']}") | |
| return final | |
| # ============================================================ | |
| # QuantStats HTML报告生成 | |
| # ============================================================ | |
| def generate_quantstats_report(returns_list, benchmark=None, title='AKQuant Strategy'): | |
| """使用QuantStats生成HTML绩效报告""" | |
| try: | |
| import quantstats as qs | |
| except ImportError: | |
| print("[QS] quantstats未安装, 跳过HTML报告生成") | |
| return None | |
| if not returns_list: | |
| return None | |
| try: | |
| returns = pd.Series(returns_list) | |
| returns.index = pd.bdate_range( | |
| start='2024-01-01', periods=len(returns)) | |
| html_file = os.path.join(REPORT_DIR, f'qs_{datetime.now().strftime("%Y%m%d_%H%M%S")}.html') | |
| if benchmark is not None and len(benchmark) == len(returns_list): | |
| bm = pd.Series(benchmark) | |
| bm.index = returns.index | |
| qs.reports.html(returns, benchmark=bm, title=title, output=html_file) | |
| else: | |
| qs.reports.html(returns, title=title, output=html_file) | |
| print(f"[QS] HTML报告: {html_file}") | |
| return html_file | |
| except Exception as e: | |
| print(f"[QS] 报告生成失败: {e}") | |
| return None | |
| def _generate_text_report(result, report_type='WF'): | |
| """生成文本摘要报告""" | |
| lines = [f"\n{'='*60}"] | |
| lines.append(f"[AKQuant {report_type}] 回测验证报告") | |
| lines.append(f"时间: {result.get('timestamp', 'N/A')}") | |
| lines.append(f"{'='*60}") | |
| cfg = result.get('config', {}) | |
| for k, v in cfg.items(): | |
| lines.append(f" {k}: {v}") | |
| lines.append('') | |
| sm = result.get('summary', {}) | |
| lines.append(f"--- 汇总 ---") | |
| for k, v in sm.items(): | |
| lines.append(f" {k}: {v}") | |
| lines.append('') | |
| dc = result.get('decay', {}) | |
| if dc: | |
| lines.append(f"--- 衰减分析 ---") | |
| for k, v in dc.items(): | |
| lines.append(f" {k}: {v}") | |
| lines.append('') | |
| lines.append(f"--- 各股结果 ---") | |
| for code, sr in result.get('stock_results', {}).items(): | |
| lines.append(f" {code}: avg_acc={sr.get('avg_accuracy',0):.1f}%, " | |
| f"windows={sr.get('n_windows',sr.get('n_folds',0))}") | |
| return '\n'.join(lines) | |
| # ============================================================ | |
| # 蒙特卡洛模拟基线 | |
| # ============================================================ | |
| def monte_carlo_simulation(n_simulations=500, n_trades=100, win_prob=None): | |
| """蒙特卡洛模拟: 500次随机策略基线 | |
| 返回95%/99%置信区间, 与实际策略对比判断显著性 | |
| """ | |
| np.random.seed(42) | |
| if win_prob is None: | |
| win_prob = 0.5 | |
| sim_results = [] | |
| for _ in range(n_simulations): | |
| trades = np.random.choice([1.0, -1.0], size=n_trades, p=[win_prob, 1-win_prob]) | |
| cum = np.cumsum(trades) | |
| peak = np.maximum.accumulate(cum) | |
| dd = (peak - cum) / (peak + 1e-8) * 100 | |
| sim_results.append({ | |
| 'total_return': float(np.sum(trades)), | |
| 'win_rate': float(np.mean(trades > 0) * 100), | |
| 'sharpe': float(np.mean(trades) / (np.std(trades) + 1e-8)), | |
| 'max_drawdown': float(np.max(dd)), | |
| }) | |
| df = pd.DataFrame(sim_results) | |
| baseline = { | |
| 'n_simulations': n_simulations, | |
| 'n_trades': n_trades, | |
| 'win_prob': win_prob, | |
| 'return_95ci': [round(float(df['total_return'].quantile(0.025)), 2), | |
| round(float(df['total_return'].quantile(0.975)), 2)], | |
| 'return_99ci': [round(float(df['total_return'].quantile(0.005)), 2), | |
| round(float(df['total_return'].quantile(0.995)), 2)], | |
| 'win_rate_mean': round(float(df['win_rate'].mean()), 2), | |
| 'win_rate_std': round(float(df['win_rate'].std()), 2), | |
| 'sharpe_95ci': [round(float(df['sharpe'].quantile(0.025)), 4), | |
| round(float(df['sharpe'].quantile(0.975)), 4)], | |
| 'max_dd_95ci': [round(float(df['max_drawdown'].quantile(0.025)), 2), | |
| round(float(df['max_drawdown'].quantile(0.975)), 2)], | |
| } | |
| print(f"[MC] 蒙特卡洛基线: {n_simulations}次模拟, {n_trades}笔交易") | |
| print(f"[MC] 收益95%CI: {baseline['return_95ci']}") | |
| print(f"[MC] Sharpe 95%CI: {baseline['sharpe_95ci']}") | |
| return baseline | |
| # ============================================================ | |
| # 便捷函数 | |
| # ============================================================ | |
| def run_walk_forward(mode='rolling', train_days=400, test_days=60, | |
| step_days=60, model_type='gbdt', n_stocks=None): | |
| """快捷: 运行Walk-Forward验证""" | |
| wf = WalkForwardValidator( | |
| mode=mode, train_days=train_days, test_days=test_days, | |
| step_days=step_days, embargo_days=5) | |
| return wf.validate(model_type=model_type) | |
| def run_purged_kfold(n_splits=5, embargo_days=5, gap_days=3, | |
| model_type='gbdt'): | |
| """快捷: 运行Purged K-Fold验证""" | |
| pkf = PurgedKFoldValidator( | |
| n_splits=n_splits, embargo_days=embargo_days, gap_days=gap_days) | |
| return pkf.validate(model_type=model_type) | |
| def run_full_validation(model_type='gbdt'): | |
| """完整验证流程: WF + PKF + 蒙特卡洛""" | |
| print("\n" + "#"*60) | |
| print("# AKQuant 完整回测验证流程") | |
| print("#"*60) | |
| wf_result = run_walk_forward(mode='rolling', model_type=model_type) | |
| pkf_result = run_purged_kfold(model_type=model_type) | |
| mc_baseline = monte_carlo_simulation(n_simulations=500, n_trades=200) | |
| text_report = _generate_text_report(wf_result, 'WF') + '\n' | |
| text_report += _generate_text_report(pkf_result, 'PKF') | |
| all_rets = [] | |
| for sr in wf_result.get('stock_results', {}).values(): | |
| for w in sr.get('windows', []): | |
| all_rets.append(1.0 if w['accuracy'] > 52 else -1.0) | |
| if all_rets: | |
| actual_sharpe = float(np.mean(all_rets) / (np.std(all_rets) + 1e-8)) | |
| mc_lower = mc_baseline['sharpe_95ci'][1] | |
| significance = actual_sharpe > mc_lower | |
| text_report += f"\n--- 显著性检验 ---\n" | |
| text_report += f" 实际Sharpe: {actual_sharpe:.4f}\n" | |
| text_report += f" MC 95%CI上界: {mc_lower:.4f}\n" | |
| text_report += f" 策略显著: {'是' if significance else '否'}\n" | |
| ts = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| rp = os.path.join(REPORT_DIR, f'full_validation_{ts}.txt') | |
| with open(rp, 'w', encoding='utf-8') as f: | |
| f.write(text_report) | |
| print(f"\n[AKQuant] 完整报告: {rp}") | |
| html = generate_quantstats_report(all_rets) if all_rets else None | |
| return { | |
| 'success': True, 'wf': wf_result, 'pkf': pkf_result, | |
| 'monte_carlo': mc_baseline, | |
| 'text_report': text_report, | |
| 'html_report': html, | |
| 'report_path': rp, | |
| } | |
| # ============================================================ | |
| # __main__ 入口 | |
| # ============================================================ | |
| if __name__ == '__main__': | |
| import sys | |
| mode = sys.argv[1] if len(sys.argv) > 1 else 'quick' | |
| if mode == 'full': | |
| result = run_full_validation() | |
| print(f"\n完成! 报告路径: {result['report_path']}") | |
| elif mode == 'mc': | |
| baseline = monte_carlo_simulation(n_simulations=500, n_trades=200) | |
| print(f"\n蒙特卡洛基线: {json.dumps(baseline, indent=2)}") | |
| elif mode == 'pkf': | |
| result = run_purged_kfold() | |
| elif mode == 'wf': | |
| result = run_walk_forward() | |
| else: # quick | |
| print("[AKQuant] 快速模式: 3股WF验证") | |
| quick_stocks = [ | |
| ('sh600519','贵州茅台'), ('sz000001','平安银行'), | |
| ('sh600036','招商银行'), | |
| ] | |
| wf = WalkForwardValidator(mode='rolling', train_days=400, | |
| test_days=60, step_days=60) | |
| result = wf.validate(stock_list=quick_stocks) | |
| if result['success']: | |
| print(f"\n快速验证完成: {result['summary']['avg_accuracy']:.1f}%") | |
| else: | |
| print(f"\n快速验证失败: {result.get('error','')}") | |