You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
"""Patch ftb_fix_v2 snapshot by merging fresh base export with existing canonical enrichments.
|
|
|
|
Strategy:
|
|
- 00_base has correct market features + labels (built from freshly-corrected DB)
|
|
- Existing canonical has enriched features (tier2/tier3/technical/macro/prior_drift)
|
|
- These enrichments don't depend on reaction_date → valid for ALL rows including 1,153 affected
|
|
- For rows in both: use base features, copy enrichments from canonical
|
|
- For new rows (not in canonical): use base features, NULL enrichments
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import datetime as dt
|
|
from pathlib import Path
|
|
|
|
import pyarrow as pa
|
|
import pyarrow.parquet as pq
|
|
|
|
CANONICAL_DIR = Path("data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_canonical")
|
|
BASE_DIR = Path("data/parquet/.midlarge-liquid-long-v1_bucketfix_full_audit_canonical_ftb_fix_v2.stage.3eb6jxkd/00_base")
|
|
OUTPUT_DIR = Path("data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_canonical_ftb_fix_v2")
|
|
|
|
# Columns that come FROM the base export (fresh correct data from DB)
|
|
# These are the columns affected by the filing_time_bucket fix
|
|
BASE_COLUMNS = {
|
|
# Market features (reaction-date-dependent)
|
|
"reaction_day_return",
|
|
"volume_ratio_20d",
|
|
"avg_dollar_volume_20d",
|
|
"gap_size",
|
|
"close_location",
|
|
"event_close",
|
|
"event_volume",
|
|
"reaction_day_low",
|
|
"reaction_day_high",
|
|
# Pre-event features (shift by 1 day for affected rows, minor change - use fresh)
|
|
"pre_event_momentum_20d",
|
|
"price_vs_sma20",
|
|
"pre_event_volatility_20d",
|
|
"pre_event_rsi_14",
|
|
"pre_event_bb_position",
|
|
"pre_event_obv_slope_20d",
|
|
"atr_14",
|
|
# Event features
|
|
"filing_time_bucket",
|
|
# Labels (reaction_date/entry_date/entry_price/fwd_returns - all depend on fix)
|
|
"reaction_date",
|
|
"entry_date",
|
|
"entry_price",
|
|
"fwd_return_1d",
|
|
"fwd_return_3d",
|
|
"fwd_return_5d",
|
|
"fwd_return_10d",
|
|
"fwd_return_20d",
|
|
"mfe_3d", "mae_3d",
|
|
"mfe_5d", "mae_5d",
|
|
"mfe_10d", "mae_10d",
|
|
"mfe_20d", "mae_20d",
|
|
"hit_pos_1r_within_3d",
|
|
"hit_neg_1r_within_3d",
|
|
"close_up_after_3d",
|
|
"close_up_after_5d",
|
|
"bars_to_mfe_3d",
|
|
"days_to_peak_close_5d",
|
|
"label_status",
|
|
"label_version",
|
|
# Metadata
|
|
"event_date",
|
|
"market_cap_proxy",
|
|
"exchange_proxy",
|
|
}
|
|
|
|
|
|
def patch_split(split: str) -> int:
|
|
base_path = BASE_DIR / f"{split}.parquet"
|
|
canonical_path = CANONICAL_DIR / f"{split}.parquet"
|
|
output_path = OUTPUT_DIR / f"{split}.parquet"
|
|
|
|
if not base_path.exists():
|
|
print(f" No {split} split in base, skipping")
|
|
return 0
|
|
|
|
base_table = pq.read_table(base_path)
|
|
n = len(base_table)
|
|
print(f" Base {split}: {n} rows, {len(base_table.column_names)} cols")
|
|
|
|
# Build index from existing canonical by event_id
|
|
canonical_data: dict[str, dict] = {}
|
|
if canonical_path.exists():
|
|
canon_table = pq.read_table(canonical_path)
|
|
canon_cols = set(canon_table.column_names)
|
|
print(f" Canonical {split}: {len(canon_table)} rows, {len(canon_cols)} cols")
|
|
|
|
# Get event_id column name (might be 'event_id' or other)
|
|
id_col = "event_id" if "event_id" in canon_cols else None
|
|
if id_col:
|
|
ids = canon_table.column(id_col).to_pylist()
|
|
# Build dict: event_id -> row dict for enrichment columns
|
|
enrichment_cols = [c for c in canon_cols if c not in BASE_COLUMNS and c != id_col]
|
|
for j, eid in enumerate(ids):
|
|
row = {}
|
|
for col in enrichment_cols:
|
|
val = canon_table.column(col)[j].as_py()
|
|
row[col] = val
|
|
canonical_data[eid] = row
|
|
print(f" Indexed {len(canonical_data)} canonical rows")
|
|
|
|
# Build output table
|
|
# Start with all base columns
|
|
base_cols = base_table.column_names
|
|
|
|
# Find enrichment columns from canonical not in base
|
|
all_canonical_cols = set()
|
|
if canonical_path.exists():
|
|
canon_table2 = pq.read_table(canonical_path)
|
|
all_canonical_cols = set(canon_table2.column_names)
|
|
|
|
enrich_cols = sorted([c for c in all_canonical_cols if c not in base_cols and c not in {"event_id"}])
|
|
print(f" Adding {len(enrich_cols)} enrichment columns from canonical")
|
|
|
|
# Get event_ids from base
|
|
base_id_col = "event_id" if "event_id" in base_cols else None
|
|
base_ids = base_table.column(base_id_col).to_pylist() if base_id_col else [None] * n
|
|
|
|
# For each enrichment column, build array
|
|
result_table = base_table
|
|
for col in enrich_cols:
|
|
# Get type from canonical
|
|
if canonical_path.exists():
|
|
canon_field = canon_table2.schema.field(col)
|
|
col_type = canon_field.type
|
|
else:
|
|
col_type = pa.float64()
|
|
|
|
values = []
|
|
for eid in base_ids:
|
|
val = canonical_data.get(eid, {}).get(col) if eid else None
|
|
values.append(val)
|
|
|
|
try:
|
|
arr = pa.array(values, type=col_type)
|
|
except Exception:
|
|
arr = pa.array(values)
|
|
result_table = result_table.append_column(col, arr)
|
|
|
|
pq.write_table(result_table, output_path)
|
|
print(f" Written {len(result_table)} rows, {len(result_table.column_names)} cols to {split}.parquet")
|
|
return n
|
|
|
|
|
|
def main():
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
print("Patching ftb_fix_v2 snapshot...")
|
|
total = 0
|
|
for split in ["train", "valid", "test"]:
|
|
print(f"\nProcessing {split}:")
|
|
total += patch_split(split)
|
|
|
|
# Copy manifest from base, update metadata
|
|
base_manifest = json.loads((BASE_DIR / "manifest.json").read_text())
|
|
base_manifest["snapshot_id"] = OUTPUT_DIR.name
|
|
base_manifest["output_dir"] = str(OUTPUT_DIR.resolve())
|
|
base_manifest["created_at_utc"] = dt.datetime.now(dt.UTC).isoformat()
|
|
base_manifest["ftb_fix_note"] = "Patched: base features from corrected DB, enrichments from existing canonical"
|
|
(OUTPUT_DIR / "manifest.json").write_text(json.dumps(base_manifest, indent=2))
|
|
|
|
print(f"\nDone! Total rows: {total}")
|
|
print(f"Output: {OUTPUT_DIR}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|