feat: liquidation_collector.py - Binance WS forceOrder realtime + 5min aggregation to market_indicators
This commit is contained in:
parent
6659c4524c
commit
abfdc63705
140
backend/liquidation_collector.py
Normal file
140
backend/liquidation_collector.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
清算数据采集器 — 币安WS forceOrder实时流
|
||||||
|
存入 market_indicators 表,indicator_type = 'liquidation'
|
||||||
|
|
||||||
|
每笔清算记录:symbol, side, price, qty, trade_time
|
||||||
|
每5分钟汇总一次:long_liq_usd, short_liq_usd, total_liq_usd, count
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import websockets
|
||||||
|
from db import get_sync_conn
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
logger = logging.getLogger("liquidation_collector")
|
||||||
|
|
||||||
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
|
||||||
|
WS_URL = "wss://fstream.binance.com/stream?streams=" + "/".join(
|
||||||
|
f"{s.lower()}@forceOrder" for s in SYMBOLS
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5分钟聚合窗口
|
||||||
|
AGG_INTERVAL = 300 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_table():
|
||||||
|
with get_sync_conn() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS liquidations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
symbol TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL,
|
||||||
|
price DOUBLE PRECISION NOT NULL,
|
||||||
|
qty DOUBLE PRECISION NOT NULL,
|
||||||
|
usd_value DOUBLE PRECISION NOT NULL,
|
||||||
|
trade_time BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_liquidations_symbol_time
|
||||||
|
ON liquidations(symbol, trade_time DESC);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
logger.info("liquidations table ensured")
|
||||||
|
|
||||||
|
|
||||||
|
def save_liquidation(symbol: str, side: str, price: float, qty: float, usd_value: float, trade_time: int):
|
||||||
|
with get_sync_conn() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO liquidations (symbol, side, price, qty, usd_value, trade_time) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
(symbol, side, price, qty, usd_value, trade_time)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def save_aggregated(symbol: str, ts_ms: int, long_liq_usd: float, short_liq_usd: float, count: int):
|
||||||
|
"""每5分钟汇总存入market_indicators,供signal_engine读取"""
|
||||||
|
payload = json.dumps({
|
||||||
|
"long_liq_usd": round(long_liq_usd, 2),
|
||||||
|
"short_liq_usd": round(short_liq_usd, 2),
|
||||||
|
"total_liq_usd": round(long_liq_usd + short_liq_usd, 2),
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
with get_sync_conn() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO market_indicators (symbol, indicator_type, timestamp_ms, value) "
|
||||||
|
"VALUES (%s, %s, %s, %s)",
|
||||||
|
(symbol, "liquidation", ts_ms, payload)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
ensure_table()
|
||||||
|
|
||||||
|
# 每个symbol的聚合缓冲
|
||||||
|
agg = {s: {"long_usd": 0.0, "short_usd": 0.0, "count": 0, "window_start": int(time.time())} for s in SYMBOLS}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info("Connecting to Binance forceOrder WS...")
|
||||||
|
async with websockets.connect(WS_URL, ping_interval=20, ping_timeout=10) as ws:
|
||||||
|
logger.info("Connected! Listening for liquidations...")
|
||||||
|
async for msg in ws:
|
||||||
|
data = json.loads(msg)
|
||||||
|
if "data" not in data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
order = data["data"]["o"]
|
||||||
|
symbol = order["s"]
|
||||||
|
side = order["S"] # BUY = short被清算, SELL = long被清算
|
||||||
|
price = float(order["p"])
|
||||||
|
qty = float(order["q"])
|
||||||
|
usd_value = price * qty
|
||||||
|
trade_time = order["T"]
|
||||||
|
|
||||||
|
# 清算方向:BUY=空头被爆仓, SELL=多头被爆仓
|
||||||
|
liq_side = "SHORT" if side == "BUY" else "LONG"
|
||||||
|
|
||||||
|
# 存入原始记录
|
||||||
|
save_liquidation(symbol, liq_side, price, qty, usd_value, trade_time)
|
||||||
|
logger.info(f"[{symbol}] 💥 {liq_side} liquidation: {qty} @ ${price:.2f} = ${usd_value:,.0f}")
|
||||||
|
|
||||||
|
# 聚合
|
||||||
|
if symbol in agg:
|
||||||
|
buf = agg[symbol]
|
||||||
|
if liq_side == "LONG":
|
||||||
|
buf["long_usd"] += usd_value
|
||||||
|
else:
|
||||||
|
buf["short_usd"] += usd_value
|
||||||
|
buf["count"] += 1
|
||||||
|
|
||||||
|
# 检查是否到了5分钟聚合窗口
|
||||||
|
now = int(time.time())
|
||||||
|
for sym in SYMBOLS:
|
||||||
|
buf = agg[sym]
|
||||||
|
if now - buf["window_start"] >= AGG_INTERVAL:
|
||||||
|
if buf["count"] > 0:
|
||||||
|
save_aggregated(sym, now * 1000, buf["long_usd"], buf["short_usd"], buf["count"])
|
||||||
|
logger.info(f"[{sym}] 📊 5min agg: long=${buf['long_usd']:,.0f} short=${buf['short_usd']:,.0f} count={buf['count']}")
|
||||||
|
# 即使没清算也写一条0记录,保持连贯
|
||||||
|
elif now - buf["window_start"] >= AGG_INTERVAL:
|
||||||
|
save_aggregated(sym, now * 1000, 0, 0, 0)
|
||||||
|
buf["long_usd"] = 0.0
|
||||||
|
buf["short_usd"] = 0.0
|
||||||
|
buf["count"] = 0
|
||||||
|
buf["window_start"] = now
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WS error: {e}, reconnecting in 5s...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
Loading…
Reference in New Issue
Block a user