feat: Telegram daily digest + System-2 manage-only refactor

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>
This commit is contained in:
k
2026-05-26 12:53:16 +08:00
parent fc735f251a
commit 3754d2caf8
14 changed files with 2162 additions and 42 deletions
@@ -0,0 +1,53 @@
"""Telegram per-user digest preferences.
Adds three columns to telegram_bindings so each subscriber can opt in/out
of the daily three-section brief (Macro Vibes / KOL / Trump) and pick
which UTC hour it lands.
digest_enabled : bool, default True. Toggleable via /digest on|off.
digest_hour_utc : int 023, default 12. Toggleable via /digest_time HH.
12 UTC ≈ Asia evening / EU afternoon / US morning —
the only single hour that's reasonable for all three
big crypto-active zones, so it's our default.
last_digest_sent_at : nullable timestamp. Guards against double-send if the
cron coalesces or the worker restarts mid-firing.
Used as "skip if < 23h ago" in send_daily_digest.
Backfills are safe: every existing binding gets digest_enabled=True so users
who subscribed before this feature start receiving the brief automatically.
If they don't want it, one /digest off keeps them off.
Revision ID: 023
Revises: 022
Create Date: 2026-05-26
"""
from alembic import op
import sqlalchemy as sa
revision = "023"
down_revision = "022"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("telegram_bindings") as batch:
batch.add_column(sa.Column(
"digest_enabled", sa.Boolean, nullable=False,
server_default=sa.true(),
))
batch.add_column(sa.Column(
"digest_hour_utc", sa.Integer, nullable=False,
server_default="12",
))
batch.add_column(sa.Column(
"last_digest_sent_at", sa.DateTime, nullable=True,
))
def downgrade() -> None:
with op.batch_alter_table("telegram_bindings") as batch:
batch.drop_column("last_digest_sent_at")
batch.drop_column("digest_hour_utc")
batch.drop_column("digest_enabled")