feat: Phase 2 - collect obi_depth_10, spot_perp_divergence, tiered_cvd_whale for BTC gate-control

This commit is contained in:
root 2026-03-03 13:42:21 +00:00
parent fcac8c2334
commit 17a387b6f4
2 changed files with 136 additions and 4 deletions

View File

@ -16,7 +16,9 @@ PG_HOST = os.getenv("PG_HOST", "10.106.0.3")
PG_PORT = int(os.getenv("PG_PORT", "5432")) PG_PORT = int(os.getenv("PG_PORT", "5432"))
PG_DB = os.getenv("PG_DB", "arb_engine") PG_DB = os.getenv("PG_DB", "arb_engine")
PG_USER = os.getenv("PG_USER", "arb") PG_USER = os.getenv("PG_USER", "arb")
PG_PASS = os.getenv("PG_PASS", "arb_engine_2026") PG_PASS = os.getenv("PG_PASS")
if not PG_PASS:
raise RuntimeError("PG_PASS 未设置,请在 .env 或环境变量中注入数据库密码")
TABLE_SQL = """ TABLE_SQL = """
CREATE TABLE IF NOT EXISTS market_indicators ( CREATE TABLE IF NOT EXISTS market_indicators (
@ -158,6 +160,121 @@ class MarketDataCollector:
} }
self.save_indicator(symbol, "funding_rate", ts, payload) self.save_indicator(symbol, "funding_rate", ts, payload)
async def collect_obi_depth(self, session: aiohttp.ClientSession, symbol: str) -> None:
"""
OBI订单簿失衡采集 Phase 2 BTC gate-control 核心特征
计算(bid_vol - ask_vol) / (bid_vol + ask_vol)范围[-1,1]
正值=买压大负值=卖压大
"""
endpoint = "https://fapi.binance.com/fapi/v1/depth"
data = await self.fetch_json(session, endpoint, {"symbol": symbol, "limit": 10})
ts = int(time.time() * 1000)
bids = data.get("bids", [])
asks = data.get("asks", [])
bid_vol = sum(float(b[1]) for b in bids)
ask_vol = sum(float(a[1]) for a in asks)
total_vol = bid_vol + ask_vol
obi = (bid_vol - ask_vol) / total_vol if total_vol > 0 else 0.0
best_bid = float(bids[0][0]) if bids else 0.0
best_ask = float(asks[0][0]) if asks else 0.0
spread_bps = ((best_ask - best_bid) / best_bid * 10000) if best_bid > 0 else 0.0
payload = {
"symbol": symbol,
"obi": round(obi, 6),
"bid_vol_10": round(bid_vol, 4),
"ask_vol_10": round(ask_vol, 4),
"best_bid": best_bid,
"best_ask": best_ask,
"spread_bps": round(spread_bps, 3),
}
self.save_indicator(symbol, "obi_depth_10", ts, payload)
async def collect_spot_perp_divergence(self, session: aiohttp.ClientSession, symbol: str) -> None:
"""
期现背离采集 Phase 2 BTC gate-control 核心特征
divergence = (spot - mark) / mark正值=现货溢价负值=现货折价
"""
spot_url = "https://api.binance.com/api/v3/ticker/price"
perp_url = "https://fapi.binance.com/fapi/v1/premiumIndex"
spot_data, perp_data = await asyncio.gather(
self.fetch_json(session, spot_url, {"symbol": symbol}),
self.fetch_json(session, perp_url, {"symbol": symbol}),
)
ts = int(time.time() * 1000)
spot_price = float(spot_data["price"])
mark_price = float(perp_data["markPrice"])
index_price = float(perp_data.get("indexPrice", mark_price))
divergence = (spot_price - mark_price) / mark_price if mark_price > 0 else 0.0
payload = {
"symbol": symbol,
"spot_price": spot_price,
"mark_price": mark_price,
"index_price": index_price,
"divergence": round(divergence, 8),
"divergence_bps": round(divergence * 10000, 3),
}
self.save_indicator(symbol, "spot_perp_divergence", ts, payload)
async def collect_tiered_cvd_whale(self, session: aiohttp.ClientSession, symbol: str) -> None:
"""
巨鲸CVD分层采集 Phase 2 BTC gate-control 核心特征
分层small(<$10k), medium($10k-$100k), whale(>$100k)
net_cvd = buy_usd - sell_usd=净买入
"""
endpoint = "https://fapi.binance.com/fapi/v1/aggTrades"
data = await self.fetch_json(session, endpoint, {"symbol": symbol, "limit": 500})
ts = int(time.time() * 1000)
tiers = {
"small": {"buy": 0.0, "sell": 0.0},
"medium": {"buy": 0.0, "sell": 0.0},
"whale": {"buy": 0.0, "sell": 0.0},
}
for trade in data:
price = float(trade["p"])
qty = float(trade["q"])
usd_val = price * qty
is_sell = trade["m"] # m=True 表示卖单taker卖
if usd_val < 10_000:
tier = "small"
elif usd_val < 100_000:
tier = "medium"
else:
tier = "whale"
if is_sell:
tiers[tier]["sell"] += usd_val
else:
tiers[tier]["buy"] += usd_val
result = {}
for name, t in tiers.items():
buy, sell = t["buy"], t["sell"]
net = buy - sell
total = buy + sell
result[name] = {
"buy_usd": round(buy, 2),
"sell_usd": round(sell, 2),
"net_cvd": round(net, 2),
"cvd_ratio": round(net / total, 4) if total > 0 else 0.0,
}
payload = {
"symbol": symbol,
"tiers": result,
"whale_net_cvd": result["whale"]["net_cvd"],
"whale_cvd_ratio": result["whale"]["cvd_ratio"],
}
self.save_indicator(symbol, "tiered_cvd_whale", ts, payload)
async def collect_symbol(self, session: aiohttp.ClientSession, symbol: str) -> None: async def collect_symbol(self, session: aiohttp.ClientSession, symbol: str) -> None:
tasks = [ tasks = [
("long_short_ratio", self.collect_long_short_ratio(session, symbol)), ("long_short_ratio", self.collect_long_short_ratio(session, symbol)),
@ -165,6 +282,9 @@ class MarketDataCollector:
("open_interest_hist", self.collect_open_interest_hist(session, symbol)), ("open_interest_hist", self.collect_open_interest_hist(session, symbol)),
("coinbase_premium", self.collect_coinbase_premium(session, symbol)), ("coinbase_premium", self.collect_coinbase_premium(session, symbol)),
("funding_rate", self.collect_funding_rate(session, symbol)), ("funding_rate", self.collect_funding_rate(session, symbol)),
("obi_depth_10", self.collect_obi_depth(session, symbol)),
("spot_perp_divergence", self.collect_spot_perp_divergence(session, symbol)),
("tiered_cvd_whale", self.collect_tiered_cvd_whale(session, symbol)),
] ]
results = await asyncio.gather(*(t[1] for t in tasks), return_exceptions=True) results = await asyncio.gather(*(t[1] for t in tasks), return_exceptions=True)

View File

@ -126,7 +126,12 @@ def fetch_market_indicators(symbol: str) -> dict:
with get_sync_conn() as conn: with get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
indicators = {} indicators = {}
for ind_type in ["long_short_ratio", "top_trader_position", "open_interest_hist", "coinbase_premium", "funding_rate"]: ind_types = [
"long_short_ratio", "top_trader_position", "open_interest_hist",
"coinbase_premium", "funding_rate",
"obi_depth_10", "spot_perp_divergence", "tiered_cvd_whale", # Phase 2
]
for ind_type in ind_types:
cur.execute( cur.execute(
"SELECT value FROM market_indicators WHERE symbol=%s AND indicator_type=%s ORDER BY timestamp_ms DESC LIMIT 1", "SELECT value FROM market_indicators WHERE symbol=%s AND indicator_type=%s ORDER BY timestamp_ms DESC LIMIT 1",
(symbol, ind_type), (symbol, ind_type),
@ -135,7 +140,6 @@ def fetch_market_indicators(symbol: str) -> dict:
if not row or row[0] is None: if not row or row[0] is None:
indicators[ind_type] = None indicators[ind_type] = None
continue continue
# value可能是JSON字符串或已解析的dict
val = row[0] val = row[0]
if isinstance(val, str): if isinstance(val, str):
try: try:
@ -151,10 +155,18 @@ def fetch_market_indicators(symbol: str) -> dict:
elif ind_type == "open_interest_hist": elif ind_type == "open_interest_hist":
indicators[ind_type] = float(val.get("sumOpenInterestValue", 0)) indicators[ind_type] = float(val.get("sumOpenInterestValue", 0))
elif ind_type == "coinbase_premium": elif ind_type == "coinbase_premium":
# premium_pct存的是百分比(如0.05=0.05%),转成比例(0.0005)
indicators[ind_type] = float(val.get("premium_pct", 0)) / 100.0 indicators[ind_type] = float(val.get("premium_pct", 0)) / 100.0
elif ind_type == "funding_rate": elif ind_type == "funding_rate":
indicators[ind_type] = float(val.get("fundingRate", val.get("lastFundingRate", 0))) indicators[ind_type] = float(val.get("fundingRate", val.get("lastFundingRate", 0)))
elif ind_type == "obi_depth_10":
# obi范围[-1,1],正值=买压,负值=卖压
indicators[ind_type] = float(val.get("obi", 0))
elif ind_type == "spot_perp_divergence":
# divergence = (spot - mark) / mark
indicators[ind_type] = float(val.get("divergence", 0))
elif ind_type == "tiered_cvd_whale":
# 巨鲸净CVD比率[-1,1],正值=净买入
indicators[ind_type] = float(val.get("whale_cvd_ratio", 0))
return indicators return indicators