Jelajahi Sumber

Refactor Kalman strategy engine and audit reporting

erwin 1 bulan lalu
induk
melakukan
13993e2d3e

+ 4 - 800
kalman-filter/v2/cyb50_kalman_filter_daily.py

@@ -1,804 +1,8 @@
 # -*- coding: utf-8 -*-
-# cyb50_kalman_filter_daily.py
+# 兼容入口:保留原脚本名,内部委托给新的模块化实现
 
-import akshare as ak
-import pandas as pd
-import numpy as np
-from pykalman import KalmanFilter
+from kalman_strategy_engine import main
 
-# === 策略参数 ===
-TREND_THRESHOLD_BUY = 0.00     # 趋势死区:trend必须超过此值才触发买入(扫描最优:去掉死区)
-TREND_THRESHOLD_SELL = -0.10   # 趋势死区:trend必须低于此值才触发卖出
-CONFIRMATION_DAYS_BUY = 1      # 买入确认天数
-CONFIRMATION_DAYS_SELL = 2     # 卖出确认天数
-MIN_HOLDING_DAYS = 10          # 最小持仓天数
-TRAILING_STOP_PCT = 0.08       # 追踪止损(基于卡尔曼滤波价格)
-MAX_LOSS_STOP = 0.12           # 最大亏损止损(基于入场价)
-TREND_PEAK_DECAY = 0.85        # 趋势衰减退出阈值(修复后重新优化:0.80→0.85)
-KALMAN_SLOPE_THRESH = -0.05    # Kalman偏离过滤:kalman价格相对60日均线偏离阈值
-VOL_THRESHOLD_FIXED = 0.30     # 波动率过滤:年化波动率超过此值时不买入(A股高波动分界线)
-EARLY_STOP_DAYS = 3            # 早期止损观察天数(修复后重新优化:5→3)
-EARLY_STOP_LOSS = 0.025        # 早期止损阈值(2.5%)
-SCALE_IN_DAYS = 3              # 加仓观察天数:持仓N天后判断是否加仓(修复后重新优化:5→3)
-SCALE_IN_THRESH = 0.025        # 加仓阈值:前N天涨幅超过此值才加仓(修复后重新优化:3%→2.5%)
-SCALE_IN_SIZE = 0.8            # 加仓比例:加仓后总仓位 = 1.0 + SCALE_IN_SIZE = 1.8(上限1.8)
 
-# --- 1. 数据获取 (日线级别 + 实时数据) ---
-def get_index_data_with_realtime(index_code, start_date="2018-01-01", manual_today_price=None):
-    """
-    获取指数历史数据 + 当天实时数据
-
-    Args:
-        index_code: 指数代码 (如 "sz399673")
-        start_date: 起始日期
-        manual_today_price: 手动输入的今日价格(如果提供,将使用该价格)
-
-    Returns:
-        DataFrame: 包含历史数据和当天数据的完整数据集
-    """
-    import datetime
-
-    print(f"正在获取指数 {index_code} 的数据...")
-
-    # 1. 获取历史日线数据 - 尝试多个数据源
-    print("  - 获取历史日线数据...")
-    hist_df = pd.DataFrame()
-
-    # 尝试使用 stock_zh_index_daily_em
-    try:
-        hist_df = ak.stock_zh_index_daily_em(symbol=index_code)
-        hist_df['date'] = pd.to_datetime(hist_df['date'])
-        hist_df.set_index('date', inplace=True)
-        hist_df = hist_df[hist_df.index >= start_date]
-        print(f"    历史数据: {hist_df.index[0]} 到 {hist_df.index[-1]} ({len(hist_df)} 条)")
-    except Exception as e:
-        print(f"    stock_zh_index_daily_em 失败: {e}")
-
-    # 如果失败,尝试使用原始接口
-    if hist_df.empty:
-        try:
-            hist_df = ak.stock_zh_index_daily(symbol=index_code)
-            hist_df['date'] = pd.to_datetime(hist_df['date'])
-            hist_df.set_index('date', inplace=True)
-            hist_df = hist_df[hist_df.index >= start_date]
-            print(f"    历史数据(备用源): {hist_df.index[0]} 到 {hist_df.index[-1]} ({len(hist_df)} 条)")
-        except Exception as e:
-            print(f"    stock_zh_index_daily 失败: {e}")
-
-    if hist_df.empty:
-        raise ValueError(f"无法获取指数 {index_code} 的历史数据")
-
-    # 2. 获取当天实时数据
-    print("  - 获取当天实时数据...")
-    today = datetime.datetime.now().date()
-    has_today_data = not hist_df.empty and hist_df.index[-1].date() == today
-
-    if has_today_data:
-        print(f"    历史数据已包含今日数据,收盘价: {hist_df.iloc[-1]['close']:.2f}")
-    elif manual_today_price is not None:
-        # 使用手动输入的价格
-        print(f"    使用手动输入的今日价格: {manual_today_price:.2f}")
-        today_df = pd.DataFrame({
-            'close': [manual_today_price],
-            'open': [manual_today_price],  # 如果只有收盘价,就用收盘价填充
-            'high': [manual_today_price],
-            'low': [manual_today_price],
-            'volume': [hist_df['volume'].iloc[-1] if len(hist_df) > 0 else 0],
-        }, index=[pd.Timestamp(today)])
-        hist_df = pd.concat([hist_df, today_df])
-    else:
-        # 尝试获取实时数据
-        # 方法1: 东方财富接口
-        realtime_ok = False
-        try:
-            spot_df = ak.stock_zh_index_spot_em()
-            current_data = spot_df[spot_df['代码'] == index_code]
-
-            if not current_data.empty:
-                today_df = pd.DataFrame({
-                    'close': [float(current_data.iloc[0]['最新价'])],
-                    'open': [float(current_data.iloc[0]['开盘'])],
-                    'high': [float(current_data.iloc[0]['最高'])],
-                    'low': [float(current_data.iloc[0]['最低'])],
-                    'volume': [float(current_data.iloc[0]['成交量']) if '成交量' in current_data.columns else 0],
-                }, index=[pd.Timestamp(today)])
-
-                print(f"    [东财] 获取到今日数据: {today}, 最新价 {today_df.iloc[0]['close']:.2f}")
-                hist_df = pd.concat([hist_df, today_df])
-                realtime_ok = True
-            else:
-                print(f"    [东财] 未找到指数 {index_code} 的实时数据")
-        except Exception as e:
-            print(f"    [东财] 实时数据获取失败: {e}")
-
-        # 方法2: 腾讯财经接口(备用)
-        if not realtime_ok:
-            try:
-                import urllib.request
-                pure_code = index_code.replace("sz", "").replace("sh", "")
-                prefix = "sz" if index_code.startswith("sz") else "sh"
-                qq_url = f"https://qt.gtimg.cn/q={prefix}{pure_code}"
-                req = urllib.request.Request(qq_url, headers={"User-Agent": "Mozilla/5.0"})
-                resp = urllib.request.urlopen(req, timeout=10)
-                raw = resp.read().decode("gbk")
-                parts = raw.split("~")
-                if len(parts) > 35:
-                    latest_price = float(parts[3])
-                    open_price = float(parts[5])
-                    high_price = float(parts[33])
-                    low_price = float(parts[34])
-                    volume = int(parts[6]) if parts[6].isdigit() else 0
-
-                    today_df = pd.DataFrame({
-                        'close': [latest_price],
-                        'open': [open_price],
-                        'high': [high_price],
-                        'low': [low_price],
-                        'volume': [volume],
-                    }, index=[pd.Timestamp(today)])
-
-                    print(f"    [腾讯] 获取到今日数据: {today}, 最新价 {latest_price:.2f}")
-                    hist_df = pd.concat([hist_df, today_df])
-                    realtime_ok = True
-                else:
-                    print(f"    [腾讯] 数据格式异常")
-            except Exception as e:
-                print(f"    [腾讯] 实时数据获取失败: {e}")
-
-        if not realtime_ok:
-            print(f"    所有实时数据源均失败")
-            print(f"    提示: 可以通过 manual_today_price 参数手动输入今日价格")
-
-    return hist_df
-
-
-try:
-    index_code = "sz399673"
-
-    # 检查是否需要手动输入今日价格
-    import datetime
-    import sys
-    today = datetime.datetime.now().date()
-    manual_price = None
-
-    # 尝试获取数据,看是否包含今日
-    try:
-        temp_df = ak.stock_zh_index_daily_em(symbol=index_code)
-        temp_df['date'] = pd.to_datetime(temp_df['date'])
-
-        if temp_df['date'].max().date() < today:
-            print(f"\n[提示] 历史数据最新日期为 {temp_df['date'].max().date()},早于今天 {today}")
-            print(f"[提示] 可以在代码中设置 manual_today_price 参数来添加今日数据")
-
-            # 只有在交互式终端下才尝试获取用户输入
-            if sys.stdin.isatty():
-                try:
-                    user_input = input("请输入今日收盘价 (直接回车跳过): ").strip()
-                    if user_input and user_input.replace('.', '').replace('-', '').isdigit():
-                        manual_price = float(user_input)
-                        print(f"已设置今日收盘价为: {manual_price}")
-                except (EOFError, ValueError):
-                    print("跳过手动输入,使用历史数据")
-    except Exception as e:
-        print(f"检查数据日期失败: {e}")
-
-    stock_zh_index_daily_df = get_index_data_with_realtime(index_code, "2018-01-01", manual_price)
-
-    if stock_zh_index_daily_df.empty:
-        raise ValueError(f"无法获取指数 {index_code} 的数据,请检查代码或网络。")
-
-    print(f"\n[OK] 数据获取成功,期间为 {stock_zh_index_daily_df.index[0]} 到 {stock_zh_index_daily_df.index[-1]}")
-    print(f"   数据量: {len(stock_zh_index_daily_df)} 条")
-    print(f"   最新收盘价: {stock_zh_index_daily_df['close'].iloc[-1]:.2f}")
-
-    observations = stock_zh_index_daily_df['close']
-    dates = stock_zh_index_daily_df.index
-
-except Exception as e:
-    print(f"数据获取失败: {e}")
-    # 如果数据获取失败,则退出
-    exit()
-
-# --- 2. 构建卡尔曼滤波模型 ---
-print("\n正在构建卡尔曼滤波模型...")
-# 状态转移矩阵 F: level(t) = level(t-1) + trend(t-1), trend(t) = trend(t-1)
-transition_matrix = [[1, 1], [0, 1]]
-
-# 观测矩阵 H: 我们只能观测到价格 level
-observation_matrix = [[1, 0]]
-
-# 初始状态均值
-initial_state_mean = [observations.iloc[0], 0]
-
-# 创建卡尔曼滤波器
-kf = KalmanFilter(
-    transition_matrices=transition_matrix,
-    observation_matrices=observation_matrix,
-    initial_state_mean=initial_state_mean,
-    initial_state_covariance=np.eye(2),
-    transition_covariance=np.eye(2) * 0.005,
-    observation_covariance=2.0
-)
-
-# --- 3. 应用滤波器 ---
-print("\n正在应用滤波器...")
-# 使用前 EM_TRAIN_DAYS 个交易日的数据训练协方差参数,避免未来信息泄露
-EM_TRAIN_DAYS = 250  # 约一年的交易日
-em_train_data = observations.iloc[:EM_TRAIN_DAYS]
-kf = kf.em(em_train_data, n_iter=5)
-print(f"  EM训练期: {dates[0].strftime('%Y-%m-%d')} ~ {dates[min(EM_TRAIN_DAYS-1, len(dates)-1)].strftime('%Y-%m-%d')} ({len(em_train_data)}天)")
-# 对全量观测值应用卡尔曼滤波(filter只用过去数据,不泄露未来信息)
-(filtered_state_means, filtered_state_covariances) = kf.filter(observations)
-
-
-# --- 4. 信号生成与回测 ---
-
-# --- 4.1. 计算波动率 ---
-daily_returns = observations.pct_change()
-# 使用20天窗口计算滚动波动率
-rolling_vol = daily_returns.rolling(window=20).std() * np.sqrt(252) # 年化波动率
-# 使用固定波动率阈值(基于A股长期波动率分布的先验知识,无未来信息泄露)
-vol_threshold = VOL_THRESHOLD_FIXED
-
-# --- 4.2. 信号生成与过滤 (优化版) ---
-print("\n正在分析交易信号 (应用趋势死区 + 确认期 + 波动率过滤器 + Kalman偏离过滤)...")
-trend_series = pd.Series(filtered_state_means[:, 1], index=dates)
-kalman_price_series = pd.Series(filtered_state_means[:, 0], index=dates)
-
-# 计算趋势加速度(一阶差分)
-trend_acceleration = trend_series.diff()
-
-# 计算Kalman价格相对60日均线的偏离度
-kalman_ma60 = kalman_price_series.rolling(60).mean()
-kalman_slope_60 = (kalman_price_series - kalman_ma60) / kalman_ma60
-
-# === 买入信号生成 ===
-# 1. 趋势确认期:trend必须连续N天保持在阈值之上
-trend_confirmed_buy = trend_series.rolling(window=CONFIRMATION_DAYS_BUY).min() >= TREND_THRESHOLD_BUY
-
-# 2. 确认期刚刚满足(前一天未满足,今天满足 = 确认完成的那一天触发)
-trend_just_confirmed_buy = trend_confirmed_buy & (~trend_confirmed_buy.shift(1, fill_value=False))
-
-# 3. 趋势加速度过滤:买入时trend必须在增强(加速度≥0)
-trend_accelerating = trend_acceleration >= 0
-
-# 4. 波动率过滤器:只在低波动率时买入
-vol_below_threshold = rolling_vol < vol_threshold
-
-# 5. Kalman偏离过滤:kalman价格不能偏离60日均线太多(过滤深度熊市)
-kalman_above_threshold = kalman_slope_60 > KALMAN_SLOPE_THRESH
-
-# 综合买入信号:确认期刚满足 + 加速度 + 低波动率 + Kalman偏离
-valid_buy_signals = trend_just_confirmed_buy & trend_accelerating & vol_below_threshold & kalman_above_threshold
-up_dates = trend_series[valid_buy_signals].index
-
-# === 卖出信号生成 ===
-# 1. 趋势确认期:trend必须连续N天保持在阈值之下
-trend_confirmed_sell = trend_series.rolling(window=CONFIRMATION_DAYS_SELL).max() <= TREND_THRESHOLD_SELL
-
-# 2. 确认期刚刚满足
-trend_just_confirmed_sell = trend_confirmed_sell & (~trend_confirmed_sell.shift(1, fill_value=False))
-
-# 综合卖出信号:确认期刚满足
-valid_sell_signals = trend_just_confirmed_sell
-down_dates = trend_series[valid_sell_signals].index
-
-# --- 4.3. 生成详细交易日志 ---
-print("\n--- 详细交易信号日志 ---")
-print(f"  信号参数: 买入确认={CONFIRMATION_DAYS_BUY}天, 卖出确认={CONFIRMATION_DAYS_SELL}天, "
-      f"趋势买入阈值={TREND_THRESHOLD_BUY}, 趋势卖出阈值={TREND_THRESHOLD_SELL}")
-print(f"  过滤条件: 波动率<{VOL_THRESHOLD_FIXED:.2f}, Kalman偏离>{KALMAN_SLOPE_THRESH}")
-print(f"  风控参数: 早期止损={EARLY_STOP_DAYS}天/{EARLY_STOP_LOSS:.1%}, "
-      f"追踪止损={TRAILING_STOP_PCT:.0%}, 最大亏损={MAX_LOSS_STOP:.0%}, "
-      f"趋势衰减={TREND_PEAK_DECAY:.0%}, 最小持仓={MIN_HOLDING_DAYS}天")
-print(f"  加仓逻辑: {SCALE_IN_DAYS}天涨幅>{SCALE_IN_THRESH:.1%}时加仓{SCALE_IN_SIZE:.0%}(总仓位{1.0+SCALE_IN_SIZE:.1f}x)")
-buy_signals_df = pd.DataFrame({
-    'Signal': 'Buy',
-    'Price': observations.loc[up_dates],
-    'Kalman Trend': trend_series.loc[up_dates],
-    'Volatility': rolling_vol.loc[up_dates]
-})
-
-sell_signals_df = pd.DataFrame({
-    'Signal': 'Sell',
-    'Price': observations.loc[down_dates],
-    'Kalman Trend': trend_series.loc[down_dates],
-    'Volatility': rolling_vol.loc[down_dates]
-})
-
-all_trades_log = pd.concat([buy_signals_df, sell_signals_df]).sort_index()
-
-# 打印完整的交易日志
-if not all_trades_log.empty:
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 1000):
-        print(all_trades_log)
-else:
-    print("在当前过滤条件下没有生成任何交易信号。")
-
-# --- 5. 可视化分析 ---
-import matplotlib.pyplot as plt
-from matplotlib import rcParams
-
-# 设置中文字体
-rcParams['font.sans-serif'] = ['SimHei']  
-rcParams['axes.unicode_minus'] = False
-
-def plot_kalman_analysis(prices, smoothed_means, trend_series, up_dates, down_dates, equity_curve):
-    """绘制卡尔曼滤波分析图表"""
-    fig, axes = plt.subplots(4, 1, figsize=(14, 16))
-    
-    # 图1: 价格与卡尔曼滤波估计
-    axes[0].plot(prices.index, prices, 'o-', alpha=0.3, label='原始价格', markersize=2, color='gray')
-    axes[0].plot(prices.index, smoothed_means[:, 0], '-', label='卡尔曼滤波估计', linewidth=2, color='blue')
-    axes[0].set_title('创业板50指数 - 卡尔曼滤波价格估计', fontsize=14, fontweight='bold')
-    axes[0].set_ylabel('价格', fontsize=12)
-    axes[0].legend(loc='upper left')
-    axes[0].grid(True, alpha=0.3)
-    
-    # 图2: 趋势分析
-    axes[1].plot(trend_series.index, trend_series, '-', label='卡尔曼趋势', linewidth=2, color='green')
-    axes[1].axhline(y=0, color='red', linestyle='--', linewidth=1.5, label='0轴')
-    
-    # 标记买卖信号
-    if not up_dates.empty:
-        buy_prices = prices.loc[up_dates]
-        axes[1].scatter(up_dates, trend_series.loc[up_dates], color='red', s=100, zorder=5, 
-                       marker='^', label=f'买入信号({len(up_dates)}次)')
-    
-    if not down_dates.empty:
-        axes[1].scatter(down_dates, trend_series.loc[down_dates], color='blue', s=100, zorder=5, 
-                       marker='v', label=f'卖出信号({len(down_dates)}次)')
-    
-    # 填充正负趋势区域
-    axes[1].fill_between(trend_series.index, 0, trend_series, where=trend_series >= 0, 
-                        alpha=0.2, color='green')
-    axes[1].fill_between(trend_series.index, 0, trend_series, where=trend_series < 0, 
-                        alpha=0.2, color='red')
-    
-    axes[1].set_title('卡尔曼趋势分析与交易信号', fontsize=14, fontweight='bold')
-    axes[1].set_ylabel('趋势值', fontsize=12)
-    axes[1].legend(loc='upper left')
-    axes[1].grid(True, alpha=0.3)
-    
-    # 图3: 波动率分析
-    daily_returns = prices.pct_change()
-    rolling_vol = daily_returns.rolling(window=20).std() * np.sqrt(252)
-
-    axes[2].plot(rolling_vol.index, rolling_vol, '-', label='滚动波动率(20日)', linewidth=2, color='orange')
-    axes[2].axhline(y=VOL_THRESHOLD_FIXED, color='red', linestyle='--', linewidth=1.5, label=f'波动率阈值({VOL_THRESHOLD_FIXED:.2f})')
-    axes[2].fill_between(rolling_vol.index, 0, rolling_vol, alpha=0.3, color='yellow')
-    axes[2].set_title('市场波动率分析', fontsize=14, fontweight='bold')
-    axes[2].set_ylabel('年化波动率', fontsize=12)
-    axes[2].legend(loc='upper left')
-    axes[2].grid(True, alpha=0.3)
-    
-    # 图4: 资产曲线
-    axes[3].plot(equity_curve.index, equity_curve, '-', label='资产曲线', linewidth=2, color='purple')
-    axes[3].fill_between(equity_curve.index, 100000, equity_curve, where=equity_curve >= 100000, 
-                        alpha=0.3, color='green')
-    axes[3].fill_between(equity_curve.index, 100000, equity_curve, where=equity_curve < 100000, 
-                        alpha=0.3, color='red')
-    axes[3].axhline(y=100000, color='black', linestyle='--', linewidth=1, label='初始资金')
-    
-    # 计算最大回撤区域
-    running_max = equity_curve.cummax()
-    drawdown = (equity_curve - running_max) / running_max
-    max_dd_idx = drawdown.idxmin()
-    max_dd_value = drawdown.min()
-    axes[3].scatter([max_dd_idx], [equity_curve.loc[max_dd_idx]], color='red', s=200, zorder=5, 
-                   marker='v', label=f'最大回撤({max_dd_value:.1%})')
-    
-    axes[3].set_title('策略资产曲线', fontsize=14, fontweight='bold')
-    axes[3].set_xlabel('日期', fontsize=12)
-    axes[3].set_ylabel('资产价值 ($)', fontsize=12)
-    axes[3].legend(loc='upper left')
-    axes[3].grid(True, alpha=0.3)
-    
-    plt.tight_layout()
-    plt.savefig('kalman_filter_analysis.png', dpi=300, bbox_inches='tight')
-    print("卡尔曼滤波分析图表已保存为 kalman_filter_analysis.png")
-    plt.close()  # 关闭图形,避免阻塞
-
-# 注释掉临时图表绘制,将在回测完成后绘制完整图表
-# plot_kalman_analysis(observations, filtered_state_means, trend_series, up_dates, down_dates, 
-#                     pd.Series(100000, index=observations.index).cumprod())
-
-# --- 6. 高级回测与统计 ---
-def run_advanced_backtest(up_dates, down_dates, prices, trend_series, initial_capital=100000.0):
-    print("\n--- 开始高级回测与统计 (日线级别) ---")
-    print(f"  风控参数: 追踪止损={TRAILING_STOP_PCT:.0%}, 最大亏损止损={MAX_LOSS_STOP:.0%}, "
-          f"最小持仓={MIN_HOLDING_DAYS}天, 趋势衰减={TREND_PEAK_DECAY:.0%}")
-    print(f"  早期止损: {EARLY_STOP_DAYS}天/{EARLY_STOP_LOSS:.1%}")
-    print(f"  加仓逻辑: {SCALE_IN_DAYS}天涨幅>{SCALE_IN_THRESH:.1%}时加仓{SCALE_IN_SIZE:.0%}(总仓位{1.0+SCALE_IN_SIZE:.1f}x)")
-
-    # 1. 构建原始信号序列
-    signals = pd.Series(0, index=prices.index)
-    signals.loc[up_dates] = 1   # 买入信号
-    signals.loc[down_dates] = -1  # 卖出信号
-
-    # 2. 逐日模拟:T日产生信号,T+1日以收盘价执行
-    position_state = 0       # 0=空仓, 1=持仓
-    entry_price = 0.0        # 实际入场价格(T+1日收盘价)
-    entry_date = None         # 实际入场日期
-    holding_days = 0          # 持仓天数(从实际入场日开始计)
-    kalman_high = 0.0         # 持仓期间卡尔曼滤波价格最高点
-    trend_peak = 0.0          # 持仓期间趋势峰值
-    position_size = 0.0       # 仓位大小: 0, 1.0, 或 1.0+SCALE_IN_SIZE
-    scaled_in = False         # 是否已加仓
-    pending_entry = False     # 待执行的买入信号(T日产生,T+1执行)
-    pending_exit = False      # 待执行的卖出信号
-    pending_exit_reason = ''  # 待执行卖出的原因
-
-    positions = pd.Series(0.0, index=prices.index)
-    exit_reasons = {}         # 记录每次退出原因(key=实际卖出日)
-    trade_records = []        # 逐笔交易记录
-
-    for i in range(len(prices)):
-        date = prices.index[i]
-        price = prices.iloc[i]
-        trend = trend_series.iloc[i] if i < len(trend_series) else 0
-
-        # === 执行前一天的待处理卖出信号 ===
-        # 卖出日仍持仓(承受当天涨跌),以收盘价卖出
-        if pending_exit and position_state == 1:
-            sell_position_size = 1.0 + (SCALE_IN_SIZE if scaled_in else 0)
-            trade_return = (price - entry_price) / entry_price if entry_price > 0 else 0
-            trade_records.append({
-                'buy_date': entry_date, 'buy_price': entry_price,
-                'sell_date': date, 'sell_price': price,
-                'return': trade_return, 'reason': pending_exit_reason,
-                'scaled': scaled_in, 'holding_days': holding_days + 1,
-                'position_size': sell_position_size
-            })
-            exit_reasons[date] = pending_exit_reason
-            positions.iloc[i] = sell_position_size  # 卖出日仍按原仓位计算收益
-            position_state = 0
-            position_size = 0.0
-            pending_exit = False
-            pending_exit_reason = ''
-            continue  # 卖出日处理完毕,不再进入持仓逻辑
-
-        # === 执行前一天的待处理买入信号 ===
-        # 买入日以收盘价买入,当天不计收益
-        if pending_entry and position_state == 0:
-            position_state = 1
-            entry_price = price  # 以T+1日收盘价作为实际入场价
-            entry_date = date
-            holding_days = 0
-            kalman_high = filtered_state_means[i, 0] if i < len(filtered_state_means) else price
-            trend_peak = trend
-            position_size = 1.0
-            scaled_in = False
-            pending_entry = False
-            positions.iloc[i] = 0  # 买入日不计收益
-            continue  # 买入日处理完毕,不进入持仓逻辑(修复holding_days多算1天)
-
-        # === 持仓中的逻辑 ===
-        if position_state == 1:
-            holding_days += 1
-            kalman_price = filtered_state_means[i, 0] if i < len(filtered_state_means) else price
-            kalman_high = max(kalman_high, kalman_price)
-            trend_peak = max(trend_peak, trend)
-            pnl = (price - entry_price) / entry_price if entry_price > 0 else 0
-
-            # === 加仓逻辑 ===
-            if not scaled_in and holding_days == SCALE_IN_DAYS and pnl >= SCALE_IN_THRESH:
-                position_size = 1.0 + SCALE_IN_SIZE
-                scaled_in = True
-
-            # === 止损检查(T日产生信号,T+1执行)===
-            exit_reason = None
-
-            # 检查0: 早期止损
-            if holding_days >= EARLY_STOP_DAYS and holding_days < MIN_HOLDING_DAYS:
-                if pnl <= -EARLY_STOP_LOSS:
-                    exit_reason = f'早期止损({holding_days}天亏损{pnl:.2%})'
-
-            if exit_reason is None and holding_days >= MIN_HOLDING_DAYS:
-                # 检查1: 追踪止损
-                if kalman_high > 0 and (kalman_price - kalman_high) / kalman_high <= -TRAILING_STOP_PCT:
-                    exit_reason = f'追踪止损(卡尔曼价从{kalman_high:.1f}回撤{TRAILING_STOP_PCT:.0%})'
-                # 检查2: 趋势衰减退出
-                if exit_reason is None and trend_peak > 0 and trend <= trend_peak * (1 - TREND_PEAK_DECAY):
-                    exit_reason = f'趋势衰减(峰值{trend_peak:.3f}→{trend:.3f})'
-
-            # 检查3: 最大亏损止损 - 不受最小持仓天数限制
-            if exit_reason is None and entry_price > 0 and pnl <= -MAX_LOSS_STOP:
-                exit_reason = f'最大亏损止损({MAX_LOSS_STOP:.0%})'
-
-            # 检查4: 原始卖出信号(需满足最小持仓天数)
-            if exit_reason is None and signals.iloc[i] == -1 and holding_days >= MIN_HOLDING_DAYS:
-                exit_reason = '趋势卖出信号'
-
-            if exit_reason:
-                pending_exit = True
-                pending_exit_reason = exit_reason
-
-        # === 开仓信号(T日产生,T+1执行)===
-        if signals.iloc[i] == 1 and position_state == 0 and not pending_entry:
-            pending_entry = True
-
-        positions.iloc[i] = position_size
-
-    # 3. 计算策略每日收益率(positions已经是实际持仓,无需shift)
-    daily_returns = prices.pct_change().fillna(0)
-    strategy_returns = daily_returns * positions
-
-    # 4. 计算资产曲线
-    equity_curve = (1 + strategy_returns).cumprod() * initial_capital
-
-    # 5. 计算各项统计指标
-    final_value = equity_curve.iloc[-1]
-    total_return = (final_value / initial_capital) - 1
-
-    # 最大回撤
-    running_max = equity_curve.cummax()
-    drawdown = (equity_curve - running_max) / running_max
-    max_drawdown = drawdown.min()
-
-    # 年度收益率
-    yearly_returns = equity_curve.resample('YE').ffill().pct_change().fillna(0)
-
-    # 夏普比率 (日线级别年化因子)
-    sharpe_ratio = (strategy_returns.mean() / strategy_returns.std()) * np.sqrt(252) if strategy_returns.std() > 0 else 0
-
-    # --- 逐笔交易分析(直接使用trade_records,不再从positions推断)---
-    num_trades = len(trade_records)
-    winning_trades = sum(1 for t in trade_records if t['return'] > 0)
-
-    print("\n--- 逐笔交易盈亏分析 (日线级别) ---")
-    print(f"  风控参数: 早期止损={EARLY_STOP_DAYS}天/{EARLY_STOP_LOSS:.1%}, "
-          f"追踪止损={TRAILING_STOP_PCT:.0%}, 最大亏损={MAX_LOSS_STOP:.0%}, "
-          f"趋势衰减={TREND_PEAK_DECAY:.0%}, 最小持仓={MIN_HOLDING_DAYS}天")
-    print(f"  加仓逻辑: {SCALE_IN_DAYS}天涨幅>{SCALE_IN_THRESH:.1%}时加仓{SCALE_IN_SIZE:.0%}(总仓位{1.0+SCALE_IN_SIZE:.1f}x)")
-    print(f"{'#':<4} {'买入时间':<12} {'买入价':<10} {'卖出时间':<12} {'卖出价':<10} {'持仓天':<7} {'仓位':<6} {'收益率':<10} {'退出原因'}")
-    print("-" * 110)
-
-    for idx, t in enumerate(trade_records):
-        pos_str = f"{t['position_size']:.1f}x" if t['scaled'] else '1.0x'
-        print(f"{idx+1:<4} {str(t['buy_date'].date()):<12} {t['buy_price']:<10.2f} "
-              f"{str(t['sell_date'].date()):<12} {t['sell_price']:<10.2f} "
-              f"{t['holding_days']:<7} {pos_str:<6} {t['return']:<+10.2%} {t['reason']}")
-
-    print("-" * 110)
-    win_rate = winning_trades / num_trades if num_trades > 0 else 0
-
-    # 6. 打印结果
-    print("\n--- 回测结果摘要 (日线级别) ---")
-    print(f"时间范围: {prices.index[0]} to {prices.index[-1]}")
-    print(f"初始资金: {initial_capital:,.2f}")
-    print(f"最终资产: {final_value:,.2f}")
-    print(f"总收益率: {total_return:.2%}")
-    print(f"最大回撤: {max_drawdown:.2%}")
-    print(f"夏普比率: {sharpe_ratio:.2f}")
-    print(f"总交易对数 (买入-卖出): {num_trades}")
-    print(f"胜率: {win_rate:.2%}")
-
-    # 统计退出原因
-    if trade_records:
-        print("\n--- 退出原因统计 ---")
-        reason_counts = {}
-        for t in trade_records:
-            key = t['reason'].split('(')[0]
-            reason_counts[key] = reason_counts.get(key, 0) + 1
-        for reason, count in sorted(reason_counts.items(), key=lambda x: -x[1]):
-            print(f"  {reason}: {count}次")
-
-    print("\n各年度收益率:")
-    if not yearly_returns.empty:
-        for year, ret in yearly_returns.items():
-            if year.year == equity_curve.index[0].year and ret == 0:
-                first_year_end = equity_curve[equity_curve.index.year == year.year].index[-1]
-                first_year_start_val = initial_capital
-                first_year_end_val = equity_curve[first_year_end]
-                ret = (first_year_end_val / first_year_start_val) - 1
-            print(f"  {year.year}: {ret:.2%}")
-    else:
-        print("  没有足够的数据来计算年度收益。")
-
-    return equity_curve
-
-# 运行主逻辑
-equity_curve = run_advanced_backtest(up_dates, down_dates, observations, trend_series)
-
-# 重新绘制包含正确资产曲线的图表
-print("\n正在重新生成包含完整资产曲线的分析图表...")
-plot_kalman_analysis(observations, filtered_state_means, trend_series, up_dates, down_dates, equity_curve)
-
-# --- 导出交易信号到CSV ---
-if not all_trades_log.empty:
-    csv_file_path = 'kalman_daily_signals.csv'
-    all_trades_log.to_csv(csv_file_path, encoding='utf-8-sig')
-    print(f"\n交易信号已成功导出到 {csv_file_path}")
-else:
-    print("\n没有交易信号可导出。")
-
-# --- 打印所有交易信号详情 ---
-def print_all_signals(prices, trend_series, rolling_vol, up_dates, down_dates):
-    """
-    打印所有交易信号的详情
-
-    Args:
-        prices: 价格序列
-        trend_series: 趋势序列
-        rolling_vol: 波动率序列
-        up_dates: 买入信号日期
-        down_dates: 卖出信号日期
-    """
-    print(f"\n{'='*90}")
-    print(f"所有交易信号详情 (共 {len(up_dates)} 个买入 + {len(down_dates)} 个卖出 = {len(up_dates)+len(down_dates)} 个信号)")
-    print(f"{'='*90}")
-
-    print(f"\n{'-'*90}")
-    print(f"{'#':<5} {'日期':<12} {'信号':<10} {'收盘价':<10} {'Trend':<12} {'波动率':<12}")
-    print(f"{'-'*90}")
-
-    # 合并所有信号日期并排序
-    all_signal_dates = sorted(set(up_dates) | set(down_dates))
-
-    for idx, date in enumerate(all_signal_dates):
-        date_str = date.strftime('%Y-%m-%d')
-        price = prices.loc[date]
-        trend = trend_series.loc[date]
-        vol = rolling_vol.loc[date] if date in rolling_vol.index and not np.isnan(rolling_vol.loc[date]) else 0.0
-
-        if date in up_dates:
-            signal_type = '买入'
-        else:
-            signal_type = '卖出'
-
-        print(f"{idx+1:<5} {date_str:<12} {signal_type:<10} {price:<10.2f} {trend:<12.4f} {vol:<12.4f}")
-
-    print(f"{'-'*90}")
-
-    # 当前市场状态
-    last_trend = trend_series.iloc[-1]
-    last_vol = rolling_vol.iloc[-1]
-    last_price = prices.iloc[-1]
-
-    print(f"\n当前市场状态 ({prices.index[-1].strftime('%Y-%m-%d')}):")
-    print(f"  收盘价: {last_price:.2f}")
-    print(f"  趋势值: {last_trend:.4f}")
-    print(f"  波动率: {last_vol:.4f}")
-
-    if last_trend > 0:
-        print(f"  趋势判断: 上升趋势")
-        if len(down_dates) > 0:
-            last_sell = down_dates[-1]
-            days_since_sell = (prices.index[-1] - last_sell).days
-            print(f"  距上次卖出: {days_since_sell} 天 ({last_sell.strftime('%Y-%m-%d')})")
-    else:
-        print(f"  趋势判断: 下降趋势")
-        if len(up_dates) > 0:
-            last_buy = up_dates[-1]
-            days_since_buy = (prices.index[-1] - last_buy).days
-            print(f"  距上次买入: {days_since_buy} 天 ({last_buy.strftime('%Y-%m-%d')})")
-
-    if last_trend < 0:
-        print(f"\n下次买入条件: Trend需要从负值穿越到正值,且波动率 < {VOL_THRESHOLD_FIXED:.4f}")
-
-    print(f"{'='*90}\n")
-
-
-# 打印所有交易信号详情
-print_all_signals(
-    observations,
-    trend_series,
-    rolling_vol,
-    up_dates,
-    down_dates,
-)
-
-# --- 打印最近20天逐日信号判断详情 ---
-def print_recent_daily_detail(prices, trend_series, rolling_vol, kalman_slope_60,
-                               trend_acceleration, up_dates, down_dates, n_days=20):
-    """
-    打印最近N个交易日的逐日信号判断详情,帮助做出交易决策
-
-    每天显示各项买入/卖出条件是否满足,以及最终信号结果
-    """
-    print(f"\n{'='*120}")
-    print(f"最近 {n_days} 个交易日信号判断详情")
-    print(f"{'='*120}")
-    print(f"  买入条件: Trend>={TREND_THRESHOLD_BUY}(确认{CONFIRMATION_DAYS_BUY}天) + 加速度>=0 + 波动率<{VOL_THRESHOLD_FIXED} + Kalman偏离>{KALMAN_SLOPE_THRESH}")
-    print(f"  卖出条件: Trend<={TREND_THRESHOLD_SELL}(确认{CONFIRMATION_DAYS_SELL}天)")
-
-    print(f"\n{'-'*120}")
-    print(f"{'日期':<12} {'收盘价':<10} {'Trend':<10} {'加速度':<10} {'波动率':<10} {'K偏离':<10} "
-          f"{'趋势':<6} {'加速':<6} {'波动':<6} {'偏离':<6} {'信号':<10} {'说明'}")
-    print(f"{'-'*120}")
-
-    recent_idx = prices.index[-n_days:]
-
-    for date in recent_idx:
-        price = prices.loc[date]
-        trend = trend_series.loc[date]
-        accel = trend_acceleration.loc[date] if date in trend_acceleration.index else np.nan
-        vol = rolling_vol.loc[date] if date in rolling_vol.index else np.nan
-        k_slope = kalman_slope_60.loc[date] if date in kalman_slope_60.index else np.nan
-
-        # 各条件判断
-        trend_ok = trend >= TREND_THRESHOLD_BUY
-        accel_ok = (not np.isnan(accel)) and accel >= 0
-        vol_ok = (not np.isnan(vol)) and vol < VOL_THRESHOLD_FIXED
-        slope_ok = (not np.isnan(k_slope)) and k_slope > KALMAN_SLOPE_THRESH
-
-        # 信号判断
-        signal = ''
-        reason = ''
-        if date in up_dates:
-            signal = '>>买入'
-            reason = '所有买入条件满足'
-        elif date in down_dates:
-            signal = '>>卖出'
-            reason = f'Trend连续{CONFIRMATION_DAYS_SELL}天<={TREND_THRESHOLD_SELL}'
-        else:
-            signal = '-'
-            # 分析未触发原因
-            if trend >= TREND_THRESHOLD_BUY:
-                blocked = []
-                if not accel_ok:
-                    blocked.append('加速度<0')
-                if not vol_ok:
-                    blocked.append(f'波动率过高')
-                if not slope_ok:
-                    blocked.append('K偏离过低')
-                if blocked:
-                    reason = '趋势正但' + '+'.join(blocked)
-                else:
-                    reason = '非确认触发日'
-            elif trend <= TREND_THRESHOLD_SELL:
-                reason = '下降趋势(未满足确认期)'
-            else:
-                reason = '趋势在死区内' if trend < TREND_THRESHOLD_BUY else '观望'
-
-        date_str = date.strftime('%Y-%m-%d')
-        t_mark = 'Y' if trend_ok else 'N'
-        a_mark = 'Y' if accel_ok else 'N'
-        v_mark = 'Y' if vol_ok else 'N'
-        s_mark = 'Y' if slope_ok else 'N'
-
-        vol_str = f"{vol:.4f}" if not np.isnan(vol) else "N/A"
-        accel_str = f"{accel:.4f}" if not np.isnan(accel) else "N/A"
-        k_slope_str = f"{k_slope:.4f}" if not np.isnan(k_slope) else "N/A"
-
-        print(f"{date_str:<12} {price:<10.2f} {trend:<10.4f} {accel_str:<10} {vol_str:<10} {k_slope_str:<10} "
-              f"{t_mark:<6} {a_mark:<6} {v_mark:<6} {s_mark:<6} {signal:<10} {reason}")
-
-    print(f"{'-'*120}")
-    print(f"{'='*120}\n")
-
-
-# 打印最近20天逐日信号判断详情
-print_recent_daily_detail(
-    observations,
-    trend_series,
-    rolling_vol,
-    kalman_slope_60,
-    trend_acceleration,
-    up_dates,
-    down_dates,
-    n_days=20,
-)
-
-print("\n分析完成。")
-
-# --- 如何解读和评估 ---
-#
-# 1. **价格平滑**:
-#    平滑后的价格代表了我们估计的指数"真实内在价值",过滤掉了大部分短期市场噪声。
-#
-# 2. **趋势判断**:
-#    估计的趋势(速度)用于判断市场方向。
-#    - **当 Trend > 0**: 表明目前处于上升趋势。
-#    - **当 Trend < 0**: 表明目前处于下降趋势。
-#    - **Trend 从负值上穿0轴**: 可能是一个潜在的买入信号。
-#    - **Trend 从正值下穿0轴**: 可能是一个潜在的卖出信号。
-#
-# 3. **波动率过滤器**:
-#    当市场波动率低于特定阈值时,才允许买入信号生效。旨在避免在混乱、高风险的市场环境中入场。
-#
-# 4. **免责声明**:
-#    此模型是一个简化的分析工具,而非精准的预测系统。
-#    滤波器的表现依赖于参数设置和市场状态,历史规律不代表未来。
-#    在实际应用中,需要结合更多指标和风险管理策略。
+if __name__ == "__main__":
+    main()

File diff ditekan karena terlalu besar
+ 2776 - 0
kalman-filter/v2/kalman_strategy_engine.py