branch_scanner 彻底静默:自成长采集器不出操作信号
branch_scanner 是自成长数据收集器,不输出也不推送。 操作信号由 stale_push_wlin / price_monitor / 开盘收盘简报 按cron-report-format规范输出。 scanner只做三件事: 1. 每15分钟扫价格+评估分支 2. trigger_count + 1(决策树数据) 3. 更新状态快照
This commit is contained in:
+36
-120
@@ -1,28 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
branch_scanner.py — 盘中分支扫描器
|
branch_scanner.py — 分支自成长数据采集器(全静默)
|
||||||
|
|
||||||
每15分钟跑一轮:
|
核心功能(三件事,全部后台静默执行):
|
||||||
1. 读取所有有 strategy_tree 的股票
|
1. 每轮扫描42只股票,评估当前情景下各分支的适用性
|
||||||
2. 获取实时价格
|
2. 适用分支 → trigger_count + 1,记录 last_triggered
|
||||||
3. 评估每个分支在当前情景下是否适用
|
3. 保存当前状态到 scanner_state.json 供下次对比
|
||||||
4. 适用分支 → 记录 trigger_count + 推送信号
|
|
||||||
|
|
||||||
自成长核心组件:让分支条件得到实际验证。
|
无输出 → 静默运行。触发数据积累在 decisions.json。
|
||||||
|
操作信号由 stale_push_wlin / price_monitor / 开盘收盘简报 另路输出。
|
||||||
|
|
||||||
|
数据流向(自成长):每15分钟branch_scanner积累trigger_count →
|
||||||
|
每日prune_branches评估低效分支 → decisions.json修剪 → 分支越来越有效
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json, sys, os, re
|
import json, sys, re
|
||||||
from datetime import datetime, date
|
from datetime import datetime
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
||||||
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
|
SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.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):
|
def get_price(code):
|
||||||
"""腾讯API实时价格"""
|
|
||||||
mkt = "sh" if code.startswith("6") or code.startswith("5") else "sz"
|
mkt = "sh" if code.startswith("6") or code.startswith("5") else "sz"
|
||||||
url = f"http://qt.gtimg.cn/q={mkt}{code}"
|
url = f"http://qt.gtimg.cn/q={mkt}{code}"
|
||||||
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
@@ -32,12 +32,10 @@ def get_price(code):
|
|||||||
if len(parts) > 3:
|
if len(parts) > 3:
|
||||||
return float(parts[3])
|
return float(parts[3])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_scenario():
|
def get_scenario():
|
||||||
"""读当前情景"""
|
|
||||||
try:
|
try:
|
||||||
sys.path.insert(0, "/home/hmo/MoFin")
|
sys.path.insert(0, "/home/hmo/MoFin")
|
||||||
from strategy_tree import detect_scenario
|
from strategy_tree import detect_scenario
|
||||||
@@ -46,26 +44,11 @@ def get_scenario():
|
|||||||
return {"id": "unknown", "label": "未知", "confidence": 0}
|
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):
|
def check_condition(branch, scenario_id, price):
|
||||||
"""检查分支条件是否满足 — 同步 strategy_tree._check_branch_condition 逻辑"""
|
|
||||||
cond = branch.get("condition", {})
|
cond = branch.get("condition", {})
|
||||||
required_scenario = cond.get("scenario", "")
|
required_scenario = cond.get("scenario", "")
|
||||||
if required_scenario and required_scenario != scenario_id:
|
if required_scenario and required_scenario != scenario_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
price_cond = cond.get("price", "")
|
price_cond = cond.get("price", "")
|
||||||
if price_cond and price:
|
if price_cond and price:
|
||||||
ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_cond)
|
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
|
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", "")
|
price_lower = cond.get("price_lower", "")
|
||||||
if price_lower and price:
|
if price_lower and price:
|
||||||
ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_lower)
|
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
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
today = now.strftime("%Y-%m-%d")
|
if now.hour < 9 or now.hour > 16:
|
||||||
hour = now.hour
|
|
||||||
|
|
||||||
# 盘后才扫无意义
|
|
||||||
if hour < 9 or hour > 16:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
scenario = get_scenario()
|
scenario = get_scenario()
|
||||||
sid = scenario.get("id", "unknown")
|
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", [])
|
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:
|
for entry in decisions:
|
||||||
code = entry.get("code", "")
|
code = entry.get("code", "")
|
||||||
tree = entry.get("strategy_tree", {})
|
tree = entry.get("strategy_tree", {})
|
||||||
branches = tree.get("branches", [])
|
branches = tree.get("branches", [])
|
||||||
if not branches:
|
if not branches:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
price = get_price(code)
|
price = get_price(code)
|
||||||
if not price:
|
if not price:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 找出最优适用分支
|
|
||||||
best = None
|
|
||||||
for br in sorted(branches, key=lambda b: b.get("priority", 999)):
|
for br in sorted(branches, key=lambda b: b.get("priority", 999)):
|
||||||
if check_condition(br, sid, price):
|
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
|
break
|
||||||
|
|
||||||
if best:
|
with open(DECISIONS_PATH, "w") as f:
|
||||||
best["trigger_count"] = best.get("trigger_count", 0) + 1
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
best["last_triggered"] = today
|
|
||||||
triggered.append(code)
|
|
||||||
|
|
||||||
# 状态变化检测:上次不是这个分支→推送
|
# 更新状态快照
|
||||||
action_type = best.get("action", {}).get("type", "hold")
|
state = {"scenario": sid, "updated_at": now.isoformat(), "branches": {}}
|
||||||
br_id = best.get("id", "")
|
for e in decisions:
|
||||||
last_br = last_state.get("branches", {}).get(code, "")
|
code = e.get("code", "")
|
||||||
last_action = last_state.get("branches", {}).get(f"{code}_action", "")
|
tree = e.get("strategy_tree", {})
|
||||||
changed = (br_id != last_br)
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+26
-26
@@ -147,7 +147,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -411,7 +411,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 0,
|
"priority": 0,
|
||||||
"rationale": "止损保护本金",
|
"rationale": "止损保护本金",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -705,7 +705,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -852,7 +852,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 6,
|
"trigger_count": 7,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -4831,7 +4831,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5035,7 +5035,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 6,
|
"trigger_count": 7,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5197,7 +5197,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5401,7 +5401,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5642,7 +5642,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
"rationale": "达到目标价,减半仓锁定利润",
|
"rationale": "达到目标价,减半仓锁定利润",
|
||||||
"trigger_count": 5,
|
"trigger_count": 6,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5802,7 +5802,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -5978,7 +5978,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -6253,7 +6253,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -6351,7 +6351,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 10,
|
"trigger_count": 11,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -6606,7 +6606,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
"rationale": "达到目标价,减半仓锁定利润",
|
"rationale": "达到目标价,减半仓锁定利润",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -6823,7 +6823,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 6,
|
"trigger_count": 7,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -6913,7 +6913,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 6,
|
"trigger_count": 7,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -7117,7 +7117,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -7358,7 +7358,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
"rationale": "达到目标价,减半仓锁定利润",
|
"rationale": "达到目标价,减半仓锁定利润",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -7505,7 +7505,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -7716,7 +7716,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -7920,7 +7920,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 3,
|
"trigger_count": 4,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -8060,7 +8060,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"rationale": "价格回调到支撑区,弱势市场低吸",
|
"rationale": "价格回调到支撑区,弱势市场低吸",
|
||||||
"trigger_count": 6,
|
"trigger_count": 7,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
},
|
},
|
||||||
@@ -8328,7 +8328,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -8518,7 +8518,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
@@ -8653,7 +8653,7 @@
|
|||||||
},
|
},
|
||||||
"priority": 99,
|
"priority": 99,
|
||||||
"rationale": "没有分支匹配时的默认动作",
|
"rationale": "没有分支匹配时的默认动作",
|
||||||
"trigger_count": 7,
|
"trigger_count": 8,
|
||||||
"success_rate": null,
|
"success_rate": null,
|
||||||
"last_triggered": "2026-06-24"
|
"last_triggered": "2026-06-24"
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-35
@@ -55,8 +55,8 @@
|
|||||||
"action_note": "⚠️盈亏比偏低(1:1.3),不建议加仓",
|
"action_note": "⚠️盈亏比偏低(1:1.3),不建议加仓",
|
||||||
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
|
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 83.95,
|
"price": 83.65,
|
||||||
"change_pct": 7.84
|
"change_pct": 7.45
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "01088",
|
"code": "01088",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
"timing_signal": "大盘中性,行业中性,蓝筹,持有"
|
"timing_signal": "大盘中性,行业中性,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 41.52,
|
"price": 41.52,
|
||||||
"change_pct": -1.09
|
"change_pct": -1.28
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "01211",
|
"code": "01211",
|
||||||
@@ -113,8 +113,8 @@
|
|||||||
"action_note": "深套持有",
|
"action_note": "深套持有",
|
||||||
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 75.0,
|
"price": 74.95,
|
||||||
"change_pct": -1.12
|
"change_pct": -1.19
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "01478",
|
"code": "01478",
|
||||||
@@ -142,8 +142,8 @@
|
|||||||
"action_note": "深套持有",
|
"action_note": "深套持有",
|
||||||
"timing_signal": "大盘中性,行业中性,低估值,持有"
|
"timing_signal": "大盘中性,行业中性,低估值,持有"
|
||||||
},
|
},
|
||||||
"price": 7.68,
|
"price": 7.67,
|
||||||
"change_pct": -2.79
|
"change_pct": -2.66
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "01888",
|
"code": "01888",
|
||||||
@@ -171,8 +171,8 @@
|
|||||||
"action_note": "短炒强趋势持",
|
"action_note": "短炒强趋势持",
|
||||||
"timing_signal": "大盘中性,行业偏弱,高估值,强趋势持"
|
"timing_signal": "大盘中性,行业偏弱,高估值,强趋势持"
|
||||||
},
|
},
|
||||||
"price": 96.75,
|
"price": 96.15,
|
||||||
"change_pct": 10.89
|
"change_pct": 10.2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "02202",
|
"code": "02202",
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
"timing_signal": "大盘中性,行业中性,高估值,持有"
|
"timing_signal": "大盘中性,行业中性,高估值,持有"
|
||||||
},
|
},
|
||||||
"price": 2.34,
|
"price": 2.34,
|
||||||
"change_pct": -2.5
|
"change_pct": -2.92
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "02388",
|
"code": "02388",
|
||||||
@@ -229,8 +229,8 @@
|
|||||||
"action_note": "",
|
"action_note": "",
|
||||||
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,持有"
|
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 46.4,
|
"price": 46.32,
|
||||||
"change_pct": -1.02
|
"change_pct": -1.45
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "09988",
|
"code": "09988",
|
||||||
@@ -258,8 +258,8 @@
|
|||||||
"action_note": "深套持有",
|
"action_note": "深套持有",
|
||||||
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 98.25,
|
"price": 98.45,
|
||||||
"change_pct": -0.66
|
"change_pct": -0.51
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "300035",
|
"code": "300035",
|
||||||
@@ -287,8 +287,8 @@
|
|||||||
"action_note": "深套持有",
|
"action_note": "深套持有",
|
||||||
"timing_signal": "大盘中性,行业偏弱,持有"
|
"timing_signal": "大盘中性,行业偏弱,持有"
|
||||||
},
|
},
|
||||||
"price": 16.21,
|
"price": 16.17,
|
||||||
"change_pct": -0.92
|
"change_pct": -0.98
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "300548",
|
"code": "300548",
|
||||||
@@ -316,8 +316,8 @@
|
|||||||
"action_note": "短炒强趋势持",
|
"action_note": "短炒强趋势持",
|
||||||
"timing_signal": "大盘中性,行业中性,高估值,强趋势持"
|
"timing_signal": "大盘中性,行业中性,高估值,强趋势持"
|
||||||
},
|
},
|
||||||
"price": 282.89,
|
"price": 282.02,
|
||||||
"change_pct": -1.09
|
"change_pct": -1.4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "300690",
|
"code": "300690",
|
||||||
@@ -345,8 +345,8 @@
|
|||||||
"action_note": "⚠️盈亏比偏低(1:1.5),不建议加仓",
|
"action_note": "⚠️盈亏比偏低(1:1.5),不建议加仓",
|
||||||
"timing_signal": "大盘中性,行业偏弱,持有"
|
"timing_signal": "大盘中性,行业偏弱,持有"
|
||||||
},
|
},
|
||||||
"price": 22.82,
|
"price": 22.83,
|
||||||
"change_pct": -3.47
|
"change_pct": -3.39
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "300750",
|
"code": "300750",
|
||||||
@@ -374,8 +374,8 @@
|
|||||||
"action_note": "",
|
"action_note": "",
|
||||||
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 393.04,
|
"price": 392.25,
|
||||||
"change_pct": 0.14
|
"change_pct": -0.07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "518880",
|
"code": "518880",
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
"timing_signal": "大盘中性,行业偏弱,持有"
|
"timing_signal": "大盘中性,行业偏弱,持有"
|
||||||
},
|
},
|
||||||
"price": 8.45,
|
"price": 8.45,
|
||||||
"change_pct": -0.97
|
"change_pct": -1.07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "600036",
|
"code": "600036",
|
||||||
@@ -432,8 +432,8 @@
|
|||||||
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
|
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
|
||||||
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,弱势持有"
|
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,弱势持有"
|
||||||
},
|
},
|
||||||
"price": 37.04,
|
"price": 37.07,
|
||||||
"change_pct": -0.94
|
"change_pct": -0.88
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "600563",
|
"code": "600563",
|
||||||
@@ -490,8 +490,8 @@
|
|||||||
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
|
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
|
||||||
"timing_signal": "大盘中性,行业偏强,持有"
|
"timing_signal": "大盘中性,行业偏强,持有"
|
||||||
},
|
},
|
||||||
"price": 10.49,
|
"price": 10.47,
|
||||||
"change_pct": -1.5
|
"change_pct": -1.69
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "601899",
|
"code": "601899",
|
||||||
@@ -519,8 +519,8 @@
|
|||||||
"action_note": "深套持有",
|
"action_note": "深套持有",
|
||||||
"timing_signal": "大盘中性,行业偏弱,低估值,蓝筹,持有"
|
"timing_signal": "大盘中性,行业偏弱,低估值,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 27.58,
|
"price": 27.56,
|
||||||
"change_pct": -0.61
|
"change_pct": -0.68
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "603259",
|
"code": "603259",
|
||||||
@@ -548,8 +548,8 @@
|
|||||||
"action_note": "",
|
"action_note": "",
|
||||||
"timing_signal": "大盘中性,行业偏强,低估值,持有"
|
"timing_signal": "大盘中性,行业偏强,低估值,持有"
|
||||||
},
|
},
|
||||||
"price": 116.26,
|
"price": 116.18,
|
||||||
"change_pct": 9.36
|
"change_pct": 9.28
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "688411",
|
"code": "688411",
|
||||||
@@ -577,8 +577,8 @@
|
|||||||
"action_note": "",
|
"action_note": "",
|
||||||
"timing_signal": "持有"
|
"timing_signal": "持有"
|
||||||
},
|
},
|
||||||
"price": 267.1,
|
"price": 267.18,
|
||||||
"change_pct": -2.11
|
"change_pct": -1.86
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "688981",
|
"code": "688981",
|
||||||
@@ -606,8 +606,8 @@
|
|||||||
"action_note": "",
|
"action_note": "",
|
||||||
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
|
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
|
||||||
},
|
},
|
||||||
"price": 150.91,
|
"price": 151.24,
|
||||||
"change_pct": 6.5
|
"change_pct": 6.73
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+36
-120
@@ -1,28 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
branch_scanner.py — 盘中分支扫描器
|
branch_scanner.py — 分支自成长数据采集器(全静默)
|
||||||
|
|
||||||
每15分钟跑一轮:
|
核心功能(三件事,全部后台静默执行):
|
||||||
1. 读取所有有 strategy_tree 的股票
|
1. 每轮扫描42只股票,评估当前情景下各分支的适用性
|
||||||
2. 获取实时价格
|
2. 适用分支 → trigger_count + 1,记录 last_triggered
|
||||||
3. 评估每个分支在当前情景下是否适用
|
3. 保存当前状态到 scanner_state.json 供下次对比
|
||||||
4. 适用分支 → 记录 trigger_count + 推送信号
|
|
||||||
|
|
||||||
自成长核心组件:让分支条件得到实际验证。
|
无输出 → 静默运行。触发数据积累在 decisions.json。
|
||||||
|
操作信号由 stale_push_wlin / price_monitor / 开盘收盘简报 另路输出。
|
||||||
|
|
||||||
|
数据流向(自成长):每15分钟branch_scanner积累trigger_count →
|
||||||
|
每日prune_branches评估低效分支 → decisions.json修剪 → 分支越来越有效
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json, sys, os, re
|
import json, sys, re
|
||||||
from datetime import datetime, date
|
from datetime import datetime
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
||||||
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
|
SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.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):
|
def get_price(code):
|
||||||
"""腾讯API实时价格"""
|
|
||||||
mkt = "sh" if code.startswith("6") or code.startswith("5") else "sz"
|
mkt = "sh" if code.startswith("6") or code.startswith("5") else "sz"
|
||||||
url = f"http://qt.gtimg.cn/q={mkt}{code}"
|
url = f"http://qt.gtimg.cn/q={mkt}{code}"
|
||||||
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
@@ -32,12 +32,10 @@ def get_price(code):
|
|||||||
if len(parts) > 3:
|
if len(parts) > 3:
|
||||||
return float(parts[3])
|
return float(parts[3])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_scenario():
|
def get_scenario():
|
||||||
"""读当前情景"""
|
|
||||||
try:
|
try:
|
||||||
sys.path.insert(0, "/home/hmo/MoFin")
|
sys.path.insert(0, "/home/hmo/MoFin")
|
||||||
from strategy_tree import detect_scenario
|
from strategy_tree import detect_scenario
|
||||||
@@ -46,26 +44,11 @@ def get_scenario():
|
|||||||
return {"id": "unknown", "label": "未知", "confidence": 0}
|
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):
|
def check_condition(branch, scenario_id, price):
|
||||||
"""检查分支条件是否满足 — 同步 strategy_tree._check_branch_condition 逻辑"""
|
|
||||||
cond = branch.get("condition", {})
|
cond = branch.get("condition", {})
|
||||||
required_scenario = cond.get("scenario", "")
|
required_scenario = cond.get("scenario", "")
|
||||||
if required_scenario and required_scenario != scenario_id:
|
if required_scenario and required_scenario != scenario_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
price_cond = cond.get("price", "")
|
price_cond = cond.get("price", "")
|
||||||
if price_cond and price:
|
if price_cond and price:
|
||||||
ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_cond)
|
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
|
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", "")
|
price_lower = cond.get("price_lower", "")
|
||||||
if price_lower and price:
|
if price_lower and price:
|
||||||
ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_lower)
|
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
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
today = now.strftime("%Y-%m-%d")
|
if now.hour < 9 or now.hour > 16:
|
||||||
hour = now.hour
|
|
||||||
|
|
||||||
# 盘后才扫无意义
|
|
||||||
if hour < 9 or hour > 16:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
scenario = get_scenario()
|
scenario = get_scenario()
|
||||||
sid = scenario.get("id", "unknown")
|
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", [])
|
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:
|
for entry in decisions:
|
||||||
code = entry.get("code", "")
|
code = entry.get("code", "")
|
||||||
tree = entry.get("strategy_tree", {})
|
tree = entry.get("strategy_tree", {})
|
||||||
branches = tree.get("branches", [])
|
branches = tree.get("branches", [])
|
||||||
if not branches:
|
if not branches:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
price = get_price(code)
|
price = get_price(code)
|
||||||
if not price:
|
if not price:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 找出最优适用分支
|
|
||||||
best = None
|
|
||||||
for br in sorted(branches, key=lambda b: b.get("priority", 999)):
|
for br in sorted(branches, key=lambda b: b.get("priority", 999)):
|
||||||
if check_condition(br, sid, price):
|
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
|
break
|
||||||
|
|
||||||
if best:
|
with open(DECISIONS_PATH, "w") as f:
|
||||||
best["trigger_count"] = best.get("trigger_count", 0) + 1
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
best["last_triggered"] = today
|
|
||||||
triggered.append(code)
|
|
||||||
|
|
||||||
# 状态变化检测:上次不是这个分支→推送
|
# 更新状态快照
|
||||||
action_type = best.get("action", {}).get("type", "hold")
|
state = {"scenario": sid, "updated_at": now.isoformat(), "branches": {}}
|
||||||
br_id = best.get("id", "")
|
for e in decisions:
|
||||||
last_br = last_state.get("branches", {}).get(code, "")
|
code = e.get("code", "")
|
||||||
last_action = last_state.get("branches", {}).get(f"{code}_action", "")
|
tree = e.get("strategy_tree", {})
|
||||||
changed = (br_id != last_br)
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user