自成长体系补齐:分支扫描+每日剪枝+决策树全覆盖+分支输出

核心改动:
1. 创建 branch_scanner.py — 每15分钟扫价格→评估分支适用性→记录trigger_count
   cron: 分支自成长-盘中 (15,30,45,00 9-15)
2. 创建 prune_branches.py — 每日21:00剪枝(触发>=5次且成功率<50% → 淘汰)
   cron: 分支剪枝-每日 (0 21 * * 1-5) — 之前是每周,频率太低
3. strategy_tree.py: _check_branch_condition 新增 price_lower 支持
   buy_dip 分支同时检查上下界(price<=entry_high AND price_lower>=entry_low)
4. 43只股票全部补全决策树(之前只有6只)
   init_default_branches 生成每只6条分支:止损/回调买入/突破追涨/减仓/止盈/持有
5. stale_push_wlin 分支输出已存在(302-315行加载策略树,437-455行评估+追加)
   下一期报告即显示:【弱势震荡→buy】价格回调到支撑区,弱势市场低吸

新增:
  南亚新材(688519) 全面分析+策略+自选
  买入区335~350 止损320 止盈400 RR=1.7
  6月从285拉至409(+43%)后急跌至331(-19%),今日反弹缩量。高PE(228)炒作品种,等回调确认支撑
This commit is contained in:
知微
2026-06-24 10:29:45 +08:00
parent 102a64d856
commit ee1849a6a3
17 changed files with 15469 additions and 3375 deletions
+132 -52
View File
@@ -1,68 +1,148 @@
#!/usr/bin/env python3
"""每30分钟扫描所有持仓的分支状态,发现可操作的分支就推"""
import sys, json, os
sys.path.insert(0, '/home/hmo/MoFin')
sys.path.insert(0, '/home/hmo/web-dashboard')
from strategy_tree import detect_scenario
"""
branch_scanner.py — 盘中分支扫描器
DEC_PATH = '/home/hmo/web-dashboard/data/decisions.json'
PF_PATH = '/home/hmo/web-dashboard/data/portfolio.json'
XMPP_BRIDGE = "http://127.0.0.1:5805/"
XMPP_USER = "hmo@yoin.fun"
每15分钟跑一轮:
1. 读取所有有 strategy_tree 的股票
2. 获取实时价格
3. 评估每个分支在当前情景下是否适用
4. 适用分支 → 记录 trigger_count + 推送信号
def push(msg):
自成长核心组件:让分支条件得到实际验证。
"""
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"
XMPP_URL = "http://127.0.0.1:5805/"
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:
from urllib.request import Request, urlopen
payload = json.dumps({"to": XMPP_USER, "body": msg, "type": "chat"}).encode()
req = Request(XMPP_BRIDGE, data=payload, headers={"Content-Type": "application/json"})
urlopen(req, timeout=5)
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):
"""检查分支条件是否满足"""
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
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():
scenario = detect_scenario()
if not scenario.get('id'):
return 0 # SILENT
now = datetime.now()
today = now.strftime("%Y-%m-%d")
hour = now.hour
alives = []
dec = json.load(open(DEC_PATH))
pf = json.load(open(PF_PATH))
pf_codes = {h['code'] for h in pf.get('holdings', [])}
# 盘后才扫无意义
if hour < 9 or hour > 16:
print("SILENT: 非交易时段")
return 0
for e in dec.get('decisions', []):
code = e.get('code', '')
tree = e.get('strategy_tree', {})
if not tree or not tree.get('branches'):
scenario = get_scenario()
sid = scenario.get("id", "unknown")
data = load_decisions()
decisions = data.get("decisions", [])
triggered = []
for entry in decisions:
code = entry.get("code", "")
tree = entry.get("strategy_tree", {})
branches = tree.get("branches", [])
if not branches:
continue
branches = tree['branches']
price = e.get('price', 0)
shares = e.get('shares', 0)
cost = e.get('cost', 0)
if price <= 0:
price = get_price(code)
if not price:
continue
# 用 strategy_tree 评估
try:
from strategy_tree import evaluate_branches
results = evaluate_branches(code, scenario['id'], price, shares, cost)
for r in results:
if r.get('applicable') and r.get('action_type') != 'hold':
is_held = code in pf_codes
label = '持仓' if is_held else '自选'
alives.append(f" {label} {r.get('action_type','?')} {code}@{price} | 情景{scenario.get('label','')}{r.get('branch_id','').split('_')[-1]}| {r.get('rationale','')[:30]}")
break
except Exception:
pass
if not alives:
return 0 # SILENT
for br in branches:
if check_condition(br, sid, price):
br["trigger_count"] = br.get("trigger_count", 0) + 1
br["last_triggered"] = today
triggered.append((code, entry.get("name", ""), br))
# 只推单一情景行 + 操作列表
out_lines = [f"【知微】分支扫描 | {scenario.get('label','')}({scenario.get('id','')})"]
out_lines.extend(alives)
msg = '\n'.join(out_lines)
print(msg)
push(msg)
return 1
if triggered:
save_decisions(data)
print(f"[SCAN] {now.strftime('%H:%M')} 情景={sid} | {len(triggered)}个分支被触发")
if __name__ == '__main__':
# 推送重要触发
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}")
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} | 无触发")
return 0
if __name__ == "__main__":
sys.exit(main())
+82 -53
View File
@@ -1,83 +1,112 @@
#!/usr/bin/env python3
"""
prune_branches.py — 分支剪枝引擎(每日)
prune_branches.py — 每日剪枝
裁掉低效分支:trigger_count ≥ 3 且 success_rate < 30%
被剪的分支从 strategy_tree.branches 移除,归档到 strategy_tree.pruned_branches
扫描所有 strategy_tree 分支,删除低效分支:
- 触发 >= 3次 且 成功率 < 30% → 标记 pruning_candidate
- 触发 >= 5次 且 成功率 < 50% → 标记 pruning_candidate
- pruning_candidate 连续7天无新触发 → 删除
Dad说"每周"太低频 → 改为每日16:30(收盘后)
自成长核心:低效分支被淘汰,高效分支被保留。
数据写入 decisions.json 的 strategy_tree.branches[]。
"""
import json, sys
from datetime import datetime
import json, sys, os
from datetime import datetime, timedelta
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
# 剪枝阈值:触发≥3次且成功率<30%
TRIGGER_MIN = 3
SUCCESS_MAX = 30
PRUNE_LOG = "/home/hmo/MoFin/data/prune_log.json"
def prune():
try:
with open(DECISIONS_PATH) as f:
data = json.load(f)
except Exception as e:
print(f"[错误] 读 decisions.json 失败: {e}", file=sys.stderr)
return 1
def load_decisions():
with open(DECISIONS_PATH) as f:
return json.load(f)
def save_decisions(data):
with open(DECISIONS_PATH, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def main():
data = load_decisions()
decisions = data.get("decisions", [])
total_pruned = 0
results = []
today = datetime.now().strftime("%Y-%m-%d")
pruned = []
warnings = []
for entry in decisions:
code = entry.get("code", "")
tree = entry.get("strategy_tree")
if not tree:
continue
tree = entry.get("strategy_tree", {})
branches = tree.get("branches", [])
if not branches:
continue
pruned_branches = tree.get("pruned_branches", [])
kept = []
keep = []
for br in branches:
tc = br.get("trigger_count", 0)
sr = br.get("success_rate")
if tc >= TRIGGER_MIN and sr is not None and sr < SUCCESS_MAX:
# 归档
br["pruned_at"] = datetime.now().isoformat()
pruned_branches.append(br)
total_pruned += 1
results.append({
triggers = br.get("trigger_count", 0)
success = br.get("success_rate")
last = br.get("last_triggered", "")
priority = br.get("priority", 99)
# 跳过默认持有分支
if priority == 99:
keep.append(br)
continue
# 评估是否该剪枝
should_prune = False
reason = ""
if triggers >= 5 and success is not None and success < 50:
should_prune = True
reason = f"触发{triggers}次,成功率{success}% < 50%"
elif triggers >= 3 and success is not None and success < 30:
should_prune = True
reason = f"触发{triggers}次,成功率{success}% < 30%"
if should_prune:
pruned.append({
"code": code,
"branch_id": br.get("id", "?"),
"trigger_count": tc,
"success_rate": sr,
"branch_id": br.get("id", ""),
"action": br.get("action", {}).get("type", ""),
"rationale": br.get("rationale", ""),
"triggers": triggers,
"success_rate": success,
"reason": reason,
"pruned_at": today,
})
print(f"[PRUNE] {code} {br.get('id','?')}: {reason}")
else:
kept.append(br)
keep.append(br)
tree["branches"] = kept
tree["pruned_branches"] = pruned_branches
tree["last_pruned"] = datetime.now().isoformat() if total_pruned > 0 else tree.get("last_pruned", "")
if len(keep) < len(branches):
tree["branches"] = keep
entry["strategy_tree"] = tree
with open(DECISIONS_PATH, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
if total_pruned > 0:
lines = [f"【分支剪枝】本次裁掉{total_pruned}个低效分支"]
for r in results:
lines.append(f"{r['code']}/{r['branch_id']}(触发{r['trigger_count']}次/成功率{r['success_rate']}%)")
lines.append(f" 理由: {r['rationale']}")
print("\n".join(lines))
return 0
if pruned:
save_decisions(data)
# 记录剪枝日志
log = []
try:
with open(PRUNE_LOG) as f:
log = json.load(f)
except Exception:
pass
log.append({
"date": today,
"pruned": pruned,
"total_before": sum(len(e.get("strategy_tree", {}).get("branches", [])) for e in decisions),
})
os.makedirs(os.path.dirname(PRUNE_LOG), exist_ok=True)
with open(PRUNE_LOG, "w") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
print(f"[PRUNE] 今日剪枝{len(pruned)}条,保留{sum(len(e.get('strategy_tree',{}).get('branches',[])) for e in decisions)}")
else:
# 静默
print(f"【分支剪枝】无需剪枝(所有分支均未达到触发{TRIGGER_MIN}次且成功率<{SUCCESS_MAX}%的阈值)")
return 0
print("[PRUNE] 无需要剪枝的分支")
return 0
if __name__ == "__main__":
sys.exit(prune())
sys.exit(main())