CashFlow / README.md
lhllamlam's picture
feat: wire 4 institutional flow factors (L2, options, ticks, intraday); stub data committed
6b66ac0 verified
|
Raw
History Blame Contribute Delete
9.34 kB
---
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.