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>
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>
schemas.py:
- Add paper_mode: bool = False to UserResponse so frontend can distinguish
paper vs live subscribers and skip HL API key requirement accordingly
api/user.py:
- Return paper_mode=bool(sub.paper_mode) in GET /user/{wallet}
- Fix elif-chain bug in settings validation: two independent `if` blocks
instead of elif so both TP and SL ranges are checked when trump_enabled
- Conditional validation: TP/SL required only when trump_enabled=True;
daily_budget_usd optional at all times
- Persist trump_enabled / macro_enabled from PUT /user/{wallet}/settings
services/bot_engine.py:
- Remove daily_budget_usd from required fields check — it is optional
(null = no cap). Previous code silently skipped ALL trades for users
who cleared their daily budget cap.
alembic/versions/025_module_toggles.py:
- Add trump_enabled, macro_enabled columns to subscriptions table
tests/test_production_readiness.py:
- Sync test_open_positions_requires_signed_wallet_read to patch
verify_signed_request_any (the function now used) instead of
verify_signed_request; update expected call kwargs accordingly
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Convention file every new Claude session reads first when opening this
repo. Encodes the invariants that aren't visible from any single file:
- Five trading-layer non-negotiable rules (sys2 manage-only, released_at,
eff_* freezing, HL effective leverage, per-wallet locks).
- Module map with the input/output function of every service file.
- System 1 vs System 2 comparison table — the single most important
distinction in this codebase.
- /adopt flow walkthrough, including the race-protection points fixed in
7599d49.
- 8-point code-review checklist to run against any trading change.
- ADR for why sys2 became manage-only (v2.0).
- "Looks like a bug but isn't" + "Known issues not yet fixed" sections to
save the next AI agent from chasing ghosts or shipping regressions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. recovery.py rehydrated adopted trades with no System-2 ladder
─────────────────────────────────────────────────────────────────
On backend restart, recovery resolved stop_ladder / derisk_ladder /
addon_ladder / peak_trail from trigger_post.category. Adopted trades
have trigger_post_id=NULL (the user opened on HL manually, there's no
source post) so all four resolved to None and the trade restarted as
a vanilla "stop_loss + max_hold" position — the entire System-2 ladder
logic silently disappeared across any restart.
Fix: detect System-2 from the row itself (sys2_mode IS NOT NULL falls
back to the adopted-category constant), then rebuild the full ladder
set from frozen leverage + mode. derisk_steps_done / addon_steps_done /
peak_gain_pct on the row let us pick up exactly where we left off.
2. close_and_finalize raced with release_management
─────────────────────────────────────────────────────────────────
on_price_tick snapshots _watched, evaluates per trade, appends to a
triggered list, then spawns _fire_close as an async task. If
release_management runs between the snapshot and _fire_close firing,
close_and_finalize would still close the HL position the user just
took back manual control of.
Fix: atomic claim now requires released_at IS NULL by default. New
`force=True` parameter on close_and_finalize bypasses the guard for
the explicit user-close API (manual_close passes it) so a deliberate
user click still works on a released trade.
3. partial_derisk + pyramid_add raced with release_management
─────────────────────────────────────────────────────────────────
Same race shape: a price tick captured before release could still
trigger a partial reduce-only close, or pyramid INTO a position the
user is now driving themselves.
Fix: both functions return idempotent-success if released_at IS NOT
NULL — match the closed_at handling we already had.
Tests still 64/64 + preflight + smoke all green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two changes ship together — both reshape the Telegram-bot surface.
──── 1. Telegram daily digest (3-section brief)
Once-a-day push covering Macro Vibes / KOL talks-vs-trades / Trump 24h.
Body is rule-based templating (no LLM); each section reads structured DB
fields and picks a phrasing. Per-user opt-out + per-user UTC hour.
- 023 migration: TelegramBinding gains digest_enabled, digest_hour_utc,
last_digest_sent_at (idempotent against coalesced cron / restarts).
- New services/telegram_digest.py: build_global_digest +
format_digest + send_daily_digest + send_preview_for.
- Hourly cron at :00 fans out to bindings whose digest_hour_utc matches.
- New bot commands: /digest (preview), /digest on|off, /digest_time HH.
- HELP_TEXT + /status updated to surface the new prefs.
──── 2. System-2 manage-only refactor
The Macro Vibes (BTC bottom + funding) signal no longer auto-opens
positions. Strategy is day-K — a 24h entry delay is irrelevant — but the
auto-open path carried real execution surface (leverage clipping, daily
budget split, concurrency caps, paper branches, key handling) for ~zero
alpha. The valuable part — multi-month exit management (5-rung stop
ladder + de-risk + pyramid + peak-trail) — is preserved and runs
against positions the user adopts.
Flow: scanner fires → Telegram alert with "/adopt" CTA → user opens
manually on Hyperliquid → /adopt picks the position via inline keyboard
→ picks Standard or Aggressive mode → bot creates BotTrade + registers
watchdog. Escape hatch: /release marks released_at, unregisters
watchdog, leaves the HL position open under user control.
- 024 migration: BotTrade.released_at — "user took back control" marker.
- bot_engine.process_post early-returns for sys2 (no auto-open path).
- New services/adoption.py: list_hl_positions + adopt_position +
release_management + AdoptionError. Per-wallet asyncio lock prevents
dual-adopt race. Pre-flight checks: no_subscription / no_hl_key /
paper_mode / circuit_breaker / already_adopted / concurrency_cap.
Protective stop + de-risk + addon + peak-trail ladders all built
against the ACTUAL HL leverage so the "inside liquidation" guarantee
holds.
- New API: GET /positions/hl/{wallet}, POST /positions/adopt,
POST /positions/{id}/release (all signed).
- telegram.py: send_message supports reply_markup; new edit_message +
answer_callback for the inline-keyboard pickers; sys2 alert format
now ends with the "/adopt" CTA.
- telegram_bot.py: /adopt + /release commands with picker → confirm
→ execute flow via inline keyboards. New _handle_callback dispatches
on "adopt:*" / "release:*" callback_data; run_bot_loop now consumes
callback_query updates alongside messages.
- recovery.py + reconciler.py skip released_at IS NOT NULL rows so a
restart doesn't silently re-attach the watchdog to a released trade.
- /positions/open, /positions/today, and telegram_digest's user-state
line all filter released_at so they don't lie about what the bot is
actually managing.
Tests: 26 new (8 digest snapshot + 14 adoption + 4 absorbed via existing
suites). All 64 pass.
Deploy: alembic upgrade head (runs 023 + 024) → restart backend. Existing
TelegramBindings get digest_enabled=true / digest_hour_utc=12 via server
defaults. In-flight auto-opened System-2 positions continue to be managed
(recovery rehydrates them) — no in-flight trade is abandoned.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. hyperliquid.open_position now returns effective_leverage. HL clips
the requested leverage to each asset's max (e.g. memes capped at 3×)
and silently applied the lower value; we were discarding that. For
System-2 this meant sys2_protective_stop_pct was computed against the
REQUESTED leverage, so the "inside-liquidation" full-close rung was
actually well inside an 8.5% stop while the real liquidation sat ~33%
away — strategy intent broken. bot_engine now reads the effective
value back, recomputes the protective stop + derisk ladder against
it, and freezes the correct values into BotTrade.eff_* / .leverage.
2. circuit_breaker.clear_trip now clears BOTH systems by default. The
sys2 column was never reachable from any reset path — a sys2 trip
forced users to wait the full 24h lockout even after explicit
human re-arm.
3. /api/user/.../manual-window and /auto-trade re-arm endpoints now
clear sys1 AND sys2 CB on the same human ack. Symmetry with
check_and_trip / is_tripped which already supported both systems.
4. telegram.py deep-link map: btc_bottom_reversal + funding_reversal
alerts now point at /en/macro instead of the now-404 /en/btc.
5. (Already landed: _sys2_mode UnboundLocalError fix in bot_engine —
restated here for the audit trail.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New backend pipeline: 8 free public macro signals fetched in parallel,
upserted once per calendar day, served via /api/macro/{snapshot,history}.
- AHR999 (computed from Binance 200d klines)
- Altcoin Season Index (CoinGecko top-50 30d)
- Fear & Greed (alternative.me)
- BTC dominance, ETH/BTC ratio
- Stablecoin supply (DeFiLlama)
- Spot BTC ETF net flow (Farside)
- BTC perp open interest (Binance fapi)
Each fetcher is independently @_none_on_fail decorated so one outage
can't take down the snapshot; scoring renormalises across whichever
indicators returned a value. Daily cron at 03:00 UTC; on startup a
fire-and-forget bootstrap fills today's row if missing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three plumbing fixes + one ops doc that close the gaps from the audit.
scripts/rescore_v5.py
Was overwriting only signal/conf/reasoning/sentiment/relevant/
prefilter_reason/analysis_version. Now also persists target_asset,
category, expected_move_pct — without these the bot can't route
rescored posts correctly (would silently fall back to BTC).
app/schemas.py + app/api/posts.py
TrumpPost response model didn't expose target_asset/category/
expected_move_pct, so the frontend had no way to display "this
signal will trade SOL". Added the three fields + mapping in
_post_to_schema(). Pre-v5 posts return null. No frontend changes
yet — display work is a follow-up.
app/services/hyperliquid.py
HL caps max leverage per asset (BTC/ETH 50×, SOL 20×, memes 3-5×).
set_leverage() always tried to push self._leverage — if user set
30× and bot routed to TRUMP, HL rejected the order and the trade
silently dropped. Added _get_max_leverage() (queries meta()'s
maxLeverage field) and _clip_leverage() that caps to HL's max.
set_leverage now returns the effective leverage so callers can
use it for notional sizing if needed.
deploy/ENCRYPTION_KEY_BACKUP.md
Documented mandatory backup procedure for the symmetric key that
encrypts every user's HL API key. Lost key = all users' bots dead
with no recovery. Includes rotation procedure + quarterly test
step + things-not-to-do list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
scripts/sweep_500.py:
Parameter grid for capital-preservation oriented configs.
Risk filters: max DD <= 30%, max single loss <= 16% of capital.
scripts/sweep_moonshot.py:
Aggressive grid for one-trade-hits-big strategy.
Looser DD ceiling (50%), prioritizes biggest single-trade upside.
Both run on the local v4 dataset to inform initial subscription
parameter choices for live trading. Re-run after v5 accumulates
enough signals (~6 weeks) to recalibrate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the loop on the asset-routing prompt change. Previously the v5
prompt emitted target_asset (e.g. SOL, TRUMP) but bot_engine still
read price_impact_asset and only ever traded BTC/ETH. Now the trade
actually fires on whatever perp the AI picked.
Schema (alembic 006):
posts.target_asset (str) — HL perp ticker, any of the universe
posts.category (str) — 6-class enum (direct_named, etc.)
posts.expected_move_pct (float) — AI's 1h move estimate
Wiring:
truth_social.py persists the three fields when creating Post rows.
bot_engine.py routing: asset = target_asset || price_impact_asset || BTC
Old rows (target_asset=NULL) fall back to legacy BTC/ETH path — no
retroactive scoring needed; new rows route correctly from now on.
Hyperliquid trader doesn't need changes — `coin` is already a parameter,
and analysis.py validated against HL_PERPS before storing target_asset
so by the time bot_engine reads the field, it's guaranteed tradeable.
Deployment:
alembic upgrade head # adds the 3 columns
Restart api container
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why
Trading BTC on every actionable signal leaves alpha on the table.
The same Trump post moves different assets very differently:
• "Strategic Reserve including SOL" → BTC +8%, SOL +33%
• "Tokenize Treasuries on ETH" → ETH +5%, BTC +2%
• "$TRUMP coin is GREATEST" → TRUMP +50%+, BTC ~0%
Picking the wrong asset is silent alpha leak.
What changed
• SYSTEM_PROMPT: new ASSET ROUTING section with 6 named categories
(direct_named / crypto_policy / macro_risk_on / macro_risk_off /
defi_thematic / meme_named) + asset-pick rules per category.
• Few-shot examples now show category + target_asset + expected_move.
• Output JSON adds: category, target_asset, target_chain,
expected_move_pct.
• Python normalization:
- HL_PERPS whitelist (current Hyperliquid perp universe).
- CHAIN_FALLBACK map: meme not on HL → trade chain native
(e.g. $FLOKI on Solana → SOL; ETH-chain meme not on HL → ETH).
- Safe default → BTC if everything else fails.
- expected_move_pct < 0.8 → coerce to hold (not worth fees).
- Legacy `asset` field kept aligned to BTC/ETH for the existing
price_impact tracker; alts leave it null until tracker is upgraded.
Note
Bot routing (bot_engine.py) NOT yet updated — it still trades whatever
`price_impact_asset` is, which is BTC/ETH only. The new fields are
emitted and stored in DB but consumed downstream in a follow-up.
This commit is "AI says the right thing" — making the bot ACT on it
is the next step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After deploying v5 analysis.py, run this once to overwrite v4 scores in
the DB with v5's interpretation. Idempotent — skips rows already at v5.
Has --dry-run mode to preview the change without AI calls or DB writes.
Live mode prompts for confirmation (skipped if stdin is non-tty so it
also works under `docker exec`).
Touches only AI-derived columns (signal, ai_confidence, ai_reasoning,
sentiment, relevant, prefilter_reason, analysis_version). Leaves all
market-derived columns intact (price_at_post, price_impact_*) — those
stay accurate regardless of which prompt version interpreted the post.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
next_run_time=None means "paused", not "default". The fallback poller was
registered but never triggered, leaving the system on CNN archive only.
Set explicit start time = now + half the poll interval so the two pollers
offset and don't hit upstream simultaneously.
Verified via /api/health/deep: trumpstruth.last_poll was null on the live
server before this fix despite the poller having been deployed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why
v4 was firing buy/short on 13% of posts, but only 9% of those had a
≥1% move within the hour. Median move on 'actionable' was 0.298% vs
0.258% on 'hold' — a 1.15× signal-to-noise ratio (random would be 1.0).
The model was confabulating transmission chains to please the user
rather than holding when uncertain.
Separately: 'sell' meant 'close longs / de-risk' in the prompt but
was traded as 'open short' by bot_engine.py, producing systematically
negative results on sell signals (27% win rate vs 57% on real shorts).
What changed
• analysis.py rewritten as v5-extreme-alpha:
- Asymmetric error costs framing (false positive = -$30, FN = $0)
- 7-item checklist that MUST all pass before buy/short
- Only 4 named transmission paths (a/b/c/d); anything else = HOLD
- 5 positive + 5 negative few-shot examples
- UTC hour injected with liquidity context (Asia thin → stricter)
- Adversarial steelman self-check before final output
- confidence < 80 + checklist failure both force-collapse to HOLD
in code, regardless of what the model returns (defense-in-depth)
- 'sell' removed from output schema entirely
• bot_engine.py: stop trading 'sell' signals (treat as hold)
• Case-insensitive normalization on checklist values so model
returning 'None'/'True' (capitalized) doesn't slip through
Expected impact (to validate over next 2-3 weeks of new posts)
• actionable rate: 13% → 2-4%
• signal/hold MFE ratio: 1.15× → 3-5×
• ≥1% hit rate among actionable: 9% → 40-60%
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New migrations for daily_budget, active_window, and bottrade snapshot
- Add trumpstruth scraper and price_impact_monitor service
- Expand bot_engine, hyperliquid, recovery, and tp_sl_monitor logic
- Update API/schemas/models for new features
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>