fix: pre-launch hardening — HYPE price feed, KOL wallet cleanup, Telegram Trump alert, rate limiting, brittle test

Batch of the pre-launch audit campaign (BUG-01…14 plus three new features):

Pricing / TP-SL protection
- Add app/services/hl_price_feed.py: supplemental HL allMids poller for
  HL-native assets (HYPE, PURR) not listed on Binance. Pumps price_store +
  tp_sl_monitor.on_price_tick so bot trades on these assets keep full
  stop-loss / take-profit / trailing protection instead of max-hold only.
- Wire feed into main.py lifespan (startup task + graceful shutdown cancel).

Telegram
- Add format_trump_mention + PATH B in _dispatch: crypto-relevant Trump
  posts with no directional signal (relevant=True, signal=hold) now alert
  the public channel only (no per-subscriber noise).
- Rate limiter (slowapi) on the API; assorted bot/digest fixes.

KOL on-chain
- seed_kol_wallets.py: KOL_FEEDS coverage cross-check; reversibly deactivate
  orphaned wallets (handle not in KOL_FEEDS → can never produce divergence)
  so the scanner stops burning cycles on them.

Tests / misc
- Fix brittle test_macro_ahr999_uses_same_formula_as_scanner: mock now uses
  realistic ms timestamps so the in-progress-day drop fires, matching the
  fetcher's bar count (was 0.3179 vs 0.3178 off-by-one).
- Refresh stale notify_signal comment in truth_social.py.

Frontend reduce-action type fix lives in the sibling repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-29 11:57:19 +08:00
parent 6471e44aac
commit d6c802ef26
40 changed files with 1833 additions and 209 deletions
+74 -12
View File
@@ -13,7 +13,7 @@ What it does:
* KOL Substack / podcast / blog polls
* KOL on-chain snapshots (HL perps + Etherscan ERC-20 balances)
* KOL divergence detector
* BTC bottom-reversal + funding-reversal scanners
* Optional: BTC bottom-reversal + funding-reversal scanners
What it INTENTIONALLY DOES NOT touch:
* posts where source IN ('truth', 'btc_bottom_reversal', 'funding_reversal',
@@ -33,6 +33,10 @@ Usage:
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --dry-run
# Seed only (pure pre-launch warmup, no truncation / deletion):
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --seed-only
# Execute:
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --yes
@@ -198,7 +202,29 @@ async def wipe_phase(dry_run: bool) -> None:
print(green(" ✓ wipe committed"))
async def seed_real_data() -> None:
async def abort_if_live_user_state() -> bool:
"""Refuse destructive launch seeding if the DB already looks user-live."""
async with AsyncSessionLocal() as db:
active_subs = (await db.execute(
select(func.count(Subscription.id)).where(Subscription.active == True)
)).scalar() or 0
tg_bindings = (await db.execute(
select(func.count(TelegramBinding.id))
)).scalar() or 0
if active_subs or tg_bindings:
print(red(
"Refusing to run launch_seed on a DB that already has live user state "
f"(active_subscriptions={active_subs}, telegram_bindings={tg_bindings})."
))
print(yellow(
"Use --dry-run to inspect only. If you truly intend to clean this DB, "
"do it manually with a one-off migration/backup plan."
))
return True
return False
async def seed_real_data(*, exercise_scanners: bool = False) -> None:
"""Re-fetch every upstream source. All are idempotent on (source, external_id)."""
print(bold("\n── SEED (re-running upstream fetches) ──"))
@@ -237,15 +263,21 @@ async def seed_real_data() -> None:
except Exception as e:
print(red(f" ✗ FAILED: {type(e).__name__}: {e}"))
print("\n [5/5] BTC bottom + funding reversal scanners (exercise the path)...")
from app.services.scanners.btc_bottom_reversal import scan_once as btc_scan
from app.services.scanners.funding_reversal import scan_once as funding_scan
for name, fn in [("btc_bottom", btc_scan), ("funding_reversal", funding_scan)]:
try:
await fn()
print(green(f"{name} scan completed (fire conditional on market state)"))
except Exception as e:
print(red(f"{name} FAILED: {type(e).__name__}: {e}"))
if exercise_scanners:
print("\n [5/5] BTC bottom + funding reversal scanners (exercise the path)...")
from app.services.scanners.btc_bottom_reversal import scan_once as btc_scan
from app.services.scanners.funding_reversal import scan_once as funding_scan
for name, fn in [("btc_bottom", btc_scan), ("funding_reversal", funding_scan)]:
try:
await fn()
print(green(f"{name} scan completed (fire conditional on market state)"))
except Exception as e:
print(red(f"{name} FAILED: {type(e).__name__}: {e}"))
else:
print(yellow(
"\n [5/5] Scanner exercise skipped by default. "
"Use --exercise-scanners only if you explicitly want signal emission side-effects."
))
async def main() -> int:
@@ -253,16 +285,46 @@ async def main() -> int:
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--dry-run", action="store_true",
help="show what would be deleted, no DB writes")
p.add_argument("--seed-only", action="store_true",
help="run upstream fetch/warmup only; skip all wipe/truncation steps")
p.add_argument("--yes", action="store_true",
help="actually perform the wipe (required without --dry-run)")
p.add_argument("--no-seed", action="store_true",
help="skip the upstream re-fetch step")
p.add_argument("--exercise-scanners", action="store_true",
help="also run standalone scanners during seed (may emit signals/alerts)")
args = p.parse_args()
if args.seed_only and args.dry_run:
print(red("Choose either --dry-run or --seed-only, not both."))
return 2
if args.seed_only and args.no_seed:
print(red("--seed-only conflicts with --no-seed."))
return 2
if args.seed_only:
before = await report_counts("BEFORE")
await seed_real_data(exercise_scanners=args.exercise_scanners)
after = await report_counts("AFTER")
print(bold("\n── DELTA ──"))
for k in sorted(before):
if k.startswith("_"): continue
d = after[k] - before[k]
arrow = green(f"+{d}") if d > 0 else (red(str(d)) if d < 0 else " ·")
print(f" {k:25s} {before[k]:6d}{after[k]:6d} ({arrow})")
print(bold(green("\n✓ seed-only warmup complete. Next: start backend and run scripts/launch_smoke.py.")))
return 0
if not args.dry_run and not args.yes:
print(red("Refusing to run without --yes (or use --dry-run to preview)."))
return 2
if not args.dry_run and await abort_if_live_user_state():
return 2
before = await report_counts("BEFORE")
await wipe_phase(dry_run=args.dry_run)
@@ -271,7 +333,7 @@ async def main() -> int:
return 0
if not args.no_seed:
await seed_real_data()
await seed_real_data(exercise_scanners=args.exercise_scanners)
after = await report_counts("AFTER")