CoolFace
Apppublic

Qionk/a-share-quant

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
preprocessing.py244 linesDownload Raw Back to predict
1"""2统一数据预处理管线3- 生成日收益率(所有模型的预测目标)4- 生成成交量衍生特征5- 时间序列划分(禁止随机)6"""7 8import numpy as np9import pandas as pd10 11 12def preprocess_data(df: pd.DataFrame, return_clip: float = 0.10,13                     forecast_days: int = 1,14                     index_returns: pd.Series = None) -> pd.DataFrame:15    """16    统一数据预处理:计算日收益率目标 + 成交量衍生特征17 18    返回的 DataFrame 新增列:19      日收益率, future_ret, 目标收益率, 涨跌标签, 目标涨跌,20      成交量变化率, 相对成交量, 量价配合度, 放量上涨, 缩量下跌21 22    df 必须已有列: close, volume, pct_change(或从close计算)23    forecast_days: 持有天数,N日持有期收益 = close.pct_change(N).shift(-N)24    index_returns: 可选,板块/大盘日收益率Series(index对齐),用于计算相对强弱25    """26    df = df.copy()27 28    # ── 1. 日收益率(小数形式,如 0.015 = 1.5%) ──29    if 'pct_change' in df.columns:30        # AKShare/Tencent 返回的 pct_change 是百分比形式(1.5 代表 1.5%),转为小数31        df['日收益率'] = df['pct_change'] / 100.032    else:33        df['日收益率'] = df['close'].pct_change()  # 小数形式,如 0.015 = 1.5%34 35    # 截断 ±10%(符合A股涨跌幅限制)36    df['日收益率'] = df['日收益率'].clip(-return_clip, return_clip)37 38    # ── 2. 涨跌标签 ──────────────────────────────39    df['涨跌标签'] = (df['日收益率'] > 0).astype(int)40 41    # ── 3. 预测目标:下一日涨跌方向(固定1日,与持有期无关) ──42    df['future_ret'] = df['close'].pct_change(periods=forecast_days).shift(-forecast_days)43    df['目标收益率'] = df['future_ret']44    # 训练目标始终是下一日方向45    _next_ret = df['close'].pct_change(periods=1).shift(-1)46    df['目标涨跌'] = _next_ret.gt(0).astype(float)47    df.loc[_next_ret.isna(), '目标涨跌'] = np.nan48 49    # 策略回测用:下一日收益率(T日收盘买入 → T+1日收盘卖出)50    df['next_day_ret'] = df['日收益率'].shift(-1)51 52    # ── 4. 成交量衍生特征 ─────────────────────────53    # 成交量变化率54    df['成交量变化率'] = df['volume'].pct_change()55 56    # 相对成交量(与20日均值比)57    vol_ma20 = df['volume'].rolling(window=20).mean().replace(0, np.nan)58    df['相对成交量'] = df['volume'] / vol_ma2059 60    # 量价配合度(收益率与成交量变化率的滚动相关性)61    df['量价配合度'] = df['日收益率'].rolling(20).corr(df['成交量变化率'])62 63    # 放量上涨: 涨 + 相对成交量 > 1.564    df['放量上涨'] = ((df['日收益率'] > 0) & (df['相对成交量'] > 1.5)).astype(int)65 66    # 缩量下跌: 跌 + 相对成交量 < 0.767    df['缩量下跌'] = ((df['日收益率'] < 0) & (df['相对成交量'] < 0.7)).astype(int)68 69    # ── 5. 多周期动量特征 ────────────────────────────70    df['ret_2d'] = df['close'].pct_change(2)71    df['ret_3d'] = df['close'].pct_change(3)72    df['ret_5d'] = df['close'].pct_change(5)73    df['ret_10d'] = df['close'].pct_change(10)74 75    # ── 6. 均线偏离度 ─────────────────────────────76    ma5 = df['close'].rolling(5).mean()77    ma10 = df['close'].rolling(10).mean()78    ma20 = df['close'].rolling(20).mean()79    df['close_ma5_bias'] = (df['close'] - ma5) / ma580    df['close_ma10_bias'] = (df['close'] - ma10) / ma1081    df['close_ma20_bias'] = (df['close'] - ma20) / ma2082    df['ma5_ma10_cross'] = (ma5 - ma10) / ma1083 84    # ── 7. 波动率特征 ─────────────────────────────85    df['volatility_5d'] = df['日收益率'].rolling(5).std()86    df['volatility_10d'] = df['日收益率'].rolling(10).std()87    df['volatility_20d'] = df['日收益率'].rolling(20).std()88 89    # ── 8. K线形态特征 ─────────────────────────────90    df['upper_shadow'] = (df['high'] - df[['open', 'close']].max(axis=1)) / df['close']91    df['lower_shadow'] = (df[['open', 'close']].min(axis=1) - df['low']) / df['close']92    df['body_ratio'] = (df['close'] - df['open']) / (df['high'] - df['low']).replace(0, np.nan)93    df['high_low_range'] = (df['high'] - df['low']) / df['close']94 95    # ── 9. 多周期 RSI ─────────────────────────────96    for period in [6, 14]:97        delta = df['close'].diff()98        gain = delta.where(delta > 0, 0.0).rolling(period).mean()99        loss = (-delta.where(delta < 0, 0.0)).rolling(period).mean()100        rs = gain / loss.replace(0, np.nan)101        df[f'rsi_{period}'] = 100 - 100 / (1 + rs)102 103    # ── 10. 成交量多周期特征 ──────────────────────────104    df['vol_chg_3d'] = df['volume'].pct_change(3)105    df['vol_chg_5d'] = df['volume'].pct_change(5)106    vol_ma5 = df['volume'].rolling(5).mean()107    df['vol_ratio_5d'] = df['volume'] / vol_ma5.replace(0, np.nan)108 109    # ── 11. 科技股专用特征 ─────────────────────────────110 111    # 换手率系列(科技股情绪核心指标)112    # 仅在 turnover 有效数据超过 50% 时计算,避免 DB 缓存的腾讯源数据全为 NaN113    has_turnover = ('turnover' in df.columns and114                    df['turnover'].notna().mean() > 0.5)115    if has_turnover:116        df['turnover'] = pd.to_numeric(df['turnover'], errors='coerce')117        df['turnover_ma5'] = df['turnover'].rolling(5).mean()118        df['turnover_ma10'] = df['turnover'].rolling(10).mean()119        df['turnover_bias'] = (df['turnover'] - df['turnover_ma5']) / df['turnover_ma5'].replace(0, np.nan)120        df['turnover_accel'] = df['turnover'].diff().rolling(3).mean()121        df['cum_turnover_5d'] = df['turnover'].rolling(5).sum()122        df['cum_turnover_10d'] = df['turnover'].rolling(10).sum()123    elif 'turnover' in df.columns:124        df = df.drop(columns=['turnover'])125 126    # 跳空缺口127    prev_close = df['close'].shift(1)128    df['gap'] = (df['open'] - prev_close) / prev_close129    df['gap_abs'] = df['gap'].abs()130    df['gap_up'] = (df['gap'] > 0.01).astype(int)131    df['gap_down'] = (df['gap'] < -0.01).astype(int)132 133    # 价格位置特征134    for n in [5, 10, 20]:135        rolling_high = df['high'].rolling(n).max()136        rolling_low = df['low'].rolling(n).min()137        price_range = (rolling_high - rolling_low).replace(0, np.nan)138        df[f'price_pos_{n}d'] = (df['close'] - rolling_low) / price_range139        df[f'drawdown_{n}d'] = (df['close'] - rolling_high) / rolling_high140 141    # 连涨连跌天数142    up = (df['日收益率'] > 0).astype(int)143    down = (df['日收益率'] < 0).astype(int)144    streak_up = up.copy()145    streak_down = down.copy()146    for i in range(1, len(df)):147        if up.iloc[i] == 1:148            streak_up.iloc[i] = streak_up.iloc[i - 1] + 1149        if down.iloc[i] == 1:150            streak_down.iloc[i] = streak_down.iloc[i - 1] + 1151    df['streak_up'] = streak_up152    df['streak_down'] = streak_down153 154    # ATR(平均真实波幅)155    tr = pd.concat([156        df['high'] - df['low'],157        (df['high'] - df['close'].shift(1)).abs(),158        (df['low'] - df['close'].shift(1)).abs(),159    ], axis=1).max(axis=1)160    df['atr_5'] = tr.rolling(5).mean()161    df['atr_14'] = tr.rolling(14).mean()162    df['atr_ratio'] = df['atr_5'] / df['atr_14'].replace(0, np.nan)163 164    # 涨停/跌停统计(科技股涨停板效应)165    df['near_limit_up'] = (df['日收益率'] >= 0.09).astype(int)166    df['near_limit_down'] = (df['日收益率'] <= -0.09).astype(int)167    df['limit_up_count_10d'] = df['near_limit_up'].rolling(10).sum()168    df['limit_down_count_10d'] = df['near_limit_down'].rolling(10).sum()169 170    # ── 12. 相对强弱(vs 大盘/板块指数) ─────────────────171    if index_returns is not None:172        idx_ret = index_returns.reindex(df.index)173        df['excess_ret'] = df['日收益率'] - idx_ret.fillna(0)174        df['excess_ret_5d'] = df['excess_ret'].rolling(5).sum()175        df['excess_ret_10d'] = df['excess_ret'].rolling(10).sum()176        df['excess_ret_20d'] = df['excess_ret'].rolling(20).sum()177 178    # ── 13. 处理缺失值 ─────────────────────────────179    # 保留 目标涨跌/future_ret/next_day_ret 为 NaN 的行(最后一天无下日数据),180    # 仅删除特征列(日收益率)为 NaN 的行(首行)181    df = df.dropna(subset=['日收益率'])182 183    return df184 185 186def split_data(df: pd.DataFrame, test_size: float = 0.2) -> dict:187    """188    严格按时间顺序划分训练/测试集(禁止随机)。189 190    返回 dict:191      - train_df, test_df: 完整 DataFrame192      - split_index: 切分点位置193      - latest_close: 最新收盘价(用于反算预测价格)194    """195    split_idx = int(len(df) * (1 - test_size))196    train_df = df.iloc[:split_idx].copy()197    test_df = df.iloc[split_idx:].copy()198 199    return {200        'train_df': train_df,201        'test_df': test_df,202        'split_index': split_idx,203        'latest_close': float(df['close'].iloc[-1]),204    }205 206 207def returns_to_price_series(last_close: float, predicted_returns: np.ndarray,208                            limit_pct: float = None) -> np.ndarray:209    """210    将预测收益率(%)转为预测收盘价序列。211 212    predicted_price[t] = prev_price * (1 + return[t]/100)213    可选应用涨跌幅限制。214    """215    if len(predicted_returns) == 0:216        return np.array([])217 218    prices = np.zeros(len(predicted_returns))219    prev = last_close220    for i, r in enumerate(predicted_returns):221        p = prev * (1 + r / 100)222        if limit_pct is not None:223            upper = last_close * (1 + limit_pct)224            lower = last_close * (1 - limit_pct)225            p = np.clip(p, lower, upper)226        prices[i] = p227        prev = p228    return prices229 230 231def calculate_predicted_prices(last_close: float, predicted_returns: np.ndarray,232                               limit_pct: float = None) -> tuple:233    """234    便捷函数:预测收益率 → 预测收盘价 + 日收益率%。235 236    返回 (predicted_close: np.ndarray, daily_return_pct: np.ndarray)237    """238    prices = returns_to_price_series(last_close, predicted_returns, limit_pct)239    if len(prices) <= 1:240        daily_ret = np.array([])241    else:242        extended = np.concatenate([[last_close], prices])243        daily_ret = (extended[1:] / extended[:-1] - 1) * 100244    return prices, daily_ret