Files
trumpsignal-backend/app/services/price_backfill.py
T
k d6c802ef26 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>
2026-05-29 11:57:19 +08:00

127 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Historical price backfill for already-relevant posts.
Important safety rule:
This job only fills missing price fields. It must NEVER rewrite the post's
semantic classification (`relevant`, `sentiment`, signal direction) from a
keyword heuristic, otherwise historical truth rows become a mixed dataset of
AI-scored rows + script-guessed rows.
"""
import asyncio
import logging
from datetime import datetime, timezone, timedelta
from typing import Optional
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models import Post
logger = logging.getLogger(__name__)
# 每次从 Binance 拉多少分钟的 1m K 线(覆盖 1h 涨跌幅计算需要至少 60 根)
FETCH_WINDOW_MINUTES = 90
async def _fetch_klines(symbol: str, start_ms: int, limit: int = 90) -> list:
url = (
f"{settings.binance_rest_url}/api/v3/klines"
f"?symbol={symbol}&interval=1m&startTime={start_ms}&limit={limit}"
)
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url)
resp.raise_for_status()
return resp.json()
def _price_at(klines: list, target_ms: int) -> Optional[float]:
"""找最接近 target_ms 的收盘价"""
best = None
best_diff = float("inf")
for row in klines:
diff = abs(row[0] - target_ms)
if diff < best_diff:
best_diff = diff
best = float(row[4]) # close
return best
def _pct_change(klines: list, from_ms: int, delta_minutes: int) -> Optional[float]:
from_price = _price_at(klines, from_ms)
to_price = _price_at(klines, from_ms + delta_minutes * 60 * 1000)
if from_price and to_price and from_price != 0:
return round((to_price - from_price) / from_price * 100, 4)
return None
async def backfill_price_impact(db_session_factory, asset: str = "BTC") -> None:
"""Fill price-impact fields for posts that are already relevant.
The semantic label must come from the original analyzer, not from this
backfill job. This task only enriches rows with price-at-post and
subsequent returns.
"""
symbol = "BTCUSDT" if asset == "BTC" else "ETHUSDT"
async with db_session_factory() as db:
# Only touch rows whose semantic classification already exists.
result = await db.execute(
select(Post)
.where(Post.relevant == True)
.where(Post.price_at_post == None)
.where(Post.price_impact_asset == asset)
.order_by(Post.published_at.asc())
)
posts = result.scalars().all()
logger.info("找到 %d 条帖子需要价格回溯 (asset=%s)", len(posts), asset)
if not posts:
return
saved = 0
errors = 0
for i, post in enumerate(posts):
try:
published_at = post.published_at
if published_at.tzinfo is None:
published_at = published_at.replace(tzinfo=timezone.utc)
start_ms = int(published_at.timestamp() * 1000)
# 拉 Binance 历史价格
klines = await _fetch_klines(symbol, start_ms, limit=FETCH_WINDOW_MINUTES)
price_at_post = _price_at(klines, start_ms)
m5 = _pct_change(klines, start_ms, 5)
m15 = _pct_change(klines, start_ms, 15)
m1h = _pct_change(klines, start_ms, 60)
# 更新帖子
async with db_session_factory() as db:
result = await db.execute(select(Post).where(Post.id == post.id))
p = result.scalar_one_or_none()
if p:
p.price_at_post = price_at_post
p.price_impact_m5 = m5
p.price_impact_m15 = m15
p.price_impact_m1h = m1h
await db.commit()
saved += 1
if (i + 1) % 20 == 0:
logger.info("进度: %d/%d 已处理,已保存 %d", i + 1, len(posts), saved)
# 避免触发 Binance 限速(1200 requests/min
await asyncio.sleep(0.1)
except Exception as exc:
errors += 1
logger.error("帖子 id=%d 回溯失败: %s", post.id, exc)
await asyncio.sleep(1)
logger.info("✅ 价格回溯完成: 共处理 %d 条,成功 %d 条,失败 %d", len(posts), saved, errors)