generated from mathias/template-go-web
feat(features): add hl_range + ret_intrabar OHLCV features (4-channel input)
- prepare_hourly.py: keep O/H/L columns from M1 zips; compute per-hour hl_range=log(H/L) and ret_intrabar=log(close/open); backward-compat (falls back to 4-col output only when O/H/L present in input) - train.py build(): auto-detect extra features from parquet columns (FEAT_COLS = [ret, realized_vol] + [hl_range, ret_intrabar] if present) - 5 new tests (9 total in test_prepare_hourly); 24/24 pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-10
@@ -29,27 +29,45 @@ def resample_to_hourly(m1: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Aggregate M1 DataFrame to hourly bars.
|
||||
|
||||
Args:
|
||||
m1: DataFrame with columns ['ts' (datetime), 'close' (float)]
|
||||
m1: DataFrame with columns ['ts', 'open', 'high', 'low', 'close']
|
||||
('open'/'high'/'low' optional — omit for close-only data).
|
||||
|
||||
Returns:
|
||||
DataFrame with columns ['datetime', 'close', 'ret', 'realized_vol']
|
||||
sorted by datetime; hours with fewer than MIN_BARS M1 ticks dropped.
|
||||
DataFrame with columns ['datetime', 'close', 'ret', 'realized_vol',
|
||||
'hl_range', 'ret_intrabar'] sorted by datetime.
|
||||
Hours with fewer than MIN_BARS M1 ticks are dropped.
|
||||
"""
|
||||
m1 = m1.sort_values("ts").copy()
|
||||
m1["log_r"] = np.log(m1["close"]).diff()
|
||||
m1["hour"] = m1["ts"].dt.floor("h")
|
||||
|
||||
agg = m1.groupby("hour").agg(
|
||||
close = ("close", "last"),
|
||||
realized_vol= ("log_r", lambda x: np.sqrt(np.nansum(x.values ** 2))),
|
||||
n_bars = ("log_r", "count"),
|
||||
).reset_index()
|
||||
has_ohlc = all(c in m1.columns for c in ("open", "high", "low"))
|
||||
|
||||
agg_dict = dict(
|
||||
close = ("close", "last"),
|
||||
realized_vol = ("log_r", lambda x: np.sqrt(np.nansum(x.values ** 2))),
|
||||
n_bars = ("log_r", "count"),
|
||||
)
|
||||
if has_ohlc:
|
||||
agg_dict["high"] = ("high", "max")
|
||||
agg_dict["low"] = ("low", "min")
|
||||
agg_dict["open_"] = ("open", "first")
|
||||
|
||||
agg = m1.groupby("hour").agg(**agg_dict).reset_index()
|
||||
|
||||
agg = agg[agg["n_bars"] >= MIN_BARS].copy()
|
||||
agg["ret"] = np.log(agg["close"]).diff()
|
||||
agg = agg.dropna(subset=["ret"]).reset_index(drop=True)
|
||||
agg = agg.rename(columns={"hour": "datetime"})
|
||||
return agg[["datetime", "close", "ret", "realized_vol"]]
|
||||
|
||||
if has_ohlc:
|
||||
agg["hl_range"] = np.log(agg["high"] / agg["low"])
|
||||
agg["ret_intrabar"]= np.log(agg["close"] / agg["open_"])
|
||||
cols = ["datetime", "close", "ret", "realized_vol", "hl_range", "ret_intrabar"]
|
||||
else:
|
||||
cols = ["datetime", "close", "ret", "realized_vol"]
|
||||
|
||||
return agg[cols]
|
||||
|
||||
|
||||
def load_m1_from_zips(raw_dir: str) -> pd.DataFrame:
|
||||
@@ -68,7 +86,7 @@ def load_m1_from_zips(raw_dir: str) -> pd.DataFrame:
|
||||
names=["dt", "open", "high", "low", "close", "vol"],
|
||||
)
|
||||
df["ts"] = pd.to_datetime(df["dt"], format="%Y%m%d %H%M%S")
|
||||
frames.append(df[["ts", "close"]])
|
||||
frames.append(df[["ts", "open", "high", "low", "close"]])
|
||||
print(f" loaded {os.path.basename(zp)}: {len(df):,} rows")
|
||||
return pd.concat(frames).sort_values("ts").reset_index(drop=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user