first commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user