spb/hfmarketdata
Public
Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)
JavaScript 53.7%
Python 38.3%
CSS 4.6%
TypeScript 3.1%
1#!/usr/bin/env python32"""Fundamental screener + enrichment with point-in-time notes.34 screen.py --filters "pe<15,fcf_yield>0.06" [--sort fcf_yield:desc] [--as-of 2020-03-20] [--limit 25]5 [--quarters 3] [--no-enrich] [--out screen.csv]6 screen.py --tickers AAPL MSFT --quarters 4 # enrich only7"""8from __future__ import annotations910import argparse11import sys12from datetime import date13from pathlib import Path1415import pandas as pd1617sys.path.insert(0, str(Path(__file__).parent))18import hfmd # noqa: E4021920KEY_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"]21INCOME_COLS = ["period_end", "fiscal_period", "revenue", "operating_income", "net_income", "eps_diluted", "filed_at", "form"]222324def pit_note(q: pd.DataFrame) -> str:25 if q.empty or "filed_at" not in q:26 return "no quarterly statements available"27 q = q.copy()28 q["filed_at"] = pd.to_datetime(q["filed_at"], errors="coerce")29 q["period_end"] = pd.to_datetime(q.get("period_end"), errors="coerce")30 latest = q.sort_values("filed_at").iloc[-1]31 lag = (latest["filed_at"] - latest["period_end"]).days if pd.notna(latest["filed_at"]) and pd.notna(latest["period_end"]) else None32 age = (pd.Timestamp(date.today()) - latest["filed_at"]).days if pd.notna(latest["filed_at"]) else None33 return (f"latest period {latest['period_end'].date() if pd.notna(latest['period_end']) else '?'} became public on "34 f"{latest['filed_at'].date() if pd.notna(latest['filed_at']) else '?'} ({lag} days after period end; {age} days ago)"35 + (" — a newer quarter has ended but is not filed yet" if age is not None and age > 100 else ""))363738def main() -> int:39 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)40 ap.add_argument("--filters", help='e.g. "pe<15,roe>0.15,fcf_yield>0.06"')41 ap.add_argument("--sort", help="metric:asc|desc")42 ap.add_argument("--as-of", help="point-in-time date YYYY-MM-DD")43 ap.add_argument("--limit", type=int, default=25)44 ap.add_argument("--tickers", nargs="*", help="skip the screener, enrich these tickers")45 ap.add_argument("--quarters", type=int, default=3)46 ap.add_argument("--no-enrich", action="store_true")47 ap.add_argument("--out")48 a = ap.parse_args()49 if not a.filters and not a.tickers:50 ap.error("--filters or --tickers required")5152 hits = pd.DataFrame()53 if a.filters:54 try:55 hits, meta = hfmd.screener(a.filters, a.sort, a.as_of, a.limit)56 except hfmd.HfmdError as e:57 print(e, file=sys.stderr)58 if e.status == 404:59 print("→ the fundamentals module is not deployed on this server yet (v2 rollout). Nothing to screen.", file=sys.stderr)60 return 261 return 162 print(f"screen `{a.filters}`{' as of ' + a.as_of if a.as_of else ''}: {meta.get('count', len(hits))} match(es)"63 + (f" (universe {meta['universe']})" if 'universe' in meta else ""))64 if hits.empty:65 return 066 with pd.option_context("display.width", 200, "display.max_columns", 30, "display.float_format", "{:.3f}".format):67 print(hits.head(a.limit).to_string(index=False))68 tickers = list(hits["ticker"].head(a.limit)) if "ticker" in hits else []69 else:70 tickers = [t.upper() for t in a.tickers]7172 if a.no_enrich or not tickers:73 if a.out and not hits.empty:74 hits.to_csv(a.out, index=False)75 print(f"wrote {a.out}")76 return 07778 if not hfmd.API_KEY and len(tickers) > 8:79 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")8081 enriched = []82 for t in tickers:83 print(f"\n=== {t} ===")84 try:85 r = hfmd.ratios(t)86 line = " ".join(f"{k}={r[k]:.3g}" for k in KEY_RATIOS if isinstance(r.get(k), (int, float)))87 print("ratios:", line or r)88 if r.get("as_of") or r.get("period_end"):89 print(f" based on period {r.get('period_end')} · filed {r.get('filed_at')} · price as of {r.get('as_of')}")90 except hfmd.HfmdError as e:91 r = {}92 print(f"ratios: {e}")93 try:94 q = hfmd.statements(t, "income", "quarterly", a.quarters)95 cols = [c for c in INCOME_COLS if c in q.columns] or list(q.columns)[:8]96 with pd.option_context("display.width", 200, "display.float_format", "{:,.0f}".format):97 print(q[cols].to_string(index=False) if not q.empty else " no statements")98 print(" point-in-time:", pit_note(q))99 except hfmd.HfmdError as e:100 q = pd.DataFrame()101 print(f"statements: {e}")102 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)})103104 if a.out:105 out = pd.DataFrame(enriched)106 if not hits.empty and "ticker" in hits:107 out = hits.merge(out, on="ticker", how="left", suffixes=("", "_latest"))108 out.to_csv(a.out, index=False)109 print(f"\nwrote {a.out}")110 return 0111112113if __name__ == "__main__":114 sys.exit(main())115