diff --git a/branch_scanner.py b/branch_scanner.py index 70bc40b..247e50d 100644 --- a/branch_scanner.py +++ b/branch_scanner.py @@ -1,28 +1,28 @@ #!/usr/bin/env python3 """ -branch_scanner.py — 盘中分支扫描器 +branch_scanner.py — 分支自成长数据采集器(全静默) -每15分钟跑一轮: -1. 读取所有有 strategy_tree 的股票 -2. 获取实时价格 -3. 评估每个分支在当前情景下是否适用 -4. 适用分支 → 记录 trigger_count + 推送信号 +核心功能(三件事,全部后台静默执行): +1. 每轮扫描42只股票,评估当前情景下各分支的适用性 +2. 适用分支 → trigger_count + 1,记录 last_triggered +3. 保存当前状态到 scanner_state.json 供下次对比 -自成长核心组件:让分支条件得到实际验证。 +无输出 → 静默运行。触发数据积累在 decisions.json。 +操作信号由 stale_push_wlin / price_monitor / 开盘收盘简报 另路输出。 + +数据流向(自成长):每15分钟branch_scanner积累trigger_count → + 每日prune_branches评估低效分支 → decisions.json修剪 → 分支越来越有效 """ -import json, sys, os, re -from datetime import datetime, date +import json, sys, re +from datetime import datetime 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" +SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.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"}) @@ -32,12 +32,10 @@ def get_price(code): if len(parts) > 3: return float(parts[3]) except Exception: - pass - return None + return None def get_scenario(): - """读当前情景""" try: sys.path.insert(0, "/home/hmo/MoFin") from strategy_tree import detect_scenario @@ -46,26 +44,11 @@ def get_scenario(): 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) @@ -75,8 +58,6 @@ def check_condition(branch, scenario_id, price): 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) @@ -86,119 +67,54 @@ def check_condition(branch, scenario_id, price): 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: + if now.hour < 9 or now.hour > 16: return 0 scenario = get_scenario() sid = scenario.get("id", "unknown") - slabel = scenario.get("label", "未知") - data = load_decisions() + + with open(DECISIONS_PATH) as f: + data = json.load(f) 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 + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now.strftime("%Y-%m-%d") break - if best: - best["trigger_count"] = best.get("trigger_count", 0) + 1 - best["last_triggered"] = today - triggered.append(code) + with open(DECISIONS_PATH, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) - # 状态变化检测:上次不是这个分支→推送 - 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) + # 更新状态快照 + state = {"scenario": sid, "updated_at": now.isoformat(), "branches": {}} + for e in decisions: + code = e.get("code", "") + tree = e.get("strategy_tree", {}) + for br in sorted(tree.get("branches", []), key=lambda b: b.get("priority", 999)): + if check_condition(br, sid, get_price(code)): + state["branches"][code] = br.get("id", "") + break + try: + with open(SCANNER_STATE, "w") as f: + json.dump(state, f, indent=2) + except Exception: + pass - # 推送条件:分支变化(情景变化已隐含)或止损首次触发 - 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 diff --git a/data/decisions.json b/data/decisions.json index 8c31ec8..9c042b1 100644 --- a/data/decisions.json +++ b/data/decisions.json @@ -147,7 +147,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -226,7 +226,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -411,7 +411,7 @@ }, "priority": 0, "rationale": "止损保护本金", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -705,7 +705,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -852,7 +852,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 6, + "trigger_count": 7, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -4831,7 +4831,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5035,7 +5035,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 6, + "trigger_count": 7, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5197,7 +5197,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5401,7 +5401,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5642,7 +5642,7 @@ }, "priority": 4, "rationale": "达到目标价,减半仓锁定利润", - "trigger_count": 5, + "trigger_count": 6, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5802,7 +5802,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -5978,7 +5978,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -6253,7 +6253,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -6351,7 +6351,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 10, + "trigger_count": 11, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -6606,7 +6606,7 @@ }, "priority": 4, "rationale": "达到目标价,减半仓锁定利润", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -6823,7 +6823,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 6, + "trigger_count": 7, "success_rate": null, "last_triggered": "2026-06-24" } @@ -6913,7 +6913,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 6, + "trigger_count": 7, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -7117,7 +7117,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -7358,7 +7358,7 @@ }, "priority": 4, "rationale": "达到目标价,减半仓锁定利润", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -7505,7 +7505,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -7716,7 +7716,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -7920,7 +7920,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 3, + "trigger_count": 4, "success_rate": null, "last_triggered": "2026-06-24" } @@ -8060,7 +8060,7 @@ }, "priority": 1, "rationale": "价格回调到支撑区,弱势市场低吸", - "trigger_count": 6, + "trigger_count": 7, "success_rate": null, "last_triggered": "2026-06-24" }, @@ -8328,7 +8328,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -8518,7 +8518,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } @@ -8653,7 +8653,7 @@ }, "priority": 99, "rationale": "没有分支匹配时的默认动作", - "trigger_count": 7, + "trigger_count": 8, "success_rate": null, "last_triggered": "2026-06-24" } diff --git a/data/portfolio.json b/data/portfolio.json index 27329c6..6d9a4b9 100644 --- a/data/portfolio.json +++ b/data/portfolio.json @@ -55,8 +55,8 @@ "action_note": "⚠️盈亏比偏低(1:1.3),不建议加仓", "timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有" }, - "price": 83.95, - "change_pct": 7.84 + "price": 83.65, + "change_pct": 7.45 }, { "code": "01088", @@ -85,7 +85,7 @@ "timing_signal": "大盘中性,行业中性,蓝筹,持有" }, "price": 41.52, - "change_pct": -1.09 + "change_pct": -1.28 }, { "code": "01211", @@ -113,8 +113,8 @@ "action_note": "深套持有", "timing_signal": "大盘中性,行业偏弱,蓝筹,持有" }, - "price": 75.0, - "change_pct": -1.12 + "price": 74.95, + "change_pct": -1.19 }, { "code": "01478", @@ -142,8 +142,8 @@ "action_note": "深套持有", "timing_signal": "大盘中性,行业中性,低估值,持有" }, - "price": 7.68, - "change_pct": -2.79 + "price": 7.67, + "change_pct": -2.66 }, { "code": "01888", @@ -171,8 +171,8 @@ "action_note": "短炒强趋势持", "timing_signal": "大盘中性,行业偏弱,高估值,强趋势持" }, - "price": 96.75, - "change_pct": 10.89 + "price": 96.15, + "change_pct": 10.2 }, { "code": "02202", @@ -201,7 +201,7 @@ "timing_signal": "大盘中性,行业中性,高估值,持有" }, "price": 2.34, - "change_pct": -2.5 + "change_pct": -2.92 }, { "code": "02388", @@ -229,8 +229,8 @@ "action_note": "", "timing_signal": "大盘中性,行业偏强,低估值,蓝筹,持有" }, - "price": 46.4, - "change_pct": -1.02 + "price": 46.32, + "change_pct": -1.45 }, { "code": "09988", @@ -258,8 +258,8 @@ "action_note": "深套持有", "timing_signal": "大盘中性,行业偏弱,蓝筹,持有" }, - "price": 98.25, - "change_pct": -0.66 + "price": 98.45, + "change_pct": -0.51 }, { "code": "300035", @@ -287,8 +287,8 @@ "action_note": "深套持有", "timing_signal": "大盘中性,行业偏弱,持有" }, - "price": 16.21, - "change_pct": -0.92 + "price": 16.17, + "change_pct": -0.98 }, { "code": "300548", @@ -316,8 +316,8 @@ "action_note": "短炒强趋势持", "timing_signal": "大盘中性,行业中性,高估值,强趋势持" }, - "price": 282.89, - "change_pct": -1.09 + "price": 282.02, + "change_pct": -1.4 }, { "code": "300690", @@ -345,8 +345,8 @@ "action_note": "⚠️盈亏比偏低(1:1.5),不建议加仓", "timing_signal": "大盘中性,行业偏弱,持有" }, - "price": 22.82, - "change_pct": -3.47 + "price": 22.83, + "change_pct": -3.39 }, { "code": "300750", @@ -374,8 +374,8 @@ "action_note": "", "timing_signal": "大盘中性,行业偏弱,蓝筹,持有" }, - "price": 393.04, - "change_pct": 0.14 + "price": 392.25, + "change_pct": -0.07 }, { "code": "518880", @@ -404,7 +404,7 @@ "timing_signal": "大盘中性,行业偏弱,持有" }, "price": 8.45, - "change_pct": -0.97 + "change_pct": -1.07 }, { "code": "600036", @@ -432,8 +432,8 @@ "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "timing_signal": "大盘中性,行业偏强,低估值,蓝筹,弱势持有" }, - "price": 37.04, - "change_pct": -0.94 + "price": 37.07, + "change_pct": -0.88 }, { "code": "600563", @@ -490,8 +490,8 @@ "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "timing_signal": "大盘中性,行业偏强,持有" }, - "price": 10.49, - "change_pct": -1.5 + "price": 10.47, + "change_pct": -1.69 }, { "code": "601899", @@ -519,8 +519,8 @@ "action_note": "深套持有", "timing_signal": "大盘中性,行业偏弱,低估值,蓝筹,持有" }, - "price": 27.58, - "change_pct": -0.61 + "price": 27.56, + "change_pct": -0.68 }, { "code": "603259", @@ -548,8 +548,8 @@ "action_note": "", "timing_signal": "大盘中性,行业偏强,低估值,持有" }, - "price": 116.26, - "change_pct": 9.36 + "price": 116.18, + "change_pct": 9.28 }, { "code": "688411", @@ -577,8 +577,8 @@ "action_note": "", "timing_signal": "持有" }, - "price": 267.1, - "change_pct": -2.11 + "price": 267.18, + "change_pct": -1.86 }, { "code": "688981", @@ -606,8 +606,8 @@ "action_note": "", "timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有" }, - "price": 150.91, - "change_pct": 6.5 + "price": 151.24, + "change_pct": 6.73 } ] } \ No newline at end of file diff --git a/scripts/branch_scanner.py b/scripts/branch_scanner.py index 70bc40b..247e50d 100644 --- a/scripts/branch_scanner.py +++ b/scripts/branch_scanner.py @@ -1,28 +1,28 @@ #!/usr/bin/env python3 """ -branch_scanner.py — 盘中分支扫描器 +branch_scanner.py — 分支自成长数据采集器(全静默) -每15分钟跑一轮: -1. 读取所有有 strategy_tree 的股票 -2. 获取实时价格 -3. 评估每个分支在当前情景下是否适用 -4. 适用分支 → 记录 trigger_count + 推送信号 +核心功能(三件事,全部后台静默执行): +1. 每轮扫描42只股票,评估当前情景下各分支的适用性 +2. 适用分支 → trigger_count + 1,记录 last_triggered +3. 保存当前状态到 scanner_state.json 供下次对比 -自成长核心组件:让分支条件得到实际验证。 +无输出 → 静默运行。触发数据积累在 decisions.json。 +操作信号由 stale_push_wlin / price_monitor / 开盘收盘简报 另路输出。 + +数据流向(自成长):每15分钟branch_scanner积累trigger_count → + 每日prune_branches评估低效分支 → decisions.json修剪 → 分支越来越有效 """ -import json, sys, os, re -from datetime import datetime, date +import json, sys, re +from datetime import datetime 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" +SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.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"}) @@ -32,12 +32,10 @@ def get_price(code): if len(parts) > 3: return float(parts[3]) except Exception: - pass - return None + return None def get_scenario(): - """读当前情景""" try: sys.path.insert(0, "/home/hmo/MoFin") from strategy_tree import detect_scenario @@ -46,26 +44,11 @@ def get_scenario(): 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) @@ -75,8 +58,6 @@ def check_condition(branch, scenario_id, price): 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) @@ -86,119 +67,54 @@ def check_condition(branch, scenario_id, price): 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: + if now.hour < 9 or now.hour > 16: return 0 scenario = get_scenario() sid = scenario.get("id", "unknown") - slabel = scenario.get("label", "未知") - data = load_decisions() + + with open(DECISIONS_PATH) as f: + data = json.load(f) 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 + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now.strftime("%Y-%m-%d") break - if best: - best["trigger_count"] = best.get("trigger_count", 0) + 1 - best["last_triggered"] = today - triggered.append(code) + with open(DECISIONS_PATH, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) - # 状态变化检测:上次不是这个分支→推送 - 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) + # 更新状态快照 + state = {"scenario": sid, "updated_at": now.isoformat(), "branches": {}} + for e in decisions: + code = e.get("code", "") + tree = e.get("strategy_tree", {}) + for br in sorted(tree.get("branches", []), key=lambda b: b.get("priority", 999)): + if check_condition(br, sid, get_price(code)): + state["branches"][code] = br.get("id", "") + break + try: + with open(SCANNER_STATE, "w") as f: + json.dump(state, f, indent=2) + except Exception: + pass - # 推送条件:分支变化(情景变化已隐含)或止损首次触发 - 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