File size: 9,454 Bytes
8fe2df2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""
Feature extraction from 1-min OHLCV DataFrames.

Two pipelines:
  - semantic:   Hand-crafted morning indicators (gap, VWAP deviation, momentum, etc.)
  - sequential: Raw normalised return and volume vectors for the 09:15-09:30 window.

Both return (X, y, metadata) where:
  X        : pd.DataFrame of features, indexed by date
  y        : pd.Series of binary labels (1 = price up 09:30->15:10)
  metadata : dict[date] -> { c_0930, h_0930, l_0930, v_0930, c_1510, h_1510, l_1510 }
"""

import numpy as np
import pandas as pd


# ── Helpers ──────────────────────────────────────────────────────────────────

def _safe_fill(arr):
    """Forward-fill then back-fill NaNs in a 1-D array."""
    return pd.Series(arr).ffill().bfill().values


# ── Semantic features ────────────────────────────────────────────────────────

def extract_semantic_features(df):
    """
    Compute high-level morning indicators from the 09:15-09:30 candle window.

    Features: gap, return_16m, return_first_5m, return_next_11m, std_dev,
              range_pct, upper_shadow, lower_shadow, total_vol, vwap_dev,
              price_momentum, vol_momentum, morning_trend.
    """
    df = df.copy()
    df["time"]     = df.index.time
    df["date_only"] = df.index.date
    required_times = (
        pd.date_range("09:15", "09:30", freq="min").time.tolist()
        + [pd.to_datetime("15:10").time()]
    )
    df = df[~df.index.duplicated(keep="first")]

    daily_close      = df.groupby("date_only")["close"].last()
    prev_daily_close = daily_close.shift(1)

    # Compute morning session dip/peak (09:31 to 12:00) for limit-order entries
    time_0931 = pd.to_datetime("09:31").time()
    time_1200 = pd.to_datetime("12:00").time()
    df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)]
    morning_low_per_date  = df_morning.groupby("date_only")["low"].min()
    morning_high_per_date = df_morning.groupby("date_only")["high"].max()

    df_filtered = df[df["time"].isin(required_times)].copy()
    pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close")
    pivot_open  = df_filtered.pivot(index="date_only", columns="time", values="open")
    pivot_high  = df_filtered.pivot(index="date_only", columns="time", values="high")
    pivot_low   = df_filtered.pivot(index="date_only", columns="time", values="low")
    pivot_vol   = df_filtered.pivot(index="date_only", columns="time", values="volume")

    time_0930 = pd.to_datetime("09:30").time()
    time_1510 = pd.to_datetime("15:10").time()

    if time_0930 not in pivot_close.columns:
        return None, None, None

    pivot_close = pivot_close.dropna(subset=[time_0930])
    valid_dates = pivot_close.index
    times_15m   = pd.date_range("09:15", "09:30", freq="min").time

    feature_dicts = []
    metadata      = {}

    for date in valid_dates:
        f = {}
        c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float))
        o_series = _safe_fill(pivot_open.loc[date, times_15m].values.astype(float))
        h_series = _safe_fill(pivot_high.loc[date, times_15m].values.astype(float))
        l_series = _safe_fill(pivot_low.loc[date, times_15m].values.astype(float))
        v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values

        o_915 = o_series[0]
        c_930 = c_series[-1]
        c_919 = c_series[4] if len(c_series) > 4 else c_series[-1]

        pdc = prev_daily_close.get(date, np.nan)
        f["gap"]             = 0 if (pd.isna(pdc) or pdc == 0) else (o_915 / pdc) - 1.0
        f["return_16m"]      = (c_930 / o_915) - 1.0 if o_915 != 0 else 0
        f["return_first_5m"] = (c_919 / o_915) - 1.0 if o_915 != 0 else 0
        f["return_next_11m"] = (c_930 / c_919) - 1.0 if c_919 != 0 else 0
        f["std_dev"]         = np.std(c_series / (o_915 + 1e-8))

        max_h = np.max(h_series)
        min_l = np.min(l_series)
        f["range_pct"]       = (max_h - min_l) / (o_915 + 1e-8)
        f["upper_shadow"]    = (max_h - max(o_915, c_930)) / (o_915 + 1e-8)
        f["lower_shadow"]    = (min(o_915, c_930) - min_l) / (o_915 + 1e-8)
        f["total_vol"]       = np.sum(v_series)

        vwap = np.sum(((h_series + l_series + c_series) / 3.0) * v_series) / (np.sum(v_series) + 1e-8)
        f["vwap_dev"]        = (c_930 / vwap) - 1.0 if vwap != 0 else 0
        f["price_momentum"]  = (c_series[-1] - c_series[-3]) / (c_series[-3] + 1e-8)
        f["vol_momentum"]    = (v_series[-1] - v_series[-3]) / (v_series[-3] + 1e-8)
        f["morning_trend"]   = np.polyfit(np.arange(len(c_series)), c_series, 1)[0]

        feature_dicts.append(f)

        metadata[date] = {
            "c_0930": c_930,
            "h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_930,
            "l_0930": float(pivot_low.loc[date, time_0930])  if time_0930 in pivot_low.columns  else c_930,
            "v_0930": float(pivot_vol.loc[date, time_0930])  if time_0930 in pivot_vol.columns  else 0,
            "c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_930,
            "h_1510": float(pivot_high.loc[date, time_1510])  if time_1510 in pivot_high.columns else c_930,
            "l_1510": float(pivot_low.loc[date, time_1510])   if time_1510 in pivot_low.columns  else c_930,
            "dip_low":   float(morning_low_per_date.get(date, c_930)),
            "peak_high": float(morning_high_per_date.get(date, c_930)),
        }

    X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0)

    if time_1510 in pivot_close.columns:
        target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int)
    else:
        target = pd.Series(0, index=valid_dates)

    return X, target, metadata


# ── Sequential features ──────────────────────────────────────────────────────

def extract_sequential_features(df):
    """
    Compute normalised return and raw volume vectors for each minute
    in the 09:15-09:30 window, relative to the 09:30 close.
    """
    df = df.copy()
    df["time"]      = df.index.time
    df["date_only"] = df.index.date
    required_times = (
        pd.date_range("09:15", "09:30", freq="min").time.tolist()
        + [pd.to_datetime("15:10").time()]
    )
    df = df[~df.index.duplicated(keep="first")]

    # Compute morning session dip/peak (09:31 to 12:00) for limit-order entries
    time_0931 = pd.to_datetime("09:31").time()
    time_1200 = pd.to_datetime("12:00").time()
    df_morning = df[(df["time"] >= time_0931) & (df["time"] <= time_1200)]
    morning_low_per_date  = df_morning.groupby("date_only")["low"].min()
    morning_high_per_date = df_morning.groupby("date_only")["high"].max()

    df_filtered = df[df["time"].isin(required_times)].copy()
    pivot_close = df_filtered.pivot(index="date_only", columns="time", values="close")
    pivot_high  = df_filtered.pivot(index="date_only", columns="time", values="high")
    pivot_low   = df_filtered.pivot(index="date_only", columns="time", values="low")
    pivot_vol   = df_filtered.pivot(index="date_only", columns="time", values="volume")

    time_0930 = pd.to_datetime("09:30").time()
    time_1510 = pd.to_datetime("15:10").time()

    if time_0930 not in pivot_close.columns:
        return None, None, None

    pivot_close = pivot_close.dropna(subset=[time_0930])
    valid_dates = pivot_close.index
    times_15m   = pd.date_range("09:15", "09:30", freq="min").time

    feature_dicts = []
    metadata      = {}

    for date in valid_dates:
        f = {}
        c_series = _safe_fill(pivot_close.loc[date, times_15m].values.astype(float))
        v_series = pd.Series(pivot_vol.loc[date, times_15m].values.astype(float)).fillna(0).values
        c_ref    = c_series[-1]

        for i, t in enumerate(times_15m):
            f[f"ret_c_{i}"]   = (c_series[i] / (c_ref + 1e-8)) - 1.0
            f[f"raw_vol_{i}"] = v_series[i]
        feature_dicts.append(f)

        metadata[date] = {
            "c_0930": c_ref,
            "h_0930": float(pivot_high.loc[date, time_0930]) if time_0930 in pivot_high.columns else c_ref,
            "l_0930": float(pivot_low.loc[date, time_0930])  if time_0930 in pivot_low.columns  else c_ref,
            "v_0930": float(pivot_vol.loc[date, time_0930])  if time_0930 in pivot_vol.columns  else 0,
            "c_1510": float(pivot_close.loc[date, time_1510]) if time_1510 in pivot_close.columns else c_ref,
            "h_1510": float(pivot_high.loc[date, time_1510])  if time_1510 in pivot_high.columns else c_ref,
            "l_1510": float(pivot_low.loc[date, time_1510])   if time_1510 in pivot_low.columns  else c_ref,
            "dip_low":   float(morning_low_per_date.get(date, c_ref)),
            "peak_high": float(morning_high_per_date.get(date, c_ref)),
        }

    X = pd.DataFrame(feature_dicts, index=valid_dates).fillna(0)

    if time_1510 in pivot_close.columns:
        target = (pivot_close[time_1510] > pivot_close[time_0930]).astype(int)
    else:
        target = pd.Series(0, index=valid_dates)

    return X, target, metadata