File size: 6,536 Bytes
d699ea7 | 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 | #!/usr/bin/env python3
"""Train the detector models on real Binance crash data.
Trains:
1. Stage 2 Isolation Forest on normal traffic (unsupervised)
2. Stage 3 TCN on labeled crash windows (self-supervised + supervised)
Usage:
python scripts/train_models.py --data data/parquet/BTCUSDT_2021-05-18.parquet --out models/
python scripts/train_models.py --data data/parquet/BTCUSDT_2024-01-15.parquet --out models/ --epochs 20
The training data should be a NORMAL day (not a crash day) so the models
learn what "normal" looks like. Then the backtest on crash days will detect
the anomalies.
"""
import argparse
import logging
import sys
from pathlib import Path
import numpy as np
import pandas as pd
# Insert the ml directory at the FRONT of sys.path
ML_DIR = Path(__file__).resolve().parent.parent / "ml"
sys.path.insert(0, str(ML_DIR))
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from flash_crash_watchdog.data.historical_loader import df_to_ticks, load_parquet
from flash_crash_watchdog.data.windows import build_windows_from_df
from flash_crash_watchdog.features import FEATURE_NAMES, FeatureExtractor
from flash_crash_watchdog.models.stage2_isolation_forest import Stage2IsolationForest
from flash_crash_watchdog.models.stage3_tcn import Stage3TCN, TCNConfig
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
def extract_feature_matrix(df: pd.DataFrame, max_ticks: int = 100_000) -> np.ndarray:
"""Extract a feature matrix from a DataFrame for training.
Args:
df: Historical tick data.
max_ticks: Maximum number of ticks to process (for speed).
Returns:
Matrix of shape (n_ticks, 20) — the feature vector per tick.
"""
logger.info("Extracting features from %d ticks (max %d)...", len(df), max_ticks)
# Sample if too many ticks
if len(df) > max_ticks:
# Sample evenly across the day to get representative data
indices = np.linspace(0, len(df) - 1, max_ticks, dtype=int)
df_sample = df.iloc[indices].copy()
logger.info("Sampled down to %d ticks (evenly spaced)", len(df_sample))
else:
df_sample = df
extractor = FeatureExtractor()
features_list = []
for i, tick in enumerate(df_to_ticks(df_sample, symbol="TRAIN")):
if i % 10000 == 0:
logger.info(" Processing tick %d/%d...", i, len(df_sample))
features = extractor.extract(tick)
features_list.append([features.get(f, 0.0) for f in FEATURE_NAMES])
matrix = np.array(features_list, dtype=np.float32)
logger.info("Feature matrix shape: %s", matrix.shape)
# Replace NaN/Inf with 0
matrix = np.nan_to_num(matrix, nan=0.0, posinf=0.0, neginf=0.0)
return matrix
def train_stage2_isolation_forest(feature_matrix: np.ndarray, out_path: Path) -> None:
"""Train the Stage 2 Isolation Forest on normal data."""
logger.info("=" * 60)
logger.info("TRAINING STAGE 2 — ISOLATION FOREST")
logger.info("=" * 60)
# Use the first 12 features (F1 + F2) for Stage 2
stage2_features = feature_matrix[:, :12]
logger.info("Stage 2 input shape: %s", stage2_features.shape)
model = Stage2IsolationForest(n_estimators=100, contamination=0.05)
model.fit(stage2_features)
out_path.parent.mkdir(parents=True, exist_ok=True)
model.save(out_path)
logger.info("Stage 2 model saved to %s", out_path)
def train_stage3_tcn(df: pd.DataFrame, out_path: Path, epochs: int = 20,
max_ticks: int = 100_000) -> None:
"""Train the Stage 3 TCN on labeled crash windows.
Built from the same tick frame; window labels come from a forward-looking
2%-drop-in-5s rule (see data.windows.build_windows_from_df). A "normal" day
has no crash labels, so Stage 3 is skipped with a pointer rather than
silently trained to an all-zero target.
"""
logger.info("=" * 60)
logger.info("TRAINING STAGE 3 — TEMPORAL CONVOLUTIONAL NETWORK")
logger.info("=" * 60)
try:
windows, labels, _feature_names = build_windows_from_df(df, max_ticks=max_ticks)
except FileNotFoundError as e:
logger.warning("Skipping Stage 3: %s", e)
return
n_pos = int(np.sum(labels))
if n_pos == 0:
logger.warning(
"No crash labels in this day — the TCN is a crash classifier and cannot be "
"trained on normal-only data. Skip Stage 3 (or run scripts/train_tcn_windows.py "
"on labeled crash windows).")
return
logger.info("Stage 3 windows: %s (%d positive)", windows.shape, n_pos)
seq_len = windows.shape[1]
config = TCNConfig(sequence_length=seq_len)
model = Stage3TCN(config)
model.train(windows, labels, epochs=epochs)
out_path.parent.mkdir(parents=True, exist_ok=True)
model.save(out_path)
logger.info("Stage 3 model saved to %s", out_path)
def main() -> int:
parser = argparse.ArgumentParser(description="Train detector models on real data")
parser.add_argument("--data", required=True, help="Parquet file of NORMAL market data")
parser.add_argument("--out", default="models/", help="Output directory for trained models")
parser.add_argument("--epochs", type=int, default=20, help="TCN training epochs")
parser.add_argument("--max-ticks", type=int, default=100_000,
help="Max ticks to process (for speed)")
args = parser.parse_args()
# Load data
df = load_parquet(args.data)
logger.info("Loaded %d ticks from %s", len(df), args.data)
# Extract features
feature_matrix = extract_feature_matrix(df, max_ticks=args.max_ticks)
# Train Stage 2
out_dir = Path(args.out)
train_stage2_isolation_forest(feature_matrix, out_dir / "stage2_isolation_forest.joblib")
# Train Stage 3
train_stage3_tcn(df, out_dir / "stage3_tcn.pt", epochs=args.epochs,
max_ticks=args.max_ticks)
logger.info("=" * 60)
logger.info("TRAINING COMPLETE")
logger.info(" Models saved to: %s", out_dir.resolve())
logger.info(" Stage 2: stage2_isolation_forest.joblib")
logger.info(" Stage 3: stage3_tcn.pt")
logger.info("=" * 60)
logger.info("")
logger.info("Next step: re-run the backtest on crash data to see alerts!")
logger.info(" python scripts/run_backtest.py --data data/parquet/BTCUSDT_2021-05-19.parquet")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|