fix: de-risk launch_seed, monitor price feeds, enforce single-process

Three pre-launch audit findings:

launch_seed.py — destructive cleanup is now OPT-IN
  Previously `launch_seed.py --yes` truncated KOL history (divergence /
  holdings / posts older than 30d) by default — an accidental run erased
  unrecoverable data, and abort_if_live_user_state() only guards on
  subscriptions/bindings, not KOL history. Now nothing is deleted unless
  --wipe is passed; the safe path is --seed-only (pure fetch). Bare/--yes
  without --wipe refuses and prints guidance.

/api/health/deep — monitor the price feeds, not just scrapers
  The deep healthcheck only watched the (redundant) Trump scrapers + DB, so a
  dead Binance/HL price feed — which silently stops ALL tp_sl_monitor stop-loss
  / take-profit firing on live trades — left health green. Added per-feed
  liveness (binance.last_tick_at, hl_price_feed.last_tick_at) with a 180s boot
  grace so startup doesn't false-503. Body now includes price_feeds[].

Single-process enforcement (multi-worker safety)
  The backend is single-process by design (in-memory scheduler, replay cache,
  tp_sl table, price_store). systemd unit lacked the --workers 1 + rationale
  that supervisor.conf already had; added it. Added a runtime advisory file
  lock (app.main._acquire_singleton_lock): only the leader starts background
  tasks; extra workers serve HTTP reads only and log CRITICAL. health/deep now
  reports is_leader so the misconfig is visible to monitors.

72 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-29 14:29:24 +08:00
parent 520bd7243d
commit 6876c0c280
5 changed files with 167 additions and 16 deletions
+37 -14
View File
@@ -28,25 +28,29 @@ What it INTENTIONALLY DOES NOT touch:
baseline for the next on-chain poll (without it, the next snapshot
would diff against nothing and produce zero kol_holding_changes).
Usage:
# Preview (no DB writes):
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --dry-run
DESTRUCTIVE STEPS ARE OPT-IN. By default this script only SEEDS (re-fetches
upstream data); it will NOT delete or truncate anything unless you explicitly
pass --wipe. This prevents an accidental `launch_seed.py --yes` from erasing
KOL history (divergence / holdings / posts) that cannot be recovered.
# Seed only (pure pre-launch warmup, no truncation / deletion):
Usage:
# Pure pre-launch warmup — fetch only, NOTHING deleted (the safe default):
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --seed-only
# Execute:
# Preview what a wipe WOULD delete (no DB writes):
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --yes
venv/bin/python scripts/launch_seed.py --wipe --dry-run
# Skip the SEED step (just clean — useful if you'd rather let the
# scheduled poll jobs trigger naturally over the next 24h):
DATABASE_URL='...' venv/bin/python scripts/launch_seed.py --yes --no-seed
# Execute the destructive wipe + reseed (requires BOTH --wipe and --yes):
DATABASE_URL='sqlite+aiosqlite:///./trumpsignal.db' \\
venv/bin/python scripts/launch_seed.py --wipe --yes
NOT safe to re-run after real users are on the platform — the KOL truncation
will erase divergence/alignment context that you can't recover.
# Wipe only, skip the reseed (let scheduled polls refill over 24h):
DATABASE_URL='...' venv/bin/python scripts/launch_seed.py --wipe --yes --no-seed
The --wipe path is NOT safe to re-run after real users are on the platform —
the KOL truncation erases divergence/alignment context you can't recover.
"""
from __future__ import annotations
@@ -283,12 +287,15 @@ async def seed_real_data(*, exercise_scanners: bool = False) -> None:
async def main() -> int:
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--wipe", action="store_true",
help="OPT IN to destructive cleanup (drop obsolete-source posts + "
"truncate KOL window). Without this flag nothing is ever deleted.")
p.add_argument("--dry-run", action="store_true",
help="show what would be deleted, no DB writes")
help="with --wipe: 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)")
help="with --wipe: 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",
@@ -303,6 +310,22 @@ async def main() -> int:
print(red("--seed-only conflicts with --no-seed."))
return 2
if args.seed_only and args.wipe:
print(red("--seed-only conflicts with --wipe (seed-only never deletes)."))
return 2
# Default to the SAFE path. A bare invocation, or --yes/--dry-run/--no-seed
# WITHOUT --wipe, must never delete data. Steer the user to --seed-only.
if not args.wipe and not args.seed_only:
print(red("Refusing to run a destructive path without --wipe."))
print(yellow(
"This script seeds by default and only deletes when --wipe is given.\n"
" • Pure pre-launch fetch (recommended): --seed-only\n"
" • Preview a destructive wipe: --wipe --dry-run\n"
" • Execute a destructive wipe + reseed: --wipe --yes"
))
return 2
if args.seed_only:
before = await report_counts("BEFORE")
await seed_real_data(exercise_scanners=args.exercise_scanners)