first commit

This commit is contained in:
k
2026-04-20 23:05:59 +08:00
commit 9a72566753
33 changed files with 2138 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
"""
历史帖子价格回溯
对 DB 中没有价格数据的帖子,从 Binance 拉历史 K 线计算涨跌幅
"""
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:
"""
对 DB 中 relevant=True 但没有价格数据的帖子补充价格回溯。
对 relevant=False 的帖子,做简单判断(标题含 crypto/bitcoin/btc 关键词则标记为相关)。
"""
symbol = "BTCUSDT" if asset == "BTC" else "ETHUSDT"
async with db_session_factory() as db:
# 拿所有没有价格数据的帖子
result = await db.execute(
select(Post)
.where(Post.price_at_post == None)
.order_by(Post.published_at.asc())
)
posts = result.scalars().all()
logger.info("找到 %d 条帖子需要价格回溯 (asset=%s)", len(posts), asset)
if not posts:
return
# 关键词判断是否与加密相关(快速,不用 Claude API)
crypto_keywords = [
"bitcoin", "btc", "crypto", "cryptocurrency", "blockchain",
"ethereum", "eth", "digital currency", "defi", "coinbase",
"sec", "regulation", "tariff", "dollar", "inflation", "fed",
"economy", "market", "trade", "sanctions", "iran", "china",
]
def is_relevant(text: str) -> bool:
t = text.lower()
return any(kw in t for kw in crypto_keywords)
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)
# 判断相关性(用关键词,不消耗 Claude API)
relevant = is_relevant(post.text)
sentiment = "neutral"
if relevant:
t = post.text.lower()
bullish_kw = ["great", "win", "winning", "strong", "best", "love", "beautiful", "tremendous", "amazing", "pro-crypto", "bitcoin reserve"]
bearish_kw = ["bad", "terrible", "war", "crisis", "sanction", "ban", "regulate", "crack", "fraud", "scam"]
if any(k in t for k in bullish_kw):
sentiment = "bullish"
elif any(k in t for k in bearish_kw):
sentiment = "bearish"
# 拉 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.relevant = relevant
p.sentiment = sentiment
p.price_impact_asset = asset if relevant else None
p.price_at_post = price_at_post
p.price_impact_m5 = m5 if relevant else None
p.price_impact_m15 = m15 if relevant else None
p.price_impact_m1h = m1h if relevant else None
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)