Files
k 5fb1d52026 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>
2026-05-25 00:52:56 +08:00

196 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()