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:
k
2026-05-25 00:52:56 +08:00
parent b941223c88
commit 5fb1d52026
81 changed files with 13251 additions and 158 deletions
+130 -10
View File
@@ -21,6 +21,13 @@ from app.api.trades import router as trades_router
from app.api.performance import router as performance_router
from app.api.subscribe import router as subscribe_router
from app.api.user import router as user_router
from app.api.funding_signal import router as funding_signal_router
from app.api.funding_reversal import router as funding_reversal_router
from app.api.telegram import router as telegram_router
from app.api.signals import router as signals_router
from app.api.positions import router as positions_router
from app.api.scanners import router as scanners_router
from app.api.kol import router as kol_router
logging.basicConfig(
level=logging.INFO,
@@ -30,17 +37,22 @@ logger = logging.getLogger(__name__)
from typing import Optional
_binance_task: Optional[asyncio.Task] = None
_telegram_task: Optional[asyncio.Task] = None
_scheduler: Optional[AsyncIOScheduler] = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global _binance_task, _scheduler
global _binance_task, _telegram_task, _scheduler
# 1. Create DB tables (dev convenience; production uses Alembic)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables ensured.")
# 1. Dev convenience only. Production should rely on Alembic so schema
# ownership stays explicit and startup never mutates the DB implicitly.
if settings.environment == "development":
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables ensured (development mode).")
else:
logger.info("Skipping create_all; expecting schema managed by Alembic.")
# 2. Backfill historical posts on startup (fast, no Claude API call)
asyncio.create_task(backfill_history(AsyncSessionLocal, limit=500))
@@ -56,6 +68,18 @@ async def lifespan(app: FastAPI):
# 3. Start Truth Social poller via APScheduler
_scheduler = AsyncIOScheduler()
# Signal monitor — polls every 5 minutes
from app.services.funding_signal import poll_funding_signal
_scheduler.add_job(
poll_funding_signal,
"interval",
minutes=5,
id="funding_signal_poll",
max_instances=1,
coalesce=True,
)
logger.info("Breakout signal monitor scheduled every 5 minutes.")
_scheduler.add_job(
poll_truth_social,
"interval",
@@ -84,12 +108,88 @@ async def lifespan(app: FastAPI):
coalesce=True,
next_run_time=_dt.now(_tz.utc) + _td(seconds=offset),
)
# HL <-> DB reconciliation — every 60s, detects state drift
# (manual closes on HL UI, liquidations, orphan positions). See
# app/services/reconciler.py. Critical for live trading safety.
from app.services.reconciler import reconcile_all_once, RECONCILE_INTERVAL_SECONDS
_scheduler.add_job(
reconcile_all_once,
"interval",
seconds=RECONCILE_INTERVAL_SECONDS,
id="hl_reconcile",
max_instances=1,
coalesce=True,
)
logger.info("HL <-> DB reconciler scheduled every %ds.", RECONCILE_INTERVAL_SECONDS)
# ── System-2 bottom-reversal state machine ─────────────────────────────
# Low-frequency, long-only: fires when ≥2 of 3 classic bottom signals
# agree — AHR999 < 0.45, price ≤ 200-week MA ×1.05, Pi Cycle Bottom
# (150d EMA ≤ 471d SMA × 0.745). Funding is a booster inside the scanner,
# not an independent entry source. See app/services/scanners/btc_bottom_reversal.py.
from app.services.scanners.btc_bottom_reversal import scan_once as btc_bottom_scan
_scheduler.add_job(
btc_bottom_scan, "cron", hour=0, minute=45,
id="btc_bottom_reversal_scan", max_instances=1, coalesce=True,
)
logger.info("BTC bottom-reversal scanner scheduled daily at 00:45 UTC.")
# ── BTC funding-rate reversal (hourly) ────────────────────────────────
# Mean-reversion play on extreme perp positioning. Independent of the
# bottom-reversal state machine but uses the same `evaluate_funding_reversal`
# algorithm. Runs every hour because HL funding settles hourly.
from app.services.scanners.funding_reversal import scan_once as funding_scan
_scheduler.add_job(
funding_scan, "cron", minute=7, # :07 every hour, away from other jobs
id="funding_reversal_scan", max_instances=1, coalesce=True,
)
logger.info("Funding reversal scanner scheduled hourly at :07.")
# ── KOL Substack poller (daily) ──────────────────────────────────────
# Hayes & co publish at most a few times a week. Daily poll is plenty;
# RSS dedupe by URL is idempotent if it ever fires twice.
from app.services.kol_substack import run_substack_poll
_scheduler.add_job(
run_substack_poll, "cron", hour=1, minute=15,
id="kol_substack_poll", max_instances=1, coalesce=True,
)
logger.info("KOL Substack poller scheduled daily at 01:15 UTC.")
# ── KOL A-tier: on-chain holdings snapshot (daily) ────────────────────
# Polls HL public API (free) for perp positions; Arkham (key optional)
# for full portfolio. Diffs against yesterday's snapshot → writes
# kol_holding_changes. Runs at 02:00 UTC, after Substack poll finishes.
from app.services.kol_onchain import run_onchain_poll
_scheduler.add_job(
run_onchain_poll, "cron", hour=2, minute=0,
id="kol_onchain_poll", max_instances=1, coalesce=True,
)
logger.info("KOL on-chain holdings poller scheduled daily at 02:00 UTC.")
# ── KOL talks-vs-trades divergence scan (daily) ───────────────────────
# Runs after the on-chain poll finishes. Matches post ticker signals vs
# holding changes for the same KOL+ticker within ±7 days.
from app.services.kol_divergence import run_divergence_scan
_scheduler.add_job(
run_divergence_scan, "cron", hour=2, minute=15,
id="kol_divergence_scan", max_instances=1, coalesce=True,
)
logger.info("KOL divergence scan scheduled daily at 02:15 UTC.")
_scheduler.start()
logger.info(
"Truth Social pollers scheduled every %ds (CNN + trumpstruth.org).",
settings.truth_social_poll_seconds,
)
# ── Telegram bot long-poll loop (optional) ────────────────────────────
# Only started if TELEGRAM_BOT_TOKEN is set. Handles /start CODE bindings
# and /stop, /status, /test commands. Survives transient network errors
# via internal back-off.
from app.services.telegram_bot import run_bot_loop
_telegram_task = asyncio.create_task(run_bot_loop(), name="telegram_bot")
logger.info("Telegram bot task created (will no-op if token missing).")
yield
# Shutdown
@@ -102,6 +202,12 @@ async def lifespan(app: FastAPI):
await _binance_task
except asyncio.CancelledError:
pass
if _telegram_task and not _telegram_task.done():
_telegram_task.cancel()
try:
await _telegram_task
except asyncio.CancelledError:
pass
await engine.dispose()
logger.info("Shutdown complete.")
@@ -114,11 +220,18 @@ app = FastAPI(
)
# CORS
allowed_origins = [
settings.frontend_url,
"http://localhost:3001",
"http://localhost:3000",
]
# In production we only allow the canonical frontend origin (FRONTEND_URL).
# In development we additionally permit the local Next dev server. NEVER
# permit "*" here — every endpoint either reads/writes user-personalised
# data or accepts signed envelopes, both of which require credentialled
# requests (and "*" is rejected by browsers in combination with credentials
# anyway).
allowed_origins = [settings.frontend_url]
if settings.environment == "development":
allowed_origins.extend([
"http://localhost:3000",
"http://localhost:3001",
])
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
@@ -134,6 +247,13 @@ app.include_router(trades_router, prefix="/api")
app.include_router(performance_router, prefix="/api")
app.include_router(subscribe_router, prefix="/api")
app.include_router(user_router, prefix="/api")
app.include_router(funding_signal_router, prefix="/api")
app.include_router(funding_reversal_router, prefix="/api")
app.include_router(telegram_router, prefix="/api")
app.include_router(signals_router, prefix="/api")
app.include_router(positions_router, prefix="/api")
app.include_router(scanners_router, prefix="/api")
app.include_router(kol_router, prefix="/api")
@app.get("/api/health")