erwin 1 månad sedan
förälder
incheckning
67fc0ff06c

+ 0 - 76
kalman-filter/kalman_daily_signals.csv

@@ -1,76 +0,0 @@
-date,Signal,Price,Kalman Trend,Volatility
-2018-01-03,Sell,1562.52,-2.035801199149747,
-2018-02-02,Sell,1476.92,-1.7417097455449801,0.25637043701328366
-2018-03-27,Sell,1610.47,-1.0416084366367344,0.39435384853472544
-2018-04-12,Sell,1581.94,-1.2684855929760428,0.38352178961509464
-2018-05-08,Buy,1604.31,0.5989743227279396,0.2950727571567104
-2018-05-28,Sell,1508.22,-3.1745322271138114,0.21334301838292702
-2018-08-02,Sell,1264.28,-3.6517503371724462,0.25285782273350793
-2018-11-27,Sell,1067.12,-1.9200631142896563,0.3306553830414185
-2019-02-01,Buy,1027.24,0.10573385083899123,0.22942320508806657
-2019-03-28,Sell,1301.88,-3.2105593806768633,0.338863940300261
-2019-04-16,Sell,1369.38,-1.4321797407890777,0.3048944395963836
-2019-07-23,Sell,1214.61,-0.519675048595727,0.2394652038949983
-2019-07-24,Buy,1231.28,0.024901618904257905,0.24342971282602707
-2019-08-07,Sell,1177.67,-2.3103337317060904,0.1850925671421865
-2019-08-16,Buy,1242.89,0.25449376222377906,0.2020784521803723
-2019-09-27,Sell,1333.11,-1.8595535045504294,0.24989736515456584
-2019-10-28,Buy,1366.26,0.31523732141202965,0.21821949477400857
-2019-12-03,Sell,1379.46,-0.44138842107922766,0.18671141076796746
-2019-12-05,Buy,1415.65,0.6278227612270856,0.20631861438286273
-2020-03-10,Sell,1829.12,-1.0421532860359428,0.4571235016723308
-2020-05-26,Buy,1828.71,0.08839191934439089,0.2283127243269911
-2020-05-28,Sell,1783.91,-1.6994954983100372,0.2312103915951284
-2020-06-01,Buy,1869.07,0.11898824496119831,0.261710946653315
-2020-08-13,Sell,2384.16,-7.185927255230666,0.38569901317298466
-2020-09-01,Buy,2495.35,1.347451230701797,0.29261018077171547
-2020-09-09,Sell,2278.58,-5.195194663562061,0.33688061088153665
-2020-11-19,Sell,2476.45,-4.878630891308454,0.2951804259564014
-2020-12-04,Buy,2571.34,0.8882126327856175,0.2718015016891235
-2021-02-24,Sell,2931.28,-9.428293750486825,0.39482436855047287
-2021-07-28,Sell,3318.33,-10.081620741747184,0.3849124937877533
-2021-08-12,Sell,3395.61,-3.4724333813441133,0.3878850249478351
-2021-09-14,Buy,3281.78,0.5000538220971005,0.2940784925325676
-2021-09-17,Sell,3231.13,-0.7817324680383104,0.300461523740023
-2021-09-27,Buy,3262.04,1.0998665074728202,0.25785813182583484
-2021-10-13,Sell,3228.94,-0.4085184679023798,0.23803228202985793
-2021-10-15,Buy,3309.93,2.3365665606013937,0.24784581329391453
-2021-12-08,Sell,3476.46,-3.0558256880004557,0.18946831376158857
-2021-12-14,Buy,3547.91,0.30859839222677043,0.1862647773198879
-2021-12-16,Buy,3546.36,0.7531071210992673,0.18380839602128546
-2021-12-20,Sell,3369.31,-4.982774813348085,0.19383610890926953
-2022-03-04,Sell,2706.31,-2.5431070799047557,0.3155685137577168
-2022-03-29,Sell,2565.61,-2.8730001564042835,0.39531355520415945
-2022-04-08,Sell,2567.85,-3.368212859677051,0.3929435201440377
-2022-07-18,Sell,2822.81,-0.2284724984383139,0.29796712567906614
-2022-08-16,Buy,2741.24,0.6165496549246292,0.2179519001594982
-2022-08-30,Sell,2609.23,-5.758151618794717,0.25228513412417325
-2022-10-31,Sell,2238.8,-5.214198355238455,0.3490331397537357
-2022-11-24,Sell,2327.58,-2.5827685608653574,0.3125295589747524
-2022-12-06,Buy,2382.38,0.1681595590909586,0.21976580290643222
-2022-12-21,Sell,2300.7,-3.303369738718307,0.16633552249250572
-2023-01-05,Buy,2395.3,0.2968073065119319,0.20555240478731698
-2023-02-15,Sell,2527.29,-1.6880024549892592,0.17229084310112694
-2023-04-24,Sell,2201.51,-5.721957175566763,0.17268791661081376
-2023-07-20,Sell,2064.2,-1.8500167472716889,0.18823678221274634
-2023-07-31,Buy,2153.64,0.7992512275379373,0.19046084515950606
-2023-08-16,Sell,2053.81,-3.8325996119027175,0.18604461286911145
-2023-11-23,Sell,1844.58,-1.6645339410390587,0.24471966730465083
-2024-01-04,Sell,1684.0,-1.279390586094902,0.2546618558399457
-2024-03-28,Sell,1738.01,-2.9486993704770894,0.2865052699910859
-2024-04-29,Buy,1827.41,1.3394298864541132,0.299874483692149
-2024-05-27,Sell,1749.82,-1.300607762949879,0.27408542474688274
-2024-07-30,Sell,1578.29,-2.1133652218864594,0.20055222295413416
-2024-09-12,Buy,1513.18,0.17064215293430718,0.18741426171483574
-2024-11-25,Sell,2184.68,-6.358100547208809,0.4310695085974503
-2024-12-18,Sell,2213.07,-1.2155279971174435,0.2802818527276339
-2025-03-06,Sell,2205.12,-1.0373510987552161,0.2704732714820189
-2025-05-30,Sell,1962.62,-1.9913155061408723,0.2124048676442795
-2025-06-11,Buy,2029.1,0.7076046224783838,0.17586725135645348
-2025-06-24,Buy,2044.78,0.7836549830723515,0.17184604182180638
-2025-10-15,Sell,3163.51,-5.254828274267992,0.4063587500610672
-2025-11-17,Sell,3263.76,-5.031160033071558,0.3371939140881644
-2025-12-05,Buy,3302.9,1.1134415852828927,0.2755522593099839
-2026-01-23,Sell,3480.85,-0.4086708964955664,0.1898852108321811
-2026-02-25,Buy,3514.74,0.7464526844969654,0.2255076653297173
-2026-03-04,Sell,3310.59,-6.487781491696072,0.24431529487709092

BIN
kalman-filter/kalman_filter_analysis.png


kalman-filter/cyb50_kalman_filter_daily.py → kalman-filter/v1/cyb50_kalman_filter_daily.py


kalman-filter/kalman_vs_trend_analysis.md → kalman-filter/v1/kalman_vs_trend_analysis.md


kalman-filter/run_and_email.py → kalman-filter/v1/run_and_email.py


kalman-filter/test_extract.py → kalman-filter/v1/test_extract.py


+ 804 - 0
kalman-filter/v2/cyb50_kalman_filter_daily.py

@@ -0,0 +1,804 @@
+# -*- coding: utf-8 -*-
+# cyb50_kalman_filter_daily.py
+
+import akshare as ak
+import pandas as pd
+import numpy as np
+from pykalman import KalmanFilter
+
+# === 策略参数 ===
+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. **免责声明**:
+#    此模型是一个简化的分析工具,而非精准的预测系统。
+#    滤波器的表现依赖于参数设置和市场状态,历史规律不代表未来。
+#    在实际应用中,需要结合更多指标和风险管理策略。

+ 95 - 0
kalman-filter/v2/kalman_vs_trend_analysis.md

@@ -0,0 +1,95 @@
+# Trend策略 vs Kalman策略 对比分析
+
+## 策略概览
+
+### Trend策略 (当前部署)
+- **核心逻辑**: 趋势跟踪(均线突破 + 动量)
+- **买入条件**: 价格>MA10>MA30 + 突破20日高×0.995 + 10日涨幅>2%
+- **卖出条件**: 跌破MA30 或 创20日新低 或 回撤10%止损
+- **数据源**: 多源交叉验证(新浪+腾讯+baostock+akshare)
+- **发送时间**: 11:25, 14:50, 15:05(工作日)
+
+### Kalman策略
+- **核心逻辑**: 卡尔曼滤波趋势估计
+- **买入条件**: 
+  - Trend≥0(确认1天)
+  - 趋势加速度≥0
+  - 年化波动率<0.30
+  - Kalman价格偏离60日均线>-0.05
+- **卖出条件**: 
+  - Trend≤-0.10(确认2天)
+  - 追踪止损8%
+  - 最大亏损止损12%
+  - 趋势衰减退出(峰值衰减85%)
+- **特色功能**: 加仓逻辑(3天涨幅>2.5%时加仓80%)
+
+---
+
+## 关键差异对比
+
+| 维度 | Trend策略 | Kalman策略 |
+|------|-----------|------------|
+| **核心算法** | 简单均线+高低点 | 卡尔曼滤波状态估计 |
+| **趋势识别** | MA10/MA30交叉 | Kalman趋势值 |
+| **买入门槛** | 较高(需突破新高) | 中等(趋势转正) |
+| **卖出机制** | 简单(跌破均线) | 复杂(多条件风控) |
+| **仓位管理** | 固定仓位(全进全出) | 动态加仓(最高1.8x) |
+| **止损方式** | 10%回撤止损 | 8%追踪+12%硬止损+早期止损 |
+| **波动率过滤** | ❌ 无 | ✅ 有(年化<30%) |
+| **数据实时性** | 多源验证 | 东财+腾讯双源 |
+| **报告输出** | HTML+邮件 | 图表+CSV |
+
+---
+
+## 策略优劣分析
+
+### Trend策略优势
+1. **简单可靠**: 逻辑清晰,参数少,易于理解和维护
+2. **多源验证**: 4数据源交叉验证,数据准确性高
+3. **自动化**: 完整邮件报告系统,定时自动发送
+4. **反应及时**: 突破即买入,不滞后
+
+### Trend策略劣势
+1. **震荡市表现差**: 均线缠绕时频繁假突破
+2. **入场滞后**: 需等突破确认,可能错过早期涨幅
+3. **仓位固定**: 无法加仓扩大盈利
+
+### Kalman策略优势
+1. **噪声过滤**: 卡尔曼滤波有效过滤价格噪声
+2. **多维度风控**: 8种退出机制,风控更精细
+3. **加仓机制**: 盈利后可加仓至1.8倍,放大收益
+4. **波动率过滤**: 高波动时回避,降低风险
+5. **趋势平滑**: 避免均线缠绕的虚假信号
+
+### Kalman策略劣势
+1. **复杂度高**: 参数多(14个可调参数),调优困难
+2. **滞后性**: 滤波器本身有滞后,买入点偏晚
+3. **数据依赖**: 依赖akshare,数据源不如Trend策略丰富
+4. **无邮件系统**: 目前只有图表输出,无自动报告
+
+---
+
+## 建议
+
+### 方案1: 并行运行(推荐)
+- Trend策略: 主策略,简单可靠,作为基准
+- Kalman策略: 副策略,精细风控,用于对比验证
+- 相互验证,降低单一策略失效风险
+
+### 方案2: 融合策略
+- 用Trend策略的**多源数据验证**
+- 用Kalman策略的**风控机制**(追踪止损+加仓)
+- 结合两者优势
+
+### 方案3: 市场状态切换
+- 趋势明显市场 → 使用Trend策略
+- 震荡/高波动市场 → 使用Kalman策略
+
+---
+
+## 当前状态
+
+- **Trend策略**: ✅ 已部署,定时运行
+- **Kalman策略**: ⚠️ 本地脚本,需手动运行
+
+需要我把Kalman策略也改造成自动邮件报告系统吗?

+ 417 - 0
kalman-filter/v2/run_and_email.py

@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Kalman策略 - 运行并发送邮件报告 (修复版)
+"""
+
+import sys
+sys.path.insert(0, '/root/.openclaw/workspace/kalman-filter')
+
+import subprocess
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email.header import Header
+from email import encoders
+import os
+from datetime import datetime
+
+# 邮件配置
+EMAIL_CONFIG = {
+    "smtp_server": "localhost",
+    "smtp_port": 25,
+    "sender_email": "kalman@erwin.wang",
+    "receiver_email": "380880504@qq.com"
+}
+
+def run_kalman_strategy():
+    """运行 Kalman 策略"""
+    print("="*60)
+    print("📈 运行 Kalman 策略...")
+    print("="*60)
+    
+    result = subprocess.run(
+        ['python3', 'cyb50_kalman_filter_daily.py'],
+        cwd='/root/.openclaw/workspace/kalman-filter',
+        capture_output=True,
+        text=True,
+        timeout=300
+    )
+    
+    return result.stdout, result.stderr, result.returncode
+
+def parse_results(output):
+    """解析策略输出结果"""
+    lines = output.split('\n')
+    
+    summary = {
+        'total_return': 'N/A',
+        'max_drawdown': 'N/A',
+        'sharpe': 'N/A',
+        'num_trades': 'N/A',
+        'win_rate': 'N/A',
+        'final_value': 'N/A'
+    }
+    
+    for line in lines:
+        if '总收益率:' in line:
+            summary['total_return'] = line.split(':')[1].strip()
+        elif '最大回撤:' in line:
+            summary['max_drawdown'] = line.split(':')[1].strip()
+        elif '夏普比率:' in line:
+            summary['sharpe'] = line.split(':')[1].strip()
+        elif '总交易对数' in line:
+            summary['num_trades'] = line.split(':')[1].strip()
+        elif '胜率:' in line:
+            summary['win_rate'] = line.split(':')[1].strip()
+        elif '最终资产:' in line:
+            summary['final_value'] = line.split(':')[1].strip()
+    
+    return summary
+
+def simplify_reason(reason):
+    """简化退出原因"""
+    if '早期止损' in reason:
+        return '早期止损'
+    elif '趋势衰减' in reason:
+        return '趋势衰减'
+    elif '追踪止损' in reason:
+        return '追踪止损'
+    elif '最大亏损' in reason:
+        return '最大亏损止损'
+    elif '趋势卖出' in reason:
+        return '趋势卖出'
+    else:
+        return reason.split('(')[0] if '(' in reason else reason
+
+def extract_recent_trades(output):
+    """提取最近20次交易(优先从逐笔交易分析中提取,补充所有交易信号)"""
+    lines = output.split('\n')
+    trades = []
+    
+    # 方法1: 从"逐笔交易盈亏分析"中提取(包含完整交易信息)
+    in_trade_section = False
+    for line in lines:
+        if '逐笔交易盈亏分析' in line:
+            in_trade_section = True
+            continue
+        if in_trade_section:
+            stripped = line.strip()
+            # 匹配格式: "21   2026-02-26   3500.75    2026-03-04   3310.59    4       1.0x   -5.43%     早期止损(...)"
+            if len(stripped) > 60:
+                parts = stripped.split()
+                if len(parts) >= 9:
+                    try:
+                        # 检查第一列是数字,第二列是日期
+                        if parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
+                            # 简化退出原因
+                            parts[-1] = simplify_reason(parts[-1])
+                            trade_line = ' '.join(parts[:9])
+                            trades.append(trade_line)
+                    except:
+                        pass
+            if len(trades) >= 25:  # 多取一些,确保包含最近的交易
+                break
+            if '退出原因统计' in line:
+                break
+    
+    # 方法2: 如果逐笔交易不足,从"所有交易信号"中补充配对
+    if len(trades) < 20:
+        signals = []
+        in_signal_section = False
+        for line in lines:
+            if '所有交易信号详情' in line:
+                in_signal_section = True
+                continue
+            if in_signal_section:
+                stripped = line.strip()
+                if len(stripped) > 30:
+                    parts = stripped.split()
+                    if len(parts) >= 3 and parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
+                        signals.append({
+                            'num': parts[0],
+                            'date': parts[1],
+                            'action': parts[2],
+                            'price': parts[3] if len(parts) > 3 else '',
+                        })
+                if '当前市场状态' in line:
+                    break
+        
+        # 配对买卖信号(取全部信号,不只是最后40个)
+        for i in range(len(signals) - 1):
+            if signals[i]['action'] == '买入' and signals[i+1]['action'] == '卖出':
+                buy = signals[i]
+                sell = signals[i+1]
+                # 检查是否已经存在(通过日期判断)
+                trade_date = f"{buy['date']}买入"
+                if not any(trade_date in t for t in trades):
+                    trade_line = f"{buy['num']:<4} {buy['date']:<12} {buy['price']:<10} {sell['date']:<12} {sell['price']:<10} {'?':<7} {'1.0x':<6} {'?':<8} {'信号配对'}"
+                    trades.append(trade_line)
+    
+    # 按交易编号排序,取最近20条
+    def get_trade_num(trade_line):
+        try:
+            return int(trade_line.split()[0])
+        except:
+            return 0
+    
+    trades.sort(key=get_trade_num)
+    return trades[-20:]
+
+def extract_recent_signals(output):
+    """提取最近20天信号详情"""
+    lines = output.split('\n')
+    signals = []
+    in_signal_section = False
+    
+    for line in lines:
+        if '最近' in line and '个交易日信号判断详情' in line:
+            in_signal_section = True
+            continue
+        if in_signal_section:
+            stripped = line.strip()
+            if len(stripped) > 80 and stripped[0:4].isdigit():
+                signals.append(stripped)
+            if len(signals) >= 20:
+                break
+            if '分析完成' in line or '当前市场状态' in line:
+                break
+    
+    return signals
+
+def extract_current_status(output):
+    """提取当前市场状态"""
+    lines = output.split('\n')
+    status = []
+    in_status = False
+    
+    for line in lines:
+        if '当前市场状态' in line:
+            in_status = True
+        if in_status:
+            status.append(line.strip())
+            if len(status) >= 8:
+                break
+    
+    return status
+
+def send_email(subject, html_content, text_content, attachments=None):
+    """发送邮件"""
+    try:
+        msg = MIMEMultipart('alternative')
+        msg['Subject'] = Header(subject, 'utf-8')
+        msg['From'] = EMAIL_CONFIG['sender_email']
+        msg['To'] = EMAIL_CONFIG['receiver_email']
+        
+        text_part = MIMEText(text_content, 'plain', 'utf-8')
+        msg.attach(text_part)
+        
+        html_part = MIMEText(html_content, 'html', 'utf-8')
+        msg.attach(html_part)
+        
+        if attachments:
+            for filepath in attachments:
+                if os.path.exists(filepath):
+                    with open(filepath, 'rb') as f:
+                        attachment = MIMEBase('application', 'octet-stream')
+                        attachment.set_payload(f.read())
+                    encoders.encode_base64(attachment)
+                    filename = os.path.basename(filepath)
+                    attachment.add_header('Content-Disposition', f'attachment; filename="{filename}"')
+                    msg.attach(attachment)
+        
+        with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
+            server.sendmail(
+                EMAIL_CONFIG['sender_email'],
+                EMAIL_CONFIG['receiver_email'],
+                msg.as_string()
+            )
+        print(f"✅ 邮件发送成功: {subject}")
+        return True
+    except Exception as e:
+        print(f"❌ 邮件发送失败: {e}")
+        return False
+
+def main():
+    """主程序"""
+    print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    
+    stdout, stderr, returncode = run_kalman_strategy()
+    
+    if returncode != 0:
+        print(f"❌ 策略运行失败: {stderr}")
+        return
+    
+    summary = parse_results(stdout)
+    recent_trades = extract_recent_trades(stdout)
+    recent_signals = extract_recent_signals(stdout)
+    current_status = extract_current_status(stdout)
+    
+    # 构建文本邮件
+    text = f"""
+Kalman策略报告
+生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+
+【绩效摘要】
+总收益率: {summary['total_return']}
+最大回撤: {summary['max_drawdown']}
+夏普比率: {summary['sharpe']}
+总交易次数: {summary['num_trades']}
+胜率: {summary['win_rate']}
+最终资产: {summary['final_value']}
+
+【当前市场状态】
+"""
+    for line in current_status[:8]:
+        text += line + '\n'
+    
+    # 最近20次交易
+    text += "\n【最近20次交易】\n"
+    for i, trade in enumerate(recent_trades, 1):
+        text += f"{i:2d}. {trade}\n"
+    
+    # 最近20天信号
+    text += "\n【最近20天信号详情】\n"
+    for signal in recent_signals:
+        text += signal + '\n'
+    
+    # 构建HTML
+    trades_html = ""
+    for trade in recent_trades:
+        parts = trade.split()
+        if len(parts) >= 9:
+            trades_html += "<tr>"
+            # 只取前8列(编号到收益率),第9列是退出原因需要简化
+            for part in parts[:8]:
+                trades_html += f"<td>{part}</td>"
+            # 简化退出原因
+            reason = parts[8] if len(parts) > 8 else ""
+            if '早期止损' in reason:
+                reason = '早期止损'
+            elif '趋势衰减' in reason:
+                reason = '趋势衰减'
+            elif '追踪止损' in reason:
+                reason = '追踪止损'
+            elif '最大亏损' in reason:
+                reason = '最大亏损止损'
+            elif '趋势卖出' in reason:
+                reason = '趋势卖出'
+            elif '信号配对' in reason:
+                reason = '信号配对'
+            trades_html += f"<td>{reason}</td>"
+            trades_html += "</tr>"
+        elif len(parts) >= 8:
+            # 如果没有第9列,补充一个占位符
+            trades_html += "<tr>"
+            for part in parts[:8]:
+                trades_html += f"<td>{part}</td>"
+            trades_html += "<td>-</td>"
+            trades_html += "</tr>"
+    
+    signals_html = ""
+    for signal in recent_signals:
+        parts = signal.split()
+        if len(parts) >= 11:
+            signal_class = ""
+            if '>>买入' in signal:
+                signal_class = "buy"
+            elif '>>卖出' in signal:
+                signal_class = "sell"
+            signals_html += f"<tr class='{signal_class}'>"
+            for part in parts[:11]:
+                signals_html += f"<td>{part}</td>"
+            signals_html += "</tr>"
+    
+    html = f"""
+    <html>
+    <head>
+        <meta charset="utf-8">
+        <style>
+            body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
+            .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; }}
+            h1 {{ color: #333; border-bottom: 3px solid #9c27b0; padding-bottom: 10px; }}
+            h2 {{ color: #555; margin-top: 30px; border-left: 4px solid #9c27b0; padding-left: 10px; }}
+            .metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
+            .metric-label {{ color: #666; font-size: 12px; }}
+            .metric-value {{ font-size: 24px; font-weight: bold; }}
+            .positive {{ color: #28a745; }}
+            .negative {{ color: #dc3545; }}
+            .summary {{ background: #f3e5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
+            table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
+            th {{ background: #9c27b0; color: white; padding: 8px; text-align: left; }}
+            td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; }}
+            tr:nth-child(even) {{ background: #f8f9fa; }}
+            .buy {{ background: #e8f5e9; color: #28a745; font-weight: bold; }}
+            .sell {{ background: #ffebee; color: #dc3545; font-weight: bold; }}
+        </style>
+    </head>
+    <body>
+        <div class="container">
+            <h1>📊 Kalman策略报告</h1>
+            <p style="color: #666;">生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
+            
+            <div class="summary">
+                <h2>📈 绩效摘要</h2>
+                <div class="metric">
+                    <div class="metric-label">总收益率</div>
+                    <div class="metric-value {'positive' if '+' in summary['total_return'] else 'negative'}">{summary['total_return']}</div>
+                </div>
+                <div class="metric">
+                    <div class="metric-label">最大回撤</div>
+                    <div class="metric-value">{summary['max_drawdown']}</div>
+                </div>
+                <div class="metric">
+                    <div class="metric-label">夏普比率</div>
+                    <div class="metric-value">{summary['sharpe']}</div>
+                </div>
+                <div class="metric">
+                    <div class="metric-label">交易次数</div>
+                    <div class="metric-value">{summary['num_trades']}</div>
+                </div>
+                <div class="metric">
+                    <div class="metric-label">胜率</div>
+                    <div class="metric-value">{summary['win_rate']}</div>
+                </div>
+                <div class="metric">
+                    <div class="metric-label">最终资产</div>
+                    <div class="metric-value">{summary['final_value']}</div>
+                </div>
+            </div>
+            
+            <h2>💼 最近20次交易</h2>
+            <table>
+                <tr><th>#</th><th>买入时间</th><th>买入价</th><th>卖出时间</th><th>卖出价</th><th>持仓天</th><th>仓位</th><th>收益率</th><th>退出原因</th></tr>
+                {trades_html}
+            </table>
+            
+            <h2>📅 最近20天信号详情</h2>
+            <table>
+                <tr><th>日期</th><th>收盘</th><th>Trend</th><th>加速度</th><th>波动率</th><th>K偏离</th><th>趋势</th><th>加速</th><th>波动</th><th>偏离</th><th>信号</th><th>说明</th></tr>
+                {signals_html}
+            </table>
+            
+            <div style="margin-top: 30px; padding: 15px; background: #e8f5e9; border-radius: 5px;">
+                <strong>附件:</strong><br>
+                • kalman_filter_analysis.png - 策略分析图表<br>
+                • kalman_daily_signals.csv - 完整交易信号数据
+            </div>
+        </div>
+    </body>
+    </html>
+    """
+    
+    subject = f"📊 Kalman策略 {datetime.now().strftime('%m-%d %H:%M')} | 收益{summary['total_return']}"
+    attachments = [
+        '/root/.openclaw/workspace/kalman-filter/kalman_filter_analysis.png',
+        '/root/.openclaw/workspace/kalman-filter/kalman_daily_signals.csv'
+    ]
+    
+    send_email(subject, html, text, attachments)
+    
+    print(f"\n📊 提取到 {len(recent_trades)} 条交易记录")
+    print(f"📊 提取到 {len(recent_signals)} 条信号记录")
+    print("\n✅ 全部完成!")
+
+if __name__ == "__main__":
+    main()

+ 54 - 0
kalman-filter/v2/test_extract.py

@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+import subprocess
+
+result = subprocess.run(['python3', 'cyb50_kalman_filter_daily.py'], cwd='/root/.openclaw/workspace/kalman-filter', capture_output=True, text=True, timeout=300)
+
+output = result.stdout
+lines = output.split('\n')
+
+signals = []
+in_signal_section = False
+
+for line in lines:
+    if '所有交易信号详情' in line:
+        in_signal_section = True
+        continue
+    if in_signal_section:
+        stripped = line.strip()
+        if len(stripped) > 30:
+            parts = stripped.split()
+            if len(parts) >= 3 and parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
+                signals.append({
+                    'num': parts[0],
+                    'date': parts[1],
+                    'action': parts[2],
+                    'price': parts[3] if len(parts) > 3 else '',
+                })
+        # 获取所有信号
+        if '当前市场状态' in line:
+            break
+
+print(f'找到 {len(signals)} 个交易信号')
+
+# 取最近40个信号
+recent_signals = signals[-40:] if len(signals) > 40 else signals
+print(f'\n最近40个信号中的前10个:')
+for s in recent_signals[:10]:
+    print(f"  {s['num']:>3} {s['date']} {s['action']:>4} {s['price']}")
+
+print(f'\n最近40个信号中的后10个:')
+for s in recent_signals[-10:]:
+    print(f"  {s['num']:>3} {s['date']} {s['action']:>4} {s['price']}")
+
+# 配对
+trades = []
+for i in range(len(recent_signals) - 1):
+    if recent_signals[i]['action'] == '买入' and recent_signals[i+1]['action'] == '卖出':
+        buy = recent_signals[i]
+        sell = recent_signals[i+1]
+        trades.append(f"{buy['num']} {buy['date']}买入→{sell['date']}卖出")
+
+print(f'\n配对到 {len(trades)} 对交易')
+print('\n最近5对:')
+for t in trades[-5:]:
+    print(f'  {t}')