Add minute bar feature pipeline
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""CLI for daily feature computation."""
|
||||
|
||||
import os
|
||||
|
||||
import click
|
||||
import pandas as pd
|
||||
|
||||
from pipeline.features.compute import compute_feature
|
||||
from pipeline.features.registry import available_features, load_feature_module
|
||||
|
||||
|
||||
@click.group(name="feature")
|
||||
def feature():
|
||||
"""Compute daily feature parquet files from minute bars."""
|
||||
|
||||
|
||||
def _coerce(value: str):
|
||||
"""Best-effort coercion of a CLI string to int, then float, else str."""
|
||||
for cast in (int, float):
|
||||
try:
|
||||
return cast(value)
|
||||
except ValueError:
|
||||
continue
|
||||
return value
|
||||
|
||||
|
||||
def _parse_params(pairs: tuple[str, ...]) -> dict:
|
||||
"""Parse repeated ``name=value`` options into a params dict."""
|
||||
params: dict = {}
|
||||
for pair in pairs:
|
||||
if "=" not in pair:
|
||||
raise click.BadParameter(f"--param must be name=value, got '{pair}'")
|
||||
key, value = pair.split("=", 1)
|
||||
params[key.strip()] = _coerce(value.strip())
|
||||
return params
|
||||
|
||||
|
||||
@feature.command("list")
|
||||
@click.option(
|
||||
"--feature-module", "feature_modules", multiple=True,
|
||||
help="External module(s) to import first (dotted path or .py file)",
|
||||
)
|
||||
def list_(feature_modules):
|
||||
"""List the registered feature types."""
|
||||
for spec in feature_modules:
|
||||
load_feature_module(spec)
|
||||
for name in available_features():
|
||||
click.echo(name)
|
||||
|
||||
|
||||
@feature.command("compute")
|
||||
@click.option("--minute-path", required=True, help="Path to minute parquet dataset/file")
|
||||
@click.option("--daily-path", default=None, help="Optional daily data parquet for alignment")
|
||||
@click.option("--feature-type", required=True, help="Registry key of the feature class")
|
||||
@click.option("--feature-name", required=True, help="Name for this feature run/output file")
|
||||
@click.option("--output-dir", default="features", help="Directory to save feature parquet")
|
||||
@click.option(
|
||||
"--feature-module", "feature_modules", multiple=True,
|
||||
help="External module(s) to import so their features register (dotted path or .py file)",
|
||||
)
|
||||
@click.option(
|
||||
"--param", "extra_params", multiple=True,
|
||||
help="Extra feature constructor param as name=value (repeatable)",
|
||||
)
|
||||
def compute(
|
||||
minute_path,
|
||||
daily_path,
|
||||
feature_type,
|
||||
feature_name,
|
||||
output_dir,
|
||||
feature_modules,
|
||||
extra_params,
|
||||
):
|
||||
"""Compute one daily feature file from raw minute bars."""
|
||||
for spec in feature_modules:
|
||||
load_feature_module(spec)
|
||||
|
||||
options = available_features()
|
||||
if feature_type not in options:
|
||||
raise click.BadParameter(
|
||||
f"Unknown feature-type '{feature_type}'. Available: {options}. "
|
||||
f"Use --feature-module to register an external feature.",
|
||||
param_hint="--feature-type",
|
||||
)
|
||||
|
||||
minute = pd.read_parquet(minute_path)
|
||||
click.echo(f"Loaded minute bars: {len(minute):,} rows from {minute_path}")
|
||||
|
||||
daily = None
|
||||
if daily_path:
|
||||
daily = pd.read_parquet(daily_path)
|
||||
click.echo(f"Loaded daily data: {len(daily):,} rows from {daily_path}")
|
||||
|
||||
result = compute_feature(
|
||||
minute=minute,
|
||||
daily=daily,
|
||||
feature_type=feature_type,
|
||||
**_parse_params(extra_params),
|
||||
)
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
out_path = f"{output_dir}/{feature_name}.pq"
|
||||
result.to_parquet(out_path, index=False)
|
||||
feature_cols = [col for col in result.columns if col not in ("symbol_id", "date")]
|
||||
click.echo(
|
||||
f"Saved feature: {out_path} ({len(result):,} rows, "
|
||||
f"{len(feature_cols)} columns)"
|
||||
)
|
||||
Reference in New Issue
Block a user