File size: 2,505 Bytes
b717bee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Full V5 Feature Engineering — mirrors cleaned notebook Part A (pandas version)"""
import pandas as pd
import numpy as np
from ta.momentum import RSIIndicator # type: ignore
from ta.trend import MACD # type: ignore

def add_v5_features(df: pd.DataFrame) -> tuple[pd.DataFrame, list]:
    df = df.copy()
    if not isinstance(df.index, pd.DatetimeIndex):
        df = df.set_index('timestamp').sort_index()

    c, h, l, v, o = df['close'], df['high'], df['low'], df['volume'], df['open']

    # === Core V5 blocks from notebook ===
    for lag in [1, 2, 4, 6, 12, 24, 48, 168]:
        df[f'return_{lag}h'] = c.pct_change(lag)

    df['candle_body_ratio'] = (c - o) / (h - l + 1e-10)
    df['range_pct'] = (h - l) / (c + 1e-10)
    df['close_location_value'] = (c - l) / (h - l + 1e-10)

    for w in [4, 12, 24, 48, 168]:
        df[f'sma_{w}h'] = c.rolling(w).mean()
        df[f'vol_{w}h'] = c.rolling(w).std()
        df[f'vol_avg_{w}h'] = v.rolling(w).mean()
        df[f'price_vs_sma_{w}h'] = c / df[f'sma_{w}h'] - 1

    for period in [6, 14, 24]:
        df[f'rsi_{period}h'] = RSIIndicator(c, window=period).rsi()

    macd = MACD(c, window_slow=26, window_fast=12, window_sign=9)
    df['macd_h'] = macd.macd()
    df['macd_signal_h'] = macd.macd_signal()
    df['macd_hist_h'] = macd.macd_diff()

    df['vol_ratio_24h'] = v / (v.rolling(24).mean() + 1e-10)
    df['vol_ratio_168h'] = v / (v.rolling(168).mean() + 1e-10)

    # On-chain momentum
    for col in ['tx_count', 'active_senders', 'active_receivers', 'total_eth_transferred', 'total_gas_used']:
        if col in df.columns:
            df[f'{col}_change_24h'] = df[col] / (df[col].shift(24) + 1e-10) - 1
            df[f'{col}_ma24h'] = df[col].rolling(24).mean()

    # Cyclical
    df['hour_sin'] = np.sin(2 * np.pi * df.index.hour / 24) # type: ignore
    df['hour_cos'] = np.cos(2 * np.pi * df.index.hour / 24) # type: ignore
    df['dow_sin'] = np.sin(2 * np.pi * df.index.dayofweek / 7) # type: ignore
    df['dow_cos'] = np.cos(2 * np.pi * df.index.dayofweek / 7) # type: ignore

    # 30d regime features (used by filter & routing)
    df['ret_30d'] = c.pct_change(30*24)          # ≈30d on 1h bars
    df['vol_30d'] = df['return'].rolling(30*24).std()

    df = df.dropna()
    feature_cols = [col for col in df.columns if col not in 
                    {'open','high','low','close','volume','timestamp'}]

    print(f"✅ Built {len(feature_cols)} V5 features (matches notebook Part A)")
    return df, feature_cols