#!/usr/bin/env python3 """ branch_scanner.py — 盘中分支扫描器 每15分钟跑一轮: 1. 读取所有有 strategy_tree 的股票 2. 获取实时价格 3. 评估每个分支在当前情景下是否适用 4. 适用分支 → 记录 trigger_count + 推送信号 自成长核心组件:让分支条件得到实际验证。 """ import json, sys, os, re from datetime import datetime, date from urllib.request import Request, urlopen DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json" EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json" def get_price(code): """腾讯API实时价格""" mkt = "sh" if code.startswith("6") or code.startswith("5") else "sz" url = f"http://qt.gtimg.cn/q={mkt}{code}" req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: resp = urlopen(req, timeout=5).read().decode("gbk") parts = resp.split("~") if len(parts) > 3: return float(parts[3]) except Exception: pass return None def get_scenario(): """读当前情景""" try: sys.path.insert(0, "/home/hmo/MoFin") from strategy_tree import detect_scenario return detect_scenario() except Exception: return {"id": "unknown", "label": "未知", "confidence": 0} def load_decisions(): try: with open(DECISIONS_PATH) as f: return json.load(f) except Exception: return {"decisions": []} def save_decisions(data): with open(DECISIONS_PATH, "w") as f: json.dump(data, f, indent=2, ensure_ascii=False) def check_condition(branch, scenario_id, price): """检查分支条件是否满足 — 同步 strategy_tree._check_branch_condition 逻辑""" cond = branch.get("condition", {}) required_scenario = cond.get("scenario", "") if required_scenario and required_scenario != scenario_id: return False price_cond = cond.get("price", "") if price_cond and price: ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_cond) for op, val_str in ops: val = float(val_str) if op == "<" and not (price < val): return False if op == ">" and not (price > val): return False if op == "<=" and not (price <= val): return False if op == ">=" and not (price >= val): return False # 买入区下界(price_lower)检查 — 之前遗漏 price_lower = cond.get("price_lower", "") if price_lower and price: ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_lower) for op, val_str in ops: val = float(val_str) if op == "<" and not (price < val): return False if op == ">" and not (price > val): return False if op == "<=" and not (price <= val): return False if op == ">=" and not (price >= val): return False return True def main(): now = datetime.now() today = now.strftime("%Y-%m-%d") hour = now.hour # 盘后才扫无意义 if hour < 9 or hour > 16: return 0 scenario = get_scenario() sid = scenario.get("id", "unknown") slabel = scenario.get("label", "未知") data = load_decisions() decisions = data.get("decisions", []) # 读上次扫描的状态(记录情景+各股最优分支) state_path = "/home/hmo/web-dashboard/data/scanner_state.json" last_state = {"scenario": "", "branches": {}} try: with open(state_path) as f: last_state = json.load(f) except Exception: pass triggered = [] alerts = [] for entry in decisions: code = entry.get("code", "") tree = entry.get("strategy_tree", {}) branches = tree.get("branches", []) if not branches: continue price = get_price(code) if not price: continue # 找出最优适用分支 best = None for br in sorted(branches, key=lambda b: b.get("priority", 999)): if check_condition(br, sid, price): best = br break if best: best["trigger_count"] = best.get("trigger_count", 0) + 1 best["last_triggered"] = today triggered.append(code) # 状态变化检测:上次不是这个分支→推送 action_type = best.get("action", {}).get("type", "hold") br_id = best.get("id", "") last_br = last_state.get("branches", {}).get(code, "") last_action = last_state.get("branches", {}).get(f"{code}_action", "") changed = (br_id != last_br) # 推送条件:分支变化(情景变化已隐含)或止损首次触发 priority = best.get("priority", 99) should_alert = False if priority == 0: # 止损 # 只有止损新出现才推(上次不是止损 or 上次扫描不存在) should_alert = (changed or not last_state.get("branches")) elif sid != last_state.get("scenario"): should_alert = True elif changed: should_alert = True if should_alert: rationale = best.get("rationale", "") count = best.get("trigger_count", 1) alerts.append(f" {code} {entry.get('name','')}: {action_type}({rationale})") if triggered: save_decisions(data) # 保存当前状态供下次对比 new_state = { "scenario": sid, "scenario_label": slabel, "branches": {}, "updated_at": now.isoformat(), } for e in decisions: code = e.get("code", "") tree = e.get("strategy_tree", {}) branches = tree.get("branches", []) best = None for br in sorted(branches, key=lambda b: b.get("priority", 999)): if check_condition(br, sid, get_price(code)): best = br break if best: new_state["branches"][code] = best.get("id", "") new_state["branches"][f"{code}_action"] = best.get("action", {}).get("type", "") try: with open(state_path, "w") as f: json.dump(new_state, f, indent=2) except Exception: pass # 输出:变化才出声,平常静默 if alerts: scenario_shift = f"【情景切换】{last_state.get('scenario','')}→{sid}" if sid != last_state.get("scenario") else "" header = f"【分支扫描】{now.strftime('%H:%M')}" + (f" | {scenario_shift}" if scenario_shift else "") msg = header + "\n" + "\n".join(alerts) print(msg) return 0 # 无变化→静默 return 0 if __name__ == "__main__": sys.exit(main())