Tracks US theme ETF strength and momentum, auto-maps them to China A-share ETFs, and identifies resonance /传导 / divergence signals. Also provides a market temperature gauge (MA20/MA5 standing rate), constituent stock analysis, and portfolio monitoring to help A-share retail investors spot cross-market opportunities.
- Primary: https://im47.cn/etf-radar/ (custom domain)
- Fallback: https://im47cn.github.io/etf-radar/ (GitHub Pages default)
| Route | Name | Description |
|---|---|---|
/ /temperature |
Temperature | Market temperature gauge — MA20/MA5 standing rate, 2nd/1st-level industry aggregation + heatmap (default landing page) |
/rotation |
Rotation | Theme rotation scatter quadrant — X=long-term strength, Y=short-term strength, midline 50 splits four quadrants |
/radar |
Radar | Cross-market radar — theme list + signal details + A-share ETF mapping |
/evidence |
Evidence | Signal evidence — IC rolling/horizon charts + ARCH diagnostics + scorecard (requires membership) |
/grid |
Grid | Grid trading & suitability analysis (requires membership) |
/metals |
Metals | Precious-metals macro indicator — gold/silver ratio percentile (descriptive, no alpha claimed) |
/trading |
Trading | Environment / signals / positions / review tabs (env tab free; other tabs require membership) |
/theme/:id/stocks |
Stocks | Theme constituent stock analysis (A-share ETF holdings + spot + technical indicators) |
/portfolio → /trading?tab=holdings |
持仓(重定向) | Legacy portfolio entry — redirects to Trading 主题持仓 tab |
/watchlist |
Watchlist | Personal watchlist (requires login + membership; entry lives in Trading 自选 tab) |
/membership |
Membership | Membership info |
/auth/callback |
— | Magic Link / OAuth login callback |
- GitHub Actions triggers the Python pipeline on a schedule (
backend/src/pipeline.py) - Fetches ETF OHLC data from yfinance (US) and AkShare (China A-shares)
- Computes multi-period log returns → dual-track strength scoring (percentile × sigmoid momentum) → 60-day correlation mapping score → multi-period voting signals
- Writes to
data/latest/{themes,etfs,signals,meta}.jsonand commits back to the repo - Archives daily to
data/snapshots/<YYYY-MM-DD>/ - Frontend (React + Vite + zod) builds statically and deploys to GitHub Pages; client fetches JSON for rendering
| Time | Workflow | Description |
|---|---|---|
| Weekdays 06:30 | us-refresh |
US market full refresh |
| 1st of month 08:30 | holdings-refresh |
Monthly holdings refresh |
| Weekdays 09:15 | cn-refresh (full) |
A-share full refresh + signal recompute + market temperature |
| Weekdays 09:30-11:45 / 13:00-15:45 | cn-refresh (intraday) |
A-share price refresh every 15 min |
| Weekdays 09:00-15:30 | stocks-spot-refresh |
Stock spot snapshot every 30 min |
| Weekdays 16:30 | stocks-daily |
Stock daily candle increment + self-heal check |
| Weekdays 17:00 | trading-eod |
Trading signal EOD — OHLCV increment + signal pipeline + review & notify (staggered 30 min after stocks-daily) |
| Weekdays 18:00 | cn-eod-archive |
Post-close EOD full refresh + daily archive |
| Hourly :05 | health-monitor |
Data freshness check + critical workflow self-heal |
| 2nd of month 04:00 | stock-industry-map |
Stock → JR (juchao) industry mapping refresh |
| 2nd of month 08:07 | evidence-monthly |
Signal evidence computation (IC + ARCH) + commit & deploy |
| Manual | membership-digest |
Membership change digest email |
| Manual | stocks-history-backfill |
Stock history backfill |
| Manual | stocks-archive |
Monthly/yearly archive sharding (overseas IP rate-limited; dispatch manually) |
cd backend
uv venv && uv sync --extra dev
uv run pytest # run tests (see CI for counts)
uv run python -m src.pipeline --mode=full --data-root=../data --config-dir=../configcd frontend
npm ci
npm run dev # http://localhost:5173/etf-radar/
npm run build
npm test -- --run # run tests (see CI for counts)The /portfolio and /watchlist routes require Supabase credentials. Other pages work fine without them, but these pages will show an "unconfigured" notice.
cp frontend/.env.local.example frontend/.env.local
# Edit .env.local with your Supabase Project URL and anon key
# Ask the project maintainer for credentials, or create your own Supabase project
npm run dev # http://localhost:5173/etf-radar/#/trading?tab=holdingsMagic Link login: emails may land in the spam folder of Chinese mailboxes (QQ/163) — please check; or use Google OAuth for one-click login.
Database Schema: see backend/migrations/001_user_holdings.sql and subsequent migration files; execute them sequentially in the Supabase SQL Editor.
cd backend && uv run python ../scripts/bootstrap_data.py
git add data/latest/ && git commit -m "data: initial seed" && git pushcd backend && uv run python ../scripts/calibrate_algo.pyOutputs strength distributions at different K values; deviation ≤15% is marked ✓. Adjust config/algo.yml accordingly.
A daily cron archives data/latest/ to data/snapshots/<BJT-date>/. The data/latest/snapshots-index.json is maintained by the archive/backfill scripts; the frontend uses it to discover available dates.
First-time backfill (one-off): if snapshot history is insufficient, run the backfill script to generate historical data:
cd backend
uv run --all-extras python -m scripts.backfill_snapshots --start 2026-01-02 --end 2026-06-13Backfilled meta.json carries a backfilled: true flag to distinguish from automated archives. --skip-existing is on by default to protect real archived data.
backend/scripts/backfill_snapshots.py can rebuild snapshots for the past N trading days in one pass without modifying any scoring logic, used to drive the /rotation timeline animation. How it works: compute_outputs() accepts an asof_bjt anchor, slices the in-memory OHLC DataFrame by day, and reuses the same scoring functions.
cd backend && uv run --all-extras python -m scripts.backfill_snapshots \
--start 2026-01-02 --end 2026-06-13 \
--data-root ../data --config-dir ../config--skip-existing(on by default) protects existing archives; will not overwrite real cron output--forceforces rewrite- Output:
data/snapshots/<YYYY-MM-DD>/{themes,signals,etfs,meta}.json+data/latest/snapshots-index.json - ~120 trading days take 1-2 minutes (one network fetch + in-memory slicing)
- akshare occasional rate limiting: failed CN ETFs are recorded in
meta.providers.cn.failed_symbolsand overall status degrades todegraded; other themes are unaffected
python scripts/archive_cleanup.py- US market: yfinance (Yahoo Finance, ~15 min delay)
- A-shares: AkShare (Eastmoney data, ~15 min delay)
- Market temperature: dapanyuntu.com (market cloud map, 2nd-level industry MA20 standing rate)
- L1 soft resilience: on failure, retains the last successful snapshot; UI shows a yellow banner warning "data is XX minutes stale"
- mypy strict + ruff + jsonschema: multi-layer data contract validation (Pydantic on write + JSON Schema validation + frontend zod runtime parse)
- Health monitor + self-heal: hourly checks of data freshness and critical workflow run status; auto-dispatches compensating workflows on anomalies
- GitHub Pages Source = GitHub Actions (uses
actions/deploy-pages; not Jekyll /gh-pagesbranch) deploy-frontend.ymltrigger: push to main with changes infrontend/**ordata/latest/**- Data refresh workflows (cn/us-refresh) explicitly call
gh workflow run deploy-frontend.ymlat the end to trigger deployment- Reason: commits pushed by
GITHUB_TOKENdo not trigger downstream workflows (anti-loop safety policy)
- Reason: commits pushed by
- Custom domain (optional): currently set to
im47.cn; change in Settings → Pages → Custom domain
- Design & implementation process docs: session-scoped artifacts, not tracked in-repo (see docs/CONVENTIONS.md)
- Original product doc:
docs/htsc-us-cn-linkage-product-doc.md - Original requirements:
docs/htsc-us-cn-linkage-requirements.md
| Layer | Technology |
|---|---|
| Backend | Python 3.11, uv, pandas, numpy, scipy, pydantic v2, yfinance, akshare, chinese_calendar, pandas_market_calendars, ta (technical indicators) |
| Frontend | React 19, Vite, TypeScript strict, Tailwind v4, Base UI, Recharts, SWR, zod, react-router-dom, lucide-react |
| DevOps | GitHub Actions, GitHub Pages |
| Testing | pytest, vitest, ruff, mypy strict, jsonschema |
MIT