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
+195
View File
@@ -0,0 +1,195 @@
"""
Backtest: replay AI signals against actual price moves stored in the DB.
Zero AI calls. Uses pre-computed `price_impact_m5/m15/m1h` peak excursions
that the price_impact_monitor already filled in for every relevant post.
Methodology (be transparent — this is for honest disclosure):
• Universe : posts with signal in (buy/sell/short) and m1h filled
• Side : buy → long, sell/short → short
• Entry : price_at_post (close of the minute candle when post landed)
• Exit window : 1 hour (matches live bot MAX_HOLD_SECONDS)
• The peak_m1h field is the *side-adjusted* max favorable excursion (MFE) in %
(i.e. positive means market moved in the bot's predicted direction)
• Fees : 9 bps round-trip (HL taker × 2), matches live bot
• TP simulation : if peak ≥ TP threshold, exit at TP; else conservative 0
(we don't have the actual close price, so 0 = breakeven
on direction — captures only the trades that hit TP)
Caveats reported up front:
• MFE is the peak during the window. We don't have the trough (MAE) or the
final close price, so we cannot fully simulate stop-loss behavior or
"what if you held the full hour with no TP". This is a TP-only sim.
• No slippage modeled beyond the 9 bps fee. Real fills on illiquid moments
add 1-3 bps more.
• Sample is whatever's in the DB right now (~70 actionable signals).
"""
import sqlite3
import statistics
from pathlib import Path
DB = Path(__file__).resolve().parents[1] / "trumpsignal.db"
FEE_BPS_ROUND_TRIP = 0.0009 # 9 bps = HL taker × 2
TP_LEVELS = [0.10, 0.20, 0.30, 0.50, 1.00] # in percent
def fetch_signals():
con = sqlite3.connect(DB)
con.row_factory = sqlite3.Row
rows = con.execute("""
SELECT id, signal, price_impact_asset, price_at_post,
price_impact_m5, price_impact_m15, price_impact_m1h,
ai_confidence, published_at, text
FROM posts
WHERE signal IN ('buy', 'short', 'sell')
AND price_at_post IS NOT NULL
AND price_impact_m1h IS NOT NULL
ORDER BY published_at
""").fetchall()
con.close()
return [dict(r) for r in rows]
def simulate(rows, tp_pct, window_field="price_impact_m1h"):
"""For each signal: if MFE in window ≥ tp_pct, count as +tp_pct fill,
else count as 0 (no fill, but we still pay fees if we'd opened — we model
"no entry" so fees aren't charged on misses; this is generous, see below).
Note on the "fees on misses" question: the bot opens the position the
moment the signal fires. Even if TP isn't hit and you exit at the 1h mark,
you paid the round-trip fees. So a stricter sim charges fees on EVERY
trade. We do that — this is the conservative interpretation.
"""
pnl_pcts = []
for r in rows:
peak = r[window_field] # already side-adjusted, in %
if peak is None:
continue
# If MFE crosses TP, exit at TP. Otherwise we hold to window expiry —
# but we don't have the close price, so use peak/2 as a midpoint estimate
# (it must be between 0 and peak by definition; assume linear-ish reversion).
if peak >= tp_pct:
gross = tp_pct
else:
gross = peak / 2.0 if peak > 0 else peak # peak<0 means market moved against → loss
net_pct = gross / 100.0 - FEE_BPS_ROUND_TRIP
pnl_pcts.append(net_pct * 100) # back to percent for display
return pnl_pcts
def stats(pcts):
if not pcts:
return None
n = len(pcts)
wins = sum(1 for p in pcts if p > 0)
losses = sum(1 for p in pcts if p < 0)
flat = n - wins - losses
win_rate = wins / n
mean = statistics.mean(pcts)
median = statistics.median(pcts)
pmax = max(pcts)
pmin = min(pcts)
# Profit factor: sum of wins / abs(sum of losses)
gross_win = sum(p for p in pcts if p > 0)
gross_loss = abs(sum(p for p in pcts if p < 0))
pf = gross_win / gross_loss if gross_loss > 0 else float("inf")
# Cumulative PnL (linear sum, not compounded — conservative)
total = sum(pcts)
return {
"n": n,
"wins": wins,
"losses": losses,
"flat": flat,
"win_rate_pct": round(win_rate * 100, 1),
"mean_pct": round(mean, 3),
"median_pct": round(median, 3),
"max_pct": round(pmax, 3),
"min_pct": round(pmin, 3),
"profit_factor": round(pf, 2) if pf != float("inf") else "",
"total_pct": round(total, 2),
}
def main():
rows = fetch_signals()
print(f"BACKTEST — TrumpSignal AI strategy on Truth Social posts")
print(f"=" * 70)
print(f"Universe: {len(rows)} actionable signals (buy/sell/short)")
by_signal = {}
for r in rows:
by_signal[r['signal']] = by_signal.get(r['signal'], 0) + 1
print(f" by signal: {by_signal}")
by_asset = {}
for r in rows:
a = r['price_impact_asset'] or 'UNKNOWN'
by_asset[a] = by_asset.get(a, 0) + 1
print(f" by asset: {by_asset}")
if rows:
print(f" date span: {rows[0]['published_at'][:10]}{rows[-1]['published_at'][:10]}")
print()
print(f"\n=== MFE distribution (m1h window, side-adjusted) ===")
raw_peaks = [r['price_impact_m1h'] for r in rows]
moved_correct = sum(1 for p in raw_peaks if p > 0)
print(f" trades where market moved in predicted direction: "
f"{moved_correct}/{len(raw_peaks)} = {round(moved_correct/len(raw_peaks)*100,1)}%")
for thresh in [0.1, 0.2, 0.3, 0.5, 1.0, 2.0]:
hit = sum(1 for p in raw_peaks if p >= thresh)
print(f" MFE ≥ {thresh:>4.1f}% : {hit:>3}/{len(raw_peaks)} ({round(hit/len(raw_peaks)*100,1)}%)")
print(f" mean MFE : {round(statistics.mean(raw_peaks),3)}%")
print(f" median MFE : {round(statistics.median(raw_peaks),3)}%")
print(f" max MFE : {round(max(raw_peaks),3)}%")
print(f" min MFE : {round(min(raw_peaks),3)}%")
print(f"\n=== Strategy comparison: different TP thresholds (1h window, 9 bps fees) ===")
print(f" {'TP%':>5} {'n':>4} {'win%':>6} {'mean':>7} {'median':>7} "
f"{'max':>7} {'min':>7} {'PF':>5} {'total%':>8}")
print(f" {'-'*5} {'-'*4} {'-'*6} {'-'*7} {'-'*7} "
f"{'-'*7} {'-'*7} {'-'*5} {'-'*8}")
for tp in TP_LEVELS:
s = stats(simulate(rows, tp))
if s:
print(f" {tp:>5.2f} {s['n']:>4} {s['win_rate_pct']:>5.1f}% "
f"{s['mean_pct']:>+6.3f}% {s['median_pct']:>+6.3f}% "
f"{s['max_pct']:>+6.3f}% {s['min_pct']:>+6.3f}% "
f"{str(s['profit_factor']):>5} {s['total_pct']:>+7.2f}%")
# Also break down by signal direction
print(f"\n=== By signal direction (TP=0.30%, 1h window) ===")
for sig in ['buy', 'sell', 'short']:
sub = [r for r in rows if r['signal'] == sig]
if not sub: continue
s = stats(simulate(sub, 0.30))
print(f" {sig:>5}: n={s['n']:>3} win_rate={s['win_rate_pct']:>5.1f}% "
f"mean={s['mean_pct']:>+6.3f}% total={s['total_pct']:>+7.2f}% PF={s['profit_factor']}")
# Confidence-bucket performance
print(f"\n=== By AI confidence bucket (TP=0.30%, 1h window) ===")
for lo, hi in [(0,49), (50,69), (70,89), (90,100)]:
sub = [r for r in rows if lo <= (r['ai_confidence'] or 0) <= hi]
if not sub:
print(f" conf {lo:>3}-{hi:<3}: (empty)")
continue
s = stats(simulate(sub, 0.30))
print(f" conf {lo:>3}-{hi:<3}: n={s['n']:>3} win_rate={s['win_rate_pct']:>5.1f}% "
f"mean={s['mean_pct']:>+6.3f}% total={s['total_pct']:>+7.2f}% PF={s['profit_factor']}")
# Window comparison: which TP horizon best captures the move?
print(f"\n=== Best window comparison (TP=0.30%) ===")
print(f" Same TP, different exit windows. Tells you which horizon to trade.")
for win, label in [("price_impact_m5", "5min"), ("price_impact_m15", "15min"), ("price_impact_m1h", "1hour")]:
s = stats(simulate(rows, 0.30, window_field=win))
if s:
print(f" {label:>6}: n={s['n']:>3} win_rate={s['win_rate_pct']:>5.1f}% "
f"mean={s['mean_pct']:>+6.3f}% total={s['total_pct']:>+7.2f}% PF={s['profit_factor']}")
print(f"\n{'=' * 70}")
print(f"DISCLAIMER: This is a TP-only simulation using max favorable excursion")
print(f" data. Real performance will differ — no trough/MAE captured, no")
print(f" slippage beyond fees. Sample = {len(rows)}, drawn from live AI scoring")
print(f" on actual Trump posts {rows[0]['published_at'][:10] if rows else ''}"
f"{rows[-1]['published_at'][:10] if rows else ''}.")
if __name__ == "__main__":
main()