A newer version of the Gradio SDK is available: 6.26.0
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 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
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:
python data/stubs/_build_stubs.py
Tests
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
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:
- Create a private HF Dataset, e.g.
us-flow-cache. - In your Space's Settings β Variables and secrets, add:
FSCANNER_CACHE_REPO=<your-username>/us-flow-cacheHF_TOKEN= (a write-enabled token, also added to your dataset repo)
- 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 β₯ $5and20-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:
- Persists the full results as a timestamped snapshot in
history/. - Computes the realised forward return for every ticker over the next
HORIZON_DAYS(default 5) trading days using the OHLCV cache. - Aggregates a Spearman IC per snapshot for the current weights.
- Runs a small Dirichlet random search (200 candidates + 5 structured seeds) over alternative weight vectors and picks the one with the highest mean IC.
- If the new vector beats the baseline IC by β₯ 0.005, it is persisted
to
learned.jsonand a row is appended toperf.parquet; otherwise the default weights are kept. - 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:
# 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.