#!/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()