Continuously scans the top-50 USDT pairs and only publishes a LONG/SHORT
signal for a coin whose fitted model clears a 60% out-of-sample holdout
accuracy bar, with multi-timeframe confluence (1h+4h), a regime filter
(4h EMA_200, 1h ADX_14), and an ATR sanity band — quality over frequency
is enforced by the gates, not just described in prose. Every signal's
real outcome (TP1/SL/expiry) is tracked, and a coin whose live results
fall below 55% over its last 20 closed signals auto-pauses.
- 5 new tables (ScannerSymbol, SignalModel, Signal, SignalSubscription,
ScannerState) and a new backend/scan/ package (universe, engine,
broadcast) — engine.py is pure logic with its own module-level
_FETCHERS/compute_indicators/compute_signal, independently testable
without routing through main.py.
- backtest/engine.py's fit_model gains allow_tree=False, used only by
the scanner, so cached models are JSON-serializable (no pickled
sklearn objects) rather than needing a TreeModel path.
- Cron-…
Revert "Drop scikit-learn/scipy dependency to fix Render OOM"
Thread-capping and a smaller candle limit weren't enough — the previous
OOM was from baseline import footprint, not per-request working memory.
scikit-learn pulls in scipy, and that combination alongside pandas/ccxt/
fastapi was too heavy for the free tier's 512MB before a single request
even arrived.
Replaced sklearn's LogisticRegression with a small pure-numpy IRLS
implementation (Newton's method, converges in a handful of iterations
for datasets this size) — numpy is already a hard dependency via pandas,
so this removes weight instead of shifting it. Dropped the gradient-
boosting candidate (TreeModel) along with it, since it required sklearn
and its own holdout-validation results weren't consistently better than
logistic regression anyway. Also removed the unused `ta` package.