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:
+137
-9
@@ -146,16 +146,100 @@ def format_post(post: Post) -> str:
|
||||
return msg[:MAX_LEN]
|
||||
|
||||
|
||||
def format_trump_mention(post: Post) -> str:
|
||||
"""Public-channel-only alert for Trump posts that are crypto-relevant
|
||||
but don't carry a clear directional signal (signal=hold, relevant=True).
|
||||
|
||||
These posts can still move markets (e.g. "I support crypto development")
|
||||
so we surface them as low-urgency monitoring alerts. Never sent per-user
|
||||
to avoid subscriber spam; only broadcast to the public channel.
|
||||
"""
|
||||
body = (post.text or "").strip()
|
||||
if len(body) > 300:
|
||||
body = body[:300].rstrip() + "…"
|
||||
|
||||
fe = (settings.frontend_url or "").rstrip("/")
|
||||
link = f'\n\n<a href="{fe}/en/trump">→ view on TrumpAlpha</a>' if fe else ""
|
||||
|
||||
msg = (
|
||||
f"📢 <b>Trump mentioned crypto</b>\n"
|
||||
f"<i>Trump · Truth Social — no clear trade signal</i>\n\n"
|
||||
f"{body}"
|
||||
f"{link}"
|
||||
)
|
||||
return msg[:MAX_LEN]
|
||||
|
||||
|
||||
def format_public_post(post: Post) -> str:
|
||||
"""Render a sanitised version of a Post for the public broadcast channel.
|
||||
|
||||
Intentionally omits:
|
||||
- expected_move_pct / invalidation_price (execution-sensitive data)
|
||||
- /adopt CTA (requires a private bot session)
|
||||
- Any wallet address or subscriber-only details
|
||||
|
||||
Keeps: signal direction, asset, confidence tier, source label, post
|
||||
excerpt (≤ 280 chars), and a link back to the relevant dashboard section.
|
||||
"""
|
||||
emoji = _signal_emoji(post)
|
||||
src = _source_label(post.source)
|
||||
sig = (post.signal or "noise").upper()
|
||||
asset = post.target_asset or "?"
|
||||
conf = post.ai_confidence or 0
|
||||
|
||||
# Confidence tier label instead of raw number (less intimidating publicly)
|
||||
if conf >= 80:
|
||||
conf_label = "HIGH"
|
||||
elif conf >= 60:
|
||||
conf_label = "MED"
|
||||
elif conf > 0:
|
||||
conf_label = "LOW"
|
||||
else:
|
||||
conf_label = "—"
|
||||
|
||||
head = f"{emoji} <b>{asset} · {sig}</b> · conf <b>{conf_label}</b>"
|
||||
sub = f"<i>{src}</i>"
|
||||
|
||||
body = (post.text or "").strip()
|
||||
if len(body) > 280:
|
||||
body = body[:280].rstrip() + "…"
|
||||
|
||||
# KOL divergence: strip wallet address from the text if present
|
||||
# (the raw text shouldn't contain one, but belt-and-suspenders)
|
||||
if post.source == "kol_divergence" and post.ai_reasoning:
|
||||
reason = post.ai_reasoning.strip()
|
||||
if len(reason) > 200:
|
||||
reason = reason[:200].rstrip() + "…"
|
||||
body = reason
|
||||
|
||||
fe = (settings.frontend_url or "").rstrip("/")
|
||||
link = ""
|
||||
if fe:
|
||||
path = {
|
||||
"truth": "/en/trump",
|
||||
"btc_bottom_reversal": "/en/macro",
|
||||
"funding_reversal": "/en/macro",
|
||||
"kol_divergence": "/en/kol",
|
||||
}.get(post.source, "/en")
|
||||
link = f'\n\n<a href="{fe}{path}">→ view on TrumpAlpha</a>'
|
||||
|
||||
msg = f"{head}\n{sub}\n\n{body}{link}"
|
||||
return msg[:MAX_LEN]
|
||||
|
||||
|
||||
# ── Low-level HTTP ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def send_message(chat_id: int, text: str, *,
|
||||
async def send_message(chat_id: int | str, text: str, *,
|
||||
parse_mode: str = "HTML",
|
||||
disable_preview: bool = True,
|
||||
reply_markup: Optional[dict] = None) -> bool:
|
||||
"""Single HTTP POST to Telegram Bot API. Returns True on 200, False on
|
||||
any failure (caller decides whether to bump the failure counter).
|
||||
|
||||
`chat_id` may be an integer user/group ID or a string channel username
|
||||
such as ``"@trumpalpha"`` (with or without the leading @).
|
||||
|
||||
`reply_markup` is the standard Bot-API inline keyboard / reply keyboard
|
||||
payload, e.g.
|
||||
{"inline_keyboard": [
|
||||
@@ -179,12 +263,12 @@ async def send_message(chat_id: int, text: str, *,
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(url, json=payload)
|
||||
if r.status_code != 200:
|
||||
logger.warning("Telegram sendMessage failed chat=%d status=%d body=%s",
|
||||
logger.warning("Telegram sendMessage failed chat=%s status=%d body=%s",
|
||||
chat_id, r.status_code, r.text[:200])
|
||||
return False
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("Telegram sendMessage exception chat=%d: %s", chat_id, exc)
|
||||
logger.warning("Telegram sendMessage exception chat=%s: %s", chat_id, exc)
|
||||
return False
|
||||
|
||||
|
||||
@@ -246,7 +330,20 @@ async def answer_callback(callback_query_id: str, text: str = "",
|
||||
|
||||
async def _dispatch(post_id: int) -> None:
|
||||
"""Fan-out a single Post to every eligible subscriber. Always runs in
|
||||
its own DB session so signal ingestion's session is unaffected."""
|
||||
its own DB session so signal ingestion's session is unaffected.
|
||||
|
||||
Two distinct paths:
|
||||
|
||||
PATH A — actionable signal (signal=buy/short):
|
||||
• Per-subscriber fan-out filtered by alert preference + min_confidence
|
||||
• Public channel broadcast (format_public_post)
|
||||
|
||||
PATH B — crypto mention (source=truth, relevant=True, signal=hold):
|
||||
• Public channel ONLY (format_trump_mention) — no per-subscriber push
|
||||
to avoid noise for individual users who subscribed for trade signals.
|
||||
• These posts are crypto-relevant but lack a clear directional call.
|
||||
They can still move markets so we surface them as monitoring alerts.
|
||||
"""
|
||||
if not settings.telegram_bot_token:
|
||||
return
|
||||
|
||||
@@ -256,10 +353,32 @@ async def _dispatch(post_id: int) -> None:
|
||||
logger.warning("Telegram dispatch: post id=%d not found", post_id)
|
||||
return
|
||||
|
||||
# Only fan out for actionable signals (not NOISE / null)
|
||||
if not post.signal or post.signal not in ("buy", "short"):
|
||||
is_actionable = post.signal in ("buy", "short")
|
||||
is_trump_mention = (
|
||||
post.source == "truth"
|
||||
and bool(post.relevant)
|
||||
and post.signal == "hold"
|
||||
)
|
||||
|
||||
if not is_actionable and not is_trump_mention:
|
||||
# Non-relevant noise — nothing to send.
|
||||
return
|
||||
|
||||
# ── PATH B: crypto mention (public channel only) ──────────────────
|
||||
if is_trump_mention and not is_actionable:
|
||||
channel_id = settings.telegram_public_channel_id
|
||||
if channel_id:
|
||||
mention_text = format_trump_mention(post)
|
||||
ok = await send_message(channel_id, mention_text)
|
||||
if ok:
|
||||
logger.info("Telegram mention alert: post=%d sent to %s",
|
||||
post_id, channel_id)
|
||||
else:
|
||||
logger.warning("Telegram mention alert: post=%d send FAILED to %s",
|
||||
post_id, channel_id)
|
||||
return
|
||||
|
||||
# ── PATH A: actionable signal (buy/short) ─────────────────────────
|
||||
pref_col = _pref_column_for_source(post.source)
|
||||
if pref_col is None:
|
||||
logger.debug("Telegram: unknown source %r — not fanning out", post.source)
|
||||
@@ -282,9 +401,6 @@ async def _dispatch(post_id: int) -> None:
|
||||
q = select(TelegramBinding).where(*base_filters)
|
||||
bindings = (await db.execute(q)).scalars().all()
|
||||
|
||||
if not bindings:
|
||||
return
|
||||
|
||||
text = format_post(post)
|
||||
now = datetime.now(timezone.utc)
|
||||
hour = now.hour
|
||||
@@ -318,6 +434,18 @@ async def _dispatch(post_id: int) -> None:
|
||||
logger.info("Telegram fan-out: post=%d source=%s sent=%d/%d",
|
||||
post_id, post.source, sent, len(bindings))
|
||||
|
||||
# ── Public channel broadcast ──────────────────────────────────────
|
||||
channel_id = settings.telegram_public_channel_id
|
||||
if channel_id:
|
||||
public_text = format_public_post(post)
|
||||
ok = await send_message(channel_id, public_text)
|
||||
if ok:
|
||||
logger.info("Telegram public channel: post=%d sent to %s",
|
||||
post_id, channel_id)
|
||||
else:
|
||||
logger.warning("Telegram public channel: post=%d send FAILED to %s",
|
||||
post_id, channel_id)
|
||||
|
||||
|
||||
def notify_signal(post: Post) -> None:
|
||||
"""Fire-and-forget. Schedules `_dispatch(post.id)` on the running loop
|
||||
|
||||
Reference in New Issue
Block a user