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:
+130
-10
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user