Files
trumpsignal-backend/CLAUDE.md
T
k 520bd7243d fix(BUG-02): rate limiter now keys on real client IP + enforces default_limits
The frontend proxy fix alone was incomplete. Backend slowapi used the default
get_remote_address (request.client.host), which is the proxy's IP because
uvicorn runs without --proxy-headers — so the relayed x-forwarded-for was
ignored and all users still shared one rate-limit bucket.

- Add app/ratelimit.py: shared `limiter` + `client_ip_key` that reads
  x-forwarded-for[0] → x-real-ip → peer. Replaces the three independent
  Limiter(get_remote_address) instances in main.py / posts.py / prices.py
  (which also had separate, non-shared storage).
- Register SlowAPIMiddleware so default_limits ("60/minute") applies to EVERY
  route. Previously only the 2 decorated read endpoints were limited; all
  signed-mutation routes had no rate limit at all (the "20/min per-route"
  comment was aspirational — no such decorator existed).
- Add tests/test_ratelimit.py (7 tests): XFF precedence, fallbacks, two users
  behind one proxy get distinct keys, middleware-registered guard.

72 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:55:00 +08:00

27 KiB
Raw Blame History

Trump Alpha — Backend

AI-powered crypto signal aggregator. Surfaces four uncorrelated signal streams (Trump Truth Social, Macro Vibes, KOL talks-vs-trades, funding reversal) and runs an optional execution layer on Hyperliquid perps. Real money — handle every change to the trading layer like surgery.

This file is the first thing an AI agent should read when entering this repo. It encodes the invariants that aren't visible from any single file.


This backend manages real Hyperliquid leveraged positions for real users. Bugs cost users money. Five non-negotiable rules:

  1. Two systems, one execution layer. System 1 (Trump scalp) auto-opens. System 2 (Macro Vibes / reversal) is MANAGE-ONLY since v2.0 — the user opens manually on Hyperliquid, then /adopt hands it to the bot. process_post() early-returns for sys2 sources. Re-enabling sys2 auto-open would silently reintroduce all the leverage/budget/concurrency race conditions we excised. Don't do it without an ADR.

  2. released_at is the "user took back control" marker. A trade with released_at IS NOT NULL is OUT OF BOUNDS for the bot:

    • recovery.rehydrate_open_trades skips it
    • reconciler skips it
    • close_and_finalize's atomic claim requires released_at IS NULL unless force=True (only manual_close passes that)
    • partial_derisk and pyramid_add early-return idempotent-success
    • /positions/open, /positions/today, telegram_digest all filter it If you add ANY new code path that touches BotTrade rows, ask yourself "does this respect released_at?" Almost always yes.
  3. Effective exit params are FROZEN on the BotTrade row at open time. See the eff_* columns on BotTrade. Recovery rebuilds the watchdog from these, NOT from the live Subscription. Without this, restarting the backend silently rewrites every open System-2 reversal's stop loss to the user's Trump scalp setting (1.5%). NEVER read live Subscription.stop_loss_pct etc. in the close path.

  4. HL leverage is what HL says, not what the user requested. Hyperliquid silently clips the requested leverage to the asset's max (memes capped at 3×). hyperliquid.open_position() returns effective_leverage — bot_engine and adoption must use THAT value to compute sys2_protective_stop_pct(lev) and the derisk ladder. Using the requested value puts the stop OUTSIDE the real liquidation line.

  5. Per-wallet asyncio locks wrap "check + open" critical sections. See _wallet_lock in bot_engine and _wallet_adopt_lock in adoption. Without them, two concurrent signals can both pass the daily-budget / concurrency / already-open check before either commits.


What this product does (90 seconds)

Four signal sources → one bot → optional Hyperliquid execution

┌──────────────────────────┐
│ 1. Trump Truth Social    │── auto-classify (DeepSeek) → "buy"/"short"/"noise"
│    (every post, <3s)     │   if actionable: Trump scalp auto-open (System 1)
└──────────────────────────┘   Tight 1.5% SL, 12h cooldown, ≥30min min-hold

┌──────────────────────────┐
│ 2. Macro Vibes           │── 8 daily macro indicators (AHR999, F&G, etc.)
│    (BTC bottom + funding)│   + 2-of-3 bottom-reversal trigger
└──────────────────────────┘   Telegram alert with /adopt CTA — NO auto-open.
                                User opens on HL → /adopt → bot manages with
                                5-rung stop ladder, de-risk, pyramid, peak-trail.

┌──────────────────────────┐
│ 3. KOL talks-vs-trades   │── Substack/podcast ingest + ETH on-chain diff
│    (19 KOLs, daily)      │   Divergence (publicly bullish, secretly selling)
└──────────────────────────┘   is the platform's highest-conviction signal.
                                Telegram alert only — never auto-trades.

┌──────────────────────────┐
│ 4. Funding extreme       │── Hourly BTC perp funding scan
└──────────────────────────┘   Alert only (manage-only via /adopt like Macro)

┌──────────────────────────┐
│   Telegram daily digest  │── Once-a-day 3-section brief (Macro/KOL/Trump)
│   (per-user hour, opt-out)│  to every subscriber. Cron at minute=0 each hour.
└──────────────────────────┘

Free tier = read everything + Telegram alerts. Pro tier (Hyperliquid wallet linked) = Trump auto-trade + /adopt manage-only flow for sys2.


Stack

  • Python 3.9+ / FastAPI / async SQLAlchemy 2.x / APScheduler
  • DB: SQLite dev, Postgres prod. All schema lives in alembic/versions/NNN_*.py, ordered. Currently at head 026.
  • AI: DeepSeek via OpenAI-compatible API (AI_BASE_URL, AI_MODEL).
    • Live scoring uses AI_LIVE_MODEL (~2s, latency-critical)
    • Batch / reanalysis uses AI_MODEL (quality, ~10s)
  • Trading: Hyperliquid SDK; API-wallet keys are envelope-encrypted with ENCRYPTION_KEY (KEK), per-user DEK derivation via crypto.py.
  • Prices: Binance WS (binance.py) feeds price_store + powers the tp_sl_monitor per-tick evaluator.
  • Telegram: long-poll mode (single instance), HTML messages, inline keyboards. telegram.py send/edit/answer + telegram_bot.py commands. Public channel broadcast (TELEGRAM_PUBLIC_CHANNEL_ID env) sends a sanitised format_public_post version (no execution details, tier label instead of raw confidence) after every per-user fan-out in _dispatch.

Module map (where things live)

app/
├── api/                    HTTP routes
│   ├── signals.py          POST /api/signals/ingest  ← scanners write here
│   ├── positions.py        /positions/open|today|close|grow|adopt|release|hl
│   ├── user.py             /subscribe|settings|manual-window|auto-trade
│   ├── telegram.py         /telegram/{preferences,bind,unbind,test}
│   ├── macro.py            /macro/{snapshot,history}
│   └── kol.py              /kol/{posts,digest,wallets,divergence}
├── services/
│   ├── bot_engine.py       ★ TRADING CORE — process_post, _execute_for_subscriber,
│   │                         close_and_finalize, partial_derisk, pyramid_add
│   ├── adoption.py         ★ /adopt + /release flow (sys2 manage-only)
│   ├── tp_sl_monitor.py    Per-price-tick close evaluator. on_price_tick is
│   │                         called from binance.py once per second
│   ├── hyperliquid.py      HL trader (open/close/reduce/leverage)
│   ├── recovery.py         Startup rehydration of open BotTrades into watchdog
│   ├── reconciler.py       Every 60s: compare DB ↔ HL state, mark drift
│   ├── circuit_breaker.py  Per-system (sys1/sys2) CB, daily DD + N-loss streak
│   ├── signal_categories.py CRITICAL CONFIG — sys1/sys2 sources, ladders,
│   │                         leverage clamping, protective stop formulas
│   ├── regime_filter.py    Sys1 only — recent-move / vol-contraction gates
│   ├── analysis.py         AI signal scoring (DeepSeek)
│   ├── entry_filter.py     Cheap text-based pre-filter (skip RT/URL-only)
│   ├── telegram.py         send_message / edit_message / answer_callback
│   ├── telegram_bot.py     Long-poll loop + /start /digest /adopt /release ...
│   ├── telegram_digest.py  Daily 3-section brief (rule-based; no LLM)
│   ├── price_store.py      In-memory latest price per asset
│   ├── price_backfill.py   Backfill historical 5min bars from Binance
│   ├── crypto.py           HL API-key envelope encryption
│   ├── scanner_state.py    In-memory toggle + observability for scanners
│   ├── macro/
│   │   ├── fetchers.py     8 macro indicator HTTP fetchers (each @_none_on_fail)
│   │   ├── scoring.py      Weighted composite -100..+100
│   │   └── poll.py         Daily UPSERT into macro_snapshots
│   ├── scanners/
│   │   ├── btc_bottom_reversal.py   2-of-3 AHR999 + 200WMA + Pi Bottom
│   │   ├── funding_reversal.py       Hourly funding extreme
│   │   └── sma_reclaim.py            (archive — not scheduled)
│   ├── kol_substack.py     RSS ingest for 19 KOL feeds
│   ├── kol_onchain.py      HL public API + Etherscan diff
│   ├── kol_divergence.py   Cross-ref talks vs trades within ±7d
│   ├── kol_analysis.py     AI ticker/direction/conviction extract
│   ├── bottom_indicators.py AHR999 / Pi Cycle / 200WMA math
│   └── funding_signal.py   Real-time funding extreme detector
├── scrapers/
│   └── truth_social.py     CNN + trumpstruth.org pollers (5s interval)
├── ws/
│   └── manager.py          WebSocket fan-out for live UI updates
├── models.py               ★ All SQLAlchemy models in one file
├── database.py             Async engine + session factory
├── config.py               Pydantic Settings — reads .env
└── main.py                 FastAPI lifespan, scheduler setup, route mount

alembic/versions/           Migrations (numbered NNN). Latest = 026
                            026 = composite index (wallet_address, closed_at) on bot_trades
scripts/                    One-shot ops
├── preflight.py            Pre-launch readiness gate (env / DB / TG / AI)
├── launch_smoke.py         End-to-end smoke (14 checks against running API)
├── rescore_v5.py           Re-score every Post with current AI prompt
├── backfill_signals.py     Fill in signal for posts missing it
└── verify_sys2_lifecycle.py Manual System-2 lifecycle walk-through

tests/                      pytest, 64 tests, fast (<1s total)

The two trading systems (memorise this)

                  System 1                  System 2
                  ────────                  ────────
Source            "truth"                   "btc_bottom_reversal"
                                            (+"funding_reversal" alert-only)
Trigger           Trump posts a thing       Daily scanner: 2-of-3 confluence
Latency need      Seconds (price moves fast) Days/weeks (signal lives a long time)
Open path         Auto (bot_engine.process_post fires _execute_for_subscriber)
                                            MANUAL: user opens on HL UI, then
                                            /adopt hands it to the bot
Stop loss         User-configured + tight   sys2_protective_stop_pct(actual_lev)
                  1.5% floor (TRUMP_*)      = 85% × (100/lev), capped at 35%
Exit model        TP / trailing / SL        5-rung stop ladder + downside de-risk
                                            + pyramid + peak-trail. NO TP.
Min hold          30 min (suppresses TP)    n/a
Max hold          6h                        18 months (ladder is the real exit)
Sizing            base × regime multiplier  Whatever user opened on HL
Concurrency cap   n/a                       3 positions / wallet (correlated beta)
Confidence min    88 (platform) / user      85 (platform)
Circuit breaker   sys1_*                    sys2_*  (independent)
Daily budget      Full daily_budget_usd     n/a — user controls notional on HL
Telegram alert    Trump alert format        Macro/funding alert + /adopt CTA

If you're tempted to put sys2 logic in _execute_for_subscriber: stop. process_post() early-returns for sys2. The function only runs for sys1 now. The dead sys2 branches inside _execute_for_subscriber are kept for diff minimalism — don't extend them.


The /adopt flow (System-2 lifecycle in detail)

1. Scanner fires
   └─ POST /api/signals/ingest  (source=btc_bottom_reversal, signal=buy)
   └─ Post row created
   └─ process_post() early-returns for sys2 (no auto-open)
   └─ notify_signal() → Telegram fan-out with /adopt CTA appended

2. User opens BTC long on Hyperliquid manually
   └─ size / leverage of their choice

3. User in bot: /adopt
   └─ adoption.list_hl_positions(wallet) reads HL state
   └─ Telegram inline keyboard: tap [🟢 BTC long $1500 @72k · 2x]
   └─ Mode picker: [📈 Standard] or [🚀 Aggressive]
   └─ adoption.adopt_position(wallet, asset, mode):
      a. Per-wallet asyncio lock acquired
      b. Pre-flight: no_subscription / no_hl_key / paper_mode /
         circuit_breaker (sys2 CB still gates adopt!) /
         already_adopted / concurrency_cap (3)
      c. Re-read HL state inside lock (fresh entry/size/lev)
      d. Resolve sys2_protective_stop_pct(HL_actual_leverage)
      e. INSERT BotTrade with eff_* frozen + sys2_mode + hl_order_id="adopted:<ts>"
         + trigger_post_id=NULL
      f. register_trade() with full ladder/de-risk/addon/peak_trail
   └─ Telegram confirmation w/ ladder summary

4. tp_sl_monitor drives the position
   └─ Stop ratchet, downside de-risk partial reduces, pyramid add-ons,
      peak-trail close, max_hold backstop. All staged through the
      lock-protected partial_derisk / pyramid_add / close_and_finalize.

5a. User wants out: /release
    └─ release_management(wallet, trade_id):
       a. Sets BotTrade.released_at = now
       b. unregister(trade_id) from watchdog
    └─ HL position UNTOUCHED — bot stops driving, user has manual control
    └─ Across restarts: recovery skips released rows (released_at filter)
    └─ Reconciler skips them. Released trades don't appear in
       /positions/open or the digest "your status" line.

5b. Bot drives the close (ladder / max-hold)
    └─ close_and_finalize() atomic claim sets closed_at, computes pnl
    └─ Trade is now CLOSED (closed_at set, exit_price + pnl_usd written)

5c. User force-closes via UI: POST /api/positions/{id}/close
    └─ manual_close calls close_and_finalize(force=True) — bypasses
       released_at guard. Works on adopted-and-released trades too.

6. Recovery on restart:
   └─ recovery.rehydrate_open_trades reads BotTrade WHERE closed_at IS NULL
      AND released_at IS NULL
   └─ For each: rebuild ladder from sys2_mode + (category OR adopted fallback).
      The fallback for trigger_post_id IS NULL is the critical fix —
      without it adopted trades lose their entire sys2 ladder on restart.

Critical invariants checklist (when reviewing any trading change)

  • Does this code path respect released_at IS NULL?
  • Does it use eff_* (frozen) not live Subscription.* for exit math?
  • If it opens a new position, does it use HL's actual leverage (not requested)?
  • If it touches an open position concurrently, is it wrapped in the per-trade _lock_for(trade_id) lock?
  • If it opens, is it inside _wallet_lock(wallet) so the budget / concurrency check is atomic with the write?
  • If it closes, does it use the conditional UPDATE ... WHERE closed_at IS NULL atomic claim?
  • Does it handle the already_closed path from HL gracefully (preserve banked partial PnL)?
  • Does it correctly check sys1 vs sys2 CB independently?

Common workflows

Add a new signal source

  1. Decide: System 1 (auto-trade) or System 2 (alert + /adopt) or alert-only?
  2. Write a scanner under app/services/scanners/NEW.py that posts to POST /api/signals/ingest with {source: "your_new_source", ...}.
  3. Schedule it in app/main.py (_scheduler.add_job).
  4. Add the source to signal_categories.SYSTEM_1_SOURCES or SYSTEM_2_SOURCES if it should trade. Leave it out if alert-only.
  5. Add a Telegram preference column to TelegramBinding (migration)
    • a mapping entry in telegram._pref_column_for_source.
  6. Add a label to telegram._source_label and _signal_emoji.
  7. Add a /yoursource on|off command in telegram_bot.py.
  8. If sys2: extend signal_categories._CATEGORY_EXITS if it needs a custom exit profile (otherwise default works).
  9. Add the deep-link path in telegram.format_post AND telegram.format_public_post (both the per-user private alert and the public channel use the same path map).

Add a new bot command

  1. _cmd_x async function in telegram_bot.py.
  2. Route it in _handle_message.
  3. If it needs inline buttons: build reply_markup payload, handle callbacks in _handle_callback (route by callback_data prefix).
  4. Update HELP_TEXT and remind the user to add it to BotFather /setcommands after deploy.

Add a column to an existing table

  1. New migration alembic/versions/NNN_description.py.
    • Use op.batch_alter_table (sqlite-compatible).
    • Default values via server_default= so backfill is implicit.
  2. Mirror the field on the SQLAlchemy model in app/models.py.
  3. Apply locally: DATABASE_URL=<sqlite> alembic upgrade head.

Deploy

# On the server:
DATABASE_URL=$PROD_URL alembic upgrade head
systemctl restart trumpalpha-backend   # or whatever the unit is
python scripts/preflight.py             # MUST pass before flipping traffic
python scripts/launch_smoke.py --base https://api.trumpalpha.io

Testing

source venv/bin/activate
python -m pytest tests/ -q           # full suite, ~0.5s
python scripts/preflight.py          # env + DB + TG + AI auth checks
python scripts/launch_smoke.py       # 14 end-to-end checks vs running API

64 tests. Adoption + telegram_digest are snapshot-style (no real HL/AI). End-to-end trading is verified manually via the bot.


Telegram bot mechanics (since it's a custom integration)

  • Long-poll mode via getUpdates. Only ONE process can long-poll a given bot token at a time — if you horizontally scale, switch to webhook (not done yet).
  • Bot must be re-bound via @BotFather /setcommands whenever new commands are added (the slash-menu users see is separate from what the bot internally handles).
  • send_message returns False on failure; per-user binding rows track total_alerts_sent / total_alerts_failed counters.
  • Inline keyboards = the reply_markup payload to sendMessage. Callback data is capped at 64 bytes; keep it short (adopt:mode:BTC:standard). _handle_callback MUST end with answer_callback or the button spins forever on the user's client.
  • Free tier = walletless /start (chat_id only). Pro tier = wallet bound via /start CODE where CODE comes from Settings UI.

Why "Macro Vibes" became manage-only (the ADR)

V1.0: System 2 auto-opened sys2 trades on user wallets. Carried real execution surface: leverage clipping, daily budget split, concurrency caps, sys2 paper branches, key handling per user. Audit surfaced ~6 bugs.

V2.0 (current): sys2 manage-only. The strategy is day-K — entry delay of 24h doesn't matter. The valuable part is multi-month exit management (5-rung ladder, de-risk, pyramid, peak-trail), which still runs against positions the user adopts.

Net effect: massive reduction in execution risk surface; same alpha (strategy logic unchanged); legal/responsibility shifts from "bot opened this for you" to "you opened it, bot manages your discipline".


Things that LOOK like bugs but aren't

  • _execute_for_subscriber has lots of if sub["_is_system_2"] branches. Dead code under v2.0 (process_post early-returns for sys2). Kept for diff minimalism — don't extend or re-enable.
  • funding_reversal source is in SYSTEM_2_SOURCES? No. It's intentionally NOT in either supported set — it ingests as a Post for audit + sends Telegram alert via the CTA path, but doesn't trigger any auto-trading. Adopt still works (it's asset-based, not signal-based).
  • Subscription.sys2_budget_pct defaults to 0.7. Legacy field from the auto-open era. With v2.0 manage-only, it's effectively unused — sys1 (Trump) reads full daily_budget_usd. Don't read it for new code.
  • Adopted trades have hl_order_id starting with "adopted:". Distinct from auto-opened (HL order id integer) and paper ("paper" literal). Useful for telemetry filtering.
  • telegram.send_message accepts int | str for chat_id. Intentional. Integer = private chat, string = public channel username (e.g. "@trumpalpha"). The public channel uses the string form; per-user alerts still pass integers.
  • format_public_post deliberately omits expected_move_pct, invalidation_price, and /adopt CTA. Execution-sensitive data stays private (per-user). The public version shows confidence tier (HIGH/MED/LOW) instead of the raw score for readability.
  • _adopt_locks in adoption.py looks like it should have a cap like _wallet_open_locks (512). It doesn't yet — see Known Issues below.

Open known issues (not blocking launch but worth fixing later)

  • _time_stop_check tasks not rehydrated on restart (BUG-01, FIXED 2026-05-29): recovery.py now imports _time_stop_check + _background_tasks from bot_engine and get_exit_profile from signal_categories. After each register_trade() call for a sys2 trade, it checks exit_profile.time_stop_hours, computes remaining seconds, and reschedules the task. Elapsed windows (backend was down longer than the time-stop period) fire immediately with delay_seconds=0.

  • Rate limit bypass via proxy x-forwarded-for (BUG-02, HIGH, FIXED 2026-05-29): Two-part fix. (1) Frontend proxy app/api/proxy/[...path]/route.ts now relays the real client IP via x-forwarded-for / x-real-ip. (2) Backend had the matching gap: slowapi's default get_remote_address reads request.client.host (the proxy IP, since uvicorn runs without --proxy-headers), so the relayed header was ignored and all users still shared one bucket. Now a shared app/ratelimit.py exposes client_ip_key (reads x-forwarded-for[0]x-real-ip → peer) used by ONE shared limiter across main.py, posts.py, prices.py. Also registered SlowAPIMiddleware so default_limits (60/min) actually applies to every route — previously only the 2 decorated read endpoints were limited and all signed-mutation routes had no limit at all. Covered by tests/test_ratelimit.py.

  • close_and_finalize double-failure leaves DB/HL state inconsistent (BUG-03, MITIGATED 2026-05-29): Full two-phase-commit is out of scope. Mitigation: reconciler._reconcile_wallet now runs a "ghost position" pass — queries DB-closed trades from the last 2h and cross-checks against HL open positions. Mismatches are logged at ERROR level and broadcast via WS (reconcile_drift.ghost_positions). Manual close required on HL UI.

  • _adopt_locks has no capacity cap (BUG-04, FIXED 2026-05-29): adoption._adopt_locks is now an OrderedDict capped at _ADOPT_LOCK_MAX=512 with LRU eviction — matches the _WALLET_LOCK_MAX pattern in bot_engine.

  • Reconciler runs wallets sequentially (BUG-05, FIXED 2026-05-29): reconcile_all_once now fans out with asyncio.gather + asyncio.Semaphore(10). Worst-case tail latency is ceil(N/10) × 30s instead of N × 30s.

  • /stop also silently disables daily digest (BUG-07, FIXED 2026-05-29): send_daily_digest no longer filters on alerts_enabled — digest and real-time alerts are independent. /stop reply updated to say "Real-time alerts paused … send /digest off to stop the daily brief separately".

  • binance.py ASSET_MAP only had BTC + ETH (BUG-08, CRITICAL, FIXED 2026-05-29): Trump's AI can set target_asset to SOL/TRUMP/BNB/DOGE/LINK/AAVE. ASSET_MAP now covers all those; BINANCE_WS_URL is derived from it automatically so the stream list and routing table can never diverge. Still missing: HYPE (HL-native, not on Binance) — trades on HYPE fall back to max-hold only until a supplemental HL price feed is added.

  • _close_locks leaked one entry per concurrent-close loser (BUG-11, FIXED 2026-05-29): close_and_finalize now pops _close_locks[trade_id] in the rowcount == 0 early-return path, not just on the success paths.

  • signed_request._seen O(n) purge at 5000 entries (BUG-12, FIXED 2026-05-29): Threshold reduced to _SEEN_PURGE_THRESHOLD = 1000 and the purge now builds an expired list in one pass rather than calling dict.pop in a loop.

  • _get_max_leverage makes a fresh HL meta() call on every trade open (BUG-10, FIXED 2026-05-29): _MAX_LEV_CACHE added (same 300s TTL as _SZ_DECIMALS_CACHE). Cache miss populates ALL coins from the single meta() response, so concurrent opens pay one API call, not N.

  • Trump daily budget split (FIXED 2026-05-29): System-2 is manage-only so the sys2_pct reservation no longer makes sense for auto-opens. daily_cap for System-1 (Trump) is now total_cap × 1.0 instead of total_cap × (1 - sys2_pct). The split logic remains intact so it would work correctly if sys2 auto-open is ever re-enabled via ADR.

  • HL high-leverage adoption (BUG-09, FIXED 2026-05-29): adopt_position now rejects positions where leverage > SYS2_MAX_LEVERAGE with error code leverage_too_high. Previously the protective stop was computed for 10× but applied to a 25× position — the stop was OUTSIDE the liquidation band.

  • adopt:choose:BTC callback may show stale prices if user takes >60s to tap (HL fees, partial fills can change entry/size). adopt_position re-reads HL at mode-tap time so the FROZEN BotTrade is always fresh, but the picker label could be outdated.

  • Telegram bot offset on restart: getUpdates may replay last 24h of messages on backend restart. Stale /adopt could re-fire. User can /release to recover.

  • pyramid_add double-add on DB commit failure (BUG-13, FIXED 2026-05-29): Same pattern as BUG-03 / partial_derisk. HL open_position succeeds but the subsequent db.commit() of addon_steps_done fails → retrigger sees same step_idx and double-adds. Fix: pre-claim addon_steps_done = step_idx + 1 with a conditional UPDATE (WHERE addon_steps_done == step_idx) BEFORE calling HL. If rowcount == 0, return early. Second UPDATE writes entry_price and size_usd after fill confirmation.

  • price_impact_monitor measured wrong asset (BUG-14, FIXED 2026-05-29): truth_social.py passed analysis["asset"] (BTC/ETH sentiment proxy) to register_post instead of target_asset (SOL/TRUMP/etc. — the perp we actually trade). Impact % was measuring BTC/ETH not the traded coin. Fix: introduced tracked_asset = analysis.get("target_asset") or asset and used it for price_at_post, price_impact_asset, and register_post(asset=…).


Repos in this project

  • This repo (/Users/k/Public/Claude/backend) — Python/FastAPI backend
  • Sibling frontend (/Users/k/Public/Claude/trumpsignal) — Next.js 16 dashboard at trumpsignal.com. See its own CLAUDE.md.

Both deployed independently. Backend serves the JSON API + Telegram bot. Frontend is a thin SPA over the API + WebSocket.