diff --git a/README.md b/README.md index d749fea..2666739 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,40 @@ Fundamentals (weekly, early Monday) · Alerts (hourly, Telegram) · Backtest (we 3. **Activation gate** — a setup *qualifies* only if it clears the R:R and confidence floors **and** ranks in the top momentum percentile of the universe (the validated edge is long-only momentum). 4. **Top pick** — the highest-momentum qualified setup; highlighted on the Dashboard and labelled on the ticker page. +## Strategy Status — What's Validated and What Isn't + +**Read this before touching scoring, gating, or setup logic.** The platform measures itself — a weekly-replay backtest plus a factor rank-IC harness (`app/services/backtest_service.py`) — and the verdicts below come from those reports (June 2026, ~5 years of OHLCV), not from opinion. + +| Component | Verdict | Evidence | +|---|---|---| +| **12-1 cross-sectional momentum** (the activation gate, long-only) | **The only demonstrated edge — in-sample** | Qualified setups ≈ **+0.25R** avg vs ≈ −0.05R all-setups baseline; the percentile sweep is cleanly monotonic (cutoff 50 → +0.14R, 70 → +0.21R, 80 → +0.25R). Rank-IC ≈ 0.05, t ≈ 1.6 — right sign and size for the classic factor, **not yet statistically significant** | +| S/R setup engine (ATR stops, S/R targets, reach-probability) | **No selection edge — execution/timing only** | ≈ breakeven (+0.01R) before the momentum gate. The probability model is honest (calibrated) but does not discriminate winners | +| Composite score + 5 dimensions | **Display/ranking only** | Sub-scores are hand-built heuristics; none has a measured IC. Note: the "momentum" *dimension* is 5/20-day ROC — NOT the validated 12-1 factor (that lives in `momentum_service`) | +| LLM sentiment | Display + a bounded composite adjustment (± weight × 100 pts around neutral 50) | Deliberately kept out of the setup engine; no point-in-time history to validate against yet | +| Fundamentals | Feeds composite + confidence only | Latest values only, no history — same limitation | +| Short setups | **Excluded while the momentum gate is active** | Backtest showed shorts fight the trend and drag expectancy | +| Expected-value gate (removed June 2026) | Degenerate — do not resurrect | Structurally favored distant lottery targets; selected *worse*-than-random setups | + +Caveats on the momentum result: in-sample, roughly one market regime, no transaction costs or slippage modeled, and the factor is beta-heavy (6-month volatility posted the top IC — that's beta, not alpha). The **out-of-sample proof is the forward paper-trade record**: Signals → Track Record compares live qualified expectancy against the backtest. + +### The iron rule for strategy changes + +A signal earns its way into selection **only** through the factor harness: + +1. Add it as a point-in-time function of past bars in `_signal_values()` (`backtest_service.py`). +2. Run the backtest (Admin → Jobs, or the weekly run) and read the **Signal edge** table (Signals → Track Record). +3. Wire it into the gate or ranking **only if** |mean IC| ≳ 0.03 with a consistent sign and `reliable: true` (≥ 12 non-overlapping windows). + +Corollaries: never let an unvalidated score gate setups; the outcome evaluator must keep scoring **all** setups (unqualified ones are the control group); LLM output stays display-only in the quant path. + +### Highest-value next experiments (in order) + +1. **Volatility-scaled momentum** — add `mom_12_1 / vol_6m` to `_signal_values`; risk-adjusted momentum typically beats raw and dampens momentum crashes. +2. **Regime filter on the gate** — momentum crashes cluster in post-bear rebounds; `market_regime_service` already computes the SPY 50/200 trend, so test "qualify only in Risk-On" in the backtest before wiring it live. +3. **Cost haircut in the backtest** — subtract a fixed per-trade cost (e.g. 0.1% per side) in the outcome aggregation so expectancy is net; a thin edge must survive costs. +4. **More breadth, not more history** — widening the ranked universe (e.g. `nasdaq_all`) strengthens each week's cross-section and the IC t-stat, even if only the top slice is traded. (Deeper history was considered and declined.) +5. **Exit tuning with the existing sweeps** — the report already sweeps fixed take-profits and trailing stops against the S/R-target model; momentum's edge lives in the right tail, so wide trailing exits (already the paper-trade default) tend to beat nearby S/R targets. Also worth testing: a pure time-based exit (hold ~1 month, re-rank) instead of the 30-day target/stop race. + ## Key Use Cases - **Find today's best long setup.** On the **Dashboard**, the *Top Setups* table lists qualified setups ranked by momentum with the #1 flagged "Top pick". Each row opens the ticker page for the chart, scores, S/R targets and entry/stop. @@ -188,9 +222,10 @@ npm run preview # Preview the production build locally # Backend tests (in-memory SQLite — no PostgreSQL needed) pytest tests/ -v -# Frontend tests +# Frontend: there is no test suite — `npm test` calls vitest, which is not +# installed. The frontend check is the full TypeScript build: cd frontend -npm test +npm run build ``` ## Environment Variables @@ -230,70 +265,68 @@ Configure in `.env` (copy from `.env.example`): ## Production Deployment (Debian 12) +**Ongoing deploys are automated.** Every push to `main` triggers the Gitea Actions pipeline (`.gitea/workflows/deploy.yml`): lint → test → rsync to the server → `pip install` → `alembic upgrade head` → restart `signalplatform.service` → health check. There is no manual deploy step; the steps below are only for provisioning a new server. + ### 1. Install dependencies ```bash -sudo apt update && sudo apt install -y python3.12 python3.12-venv postgresql nginx nodejs npm +sudo apt update && sudo apt install -y python3.12 python3.12-venv postgresql nginx rsync ``` -### 2. Create service user +### 2. Create the deploy user + +The pipeline connects over SSH as this user; it owns the app directory and needs passwordless permission to restart the service: ```bash -sudo useradd -r -s /usr/sbin/nologin stockdata +sudo useradd -m deploy +sudo mkdir -p /opt/signalplatform +sudo chown deploy:deploy /opt/signalplatform +echo 'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart signalplatform.service' | sudo tee /etc/sudoers.d/deploy-restart ``` -### 3. Deploy application +### 3. Configure the pipeline (Gitea repo settings) + +Variables: `DEPLOY_HOST`, `DEPLOY_USER` (`deploy`), `DEPLOY_PATH` (`/opt/signalplatform`), `SSH_KNOWN_HOSTS` (host fingerprint), `SSH_PORT`. Secret: `SSH_PRIVATE_KEY` (matching the deploy user's authorized key). + +### 4. Configure the app ```bash -sudo mkdir -p /opt/stock-data-backend -# Copy project files to /opt/stock-data-backend -cd /opt/stock-data-backend -python3.12 -m venv .venv -source .venv/bin/activate -pip install . -``` - -### 4. Configure - -```bash -sudo cp .env.example /opt/stock-data-backend/.env -sudo chown stockdata:stockdata /opt/stock-data-backend/.env +# After the first pipeline run has synced the files: +cp /opt/signalplatform/.env.example /opt/signalplatform/.env # Edit .env with production values (strong JWT_SECRET, real API keys, etc.) ``` +`.env` is excluded from the rsync, so it survives every deploy. + ### 5. Database +Either trigger the workflow manually (workflow_dispatch) with `run_setup_db: true` — the deploy then runs `deploy/setup_db.sh` instead of plain migrations — or run it once by hand: + ```bash DB_NAME=stock_data_backend DB_USER=stock_backend DB_PASS=strong_password ./deploy/setup_db.sh ``` -### 6. Build frontend +### 6. Systemd service ```bash -cd frontend -npm ci -npm run build -``` - -### 7. Systemd service - -```bash -sudo cp deploy/stock-data-backend.service /etc/systemd/system/ +sudo cp deploy/signalplatform.service /etc/systemd/system/ sudo systemctl daemon-reload -sudo systemctl enable --now stock-data-backend +sudo systemctl enable --now signalplatform ``` -### 8. Nginx reverse proxy +The unit runs uvicorn on `127.0.0.1:8998` as the `deploy` user, with `WorkingDirectory=/opt/signalplatform`. + +### 7. Nginx reverse proxy ```bash -sudo cp deploy/nginx.conf /etc/nginx/sites-available/stock-data-backend -sudo ln -s /etc/nginx/sites-available/stock-data-backend /etc/nginx/sites-enabled/ +sudo cp deploy/nginx.conf /etc/nginx/sites-available/signalplatform +sudo ln -s /etc/nginx/sites-available/signalplatform /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` -Nginx serves the frontend static files from `frontend/dist/` and proxies `/api/v1/` to the backend. +Nginx serves the frontend static files from `frontend/dist/` (built on the CI runner and rsynced) and proxies `/api/v1/` to the backend. -### 9. SSL (recommended) +### 8. SSL (recommended) ```bash sudo apt install certbot python3-certbot-nginx @@ -359,3 +392,62 @@ tests/ ├── unit/ # Unit tests └── property/ # Property-based tests (Hypothesis) ``` + +## Maintainer Guide + +Context for whoever — human or AI — continues this work. The owner pushes straight to `main` on a self-hosted Gitea remote (no PRs) and deploys manually. + +### Invariants — do not break these + +- **`app/services/qualification.py` is mirrored in `frontend/src/lib/qualification.ts`.** Any gate change must land in both, or the UI's "qualified" flags silently disagree with the server. +- **Live scan and backtest share the same pure functions.** The backtest replays production logic through DB-free functions (`compute_technical_from_arrays`, `compute_momentum_from_closes`, `detect_sr_levels`, the recommendation helpers). New strategy logic must stay in pure functions consumed by both paths, or the backtest stops measuring what production actually does. +- **One S/R model app-wide:** `sr_service.detect_sr_levels` + `cluster_sr_zones` (2% tolerance) feed the chart, alerts, and target generation identically. +- **The outcome evaluator evaluates ALL setups**, not just qualified ones — unqualified setups are the control group that makes the Track Record meaningful. +- **`SystemSetting` access goes through `app/services/settings_store.py`** — don't query the model directly. +- **Time-series data gets a real table** (see `benchmark_prices`, `regime_snapshots`); `SystemSetting` JSON is only for config and cached reports. +- Style: surgical changes, minimal new files; extend existing services rather than adding parallel ones. + +### Where the strategy lives + +| Concern | File | +|---|---| +| Composite + 5 dimension scores, weights | `app/services/scoring_service.py` | +| 12-1 momentum ranking (the validated factor) | `app/services/momentum_service.py` | +| Setup construction (ATR stop, S/R targets) | `app/services/rr_scanner_service.py` | +| Confidence, targets, reach-probability, action | `app/services/recommendation_service.py` | +| Activation gate predicate (mirrored in TS) | `app/services/qualification.py` | +| Gate defaults / admin config | `app/services/admin_service.py` (`ACTIVATION_DEFAULTS`) | +| Backtest + factor rank-IC harness ("Signal edge") | `app/services/backtest_service.py` | +| Outcome resolution (target/stop/expired/ambiguous) | `app/services/outcome_service.py` | +| Paper trades + trailing auto-exit | `app/services/paper_trade_service.py` | +| S/R detection & zone clustering | `app/services/sr_service.py` | +| SPY benchmark for paper-trade alpha | `app/services/benchmark_service.py` | +| Pipelines & job registration | `app/scheduler.py` | + +### Verifying changes + +```bash +pytest tests/ -q # backend; in-memory SQLite, no Postgres needed +cd frontend && npm run build # full tsc check — this IS the frontend "test" +``` + +- `npm test` in `frontend/` is dead (vitest isn't installed; there are no frontend test files). Use `npm run build`. +- Backend tests that exercise services which `commit()` need a plain session fixture, not the rolling-back `db_session` — copy the pattern in `tests/unit/test_rr_scanner_integration.py`. +- `ruff` reports ~11 pre-existing errors in old test files; those are not regressions. + +### Deploying + +Automated by Gitea Actions (`.gitea/workflows/deploy.yml`) on every push to `main`: **lint** (`ruff check app/`) → **test** (pytest; `alembic upgrade head` validated against a real Postgres 16 service; frontend `npm ci && npm run build`) → **deploy** (frontend built on the runner, rsync to the server excluding `.env`, `pip install -e .`, `alembic upgrade head`, restart `signalplatform.service`, health check on `127.0.0.1:8998`). Deploys are serialized by a concurrency group so overlapping pushes can't race. + +Practical consequences: + +- **A `ruff` error in `app/` or any failing backend test blocks the deploy.** (CI lints only `app/`, so the pre-existing ruff noise in old test files doesn't.) +- **Migrations run automatically on deploy** — no manual `alembic` step. A migration that only works on SQLite will fail CI against Postgres, by design. +- Pushing to `main` **is** deploying to production — there is no separate release step. +- After a gate or scanner change ships, trigger an R:R scan (Admin → Jobs) so live setups pick up new fields. + +### Roadmap (agreed June 2026) + +1. **Forward paper-test the momentum book** — the out-of-sample proof the backtest can't give. Watch Signals → Track Record (live vs backtest). +2. **Full IBKR integration** — read real positions, overlay entries/stops on charts, alert on holdings' score deterioration. (Paper trading, the lighter alternative, is done.) +3. Strategy experiments in the order listed under **Strategy Status** above — each one goes through the factor harness first.