Spaces:
Runtime error
Runtime error
| """Command-line entry point. Run ``python -m eca.cli --help``.""" | |
| from __future__ import annotations | |
| from collections.abc import Iterator | |
| from pathlib import Path | |
| import pandas as pd | |
| import typer | |
| from eca.config import settings | |
| from eca.features import build_features_df | |
| from eca.ingest import Transcript, load_hf_earnings_calls | |
| from eca.utils import logger | |
| app = typer.Typer(add_completion=False, no_args_is_help=True, help="Earnings Call Analyzer") | |
| # ----- ingest + features ----- | |
| def build_dataset( | |
| source: str = typer.Option("hf", help="hf | edgar | motley_fool"), | |
| tickers: str = typer.Option("", help="comma-separated tickers (edgar/motley_fool only)"), | |
| limit: int = typer.Option(200, help="hf row cap / per-ticker filing cap"), | |
| output: Path | None = typer.Option(None, help="output parquet (default: data/processed/features_labelled.parquet)"), | |
| skip_labels: bool = typer.Option(False, help="skip yfinance label download (offline mode)"), | |
| ) -> None: | |
| """Pull transcripts, extract features, attach T+1 excess-return labels.""" | |
| settings.processed_dir.mkdir(parents=True, exist_ok=True) | |
| output = output or (settings.processed_dir / "features_labelled.parquet") | |
| transcripts: Iterator[Transcript] | |
| if source == "hf": | |
| transcripts = load_hf_earnings_calls(limit=limit) | |
| elif source == "edgar": | |
| from eca.ingest.edgar import fetch_many | |
| if not tickers: | |
| raise typer.BadParameter("--tickers required for edgar") | |
| transcripts = fetch_many([t.strip().upper() for t in tickers.split(",") if t.strip()], per_ticker=limit) | |
| elif source == "motley_fool": | |
| from eca.ingest.motley_fool import fetch_many | |
| if not tickers: | |
| raise typer.BadParameter("--tickers required for motley_fool") | |
| typer.secho( | |
| "Motley Fool ToS prohibits scraping. Use only for personal research.", | |
| fg=typer.colors.YELLOW, | |
| ) | |
| transcripts = fetch_many([t.strip().upper() for t in tickers.split(",") if t.strip()], per_ticker=limit) | |
| else: | |
| raise typer.BadParameter(f"unknown source: {source}") | |
| feats = build_features_df(transcripts) | |
| if feats.empty: | |
| typer.secho("no transcripts ingested", fg=typer.colors.RED) | |
| raise typer.Exit(code=1) | |
| logger.info(f"built features for {len(feats)} transcripts") | |
| if not skip_labels: | |
| from eca.prices import attach_labels | |
| feats = attach_labels(feats) | |
| feats.to_parquet(output, index=False) | |
| typer.secho(f"wrote {len(feats)} rows -> {output}", fg=typer.colors.GREEN) | |
| # ----- train ----- | |
| def train_cmd( | |
| input: Path = typer.Option( | |
| None, | |
| help="features parquet with labels (default: data/processed/features_labelled.parquet)", | |
| ), | |
| n_splits: int = typer.Option(5, help="walk-forward CV folds"), | |
| ) -> None: | |
| """Train the LightGBM directional classifier.""" | |
| from eca.model.train import train | |
| input = input or (settings.processed_dir / "features_labelled.parquet") | |
| if not input.exists(): | |
| typer.secho(f"missing {input}; run `build-dataset` first", fg=typer.colors.RED) | |
| raise typer.Exit(code=1) | |
| df = pd.read_parquet(input) | |
| result = train(df, n_splits=n_splits) | |
| typer.secho( | |
| f"cv_mean_accuracy={result.mean_accuracy:.3f} cv_mean_auc={result.mean_auc:.3f}", | |
| fg=typer.colors.GREEN, | |
| ) | |
| top = list(result.feature_importances.items())[:10] | |
| typer.echo("top features:") | |
| for k, v in top: | |
| typer.echo(f" {k:30s} {v:.1f}") | |
| # ----- predict (cache predictions for the backtest endpoint) ----- | |
| def predict_cmd( | |
| input: Path | None = typer.Option(None), | |
| output: Path | None = typer.Option(None), | |
| ) -> None: | |
| """Score every labelled row and cache predictions for backtesting.""" | |
| from eca.model.predict import load_model | |
| input = input or (settings.processed_dir / "features_labelled.parquet") | |
| output = output or (settings.processed_dir / "predictions.parquet") | |
| if not input.exists(): | |
| typer.secho(f"missing {input}", fg=typer.colors.RED) | |
| raise typer.Exit(code=1) | |
| df = pd.read_parquet(input) | |
| predictor = load_model() | |
| df["prob_up"] = predictor.predict_frame(df) | |
| df.to_parquet(output, index=False) | |
| typer.secho(f"wrote predictions ({len(df)} rows) -> {output}", fg=typer.colors.GREEN) | |
| # ----- backtest ----- | |
| def backtest_cmd( | |
| ticker: str | None = typer.Option(None, help="restrict to one ticker"), | |
| threshold: float = typer.Option(0.0, help="trade only when |prob - 0.5| > threshold"), | |
| input: Path | None = typer.Option(None), | |
| ) -> None: | |
| """Run the vectorized backtest and print summary.""" | |
| from eca.backtest import run_backtest | |
| input = input or (settings.processed_dir / "predictions.parquet") | |
| if not input.exists(): | |
| typer.secho(f"missing {input}; run `predict` first", fg=typer.colors.RED) | |
| raise typer.Exit(code=1) | |
| df = pd.read_parquet(input) | |
| if ticker: | |
| df = df[df["ticker"].str.upper() == ticker.upper()] | |
| if df.empty: | |
| typer.secho(f"no predictions for {ticker}", fg=typer.colors.RED) | |
| raise typer.Exit(code=1) | |
| result = run_backtest(df, threshold=threshold) | |
| for k, v in result.summary().items(): | |
| typer.echo(f"{k:24s} {v}") | |
| # ----- serve ----- | |
| def serve(host: str = "127.0.0.1", port: int = 8000, reload: bool = False) -> None: | |
| """Run the FastAPI app via uvicorn.""" | |
| import uvicorn | |
| uvicorn.run("eca.api.main:app", host=host, port=port, reload=reload) | |
| if __name__ == "__main__": | |
| app() | |