Adds the validated-vs-not verdict table, the iron rule for strategy changes, ranked next experiments, a maintainer guide (invariants, file map, verification, roadmap), and corrects the deploy docs: deploys are automated by Gitea Actions (push to main = deploy), service is signalplatform.service at /opt/signalplatform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signal Dashboard
Investing-signal platform for NASDAQ stocks. Surfaces the best trading opportunities through weighted multi-dimensional scoring — technical indicators, support/resistance quality, sentiment, fundamentals, and momentum — with asymmetric risk:reward scanning.
Philosophy: Don't predict price. Find the path of least resistance, key S/R zones, and asymmetric R:R setups.
How It Works
Scheduled pipelines turn raw prices into a ranked, gated list of tradeable setups. Everything downstream of OHLCV is recomputed from stored data, so each refresh is cheap and idempotent. Job timing is cron-based and configurable in Admin → Jobs (default timezone Europe/Berlin).
Daily Load — the full refresh
Once a day (default 07:00). Steps run in dependency order, each consuming the previous step's fresh output:
- OHLCV — fetch the latest daily bars for every tracked ticker (Alpaca); new tickers backfill ~5 years.
- Sentiment — fetch sentiment for the names that matter and are stale (> 5 days): top-pick feeders (momentum leaders with a tradeable long setup), the watchlist, and open paper trades, plus a top-N-by-composite discovery net. Runs before the scan so the scan sees fresh sentiment.
- R:R Scan — recompute S/R zones, the 5-dimension scores and long/short setups (ATR stops, S/R targets) for every ticker, and attach each ticker's 12‑1 momentum percentile.
- Outcome Eval — resolve setups that hit target/stop or expired (default 30 trading days) and close paper trades that hit a level.
- Market Regime — recompute the regime index (breadth/trend).
- Regime Monitor — observational early-warning snapshot (VIX, credit spreads via FRED); feeds nothing else.
A failing step is logged; the pipeline continues with the next.
Intraday — light refresh
Hourly across the US session (Mon–Fri): only OHLCV → Outcome Eval, to keep prices current and close paper trades intraday. No scan/sentiment — the dashboard recomputes live R:R from the latest price, so fresh prices are enough.
Other jobs
Fundamentals (weekly, early Monday) · Alerts (hourly, Telegram) · Backtest (weekly) · Ticker-universe sync (daily). Deep history backfill and event study are manual-only (Admin → Jobs).
From score to "top pick"
- Composite score — technical, S/R-quality, sentiment, fundamental and momentum sub-scores (0–100) combine into a weighted composite (weights configurable; missing dimensions re-normalize).
- Setups — the scanner builds long/short setups with ATR stops and S/R targets, then adds a confidence score, conflict flags and a target reach-probability.
- 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).
- 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:
- Add it as a point-in-time function of past bars in
_signal_values()(backtest_service.py). - Run the backtest (Admin → Jobs, or the weekly run) and read the Signal edge table (Signals → Track Record).
- 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)
- Volatility-scaled momentum — add
mom_12_1 / vol_6mto_signal_values; risk-adjusted momentum typically beats raw and dampens momentum crashes. - Regime filter on the gate — momentum crashes cluster in post-bear rebounds;
market_regime_servicealready computes the SPY 50/200 trend, so test "qualify only in Risk-On" in the backtest before wiring it live. - 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.
- 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.) - 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.
- Track a trade you took. Mark a setup as a paper trade: it's marked-to-market against the latest close, auto-closed on stop/target, and its sentiment stays fresh while open. Signals → Track Record shows the realized edge.
Stack
| Layer | Tech |
|---|---|
| Backend | Python 3.12+, FastAPI, Uvicorn, async SQLAlchemy, Alembic |
| Database | PostgreSQL (asyncpg) |
| Scheduler | APScheduler — daily & intraday pipelines, fundamentals, alerts, regime, backtest |
| Frontend | React 18, TypeScript, Vite 5 |
| Styling | Tailwind CSS 3 with custom glassmorphism design system |
| State | TanStack React Query v5 (server), Zustand (client/auth) |
| Charts | Canvas 2D candlestick chart with S/R overlays |
| Routing | React Router v6 (SPA) |
| HTTP | Axios with JWT interceptor |
| Data providers | Alpaca (OHLCV); OpenAI / Gemini / DeepSeek / xAI (sentiment, pluggable); Fundamentals chain: FMP → Finnhub → Alpha Vantage; FRED (regime); Telegram (alerts) |
Features
Backend
- Ticker registry with full cascade delete
- Universe bootstrap for
sp500,nasdaq100,nasdaq_allvia admin endpoint - OHLCV price storage with upsert and validation
- Technical indicators: ADX, EMA, RSI, ATR, Volume Profile, Pivot Points, EMA Cross
- Support/Resistance detection with strength scoring and merge-within-tolerance
- Sentiment analysis with time-decay weighted scoring
- Fundamental data tracking (P/E, revenue growth, earnings surprise, market cap)
- 5-dimension scoring engine (technical, S/R quality, sentiment, fundamental, momentum) with configurable weights
- Risk:Reward scanner — long and short setups, ATR-based stops, S/R-based targets, configurable R:R threshold (default 1.5:1)
- Activation gate — qualifies setups on a momentum-percentile floor plus R:R/confidence (validated long-only edge); ranks the rest by expected value
- Recommendation layer — directional confidence, conflict detection, per-target reach-probability
- Paper trading — take a setup, mark-to-market vs. latest close, auto-close on stop/target, realized track record + outcome evaluation
- Market-regime index + FRED early-warning monitor (VIX, credit spreads); weekly backtest + manual event study
- Telegram alerts (e.g. regime-quadrant changes)
- User-curated watchlist (cap: 20), enriched with composite score, R:R and S/R summary
- JWT auth with admin role, configurable registration, user access control
- Cron-scheduled pipelines (admin-configurable) with per-job enable/disable and live status monitoring
- Admin panel: user management, data cleanup, job control, system settings
Frontend
- Glassmorphism UI with frosted glass panels, gradient text, ambient glow effects, mesh gradient background
- Interactive candlestick chart (Canvas 2D) with hover tooltips showing OHLCV values
- Support/Resistance level overlays on chart (top 6 by strength, dashed lines with labels)
- Data freshness bar showing availability and recency of each data source
- Watchlist with composite scores, R:R ratios, and S/R summaries
- Ticker detail page: chart, scores, sentiment breakdown, fundamentals, technical indicators, S/R table
- Rankings table with configurable dimension weights
- Trade scanner showing detected R:R setups
- Admin page: user management, job status with live indicators, enable/disable toggles, data cleanup, system settings
- Protected routes with JWT auth, admin-only sections
- Responsive layout with mobile navigation
- Toast notifications for async operations
Pages
| Route | Page | Access |
|---|---|---|
/login |
Login | Public |
/register |
Register | Public (when enabled) |
/ |
Dashboard — top setups, open trades, regime (default) | Authenticated |
/market |
Market — watchlist + rankings tabs | Authenticated |
/signals |
Signals — scanner + track record tabs | Authenticated |
/regime |
Market Regime | Authenticated |
/ticker/:symbol |
Ticker Detail | Authenticated |
/admin |
Admin Panel | Admin only |
Legacy routes redirect: /watchlist → /market, /rankings → /market?tab=rankings, /scanner → /signals, /performance → /signals?tab=track.
API Endpoints
All under /api/v1/. Interactive docs at /docs (Swagger) and /redoc.
| Group | Endpoints |
|---|---|
| Health | GET /health |
| Auth | POST /auth/register, POST /auth/login |
| Tickers | POST /tickers, GET /tickers, DELETE /tickers/{symbol} |
| OHLCV | POST /ohlcv, GET /ohlcv/{symbol} |
| Ingestion | POST /ingestion/fetch/{symbol} |
| Indicators | GET /indicators/{symbol}/{type}, GET /indicators/{symbol}/ema-cross |
| S/R Levels | GET /sr-levels/{symbol} |
| Sentiment | GET /sentiment/{symbol} |
| Fundamentals | GET /fundamentals/{symbol} |
| Scores | GET /scores/{symbol}, GET /rankings, PUT /scores/weights |
| Trades | GET /trades, GET /trades/{symbol}, GET /trades/{symbol}/history, GET /trades/activation, GET /trades/performance |
| Paper Trades | GET /paper-trades, POST /paper-trades, POST /paper-trades/{id}/close |
| Market / Regime | GET /market/regime, GET /regime/monitor, GET/PUT /regime/config, GET /regime/history, GET /regime/event-study, GET/PUT /regime/fundamentals, GET /backtest/report |
| Jobs | GET /jobs/running |
| Watchlist | GET /watchlist, POST /watchlist/{symbol}, DELETE /watchlist/{symbol} |
| Admin | GET /admin/users, POST /admin/users, PUT /admin/users/{id}/access, PUT /admin/users/{id}/password, PUT /admin/settings/registration, GET /admin/settings, PUT /admin/settings/{key}, GET/PUT /admin/settings/recommendations, GET/PUT /admin/settings/ticker-universe, POST /admin/tickers/bootstrap, POST /admin/data/cleanup, GET /admin/jobs, POST /admin/jobs/{name}/trigger, PUT /admin/jobs/{name}/toggle, GET /admin/pipeline/readiness |
Development Setup
Prerequisites
- Python 3.12+
- PostgreSQL (via Homebrew on macOS:
brew install postgresql@17) - Node.js 18+ and npm
Backend Setup
# Create and activate virtual environment
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Configure environment
cp .env.example .env
# Edit .env with your values (see Environment Variables below)
# Start PostgreSQL and create database
brew services start postgresql@17
createdb stock_data_backend
createuser stock_backend
# Run migrations
alembic upgrade head
# Start the backend
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
A default admin/admin account is created on first startup. Open http://localhost:8000/docs for Swagger UI.
Frontend Setup
cd frontend
npm install
npm run dev
Open http://localhost:5173 for the Signal Dashboard. The Vite dev server proxies /api/v1/ requests to the backend at http://127.0.0.1:8000.
Frontend Build
cd frontend
npm run build # TypeScript check + production build → frontend/dist/
npm run preview # Preview the production build locally
Tests
# Backend tests (in-memory SQLite — no PostgreSQL needed)
pytest tests/ -v
# 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 run build
Environment Variables
Configure in .env (copy from .env.example):
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string (postgresql+asyncpg://...) |
JWT_SECRET |
Yes | — | Random secret for JWT signing |
JWT_EXPIRY_MINUTES |
No | 60 |
JWT token expiry |
ALPACA_API_KEY |
For OHLCV | — | Alpaca Markets API key |
ALPACA_API_SECRET |
For OHLCV | — | Alpaca Markets API secret |
GEMINI_API_KEY |
For sentiment | — | Google Gemini API key |
GEMINI_MODEL |
No | gemini-2.0-flash |
Gemini model name |
OPENAI_API_KEY |
For sentiment (OpenAI path) | — | OpenAI API key |
OPENAI_MODEL |
No | gpt-4o-mini |
OpenAI model name |
OPENAI_SENTIMENT_BATCH_SIZE |
No | 5 |
Micro-batch size for sentiment collector |
FMP_API_KEY |
Optional (fundamentals) | — | Financial Modeling Prep API key (first provider in chain) |
FINNHUB_API_KEY |
Optional (fundamentals) | — | Finnhub API key (fallback provider) |
ALPHA_VANTAGE_API_KEY |
Optional (fundamentals) | — | Alpha Vantage API key (fallback provider) |
FRED_API_KEY |
Optional (regime) | — | FRED key for the regime monitor (VIX, credit spreads) |
TELEGRAM_BOT_TOKEN |
Optional (alerts) | — | Telegram bot token for alerts (can also be set in Admin) |
TELEGRAM_CHAT_ID |
Optional (alerts) | — | Telegram chat id for alerts |
DATA_COLLECTOR_FREQUENCY |
No | daily |
OHLCV collection schedule (legacy — see note below) |
SENTIMENT_POLL_INTERVAL_MINUTES |
No | 30 |
Sentiment polling interval |
FUNDAMENTAL_FETCH_FREQUENCY |
No | weekly |
Fundamentals fetch cadence |
RR_SCAN_FREQUENCY |
No | daily |
R:R scanner schedule |
FUNDAMENTAL_RATE_LIMIT_RETRIES |
No | 3 |
Retries per ticker on fundamentals rate-limit |
FUNDAMENTAL_RATE_LIMIT_BACKOFF_SECONDS |
No | 15 |
Base backoff seconds for fundamentals retry (exponential) |
DEFAULT_WATCHLIST_AUTO_SIZE |
No | 10 |
Auto-watchlist size |
DEFAULT_RR_THRESHOLD |
No | 1.5 |
Minimum R:R ratio for setups |
DB_POOL_SIZE |
No | 5 |
Database connection pool size |
LOG_LEVEL |
No | INFO |
Logging level |
Note: Pipeline timing (daily / intraday / fundamentals cron, timezone) is configured at runtime in Admin → Jobs and stored in the DB — the
*_FREQUENCYenv vars are legacy fallbacks for the few jobs still on interval triggers (alerts, universe sync).
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
sudo apt update && sudo apt install -y python3.12 python3.12-venv postgresql nginx rsync
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:
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. 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
# 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:
DB_NAME=stock_data_backend DB_USER=stock_backend DB_PASS=strong_password ./deploy/setup_db.sh
6. Systemd service
sudo cp deploy/signalplatform.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now signalplatform
The unit runs uvicorn on 127.0.0.1:8998 as the deploy user, with WorkingDirectory=/opt/signalplatform.
7. Nginx reverse proxy
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/ (built on the CI runner and rsynced) and proxies /api/v1/ to the backend.
8. SSL (recommended)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d signal.thiessen.io
Verify
curl https://signal.thiessen.io/api/v1/health
Project Structure
app/
├── main.py # FastAPI app, lifespan, router wiring
├── config.py # Pydantic settings from .env
├── database.py # Async SQLAlchemy engine + session
├── dependencies.py # DI: DB session, auth guards
├── exceptions.py # Exception hierarchy
├── middleware.py # Global error handler → JSON envelope
├── cache.py # LRU cache with per-ticker invalidation
├── scheduler.py # APScheduler job definitions
├── models/ # SQLAlchemy ORM models
├── schemas/ # Pydantic request/response schemas
├── services/ # Business logic layer
├── providers/ # External data provider integrations
└── routers/ # FastAPI route handlers
frontend/
├── index.html # SPA entry point
├── vite.config.ts # Vite config with API proxy
├── tailwind.config.ts # Tailwind + glassmorphism theme
├── package.json
└── src/
├── App.tsx # Route definitions
├── main.tsx # React entry point
├── api/ # Axios API client modules (one per resource)
├── components/
│ ├── admin/ # User table, job controls, settings, data cleanup
│ ├── auth/ # Protected route wrapper
│ ├── charts/ # Canvas candlestick chart
│ ├── layout/ # App shell, sidebar, mobile nav
│ ├── rankings/ # Rankings table, weights form
│ ├── scanner/ # Trade table
│ ├── ticker/ # Sentiment panel, fundamentals, indicators, S/R overlay
│ ├── ui/ # Badge, toast, skeleton, score card, confirm dialog
│ └── watchlist/ # Watchlist table, add ticker form
├── hooks/ # React Query hooks (one per resource)
├── lib/ # Types, formatting utilities
├── pages/ # Page components (Login, Register, Dashboard, Market, Signals, Regime, Ticker, Admin)
├── stores/ # Zustand auth store
└── styles/ # Global CSS with glassmorphism classes
deploy/
├── nginx.conf # Reverse proxy + static file serving
├── setup_db.sh # Idempotent DB setup script
└── stock-data-backend.service # systemd unit
tests/
├── conftest.py # Fixtures, strategies, test DB
├── 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.pyis mirrored infrontend/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.
SystemSettingaccess goes throughapp/services/settings_store.py— don't query the model directly.- Time-series data gets a real table (see
benchmark_prices,regime_snapshots);SystemSettingJSON 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
pytest tests/ -q # backend; in-memory SQLite, no Postgres needed
cd frontend && npm run build # full tsc check — this IS the frontend "test"
npm testinfrontend/is dead (vitest isn't installed; there are no frontend test files). Usenpm run build.- Backend tests that exercise services which
commit()need a plain session fixture, not the rolling-backdb_session— copy the pattern intests/unit/test_rr_scanner_integration.py. ruffreports ~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
rufferror inapp/or any failing backend test blocks the deploy. (CI lints onlyapp/, so the pre-existing ruff noise in old test files doesn't.) - Migrations run automatically on deploy — no manual
alembicstep. A migration that only works on SQLite will fail CI against Postgres, by design. - Pushing to
mainis 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)
- Forward paper-test the momentum book — the out-of-sample proof the backtest can't give. Watch Signals → Track Record (live vs backtest).
- Full IBKR integration — read real positions, overlay entries/stops on charts, alert on holdings' score deterioration. (Paper trading, the lighter alternative, is done.)
- Strategy experiments in the order listed under Strategy Status above — each one goes through the factor harness first.