branch_scanner: 状态变化驱动推送,停止15分钟噪音

核心逻辑重写:
- 不再每15分钟推30只股票的买入信号(噪音)
- 改为静默数据采集 + 状态变化检测
- scanner_state.json 记录上一轮各股最优分支
- 只有以下情况才推:
  ① 情景切换(如弱势震荡→急跌防御)
  ② 某只股票的最优分支变化(如持有→买入/止损)
  ③ 止损首次触发(P0新出现才推,不重复推)

日常运行时完全静默,决策树数据持续累积
This commit is contained in:
知微
2026-06-24 10:37:39 +08:00
parent e1c426fb96
commit 68e806cfa1
4 changed files with 202 additions and 122 deletions
+67 -27
View File
@@ -19,7 +19,6 @@ DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json" MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json"
EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json" EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
XMPP_URL = "http://127.0.0.1:5805/"
def get_price(code): def get_price(code):
@@ -91,14 +90,6 @@ def check_condition(branch, scenario_id, price):
return True 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(): def main():
now = datetime.now() now = datetime.now()
today = now.strftime("%Y-%m-%d") today = now.strftime("%Y-%m-%d")
@@ -106,15 +97,25 @@ def main():
# 盘后才扫无意义 # 盘后才扫无意义
if hour < 9 or hour > 16: if hour < 9 or hour > 16:
print("SILENT: 非交易时段")
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() data = load_decisions()
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 = [] 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", {})
@@ -126,7 +127,7 @@ def main():
if not price: if not price:
continue continue
# 找出所有适用分支,选最优(优先级数字最低) # 找出最优适用分支
best = None 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):
@@ -136,29 +137,68 @@ def main():
if best: if best:
best["trigger_count"] = best.get("trigger_count", 0) + 1 best["trigger_count"] = best.get("trigger_count", 0) + 1
best["last_triggered"] = today 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: if triggered:
save_decisions(data) save_decisions(data)
print(f"[SCAN] {now.strftime('%H:%M')} 情景={sid} | {len(triggered)}个分支被触发")
# 推送重要触发 # 保存当前状态供下次对比
alerts = [] new_state = {
for code, name, br in triggered: "scenario": sid,
action = br.get("action", {}) "scenario_label": slabel,
action_type = action.get("type", "hold") "branches": {},
priority = br.get("priority", 99) "updated_at": now.isoformat(),
rationale = br.get("rationale", "") }
count = br.get("trigger_count", 1) for e in decisions:
if action_type != "hold": code = e.get("code", "")
alerts.append(f" {code} {name}: {action_type}{rationale})触发{count}") 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: if alerts:
msg = f"分支扫描{now.strftime('%H:%M')} | 情景{sid}\n" + "\n".join(alerts) scenario_shift = f"情景切换{last_state.get('scenario','')}{sid}" if sid != last_state.get("scenario") else ""
push_alert(msg) header = f"【分支扫描】{now.strftime('%H:%M')}" + (f" | {scenario_shift}" if scenario_shift else "")
else: msg = header + "\n" + "\n".join(alerts)
print(f"[SCAN] {now.strftime('%H:%M')} | 情景{sid} | 无触发") print(msg)
return 0
# 无变化→静默
return 0 return 0
+27 -27
View File
@@ -147,7 +147,7 @@
}, },
"priority": 99, "priority": 99,
"rationale": "没有分支匹配时的默认动作", "rationale": "没有分支匹配时的默认动作",
"trigger_count": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 2, "trigger_count": 6,
"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": 3, "trigger_count": 7,
"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": 2, "trigger_count": 6,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 1, "trigger_count": 5,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 6, "trigger_count": 10,
"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": 3, "trigger_count": 7,
"success_rate": null, "success_rate": null,
"last_triggered": "2026-06-24" "last_triggered": "2026-06-24"
}, },
@@ -6759,7 +6759,7 @@
}, },
"priority": 1, "priority": 1,
"rationale": "价格回调到支撑区,弱势市场低吸", "rationale": "价格回调到支撑区,弱势市场低吸",
"trigger_count": 1, "trigger_count": 2,
"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": 3, "trigger_count": 6,
"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": 2, "trigger_count": 6,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"success_rate": null, "success_rate": null,
"last_triggered": "2026-06-24" "last_triggered": "2026-06-24"
} }
@@ -7856,7 +7856,7 @@
}, },
"priority": 1, "priority": 1,
"rationale": "价格回调到支撑区,弱势市场低吸", "rationale": "价格回调到支撑区,弱势市场低吸",
"trigger_count": 1, "trigger_count": 5,
"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": 2, "trigger_count": 6,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"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": 3, "trigger_count": 7,
"success_rate": null, "success_rate": null,
"last_triggered": "2026-06-24" "last_triggered": "2026-06-24"
} }
+37 -37
View File
@@ -26,8 +26,8 @@
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有" "timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
}, },
"price": 416.6, "price": 416.4,
"change_pct": 0.68 "change_pct": 0.39
}, },
{ {
"code": "00981", "code": "00981",
@@ -55,8 +55,8 @@
"action_note": "⚠️盈亏比偏低(1:1.3),不建议加仓", "action_note": "⚠️盈亏比偏低(1:1.3),不建议加仓",
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有" "timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
}, },
"price": 83.9, "price": 83.95,
"change_pct": 7.77 "change_pct": 7.84
}, },
{ {
"code": "01088", "code": "01088",
@@ -84,8 +84,8 @@
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
"timing_signal": "大盘中性,行业中性,蓝筹,持有" "timing_signal": "大盘中性,行业中性,蓝筹,持有"
}, },
"price": 41.54, "price": 41.52,
"change_pct": -1.28 "change_pct": -1.09
}, },
{ {
"code": "01211", "code": "01211",
@@ -113,8 +113,8 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有" "timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
}, },
"price": 75.1, "price": 75.0,
"change_pct": -0.99 "change_pct": -1.12
}, },
{ {
"code": "01478", "code": "01478",
@@ -142,7 +142,7 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业中性,低估值,持有" "timing_signal": "大盘中性,行业中性,低估值,持有"
}, },
"price": 7.67, "price": 7.68,
"change_pct": -2.79 "change_pct": -2.79
}, },
{ {
@@ -171,8 +171,8 @@
"action_note": "短炒强趋势持", "action_note": "短炒强趋势持",
"timing_signal": "大盘中性,行业偏弱,高估值,强趋势持" "timing_signal": "大盘中性,行业偏弱,高估值,强趋势持"
}, },
"price": 94.8, "price": 96.75,
"change_pct": 8.19 "change_pct": 10.89
}, },
{ {
"code": "02202", "code": "02202",
@@ -201,7 +201,7 @@
"timing_signal": "大盘中性,行业中性,高估值,持有" "timing_signal": "大盘中性,行业中性,高估值,持有"
}, },
"price": 2.34, "price": 2.34,
"change_pct": -2.92 "change_pct": -2.5
}, },
{ {
"code": "02388", "code": "02388",
@@ -229,8 +229,8 @@
"action_note": "", "action_note": "",
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,持有" "timing_signal": "大盘中性,行业偏强,低估值,蓝筹,持有"
}, },
"price": 46.58, "price": 46.4,
"change_pct": -0.89 "change_pct": -1.02
}, },
{ {
"code": "09988", "code": "09988",
@@ -258,8 +258,8 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有" "timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
}, },
"price": 98.45, "price": 98.25,
"change_pct": -0.51 "change_pct": -0.66
}, },
{ {
"code": "300035", "code": "300035",
@@ -287,8 +287,8 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业偏弱,持有" "timing_signal": "大盘中性,行业偏弱,持有"
}, },
"price": 16.06, "price": 16.21,
"change_pct": -1.83 "change_pct": -0.92
}, },
{ {
"code": "300548", "code": "300548",
@@ -316,8 +316,8 @@
"action_note": "短炒强趋势持", "action_note": "短炒强趋势持",
"timing_signal": "大盘中性,行业中性,高估值,强趋势持" "timing_signal": "大盘中性,行业中性,高估值,强趋势持"
}, },
"price": 283.8, "price": 282.89,
"change_pct": -0.78 "change_pct": -1.09
}, },
{ {
"code": "300690", "code": "300690",
@@ -345,8 +345,8 @@
"action_note": "⚠️盈亏比偏低(1:1.5),不建议加仓", "action_note": "⚠️盈亏比偏低(1:1.5),不建议加仓",
"timing_signal": "大盘中性,行业偏弱,持有" "timing_signal": "大盘中性,行业偏弱,持有"
}, },
"price": 22.79, "price": 22.82,
"change_pct": -3.55 "change_pct": -3.47
}, },
{ {
"code": "300750", "code": "300750",
@@ -374,8 +374,8 @@
"action_note": "", "action_note": "",
"timing_signal": "大盘中性,行业偏弱,蓝筹,持有" "timing_signal": "大盘中性,行业偏弱,蓝筹,持有"
}, },
"price": 392.99, "price": 393.04,
"change_pct": 0.12 "change_pct": 0.14
}, },
{ {
"code": "518880", "code": "518880",
@@ -403,8 +403,8 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业偏弱,持有" "timing_signal": "大盘中性,行业偏弱,持有"
}, },
"price": 8.46, "price": 8.45,
"change_pct": -0.98 "change_pct": -0.97
}, },
{ {
"code": "600036", "code": "600036",
@@ -433,7 +433,7 @@
"timing_signal": "大盘中性,行业偏强,低估值,蓝筹,弱势持有" "timing_signal": "大盘中性,行业偏强,低估值,蓝筹,弱势持有"
}, },
"price": 37.04, "price": 37.04,
"change_pct": -0.96 "change_pct": -0.94
}, },
{ {
"code": "600563", "code": "600563",
@@ -490,8 +490,8 @@
"action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓",
"timing_signal": "大盘中性,行业偏强,持有" "timing_signal": "大盘中性,行业偏强,持有"
}, },
"price": 10.47, "price": 10.49,
"change_pct": -1.69 "change_pct": -1.5
}, },
{ {
"code": "601899", "code": "601899",
@@ -519,8 +519,8 @@
"action_note": "深套持有", "action_note": "深套持有",
"timing_signal": "大盘中性,行业偏弱,低估值,蓝筹,持有" "timing_signal": "大盘中性,行业偏弱,低估值,蓝筹,持有"
}, },
"price": 27.51, "price": 27.58,
"change_pct": -0.94 "change_pct": -0.61
}, },
{ {
"code": "603259", "code": "603259",
@@ -548,8 +548,8 @@
"action_note": "", "action_note": "",
"timing_signal": "大盘中性,行业偏强,低估值,持有" "timing_signal": "大盘中性,行业偏强,低估值,持有"
}, },
"price": 116.45, "price": 116.26,
"change_pct": 9.54 "change_pct": 9.36
}, },
{ {
"code": "688411", "code": "688411",
@@ -577,8 +577,8 @@
"action_note": "", "action_note": "",
"timing_signal": "持有" "timing_signal": "持有"
}, },
"price": 266.91, "price": 267.1,
"change_pct": -1.96 "change_pct": -2.11
}, },
{ {
"code": "688981", "code": "688981",
@@ -606,8 +606,8 @@
"action_note": "", "action_note": "",
"timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有" "timing_signal": "大盘中性,行业中性,高估值,蓝筹,持有"
}, },
"price": 151.13, "price": 150.91,
"change_pct": 6.65 "change_pct": 6.5
} }
] ]
} }
+67 -27
View File
@@ -19,7 +19,6 @@ DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json" MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json"
EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json" EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
XMPP_URL = "http://127.0.0.1:5805/"
def get_price(code): def get_price(code):
@@ -91,14 +90,6 @@ def check_condition(branch, scenario_id, price):
return True 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(): def main():
now = datetime.now() now = datetime.now()
today = now.strftime("%Y-%m-%d") today = now.strftime("%Y-%m-%d")
@@ -106,15 +97,25 @@ def main():
# 盘后才扫无意义 # 盘后才扫无意义
if hour < 9 or hour > 16: if hour < 9 or hour > 16:
print("SILENT: 非交易时段")
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() data = load_decisions()
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 = [] 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", {})
@@ -126,7 +127,7 @@ def main():
if not price: if not price:
continue continue
# 找出所有适用分支,选最优(优先级数字最低) # 找出最优适用分支
best = None 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):
@@ -136,29 +137,68 @@ def main():
if best: if best:
best["trigger_count"] = best.get("trigger_count", 0) + 1 best["trigger_count"] = best.get("trigger_count", 0) + 1
best["last_triggered"] = today 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: if triggered:
save_decisions(data) save_decisions(data)
print(f"[SCAN] {now.strftime('%H:%M')} 情景={sid} | {len(triggered)}个分支被触发")
# 推送重要触发 # 保存当前状态供下次对比
alerts = [] new_state = {
for code, name, br in triggered: "scenario": sid,
action = br.get("action", {}) "scenario_label": slabel,
action_type = action.get("type", "hold") "branches": {},
priority = br.get("priority", 99) "updated_at": now.isoformat(),
rationale = br.get("rationale", "") }
count = br.get("trigger_count", 1) for e in decisions:
if action_type != "hold": code = e.get("code", "")
alerts.append(f" {code} {name}: {action_type}{rationale})触发{count}") 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: if alerts:
msg = f"分支扫描{now.strftime('%H:%M')} | 情景{sid}\n" + "\n".join(alerts) scenario_shift = f"情景切换{last_state.get('scenario','')}{sid}" if sid != last_state.get("scenario") else ""
push_alert(msg) header = f"【分支扫描】{now.strftime('%H:%M')}" + (f" | {scenario_shift}" if scenario_shift else "")
else: msg = header + "\n" + "\n".join(alerts)
print(f"[SCAN] {now.strftime('%H:%M')} | 情景{sid} | 无触发") print(msg)
return 0
# 无变化→静默
return 0 return 0