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