#!/usr/bin/env python3 """Fundamental screener + enrichment with point-in-time notes. screen.py --filters "pe<15,fcf_yield>0.06" [--sort fcf_yield:desc] [--as-of 2020-03-20] [--limit 25] [--quarters 3] [--no-enrich] [--out screen.csv] screen.py --tickers AAPL MSFT --quarters 4 # enrich only """ from __future__ import annotations import argparse import sys from datetime import date from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) import hfmd # noqa: E402 KEY_RATIOS = ["pe", "pb", "ev_ebitda", "fcf_yield", "dividend_yield", "roe", "roa", "gross_margin", "operating_margin", "net_margin", "debt_to_equity", "current_ratio", "revenue_growth", "eps_growth", "market_cap"] INCOME_COLS = ["period_end", "fiscal_period", "revenue", "operating_income", "net_income", "eps_diluted", "filed_at", "form"] def pit_note(q: pd.DataFrame) -> str: if q.empty or "filed_at" not in q: return "no quarterly statements available" q = q.copy() q["filed_at"] = pd.to_datetime(q["filed_at"], errors="coerce") q["period_end"] = pd.to_datetime(q.get("period_end"), errors="coerce") latest = q.sort_values("filed_at").iloc[-1] lag = (latest["filed_at"] - latest["period_end"]).days if pd.notna(latest["filed_at"]) and pd.notna(latest["period_end"]) else None age = (pd.Timestamp(date.today()) - latest["filed_at"]).days if pd.notna(latest["filed_at"]) else None return (f"latest period {latest['period_end'].date() if pd.notna(latest['period_end']) else '?'} became public on " f"{latest['filed_at'].date() if pd.notna(latest['filed_at']) else '?'} ({lag} days after period end; {age} days ago)" + (" — a newer quarter has ended but is not filed yet" if age is not None and age > 100 else "")) def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--filters", help='e.g. "pe<15,roe>0.15,fcf_yield>0.06"') ap.add_argument("--sort", help="metric:asc|desc") ap.add_argument("--as-of", help="point-in-time date YYYY-MM-DD") ap.add_argument("--limit", type=int, default=25) ap.add_argument("--tickers", nargs="*", help="skip the screener, enrich these tickers") ap.add_argument("--quarters", type=int, default=3) ap.add_argument("--no-enrich", action="store_true") ap.add_argument("--out") a = ap.parse_args() if not a.filters and not a.tickers: ap.error("--filters or --tickers required") hits = pd.DataFrame() if a.filters: try: hits, meta = hfmd.screener(a.filters, a.sort, a.as_of, a.limit) except hfmd.HfmdError as e: print(e, file=sys.stderr) if e.status == 404: print("→ the fundamentals module is not deployed on this server yet (v2 rollout). Nothing to screen.", file=sys.stderr) return 2 return 1 print(f"screen `{a.filters}`{' as of ' + a.as_of if a.as_of else ''}: {meta.get('count', len(hits))} match(es)" + (f" (universe {meta['universe']})" if 'universe' in meta else "")) if hits.empty: return 0 with pd.option_context("display.width", 200, "display.max_columns", 30, "display.float_format", "{:.3f}".format): print(hits.head(a.limit).to_string(index=False)) tickers = list(hits["ticker"].head(a.limit)) if "ticker" in hits else [] else: tickers = [t.upper() for t in a.tickers] if a.no_enrich or not tickers: if a.out and not hits.empty: hits.to_csv(a.out, index=False) print(f"wrote {a.out}") return 0 if not hfmd.API_KEY and len(tickers) > 8: hfmd.log(f"keyless mode: enriching {len(tickers)} tickers needs {2 * len(tickers)} requests (30/h quota) — set HFMD_API_KEY or use --limit 8") enriched = [] for t in tickers: print(f"\n=== {t} ===") try: r = hfmd.ratios(t) line = " ".join(f"{k}={r[k]:.3g}" for k in KEY_RATIOS if isinstance(r.get(k), (int, float))) print("ratios:", line or r) if r.get("as_of") or r.get("period_end"): print(f" based on period {r.get('period_end')} · filed {r.get('filed_at')} · price as of {r.get('as_of')}") except hfmd.HfmdError as e: r = {} print(f"ratios: {e}") try: q = hfmd.statements(t, "income", "quarterly", a.quarters) cols = [c for c in INCOME_COLS if c in q.columns] or list(q.columns)[:8] with pd.option_context("display.width", 200, "display.float_format", "{:,.0f}".format): print(q[cols].to_string(index=False) if not q.empty else " no statements") print(" point-in-time:", pit_note(q)) except hfmd.HfmdError as e: q = pd.DataFrame() print(f"statements: {e}") enriched.append({"ticker": t, **{k: r.get(k) for k in KEY_RATIOS}, "latest_filed_at": (q["filed_at"].max() if "filed_at" in q else None)}) if a.out: out = pd.DataFrame(enriched) if not hits.empty and "ticker" in hits: out = hits.merge(out, on="ticker", how="left", suffixes=("", "_latest")) out.to_csv(a.out, index=False) print(f"\nwrote {a.out}") return 0 if __name__ == "__main__": sys.exit(main())