branch_scanner: 状态变化驱动推送,停止15分钟噪音
核心逻辑重写: - 不再每15分钟推30只股票的买入信号(噪音) - 改为静默数据采集 + 状态变化检测 - scanner_state.json 记录上一轮各股最优分支 - 只有以下情况才推: ① 情景切换(如弱势震荡→急跌防御) ② 某只股票的最优分支变化(如持有→买入/止损) ③ 止损首次触发(P0新出现才推,不重复推) 日常运行时完全静默,决策树数据持续累积
This commit is contained in:
+69
-29
@@ -19,7 +19,6 @@ 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"
|
||||
XMPP_URL = "http://127.0.0.1:5805/"
|
||||
|
||||
|
||||
def get_price(code):
|
||||
@@ -91,14 +90,6 @@ def check_condition(branch, scenario_id, price):
|
||||
return True
|
||||
|
||||
|
||||
def push_alert(msg):
|
||||
try:
|
||||
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
|
||||
urlopen(XMPP_URL, data=payload, timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
now = datetime.now()
|
||||
today = now.strftime("%Y-%m-%d")
|
||||
@@ -106,15 +97,25 @@ def main():
|
||||
|
||||
# 盘后才扫无意义
|
||||
if hour < 9 or hour > 16:
|
||||
print("SILENT: 非交易时段")
|
||||
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", {})
|
||||
@@ -126,39 +127,78 @@ def main():
|
||||
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, entry.get("name", ""), best))
|
||||
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)
|
||||
print(f"[SCAN] {now.strftime('%H:%M')} 情景={sid} | {len(triggered)}个分支被触发")
|
||||
|
||||
# 推送重要触发
|
||||
alerts = []
|
||||
for code, name, br in triggered:
|
||||
action = br.get("action", {})
|
||||
action_type = action.get("type", "hold")
|
||||
priority = br.get("priority", 99)
|
||||
rationale = br.get("rationale", "")
|
||||
count = br.get("trigger_count", 1)
|
||||
if action_type != "hold":
|
||||
alerts.append(f" {code} {name}: {action_type}({rationale})触发{count}次")
|
||||
# 保存当前状态供下次对比
|
||||
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:
|
||||
msg = f"【分支扫描】{now.strftime('%H:%M')} | 情景{sid}\n" + "\n".join(alerts)
|
||||
push_alert(msg)
|
||||
else:
|
||||
print(f"[SCAN] {now.strftime('%H:%M')} | 情景{sid} | 无触发")
|
||||
# 输出:变化才出声,平常静默
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user