alphax/price_tracker_ws.py
2026-05-13 22:32:50 +08:00

183 lines
6.6 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.

#!/usr/bin/env python3
"""
山寨币实时价格监控 — ccxt REST 高频轮询版。
职责边界:本进程只做实时价格采集和“候选状态”提交;最终状态落库、是否推送、推送价格口径
全部由 altcoin_db.apply_recommendation_state_transition 主链路决定。
"""
import sys
import os
import time
import ccxt
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from altcoin_db import (
init_db,
get_active_recommendations_deduped,
update_recommendation_tracking,
apply_recommendation_state_transition,
should_push,
log_push,
)
from feishu_push import push_altcoin_tp_sl_alert
POLL_INTERVAL = 5
REFRESH_INTERVAL = 60
BATCH_SIZE = 50
exchange = ccxt.binance({"enableRateLimit": True})
last_refresh = 0
active_map = {}
def load_active():
try:
recs = get_active_recommendations_deduped(actionable_only=False)
# 只监控 active 最新去重记录;结案记录由主链路/网站展示,不在实时入口反复触发。
return {r["symbol"]: r for r in recs if r.get("symbol") and r.get("status") == "active"}
except Exception as e:
print(f"[{datetime.now():%H:%M:%S}] 加载推荐失败: {e}", flush=True)
return {}
def check_triggers(symbol, rec, current_price):
"""向后兼容旧测试:结案记录不得再产生入场触发。"""
if rec.get("status") != "active":
return None
action, signals = detect_candidate_action(rec, current_price, {})
if not action:
return None
return {"action_status": action, "signals": signals}
def detect_candidate_action(rec, current_price, track_result):
"""仅产生候选状态;不得在这里推送或落最终状态。"""
terminal_action = {
"hit_tp1": "止盈1",
"hit_tp2": "止盈2",
"stopped_out": "止损",
}.get((track_result or {}).get("status"))
if terminal_action:
return terminal_action, [f"状态机检测到{terminal_action}"]
ep = rec.get("entry_plan") or {}
entry_action = ep.get("entry_action", "") or rec.get("initial_action", "")
plan_entry_price = ep.get("entry_price", 0) or 0
entry_price = rec.get("entry_price", 0) or 0
cur_action = rec.get("action_status", "持有")
if entry_action == "等回踩" and plan_entry_price > 0 and current_price <= plan_entry_price:
ep["entry_trigger_confirmed"] = True
return "可即刻买入", [f"回踩到位 ${plan_entry_price:.6f}"]
if entry_action in ("即刻买入", "可即刻买入") and current_price <= (plan_entry_price or entry_price):
ep["entry_trigger_confirmed"] = True
return "可即刻买入", ["入场条件仍满足"]
if cur_action == "可即刻买入":
return "可即刻买入", ["入场窗口延续"]
return None, []
def fetch_prices(symbols):
all_tickers = {}
for i in range(0, len(symbols), BATCH_SIZE):
batch = symbols[i:i + BATCH_SIZE]
try:
tickers = exchange.fetch_tickers(batch)
for s in batch:
if s in tickers:
all_tickers[s] = tickers[s]
except Exception as e:
print(f"[{datetime.now():%H:%M:%S}] 拉取价格失败(批次{i//BATCH_SIZE}): {e}", flush=True)
time.sleep(1)
return all_tickers
def maybe_push_from_decision(symbol, decision):
"""推送层只消费主链路 decision不自行判断状态。"""
action = decision.get("action_status")
if not decision.get("push_required"):
return
if not should_push(symbol, "entry", action):
print(f"[{datetime.now():%H:%M:%S}] ⏭ 跳过推送 {symbol}: entry/{action} 冷却中", flush=True)
return
push_altcoin_tp_sl_alert(
decision["push_symbol"],
decision["push_current_price"],
decision["push_entry_price"],
decision["push_pnl_pct"],
action,
decision.get("push_signals", []),
decision.get("stop_loss", 0),
decision.get("tp1", 0),
decision.get("tp2", 0),
)
log_push(symbol, "entry", action, rec_id=decision.get("id", 0))
print(f"[{datetime.now():%H:%M:%S}] 📲 {symbol}{action} (盈亏{decision['push_pnl_pct']}%)", flush=True)
def main_loop():
global active_map, last_refresh
init_db()
active_map = load_active()
last_refresh = time.time()
print(f"[{datetime.now():%H:%M:%S}] 🚀 实时价格监控启动,活跃: {len(active_map)}只,轮询间隔{POLL_INTERVAL}s", flush=True)
while True:
loop_start = time.time()
if loop_start - last_refresh >= REFRESH_INTERVAL:
active_map = load_active()
last_refresh = loop_start
if not active_map:
time.sleep(POLL_INTERVAL)
continue
tickers = fetch_prices(list(active_map.keys()))
for symbol, rec in list(active_map.items()):
ticker = tickers.get(symbol)
if not ticker:
continue
current_price = ticker.get("last", 0)
if current_price <= 0:
continue
try:
track_result = update_recommendation_tracking(rec["id"], current_price) or {}
except Exception as e:
print(f"[{datetime.now():%H:%M:%S}] 跟踪价格失败 {symbol}: {e}", flush=True)
continue
action, signals = detect_candidate_action(rec, current_price, track_result)
if not action:
continue
decision = apply_recommendation_state_transition(
rec["id"],
requested_action=action,
current_price=current_price,
event_time=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
signals=signals,
)
if decision.get("action_status") in ("止盈1", "止盈2", "止损"):
active_map.pop(symbol, None)
else:
rec["action_status"] = decision.get("action_status", rec.get("action_status"))
rec["entry_price"] = decision.get("entry_price", rec.get("entry_price"))
rec["current_price"] = decision.get("current_price", current_price)
rec["pnl_pct"] = decision.get("pnl_pct", rec.get("pnl_pct"))
active_map[symbol] = rec
try:
maybe_push_from_decision(symbol, decision)
except Exception as e:
print(f"[{datetime.now():%H:%M:%S}] 推送失败 {symbol}: {e}", flush=True)
elapsed = time.time() - loop_start
time.sleep(max(0.5, POLL_INTERVAL - elapsed))
if __name__ == "__main__":
main_loop()