| --- |
| title: US Flow Scanner |
| emoji: "π" |
| colorFrom: blue |
| colorTo: green |
| sdk: gradio |
| sdk_version: 5.13.0 |
| app_file: app.py |
| pinned: false |
| license: mit |
| --- |
| |
| # US Stock Institutional Flow Scanner |
|
|
| A Gradio web app that scans the US common-stock universe for stocks showing |
| institutional-style buying or selling pressure. Designed to run on |
| **Hugging Face Spaces** (free CPU) or locally. |
|
|
| > **Disclaimer:** this is a public-data proxy built from price/volume only. |
| > It is *not* actual 13F / block-trade / dark-pool data. Not financial advice. |
|
|
| ## What it does |
|
|
| For each of ~5000+ US-listed common stocks, the app computes **9 factors** |
| (5 daily flow proxies + 4 "real" institutional-flow signals) and combines |
| them into a single **-100 to +100 institutional-flow score**: |
|
|
| | Factor | What it measures | |
| |----------------------|---------------------------------------------------------------| |
| | **CMF(20)** | Chaikin Money Flow over 20 days | |
| | **OBV slope** | 20-day linear regression of On-Balance Volume, normalized | |
| | **Big-bar ratio** | Unusual-volume + wide-range bars today vs 20-day baseline | |
| | **VWAP dev** | Close vs 20-day volume-weighted average price | |
| | **RVOL signed** | Relative volume, sign-flipped by CMF direction | |
| | **L2 imbalance** | Top-of-book depth + large resting orders (with spoofing discount) | |
| | **Unusual options** | Vol/OI z-score across 20 days, weighted by moneyness | |
| | **Block aggression** | Net buy/sell bias in β₯10k-share trades (Lee-Ready tick rule) | |
| | **Buy persistence** | Buy-ratio average over last 1 hour of 5-min bars | |
|
|
| Factors are z-scored cross-sectionally (median + MAD robust normalization) |
| so the most-extreme name in the universe gets Β±100. |
|
|
| The last 4 factors come from a pluggable `FactorDataSource` (see |
| [Data sources](#data-sources) below). By default they are computed from |
| synthetic stub data committed to the repo, so the app works on |
| Hugging Face Spaces with **no live market feed**. |
|
|
| **Rating thresholds** (tweakable in the UI): |
| - `β₯ +50` β **Strong Buy** |
| - `+20 β¦ +49` β **Buy** |
| - `-19 β¦ +19` β **Neutral** |
| - `-49 β¦ -20` β **Sell** |
| - `β€ -50` β **Strong Sell** |
|
|
| The default weights are |
| `0.20 / 0.15 / 0.10 / 0.10 / 0.05` for the daily flow factors and |
| `0.15 / 0.10 / 0.10 / 0.05` for the institutional factors. |
| The 5 daily sliders are exposed in the sidebar accordion; the 4 |
| institutional factors always run at their default weights. |
|
|
| ## Project layout |
|
|
| ``` |
| us-flow-scanner/ |
| βββ app.py # Gradio UI |
| βββ scanner/ |
| β βββ __init__.py |
| β βββ universe.py # Load & filter ~5000 US tickers |
| β βββ data_fetcher.py # yfinance + Polygon fallback + cache |
| β βββ flow_algo.py # 5 daily-flow factors |
| β βββ l2_factor.py # Level-2 large-resting-order factor |
| β βββ options_factor.py # Unusual options activity |
| β βββ tick_factor.py # Lee-Ready tick + size buckets |
| β βββ intraday_factor.py # Intraday VWAP + buy persistence |
| β βββ factor_sources.py # StubDataSource (default) + FutuDataSource (live) |
| β βββ scorer.py # Cross-sectional z-score + composite |
| β βββ persistence.py # Optional HF Dataset round-trip |
| β βββ history.py # Per-scan snapshots + delta vs prior |
| β βββ watchlist.py # User watchlist (parse + persist) |
| β βββ performance.py # IC evaluation + self-tuning weights |
| β βββ paths.py # Centralised, env-var-overridable paths |
| βββ data/ |
| β βββ us_tickers.csv # ~5260 US common stocks (committed) |
| β βββ stubs/ # 30-ticker synthetic L2/options/ticks/intraday |
| βββ tests/ # pytest suite (isolated disk paths) |
| βββ requirements.txt |
| βββ requirements-dev.txt |
| βββ README.md |
| βββ .gitignore |
| ``` |
|
|
| ## Run locally |
|
|
| ```bash |
| pip install -r requirements.txt |
| python app.py |
| ``` |
|
|
| Then open http://localhost:7860. |
|
|
| ## Data sources |
|
|
| The four institutional-flow factors (`l2_imbalance`, `unusual_options`, |
| `block_aggression`, `buy_persistence`) read from a pluggable |
| `FactorDataSource` (`scanner/factor_sources.py`). |
|
|
| | Source | When to use | Config env var | |
| |---------------------|----------------------------------------------------------|----------------------------| |
| | `StubDataSource` | **Default.** Reads `data/stubs/` synthetic data | `FSCANNER_DATA_SOURCE` unset or `=stub` | |
| | `FutuDataSource` | Live Level-2 / options / tick data via local Futu OpenD | `FSCANNER_DATA_SOURCE=futu` + `FUTU_OPEND_HOST` + `FUTU_OPEND_PORT` | |
|
|
| The Space uses `StubDataSource` so it never depends on a live feed. |
| To regenerate the stub data: |
|
|
| ```bash |
| python data/stubs/_build_stubs.py |
| ``` |
|
|
| ## Tests |
|
|
| ```bash |
| pip install -r requirements-dev.txt |
| pytest -q |
| ``` |
|
|
| The suite isolates every persistent path via an autouse fixture, so it |
| runs in a sandboxed `tmp_path` and never touches your real cache or |
| watchlist. Tests cover factor math, scoring, history snapshots, |
| watchlist parsing, the sector cache, and the auto-tuning loop (the |
| optimizer reliably discovers which factor is the true signal in a |
| synthetic dataset). |
|
|
| ## Deploy to Hugging Face Spaces |
|
|
| ### One-time setup |
|
|
| ```bash |
| pip install "huggingface_hub[cli]" |
| huggingface-cli login |
| |
| # Create a new Space (Gradio SDK) |
| huggingface-cli repo create us-flow-scanner --type space --space-sdk gradio |
| cd .. |
| git clone https://huggingface.co/spaces/<your-username>/us-flow-scanner |
| cp -r us-flow-scanner/* us-flow-scanner/.gitignore us-flow-scanner/ |
| cd us-flow-scanner |
| git add . |
| git commit -m "Initial commit" |
| git push |
| ``` |
|
|
| The Space will install dependencies and start the app. The public URL is |
| `https://huggingface.co/spaces/<your-username>/us-flow-scanner`. |
|
|
| ### Optional: persistent cache |
|
|
| The OHLCV cache is stored under `/tmp`, which is wiped on container restart. |
| For persistence across restarts: |
|
|
| 1. Create a private HF Dataset, e.g. `us-flow-cache`. |
| 2. In your Space's **Settings β Variables and secrets**, add: |
| - `FSCANNER_CACHE_REPO` = `<your-username>/us-flow-cache` |
| - `HF_TOKEN` = (a write-enabled token, also added to your dataset repo) |
| 3. The app will push the cache after each scan and pull it on startup. |
|
|
| ### Optional: Polygon fallback |
|
|
| If you hit yfinance rate limits, set: |
| - `POLYGON_API_KEY` = (your polygon.io API key) |
|
|
| in Space secrets. Tickers that fail on yfinance are retried with Polygon. |
|
|
| ## Performance & limitations |
|
|
| - **Full universe scan** of ~5000 liquid names: ~3β6 min on a free HF Space |
| CPU. Use **Filtered** mode (default) to keep it under ~2 min. |
| - **Filtered** default: `price β₯ $5` and `20-day ADV β₯ $5M`, which trims |
| the universe to ~1500β3000 liquid names. |
| - yfinance is an unofficial scraper and may be flaky. The app retries with |
| backoff and falls back to Polygon (if configured) for individual tickers. |
| - The algorithm is a **proxy** for institutional flow based on public |
| OHLCV. It does not see 13F filings, dark-pool prints, or order-book |
| imbalance. |
| - Free HF Spaces may sleep after ~48 h of inactivity; the app restarts on |
| the next visit. |
|
|
| ## Self-tuning weights |
|
|
| After every scan, the app: |
| 1. Persists the full results as a timestamped snapshot in `history/`. |
| 2. Computes the realised forward return for every ticker over the next |
| `HORIZON_DAYS` (default 5) trading days using the OHLCV cache. |
| 3. Aggregates a **Spearman IC** per snapshot for the current weights. |
| 4. Runs a small Dirichlet random search (200 candidates + 5 structured |
| seeds) over alternative weight vectors and picks the one with the |
| highest mean IC. |
| 5. If the new vector beats the baseline IC by β₯ 0.005, it is persisted |
| to `learned.json` and a row is appended to `perf.parquet`; otherwise |
| the default weights are kept. |
| 6. The "Use auto-tuned weights" checkbox in the Performance tab tells |
| the next scan to use the learned vector instead of the slider |
| values, so you can A/B test the improvement. |
|
|
| Tuning is intentionally lightweight (seconds, not minutes) so it can |
| run in a background thread after every scan. It needs at least 5 |
| snapshots with valid forward data and at least 30 tickers per snapshot |
| to fire. |
|
|
| ## Tickers list |
|
|
| `data/us_tickers.csv` contains ~5260 tickers filtered from Nasdaq Trader's |
| official daily files (`nasdaqlisted.txt` + `otherlisted.txt`): |
| - ETFs, warrants, units, rights, preferreds, structured products excluded |
| - Test issues and distressed financial-status tickers excluded |
| - Tickers must be β€5 chars, no `.`/`-`/space (i.e. no class-B or dual-class |
| weirdness) |
|
|
| To refresh the list: |
|
|
| ```powershell |
| # From the data/ directory |
| Invoke-WebRequest -Uri "https://www.nasdaqtrader.com/dynamic/SymDir/nasdaqlisted.txt" -OutFile nasdaqlisted_raw.txt |
| Invoke-WebRequest -Uri "https://www.nasdaqtrader.com/dynamic/SymDir/otherlisted.txt" -OutFile otherlisted_raw.txt |
| # ... (use the same PowerShell filter as in earlier dev work) |
| ``` |
|
|
| ## License |
|
|
| MIT. |
|
|