Pre-launch hardening: KOL module, Telegram, scanners, WS resilience
Big-picture changes since b941223:
KOL pipeline (new) — Substack/podcast/blog RSS → AI ticker extraction →
on-chain wallet diff → talks-vs-trades divergence detection. Daily polls,
19 feeds, divergence emits Post + Telegram fan-out.
Telegram push (new) — walletless free tier + wallet-linked Pro upgrade,
in-bot preference commands (/trump /btc /funding /kol /conf /quiet),
signed-envelope API for dashboard. Disconnect-wallet keeps free
subscription.
BTC funding-rate reversal scanner (new) — hourly cron, 30d cumulative
funding threshold + mean-revert + 7d price confirm, emits via
/api/signals/ingest. BTC bottom-reversal scanner promoted to System 2.
WS broadcast rewrite — per-client send timeout + parallel fan-out
(asyncio.gather). Fixes "Binance WS no close frame" reconnect storms +
APScheduler 11-min job misses, both caused by one slow client stalling
the kline loop.
Error visibility — three silent-error sites (trumpstruth/truth_social
fetchers, funding_reversal scanner) now include exception type name so
httpx ConnectError-style empty-message errors stop logging blank lines.
Telegram bot loop now classifies ReadTimeout vs network vs unknown +
logger.exception for the unknown bucket.
Security hygiene — trumpsignal.db untracked from git (held subscriber
wallets + encrypted HL keys + 22 bot trades); .gitignore now blocks
*.db/.next/backups. CORS only allows FRONTEND_URL in production.
New ops scripts —
- scripts/preflight.py: env/DB/Telegram/AI auth verification gate
- scripts/backup_db.sh: cron-friendly daily DB backup (SQLite + Postgres)
- scripts/seed_kol_wallets.py: idempotent KOL on-chain wallet seeder
15 new Alembic migrations (007-021) covering convex strategy fields,
phase-1 safety, two-system frozen exits, invalidation prices, dynamic
SYS2 leverage, staged de-risk + pyramiding, peak gain tracking, risk
mode, auto-trade + grow flags, KOL module, KOL on-chain, KOL divergence,
Telegram bindings + walletless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
200-day SMA Reclaim scanner.
|
||||
|
||||
What it catches:
|
||||
The moment a price that has been LIVING BELOW its 200-day moving average
|
||||
for a sustained period climbs back ABOVE it on real volume. Historically
|
||||
one of the most reliable "trend has changed" markers in any market —
|
||||
hedge fund books, retail TA tools, momentum quants, everyone watches it.
|
||||
|
||||
Examples this would have caught:
|
||||
BTC 2023-01 (~$22k, after the FTX flush)
|
||||
BTC 2024-09 (after Q3 chop)
|
||||
ETH 2023-01 (~$1500)
|
||||
SOL 2023-02 (~$24, after FTX)
|
||||
|
||||
Trigger logic:
|
||||
|
||||
PRE-CONDITION: For the past DAYS_BELOW_REQUIRED days, daily close has been
|
||||
BELOW the rolling 200-day SMA. (proves we're reversing a
|
||||
sustained downtrend, not crossing a flat MA in chop)
|
||||
|
||||
TRIGGER: Today's close > 200-day SMA, AND
|
||||
Today's volume > 1.3 × 30-day avg volume.
|
||||
|
||||
COOLDOWN: 30 days — false reclaims and shake-outs happen, don't
|
||||
re-fire on noise.
|
||||
|
||||
Companion exit profile:
|
||||
SL = 6%
|
||||
TRAILING_ACTIVATE = 12%
|
||||
TRAILING_STOP = 5%
|
||||
MAX_HOLD = 90 days
|
||||
|
||||
The 90-day max-hold matches the holding period needed for a real trend
|
||||
change to play out (~3 months is the historical median for a confirmed
|
||||
200d-SMA-reclaim trend run).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import settings
|
||||
from app.services.market_data import REVERSAL_BASKET, for_asset, drop_in_progress_bar
|
||||
|
||||
# LIBRARY MODULE — NOT a standalone scanner. evaluate_sma_reclaim() is
|
||||
# imported by btc_bottom_reversal.py as the price-reclaim entry gate. It
|
||||
# deliberately does NOT register with scanner_state: no UI toggle, no
|
||||
# schedule. (Old standalone scan_once/_emit_signal removed — see git log.)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─── Tunables ───────────────────────────────────────────────────────────────
|
||||
SMA_PERIOD = 200
|
||||
DAYS_BELOW_REQUIRED = 30 # how long the asset must have been under SMA
|
||||
VOLUME_LOOKBACK_DAYS = 30
|
||||
VOLUME_MULT_MIN = 1.3
|
||||
DAYS_TO_FETCH = 260 # SMA(200) + 30d-below check + safety margin
|
||||
COOLDOWN_DAYS = 30
|
||||
|
||||
PAYLOAD_CONFIDENCE = 85
|
||||
PAYLOAD_EXPECTED_MOVE = 20.0 # historical median 90-day run after reclaim
|
||||
|
||||
|
||||
# Cooldown via scanner_state.in_cooldown — DB-backed, restart-safe.
|
||||
|
||||
|
||||
# ─── Signal logic ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def evaluate_sma_reclaim(daily_candles: list[dict]) -> tuple[bool, dict]:
|
||||
"""Pure function. Returns (is_signal, debug).
|
||||
|
||||
Expects `daily_candles` ordered chronologically (oldest first), each
|
||||
having keys close, volume.
|
||||
"""
|
||||
if len(daily_candles) < SMA_PERIOD + DAYS_BELOW_REQUIRED + 2:
|
||||
return False, {"reason": "insufficient_data", "bars": len(daily_candles)}
|
||||
|
||||
closes = [c["close"] for c in daily_candles]
|
||||
volumes = [c["volume"] for c in daily_candles]
|
||||
|
||||
# Rolling 200-day SMA at each bar from index SMA_PERIOD-1 onwards
|
||||
smas: list[Optional[float]] = [None] * len(closes)
|
||||
running_sum = sum(closes[:SMA_PERIOD])
|
||||
smas[SMA_PERIOD - 1] = running_sum / SMA_PERIOD
|
||||
for i in range(SMA_PERIOD, len(closes)):
|
||||
running_sum += closes[i] - closes[i - SMA_PERIOD]
|
||||
smas[i] = running_sum / SMA_PERIOD
|
||||
|
||||
today_close = closes[-1]
|
||||
today_sma = smas[-1]
|
||||
if today_sma is None:
|
||||
return False, {"reason": "sma_not_computable"}
|
||||
|
||||
# Bottom-reversal mode is LONG-only:
|
||||
# reclaim (long): was BELOW the SMA for N days, today closes ABOVE
|
||||
# We explicitly do not trade symmetric short breakdowns here. Crypto
|
||||
# top-calling is a different strategy with different risk.
|
||||
reclaimed = today_close > today_sma
|
||||
brokedown = today_close < today_sma
|
||||
if brokedown:
|
||||
return False, {
|
||||
"reason": "shorts_disabled",
|
||||
"close": round(today_close, 4),
|
||||
"sma": round(today_sma, 4),
|
||||
}
|
||||
if not reclaimed:
|
||||
return False, {"reason": "on_sma_no_cross",
|
||||
"close": round(today_close, 4), "sma": round(today_sma, 4)}
|
||||
|
||||
# Prior DAYS_BELOW_REQUIRED bars must ALL be on the OPPOSITE side of the
|
||||
# SMA from today (a real regime flip, not chop around a flat MA).
|
||||
streak = 0
|
||||
for i in range(2, DAYS_BELOW_REQUIRED + 2):
|
||||
sma_at = smas[-i]
|
||||
if sma_at is None:
|
||||
return False, {"reason": "sma_history_incomplete"}
|
||||
prior_on_wrong_side = closes[-i] >= sma_at
|
||||
if prior_on_wrong_side:
|
||||
return False, {"reason": "regime_period_too_short", "broke_at_day": i}
|
||||
streak += 1
|
||||
|
||||
# Volume confirmation: today >= VOLUME_MULT_MIN × 30-day avg
|
||||
avg_vol_30d = sum(volumes[-(VOLUME_LOOKBACK_DAYS + 1):-1]) / VOLUME_LOOKBACK_DAYS
|
||||
if avg_vol_30d <= 0:
|
||||
return False, {"reason": "no_volume_baseline"}
|
||||
vol_ratio = volumes[-1] / avg_vol_30d
|
||||
if vol_ratio < VOLUME_MULT_MIN:
|
||||
return False, {"reason": "weak_volume", "vol_ratio": round(vol_ratio, 2)}
|
||||
|
||||
return True, {
|
||||
"direction": "buy",
|
||||
"close": round(today_close, 4),
|
||||
"sma_200": round(today_sma, 4),
|
||||
"gap_pct": round(abs(today_close - today_sma) / today_sma * 100, 2),
|
||||
"streak_days": streak,
|
||||
"vol_ratio": round(vol_ratio, 2),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user