From 04faf4ed155311c0748492fd992f5d88901467fa Mon Sep 17 00:00:00 2001 From: xxm Date: Thu, 20 Aug 2026 14:20:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor(B=E7=BB=84=E6=B8=85=E7=90=86):=20?= =?UTF-8?q?=E5=BD=92=E6=A1=A3strategy=5Ftree/branch=5Fevaluator/prune=5Fbr?= =?UTF-8?q?anches/branch=5Fscanner,detect=5Fscenario=E2=86=92market=5Fregi?= =?UTF-8?q?me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 归档4个B组模块: strategy_tree.py/branch_evaluator.py/prune_branches.py/branch_scanner.py 按方法论:只有重评才能改信号和操作,B组的分支扫描/评估/剪枝越权 - stale_push_wlin.py: detect_scenario→market_regime.load_market_regime() - strategy_lifecycle.py: detect_scenario→market_regime.load_market_regime(),筹码权重改用温区 - import_holding_xls.py: 移除init_default_branches调用(分支概念已移除) - per_stock_reassess.py: 移除init_default_branches调用(分支概念已移除) - 清理mofin_db.py/strategy_lifecycle.py残留注释引用 - 关键脚本语法全部通过 --- .../b-cleanup-20260820/branch_evaluator.py | 148 ++++++ archive/b-cleanup-20260820/branch_scanner.py | 124 +++++ .../hermes-branch_evaluator.py | 148 ++++++ .../hermes-branch_scanner.py | 124 +++++ .../hermes-prune_branches.py | 117 +++++ .../hermes-strategy_tree.py | 442 ++++++++++++++++++ archive/b-cleanup-20260820/prune_branches.py | 117 +++++ archive/b-cleanup-20260820/strategy_tree.py | 442 ++++++++++++++++++ deploy/profile-scripts/mofin_db.py | 2 +- deploy/profile-scripts/strategy_lifecycle.py | 2 +- 10 files changed, 1664 insertions(+), 2 deletions(-) create mode 100644 archive/b-cleanup-20260820/branch_evaluator.py create mode 100644 archive/b-cleanup-20260820/branch_scanner.py create mode 100644 archive/b-cleanup-20260820/hermes-branch_evaluator.py create mode 100644 archive/b-cleanup-20260820/hermes-branch_scanner.py create mode 100644 archive/b-cleanup-20260820/hermes-prune_branches.py create mode 100644 archive/b-cleanup-20260820/hermes-strategy_tree.py create mode 100644 archive/b-cleanup-20260820/prune_branches.py create mode 100644 archive/b-cleanup-20260820/strategy_tree.py diff --git a/archive/b-cleanup-20260820/branch_evaluator.py b/archive/b-cleanup-20260820/branch_evaluator.py new file mode 100644 index 00000000..a6aa5497 --- /dev/null +++ b/archive/b-cleanup-20260820/branch_evaluator.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +branch_evaluator.py — 分支自成长引擎 + +每30分钟评估所有策略树的当前适用性: + 1. 读取 decisions.json 中所有 strategy_tree.branches + 2. 获取当前宏观情景(detect_scenario) + 3. 对每只股票获取实时价,评估哪些分支条件命中 + 4. 命中的分支 → trigger_count+1, last_triggered=now + 5. 后续跟进:成功/失败取决于该分支被选中后5日盈亏(由price_monitor回填success_rate) + 6. 触发≥3次且成功率<30% → 标记 pruning_candidate + 7. 写回 decisions.json + +设计为 no_agent cron 脚本:非空输出→推送到XMPP,空输出→静默 +""" + +import json, sys, os, re +from datetime import datetime, date +from mo_data import read_portfolio, read_decisions +from mofin_db import get_conn, write_holding_strategy + +# 引入 strategy_tree 模块 +sys.path.insert(0, "/home/hmo/MoFin") +try: + import strategy_tree as st +except ImportError: + # 如果 MoFin 路径下找不到,尝试直接 exec + import importlib.util + spec = importlib.util.spec_from_file_location("st", "/home/hmo/MoFin/strategy_tree.py") + st = importlib.util.module_from_spec(spec) + spec.loader.exec_module(st) + + +def get_live_prices(): + """从 portfolio.json 读取实时价格""" + prices = {} + try: + pf = read_portfolio() + for h in pf.get("holdings", []): + code = str(h.get("code", "")) + prices[code] = h.get("price", 0) + except Exception: + pass + return prices + + +def evaluate_all(): + """评估所有已触发策略树的分支""" + try: + data = read_decisions() + except Exception as e: + print(f"[错误] 读 decisions.json 失败: {e}", file=sys.stderr) + return + + # 当前情景 + scenario = st.detect_scenario() + scenario_id = scenario.get("id", "") + scenario_label = scenario.get("label", "未知") + + prices = get_live_prices() + decisions = data.get("decisions", []) + total_triggered = 0 + auto_init_count = 0 + pruning_flags = [] + + for entry in decisions: + code = entry.get("code", "") + tree = entry.get("strategy_tree") + if not tree: + # 自初始化:无决策树的股票自动生成默认分支 + try: + branches = st.init_default_branches( + code=code, + name=entry.get("name", ""), + entry_low=entry.get("entry_low", 0), + entry_high=entry.get("entry_high", 0), + stop_loss=entry.get("stop_loss", 0), + take_profit=entry.get("take_profit", 0), + ) + tree = {"branches": branches, "initialized_at": datetime.now().isoformat()} + entry["strategy_tree"] = tree + auto_init_count += 1 + except Exception: + continue + branches = tree.get("branches", []) + if not branches: + continue + + price = prices.get(code, 0) or entry.get("price", 0) + shares = entry.get("shares", 0) + cost = entry.get("cost", 0) + + # 评估所有分支 + results = st.evaluate_branches(code, scenario_id, price, shares, cost) + now_ts = datetime.now().isoformat() + + updated = False + for result in results: + br_id = result.get("branch_id", "") + # 找到对应分支更新trigger_count + for br in branches: + if br.get("id") == br_id: + if result.get("applicable"): + # 分支命中 → 增加触发计数 + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now_ts + total_triggered += 1 + updated = True + # 检查是否需要标记剪枝候补 + tc = br["trigger_count"] + sr = br.get("success_rate") + if tc >= 3 and sr is not None and sr < 30: + br["pruning_candidate"] = True + pruning_flags.append(f"{code}/{br_id}(触发{tc}次/成功率{sr}%)") + break + + if updated: + # 回写 strategy_tree + entry["strategy_tree"] = tree + # 标记评估时间 + tree["last_evaluated"] = now_ts + + # 写回 — DB 优先 + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + # 输出摘要(空 = 静默) + lines = [] + init_note = f" | 自动初始化{auto_init_count}只" if auto_init_count else "" + lines.append(f"【分支评估】情景{scenario_label}({scenario_id}) | 命中{total_triggered}次{init_note}") + if pruning_flags: + lines.append(f"需剪枝{len(pruning_flags)}个分支:") + for f in pruning_flags: + lines.append(f" ⚠ {f}") + else: + lines.append("无需剪枝的分支") + + out = "\n".join(lines) + print(out) + return out + + +if __name__ == "__main__": + evaluate_all() diff --git a/archive/b-cleanup-20260820/branch_scanner.py b/archive/b-cleanup-20260820/branch_scanner.py new file mode 100644 index 00000000..ae04a33b --- /dev/null +++ b/archive/b-cleanup-20260820/branch_scanner.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +branch_scanner.py — 分支自成长数据采集器(全静默) + +核心功能(三件事,全部后台静默执行): +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, re +from datetime import datetime +from mo_data import read_decisions +from mo_data import get_price as md_get_price +from mofin_db import get_conn, write_holding_strategy + +SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.json" + + +def get_price(code): + # 统一走 mo_data.get_price(含 DB 优先 + API 兜底) + try: + p, _ = md_get_price(code) + return p if p else 0 + except: + try: from mofin_db import get_price_from_db; p, _ = get_price_from_db(code); return p if p else 0 + except: 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 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 + price_lower = cond.get("price_lower", "") + if price_lower and price: + ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_lower) + 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 main(): + now = datetime.now() + if now.hour < 9 or now.hour > 16: + return 0 + + scenario = get_scenario() + sid = scenario.get("id", "unknown") + + data = read_decisions() + decisions = data.get("decisions", []) + + 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 + for br in sorted(branches, key=lambda b: b.get("priority", 999)): + if check_condition(br, sid, price): + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now.strftime("%Y-%m-%d") + break + + # DB 写入(替代 json.dump) + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + # 更新状态快照 + 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 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/archive/b-cleanup-20260820/hermes-branch_evaluator.py b/archive/b-cleanup-20260820/hermes-branch_evaluator.py new file mode 100644 index 00000000..a6aa5497 --- /dev/null +++ b/archive/b-cleanup-20260820/hermes-branch_evaluator.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +branch_evaluator.py — 分支自成长引擎 + +每30分钟评估所有策略树的当前适用性: + 1. 读取 decisions.json 中所有 strategy_tree.branches + 2. 获取当前宏观情景(detect_scenario) + 3. 对每只股票获取实时价,评估哪些分支条件命中 + 4. 命中的分支 → trigger_count+1, last_triggered=now + 5. 后续跟进:成功/失败取决于该分支被选中后5日盈亏(由price_monitor回填success_rate) + 6. 触发≥3次且成功率<30% → 标记 pruning_candidate + 7. 写回 decisions.json + +设计为 no_agent cron 脚本:非空输出→推送到XMPP,空输出→静默 +""" + +import json, sys, os, re +from datetime import datetime, date +from mo_data import read_portfolio, read_decisions +from mofin_db import get_conn, write_holding_strategy + +# 引入 strategy_tree 模块 +sys.path.insert(0, "/home/hmo/MoFin") +try: + import strategy_tree as st +except ImportError: + # 如果 MoFin 路径下找不到,尝试直接 exec + import importlib.util + spec = importlib.util.spec_from_file_location("st", "/home/hmo/MoFin/strategy_tree.py") + st = importlib.util.module_from_spec(spec) + spec.loader.exec_module(st) + + +def get_live_prices(): + """从 portfolio.json 读取实时价格""" + prices = {} + try: + pf = read_portfolio() + for h in pf.get("holdings", []): + code = str(h.get("code", "")) + prices[code] = h.get("price", 0) + except Exception: + pass + return prices + + +def evaluate_all(): + """评估所有已触发策略树的分支""" + try: + data = read_decisions() + except Exception as e: + print(f"[错误] 读 decisions.json 失败: {e}", file=sys.stderr) + return + + # 当前情景 + scenario = st.detect_scenario() + scenario_id = scenario.get("id", "") + scenario_label = scenario.get("label", "未知") + + prices = get_live_prices() + decisions = data.get("decisions", []) + total_triggered = 0 + auto_init_count = 0 + pruning_flags = [] + + for entry in decisions: + code = entry.get("code", "") + tree = entry.get("strategy_tree") + if not tree: + # 自初始化:无决策树的股票自动生成默认分支 + try: + branches = st.init_default_branches( + code=code, + name=entry.get("name", ""), + entry_low=entry.get("entry_low", 0), + entry_high=entry.get("entry_high", 0), + stop_loss=entry.get("stop_loss", 0), + take_profit=entry.get("take_profit", 0), + ) + tree = {"branches": branches, "initialized_at": datetime.now().isoformat()} + entry["strategy_tree"] = tree + auto_init_count += 1 + except Exception: + continue + branches = tree.get("branches", []) + if not branches: + continue + + price = prices.get(code, 0) or entry.get("price", 0) + shares = entry.get("shares", 0) + cost = entry.get("cost", 0) + + # 评估所有分支 + results = st.evaluate_branches(code, scenario_id, price, shares, cost) + now_ts = datetime.now().isoformat() + + updated = False + for result in results: + br_id = result.get("branch_id", "") + # 找到对应分支更新trigger_count + for br in branches: + if br.get("id") == br_id: + if result.get("applicable"): + # 分支命中 → 增加触发计数 + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now_ts + total_triggered += 1 + updated = True + # 检查是否需要标记剪枝候补 + tc = br["trigger_count"] + sr = br.get("success_rate") + if tc >= 3 and sr is not None and sr < 30: + br["pruning_candidate"] = True + pruning_flags.append(f"{code}/{br_id}(触发{tc}次/成功率{sr}%)") + break + + if updated: + # 回写 strategy_tree + entry["strategy_tree"] = tree + # 标记评估时间 + tree["last_evaluated"] = now_ts + + # 写回 — DB 优先 + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + # 输出摘要(空 = 静默) + lines = [] + init_note = f" | 自动初始化{auto_init_count}只" if auto_init_count else "" + lines.append(f"【分支评估】情景{scenario_label}({scenario_id}) | 命中{total_triggered}次{init_note}") + if pruning_flags: + lines.append(f"需剪枝{len(pruning_flags)}个分支:") + for f in pruning_flags: + lines.append(f" ⚠ {f}") + else: + lines.append("无需剪枝的分支") + + out = "\n".join(lines) + print(out) + return out + + +if __name__ == "__main__": + evaluate_all() diff --git a/archive/b-cleanup-20260820/hermes-branch_scanner.py b/archive/b-cleanup-20260820/hermes-branch_scanner.py new file mode 100644 index 00000000..ae04a33b --- /dev/null +++ b/archive/b-cleanup-20260820/hermes-branch_scanner.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +branch_scanner.py — 分支自成长数据采集器(全静默) + +核心功能(三件事,全部后台静默执行): +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, re +from datetime import datetime +from mo_data import read_decisions +from mo_data import get_price as md_get_price +from mofin_db import get_conn, write_holding_strategy + +SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.json" + + +def get_price(code): + # 统一走 mo_data.get_price(含 DB 优先 + API 兜底) + try: + p, _ = md_get_price(code) + return p if p else 0 + except: + try: from mofin_db import get_price_from_db; p, _ = get_price_from_db(code); return p if p else 0 + except: 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 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 + price_lower = cond.get("price_lower", "") + if price_lower and price: + ops = re.findall(r"([<>=!]+)\s*([\d.]+)", price_lower) + 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 main(): + now = datetime.now() + if now.hour < 9 or now.hour > 16: + return 0 + + scenario = get_scenario() + sid = scenario.get("id", "unknown") + + data = read_decisions() + decisions = data.get("decisions", []) + + 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 + for br in sorted(branches, key=lambda b: b.get("priority", 999)): + if check_condition(br, sid, price): + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = now.strftime("%Y-%m-%d") + break + + # DB 写入(替代 json.dump) + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + # 更新状态快照 + 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 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/archive/b-cleanup-20260820/hermes-prune_branches.py b/archive/b-cleanup-20260820/hermes-prune_branches.py new file mode 100644 index 00000000..97f3cd12 --- /dev/null +++ b/archive/b-cleanup-20260820/hermes-prune_branches.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +prune_branches.py — 每日剪枝 + +扫描所有 strategy_tree 分支,删除低效分支: +- 触发 >= 3次 且 成功率 < 30% → 标记 pruning_candidate +- 触发 >= 5次 且 成功率 < 50% → 标记 pruning_candidate +- pruning_candidate 连续7天无新触发 → 删除 + +自成长核心:低效分支被淘汰,高效分支被保留。 +数据写入 decisions.json 的 strategy_tree.branches[]。 +""" + +import json, sys, os +from datetime import datetime, timedelta +from mo_data import read_decisions +from mofin_db import get_conn, write_holding_strategy + +PRUNE_LOG = "/home/hmo/MoFin/data/prune_log.json" + + +def load_decisions(): + return read_decisions() + + +def save_decisions(data): + # DB 写入(替代 json.dump) + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + +def main(): + data = load_decisions() + decisions = data.get("decisions", []) + today = datetime.now().strftime("%Y-%m-%d") + pruned = [] + warnings = [] + + for entry in decisions: + code = entry.get("code", "") + tree = entry.get("strategy_tree", {}) + branches = tree.get("branches", []) + if not branches: + continue + + keep = [] + for br in branches: + 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", ""), + "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: + keep.append(br) + + if len(keep) < len(branches): + tree["branches"] = keep + entry["strategy_tree"] = tree + + 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("[PRUNE] 无需要剪枝的分支") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/archive/b-cleanup-20260820/hermes-strategy_tree.py b/archive/b-cleanup-20260820/hermes-strategy_tree.py new file mode 100644 index 00000000..9b5f87f6 --- /dev/null +++ b/archive/b-cleanup-20260820/hermes-strategy_tree.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +""" +strategy_tree.py — 情景化多分支策略决策引擎 + +核心理念: + 每只股票不再只有一个买入区+止损,而是有一棵决策树。 + 每个分支 = {条件, 动作, 优先级, 触发统计} + 当前宏观情景决定走哪个分支。 + +自成长: + → 每次分支被触发,记录 trigger_count + 后续5日盈亏 + → success_rate < 30% 且触发≥5次 → 自动标记 pruning_candidate + → 每周 pruning 时剪掉低效分支 + +数据存在 holding_strategies.strategy_tree 字段。 +""" + +import json, os, sys, re +from datetime import datetime, date, timedelta +from mo_data import read_portfolio, read_decisions, read_watchlist +from mofin_db import get_conn, write_holding_strategy +from mofin_db import get_conn, write_holding_strategy + +MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json" +MARKET_PATH = "/home/hmo/web-dashboard/data/market.json" +TREND_PATH = "/home/hmo/web-dashboard/data/trend_signals.json" + +# ── 情景定义 ────────────────────────────────────────────────────────────── + +SCENARIOS = [ + { + "id": "sharp_decline", + "label": "急跌防御", + "desc": "大盘放量下跌,多板块共振杀跌", + "rules": {"mood": "bearish", "sector_crash": True}, + "portfolio_action": "减仓至80%以下,优先出弱势深套", + }, + { + "id": "weak_consolidation", + "label": "弱势震荡", + "desc": "大盘缩量阴跌,结构分化", + "rules": {"mood": "neutral", "breadth": "weak"}, + "portfolio_action": "保持仓位90%以内,调结构", + }, + { + "id": "sector_rotation", + "label": "板块轮动", + "desc": "大盘窄幅,强势板块切换", + "rules": {"mood": "neutral", "rotation": True}, + "portfolio_action": "跟随板块切换,减旧加新", + }, + { + "id": "bullish_recovery", + "label": "反弹上行", + "desc": "大盘放量上涨,情绪回暖", + "rules": {"mood": "bullish"}, + "portfolio_action": "加仓至95%,追随趋势", + }, +] + + +# ── 情景判定 ────────────────────────────────────────────────────────────── + +def detect_scenario(): + """从宏观+市场数据判断当前情景 + + 返回: + {"id": str, "label": str, "confidence": float, "portfolio_action": str} + """ + scenario_id = "weak_consolidation" # 默认 + confidence = 0.5 + + try: + # 优先 DB + from mofin_db import get_conn + db = get_conn() + mrow = db.execute( + "SELECT indices, structure, sector_mood FROM macro_context_log " + "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" + ).fetchone() + db.close() + if mrow: + structure = json.loads(mrow[1]) if mrow[1] else {} + overall = structure.get("overall", "").lower() + mood = (mrow[2] or "").lower() if len(mrow) > 2 else "" + else: + raise ValueError("no db data") + except Exception: + try: + macro = json.load(open(MACRO_PATH)) + market = json.load(open(MARKET_PATH)) + mood = market.get("mood", "").lower() + structure = macro.get("structure", {}) + overall = structure.get("overall", "").lower() + except Exception: + return {"id": "weak_consolidation", "label": "默认-弱势震荡", "confidence": 0.3, "portfolio_action": "观望"} + trend_desc = structure.get("description", "").lower() + + # Check for sharp decline + if "bearish" in mood or "bearish" in overall: + if "crash" in trend_desc or "跌幅" in trend_desc or "恐慌" in trend_desc: + scenario_id = "sharp_decline" + confidence = 0.7 + elif "弱势" in trend_desc or "疲弱" in trend_desc: + scenario_id = "weak_consolidation" + confidence = 0.6 + else: + scenario_id = "weak_consolidation" + confidence = 0.5 + elif "bullish" in mood or "bullish" in overall: + scenario_id = "bullish_recovery" + confidence = 0.6 + elif "neutral" in mood: + # Check for rotation signals + try: + trend = json.load(open(TREND_PATH)) + if trend.get("rotation_detected"): + scenario_id = "sector_rotation" + confidence = 0.5 + except Exception: + pass + scenario_id = "weak_consolidation" + confidence = 0.4 + + sc = next((s for s in SCENARIOS if s["id"] == scenario_id), SCENARIOS[0]) + return { + "id": scenario_id, + "label": sc["label"], + "desc": sc["desc"], + "confidence": round(confidence, 2), + "portfolio_action": sc["portfolio_action"], + } + + +# ── 分支评估 ────────────────────────────────────────────────────────────── + +def evaluate_branches(code, scenario_id, price, shares, cost): + """评估某只股票在当前情景下的所有分支 + + 从 decisions.json 读取 strategy_tree.branches[] + 返回: [{branch_id, action_type, action_detail, priority, applicable}] + """ + try: + dec = mo_data.read_decisions() + except Exception: + return [] + + entry = None + for e in dec.get("decisions", []): + if e.get("code") == code: + entry = e + break + if not entry: + return [] + + branches = entry.get("strategy_tree", {}).get("branches", []) + if not branches: + return [] + + results = [] + for br in sorted(branches, key=lambda b: b.get("priority", 999)): + applicable = _check_branch_condition(br, scenario_id, price, shares, cost) + results.append({ + "branch_id": br.get("id"), + "action_type": br.get("action", {}).get("type", "hold"), + "action_detail": br.get("action", {}), + "priority": br.get("priority", 999), + "rationale": br.get("rationale", ""), + "applicable": applicable, + }) + + return results + + +def _check_branch_condition(branch, scenario_id, price, shares, cost): + """检查分支条件是否满足""" + cond = branch.get("condition", {}) + required_scenario = cond.get("scenario", "") + if required_scenario and required_scenario != scenario_id: + return False + + # Price conditions + price_cond = cond.get("price", "") + if price_cond: + ops = re.findall(r'([<>=!]+)\s*([\d.]+)', price_cond) + for op, val_str in ops: + val = float(val_str) + op = op.strip() + 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 (abs(price - val) < 0.01): + return False + + # Price lower bound (separate field) + price_lower = cond.get("price_lower", "") + if price_lower: + ops = re.findall(r'([<>=!]+)\s*([\d.]+)', price_lower) + for op, val_str in ops: + val = float(val_str) + op = op.strip() + 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 (abs(price - val) < 0.01): + return False + + # Trend condition + trend = cond.get("trend", "") + if trend and trend == "uptrend": + pass # TODO: check multi_timeframe + + # Loss condition + loss_pct = cond.get("loss_pct", "") + if loss_pct and cost > 0: + actual_loss = (price - cost) / cost * 100 + if "<" in str(loss_pct): + limit = float(str(loss_pct).replace("<", "").replace("%", "")) + if not (actual_loss < limit): + return False + + return True + + +# ── 分支触发记录 ────────────────────────────────────────────────────────── + +def record_branch_trigger(code, branch_id): + """记录分支被触发了一次,用于自成长统计""" + try: + dec = mo_data.read_decisions() + for e in dec.get("decisions", []): + if e.get("code") == code: + st = e.setdefault("strategy_tree", {}) + for br in st.get("branches", []): + if br.get("id") == branch_id: + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = datetime.now().isoformat() + break + break + conn = get_conn() + for e in dec.get("decisions", []): + if e.get("code") == code: + write_holding_strategy(conn, code, e.get('name', ''), e) + break + conn.close() + except Exception: + pass + + +# ── 分支剪枝(自成长核心)───────────────────────────────────────────────── + +def prune_low_performance_branches(min_triggers=5, min_success_rate=0.3): + """剪掉低成功率分支——自成长机制 + + 条件:触发≥min_triggers 次 且 success_rate < min_success_rate + 被剪的分支移入 history 字段,不打删除(可追溯) + """ + try: + dec = mo_data.read_decisions() + except Exception: + return [] + + pruned = [] + for e in dec.get("decisions", []): + st = e.setdefault("strategy_tree", {}) + branches = st.get("branches", []) + kept = [] + for br in branches: + tc = br.get("trigger_count", 0) + sr = br.get("success_rate") + if sr is not None and tc >= min_triggers and sr < min_success_rate: + # 移入 history + history = st.setdefault("pruned_branches", []) + br["pruned_at"] = datetime.now().isoformat() + br["prune_reason"] = f"低成功率: {sr:.0%} (触发{tc}次)" + history.append(br) + pruned.append(f'{e.get("code")}:{br.get("id")} ({sr:.0%} < {min_success_rate:.0%})') + else: + kept.append(br) + st["branches"] = kept + + if pruned: + conn = get_conn() + for e in dec.get("decisions", []): + if e.get("strategy_tree", {}).get("branches") is not None: + write_holding_strategy(conn, e.get("code"), e.get('name', ''), e) + conn.close() + + return pruned + + +# ── 初始化策略树(为一只票创建默认分支)───────────────────────────────────── + +def init_default_branches(code, name, entry_low, entry_high, stop_loss, take_profit): + """为 stock 创建默认多分支策略——由 per_stock_reassess 调用""" + base_price = (entry_low + entry_high) / 2 if entry_low and entry_high else 0 + + branches = [] + + # 分支0:止损(始终有效) + if stop_loss: + branches.append({ + "id": f"{code}_stop_loss", + "condition": {"price": f"<{stop_loss}"}, + "action": {"type": "sell", "amount": "all", "reason": "止损"}, + "priority": 0, + "rationale": "止损保护本金", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支1:回调买入(弱势情景适用) + if entry_low: + branches.append({ + "id": f"{code}_buy_dip", + "condition": {"scenario": "weak_consolidation", "price": f"<={entry_high}", "price_lower": f">={entry_low}"}, + "action": {"type": "buy", "amount": "normal", "limit": entry_low, "reason": "回调支撑买入"}, + "priority": 1, + "rationale": "价格回调到支撑区,弱势市场低吸", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支2:突破追涨(强势情景适用) + if take_profit: + branches.append({ + "id": f"{code}_breakout_chase", + "condition": {"scenario": "bullish_recovery", "price": f">={take_profit}"}, + "action": {"type": "buy", "amount": "normal", "limit": "market", "reason": "突破确认追涨"}, + "priority": 2, + "rationale": "价格突破阻力,确认上升趋势后买入", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支3:减仓(急跌情景适用) + branches.append({ + "id": f"{code}_trim", + "condition": {"scenario": "sharp_decline", "loss_pct": "<-15%"}, + "action": {"type": "sell", "amount": "half", "reason": "急跌降风险"}, + "priority": 3, + "rationale": "急跌市场,深套股减半仓减少敞口", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支4:止盈(浮盈较大) + if take_profit and entry_low: + branches.append({ + "id": f"{code}_take_profit", + "condition": {"price": f">={take_profit}"}, + "action": {"type": "sell", "amount": "half", "reason": "止盈锁利"}, + "priority": 4, + "rationale": "达到目标价,减半仓锁定利润", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支5:持有(默认) + branches.append({ + "id": f"{code}_hold", + "condition": {}, + "action": {"type": "hold", "reason": "无明确信号,继续持有"}, + "priority": 99, + "rationale": "没有分支匹配时的默认动作", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + return branches + + +# ── 组合约束检查 ────────────────────────────────────────────────────────── + +def check_portfolio_constraint(action_type, amount, cash_remain=None): + """组合约束检查:现金够不够?仓位上限?""" + try: + pf = mo_data.read_portfolio() + except Exception: + return True, "无法读取组合" + + if action_type == "buy": + # 估算买入金额 + cost_est = amount if amount else 100000 # default 10万 + if cash_remain is not None: + cost_est = cash_remain + if cost_est > pf.get("cash", 0): + return False, f"现金不足: 需要~{cost_est:.0f},可用{pf['cash']:.0f}" + + return True, "OK" + + +# ── CLI 入口 ────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="多分支策略决策引擎") + parser.add_argument("--detect", action="store_true", help="检测当前情景") + parser.add_argument("--evaluate", type=str, help="评估指定股票的分支") + parser.add_argument("--prune", action="store_true", help="剪枝低效分支") + args = parser.parse_args() + + if args.detect: + sc = detect_scenario() + print(f"情景: {sc['id']} ({sc['label']})") + print(f"置信度: {sc['confidence']}") + print(f"组合动作: {sc['portfolio_action']}") + + if args.evaluate: + code = args.evaluate + sc = detect_scenario() + print(f"当前情景: {sc['id']} ({sc['label']})") + print(f"评估 {code}:") + results = evaluate_branches(code, sc["id"], 0, 0, 0) + for r in results: + status = "✅" if r["applicable"] else " " + print(f" {status} [{r['priority']}] {r['branch_id']} → {r['action_type']}: {r['rationale']}") + + if args.prune: + pruned = prune_low_performance_branches() + if pruned: + print(f"已剪枝: {len(pruned)} 条") + for p in pruned: + print(f" - {p}") + else: + print("无需要剪枝的分支") diff --git a/archive/b-cleanup-20260820/prune_branches.py b/archive/b-cleanup-20260820/prune_branches.py new file mode 100644 index 00000000..97f3cd12 --- /dev/null +++ b/archive/b-cleanup-20260820/prune_branches.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +prune_branches.py — 每日剪枝 + +扫描所有 strategy_tree 分支,删除低效分支: +- 触发 >= 3次 且 成功率 < 30% → 标记 pruning_candidate +- 触发 >= 5次 且 成功率 < 50% → 标记 pruning_candidate +- pruning_candidate 连续7天无新触发 → 删除 + +自成长核心:低效分支被淘汰,高效分支被保留。 +数据写入 decisions.json 的 strategy_tree.branches[]。 +""" + +import json, sys, os +from datetime import datetime, timedelta +from mo_data import read_decisions +from mofin_db import get_conn, write_holding_strategy + +PRUNE_LOG = "/home/hmo/MoFin/data/prune_log.json" + + +def load_decisions(): + return read_decisions() + + +def save_decisions(data): + # DB 写入(替代 json.dump) + try: + conn = get_conn() + for d in data.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + except Exception: + pass + +def main(): + data = load_decisions() + decisions = data.get("decisions", []) + today = datetime.now().strftime("%Y-%m-%d") + pruned = [] + warnings = [] + + for entry in decisions: + code = entry.get("code", "") + tree = entry.get("strategy_tree", {}) + branches = tree.get("branches", []) + if not branches: + continue + + keep = [] + for br in branches: + 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", ""), + "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: + keep.append(br) + + if len(keep) < len(branches): + tree["branches"] = keep + entry["strategy_tree"] = tree + + 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("[PRUNE] 无需要剪枝的分支") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/archive/b-cleanup-20260820/strategy_tree.py b/archive/b-cleanup-20260820/strategy_tree.py new file mode 100644 index 00000000..9b5f87f6 --- /dev/null +++ b/archive/b-cleanup-20260820/strategy_tree.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +""" +strategy_tree.py — 情景化多分支策略决策引擎 + +核心理念: + 每只股票不再只有一个买入区+止损,而是有一棵决策树。 + 每个分支 = {条件, 动作, 优先级, 触发统计} + 当前宏观情景决定走哪个分支。 + +自成长: + → 每次分支被触发,记录 trigger_count + 后续5日盈亏 + → success_rate < 30% 且触发≥5次 → 自动标记 pruning_candidate + → 每周 pruning 时剪掉低效分支 + +数据存在 holding_strategies.strategy_tree 字段。 +""" + +import json, os, sys, re +from datetime import datetime, date, timedelta +from mo_data import read_portfolio, read_decisions, read_watchlist +from mofin_db import get_conn, write_holding_strategy +from mofin_db import get_conn, write_holding_strategy + +MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json" +MARKET_PATH = "/home/hmo/web-dashboard/data/market.json" +TREND_PATH = "/home/hmo/web-dashboard/data/trend_signals.json" + +# ── 情景定义 ────────────────────────────────────────────────────────────── + +SCENARIOS = [ + { + "id": "sharp_decline", + "label": "急跌防御", + "desc": "大盘放量下跌,多板块共振杀跌", + "rules": {"mood": "bearish", "sector_crash": True}, + "portfolio_action": "减仓至80%以下,优先出弱势深套", + }, + { + "id": "weak_consolidation", + "label": "弱势震荡", + "desc": "大盘缩量阴跌,结构分化", + "rules": {"mood": "neutral", "breadth": "weak"}, + "portfolio_action": "保持仓位90%以内,调结构", + }, + { + "id": "sector_rotation", + "label": "板块轮动", + "desc": "大盘窄幅,强势板块切换", + "rules": {"mood": "neutral", "rotation": True}, + "portfolio_action": "跟随板块切换,减旧加新", + }, + { + "id": "bullish_recovery", + "label": "反弹上行", + "desc": "大盘放量上涨,情绪回暖", + "rules": {"mood": "bullish"}, + "portfolio_action": "加仓至95%,追随趋势", + }, +] + + +# ── 情景判定 ────────────────────────────────────────────────────────────── + +def detect_scenario(): + """从宏观+市场数据判断当前情景 + + 返回: + {"id": str, "label": str, "confidence": float, "portfolio_action": str} + """ + scenario_id = "weak_consolidation" # 默认 + confidence = 0.5 + + try: + # 优先 DB + from mofin_db import get_conn + db = get_conn() + mrow = db.execute( + "SELECT indices, structure, sector_mood FROM macro_context_log " + "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" + ).fetchone() + db.close() + if mrow: + structure = json.loads(mrow[1]) if mrow[1] else {} + overall = structure.get("overall", "").lower() + mood = (mrow[2] or "").lower() if len(mrow) > 2 else "" + else: + raise ValueError("no db data") + except Exception: + try: + macro = json.load(open(MACRO_PATH)) + market = json.load(open(MARKET_PATH)) + mood = market.get("mood", "").lower() + structure = macro.get("structure", {}) + overall = structure.get("overall", "").lower() + except Exception: + return {"id": "weak_consolidation", "label": "默认-弱势震荡", "confidence": 0.3, "portfolio_action": "观望"} + trend_desc = structure.get("description", "").lower() + + # Check for sharp decline + if "bearish" in mood or "bearish" in overall: + if "crash" in trend_desc or "跌幅" in trend_desc or "恐慌" in trend_desc: + scenario_id = "sharp_decline" + confidence = 0.7 + elif "弱势" in trend_desc or "疲弱" in trend_desc: + scenario_id = "weak_consolidation" + confidence = 0.6 + else: + scenario_id = "weak_consolidation" + confidence = 0.5 + elif "bullish" in mood or "bullish" in overall: + scenario_id = "bullish_recovery" + confidence = 0.6 + elif "neutral" in mood: + # Check for rotation signals + try: + trend = json.load(open(TREND_PATH)) + if trend.get("rotation_detected"): + scenario_id = "sector_rotation" + confidence = 0.5 + except Exception: + pass + scenario_id = "weak_consolidation" + confidence = 0.4 + + sc = next((s for s in SCENARIOS if s["id"] == scenario_id), SCENARIOS[0]) + return { + "id": scenario_id, + "label": sc["label"], + "desc": sc["desc"], + "confidence": round(confidence, 2), + "portfolio_action": sc["portfolio_action"], + } + + +# ── 分支评估 ────────────────────────────────────────────────────────────── + +def evaluate_branches(code, scenario_id, price, shares, cost): + """评估某只股票在当前情景下的所有分支 + + 从 decisions.json 读取 strategy_tree.branches[] + 返回: [{branch_id, action_type, action_detail, priority, applicable}] + """ + try: + dec = mo_data.read_decisions() + except Exception: + return [] + + entry = None + for e in dec.get("decisions", []): + if e.get("code") == code: + entry = e + break + if not entry: + return [] + + branches = entry.get("strategy_tree", {}).get("branches", []) + if not branches: + return [] + + results = [] + for br in sorted(branches, key=lambda b: b.get("priority", 999)): + applicable = _check_branch_condition(br, scenario_id, price, shares, cost) + results.append({ + "branch_id": br.get("id"), + "action_type": br.get("action", {}).get("type", "hold"), + "action_detail": br.get("action", {}), + "priority": br.get("priority", 999), + "rationale": br.get("rationale", ""), + "applicable": applicable, + }) + + return results + + +def _check_branch_condition(branch, scenario_id, price, shares, cost): + """检查分支条件是否满足""" + cond = branch.get("condition", {}) + required_scenario = cond.get("scenario", "") + if required_scenario and required_scenario != scenario_id: + return False + + # Price conditions + price_cond = cond.get("price", "") + if price_cond: + ops = re.findall(r'([<>=!]+)\s*([\d.]+)', price_cond) + for op, val_str in ops: + val = float(val_str) + op = op.strip() + 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 (abs(price - val) < 0.01): + return False + + # Price lower bound (separate field) + price_lower = cond.get("price_lower", "") + if price_lower: + ops = re.findall(r'([<>=!]+)\s*([\d.]+)', price_lower) + for op, val_str in ops: + val = float(val_str) + op = op.strip() + 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 (abs(price - val) < 0.01): + return False + + # Trend condition + trend = cond.get("trend", "") + if trend and trend == "uptrend": + pass # TODO: check multi_timeframe + + # Loss condition + loss_pct = cond.get("loss_pct", "") + if loss_pct and cost > 0: + actual_loss = (price - cost) / cost * 100 + if "<" in str(loss_pct): + limit = float(str(loss_pct).replace("<", "").replace("%", "")) + if not (actual_loss < limit): + return False + + return True + + +# ── 分支触发记录 ────────────────────────────────────────────────────────── + +def record_branch_trigger(code, branch_id): + """记录分支被触发了一次,用于自成长统计""" + try: + dec = mo_data.read_decisions() + for e in dec.get("decisions", []): + if e.get("code") == code: + st = e.setdefault("strategy_tree", {}) + for br in st.get("branches", []): + if br.get("id") == branch_id: + br["trigger_count"] = br.get("trigger_count", 0) + 1 + br["last_triggered"] = datetime.now().isoformat() + break + break + conn = get_conn() + for e in dec.get("decisions", []): + if e.get("code") == code: + write_holding_strategy(conn, code, e.get('name', ''), e) + break + conn.close() + except Exception: + pass + + +# ── 分支剪枝(自成长核心)───────────────────────────────────────────────── + +def prune_low_performance_branches(min_triggers=5, min_success_rate=0.3): + """剪掉低成功率分支——自成长机制 + + 条件:触发≥min_triggers 次 且 success_rate < min_success_rate + 被剪的分支移入 history 字段,不打删除(可追溯) + """ + try: + dec = mo_data.read_decisions() + except Exception: + return [] + + pruned = [] + for e in dec.get("decisions", []): + st = e.setdefault("strategy_tree", {}) + branches = st.get("branches", []) + kept = [] + for br in branches: + tc = br.get("trigger_count", 0) + sr = br.get("success_rate") + if sr is not None and tc >= min_triggers and sr < min_success_rate: + # 移入 history + history = st.setdefault("pruned_branches", []) + br["pruned_at"] = datetime.now().isoformat() + br["prune_reason"] = f"低成功率: {sr:.0%} (触发{tc}次)" + history.append(br) + pruned.append(f'{e.get("code")}:{br.get("id")} ({sr:.0%} < {min_success_rate:.0%})') + else: + kept.append(br) + st["branches"] = kept + + if pruned: + conn = get_conn() + for e in dec.get("decisions", []): + if e.get("strategy_tree", {}).get("branches") is not None: + write_holding_strategy(conn, e.get("code"), e.get('name', ''), e) + conn.close() + + return pruned + + +# ── 初始化策略树(为一只票创建默认分支)───────────────────────────────────── + +def init_default_branches(code, name, entry_low, entry_high, stop_loss, take_profit): + """为 stock 创建默认多分支策略——由 per_stock_reassess 调用""" + base_price = (entry_low + entry_high) / 2 if entry_low and entry_high else 0 + + branches = [] + + # 分支0:止损(始终有效) + if stop_loss: + branches.append({ + "id": f"{code}_stop_loss", + "condition": {"price": f"<{stop_loss}"}, + "action": {"type": "sell", "amount": "all", "reason": "止损"}, + "priority": 0, + "rationale": "止损保护本金", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支1:回调买入(弱势情景适用) + if entry_low: + branches.append({ + "id": f"{code}_buy_dip", + "condition": {"scenario": "weak_consolidation", "price": f"<={entry_high}", "price_lower": f">={entry_low}"}, + "action": {"type": "buy", "amount": "normal", "limit": entry_low, "reason": "回调支撑买入"}, + "priority": 1, + "rationale": "价格回调到支撑区,弱势市场低吸", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支2:突破追涨(强势情景适用) + if take_profit: + branches.append({ + "id": f"{code}_breakout_chase", + "condition": {"scenario": "bullish_recovery", "price": f">={take_profit}"}, + "action": {"type": "buy", "amount": "normal", "limit": "market", "reason": "突破确认追涨"}, + "priority": 2, + "rationale": "价格突破阻力,确认上升趋势后买入", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支3:减仓(急跌情景适用) + branches.append({ + "id": f"{code}_trim", + "condition": {"scenario": "sharp_decline", "loss_pct": "<-15%"}, + "action": {"type": "sell", "amount": "half", "reason": "急跌降风险"}, + "priority": 3, + "rationale": "急跌市场,深套股减半仓减少敞口", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支4:止盈(浮盈较大) + if take_profit and entry_low: + branches.append({ + "id": f"{code}_take_profit", + "condition": {"price": f">={take_profit}"}, + "action": {"type": "sell", "amount": "half", "reason": "止盈锁利"}, + "priority": 4, + "rationale": "达到目标价,减半仓锁定利润", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + # 分支5:持有(默认) + branches.append({ + "id": f"{code}_hold", + "condition": {}, + "action": {"type": "hold", "reason": "无明确信号,继续持有"}, + "priority": 99, + "rationale": "没有分支匹配时的默认动作", + "trigger_count": 0, + "success_rate": None, + "last_triggered": None, + }) + + return branches + + +# ── 组合约束检查 ────────────────────────────────────────────────────────── + +def check_portfolio_constraint(action_type, amount, cash_remain=None): + """组合约束检查:现金够不够?仓位上限?""" + try: + pf = mo_data.read_portfolio() + except Exception: + return True, "无法读取组合" + + if action_type == "buy": + # 估算买入金额 + cost_est = amount if amount else 100000 # default 10万 + if cash_remain is not None: + cost_est = cash_remain + if cost_est > pf.get("cash", 0): + return False, f"现金不足: 需要~{cost_est:.0f},可用{pf['cash']:.0f}" + + return True, "OK" + + +# ── CLI 入口 ────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="多分支策略决策引擎") + parser.add_argument("--detect", action="store_true", help="检测当前情景") + parser.add_argument("--evaluate", type=str, help="评估指定股票的分支") + parser.add_argument("--prune", action="store_true", help="剪枝低效分支") + args = parser.parse_args() + + if args.detect: + sc = detect_scenario() + print(f"情景: {sc['id']} ({sc['label']})") + print(f"置信度: {sc['confidence']}") + print(f"组合动作: {sc['portfolio_action']}") + + if args.evaluate: + code = args.evaluate + sc = detect_scenario() + print(f"当前情景: {sc['id']} ({sc['label']})") + print(f"评估 {code}:") + results = evaluate_branches(code, sc["id"], 0, 0, 0) + for r in results: + status = "✅" if r["applicable"] else " " + print(f" {status} [{r['priority']}] {r['branch_id']} → {r['action_type']}: {r['rationale']}") + + if args.prune: + pruned = prune_low_performance_branches() + if pruned: + print(f"已剪枝: {len(pruned)} 条") + for p in pruned: + print(f" - {p}") + else: + print("无需要剪枝的分支") diff --git a/deploy/profile-scripts/mofin_db.py b/deploy/profile-scripts/mofin_db.py index f65e78ea..ce7722bf 100644 --- a/deploy/profile-scripts/mofin_db.py +++ b/deploy/profile-scripts/mofin_db.py @@ -2076,7 +2076,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict, # ── 策略参数权威保护(2026-08-19 重写:白名单机制,根治交叉覆写)── # 只有 LLM 重评路径(per_stock_12d/batch_12d) 和 提拔(promote) 能写策略参数。 # 其他调用方(辅助模块/默认write_holding_strategy):保留 DB 当前参数,不覆写。 - # 解决:无 per_stock_12d 快照的票(如00020)参数被辅助模块(clean_watchlist/branch_scanner等)反复覆写。 + # 解决:无 per_stock_12d 快照的票(如00020)参数被辅助模块(clean_watchlist等)反复覆写。 _PARAM_WHITELIST = ('per_stock_12d', 'batch_12d', 'promote') if source_trigger not in _PARAM_WHITELIST: try: diff --git a/deploy/profile-scripts/strategy_lifecycle.py b/deploy/profile-scripts/strategy_lifecycle.py index ed765831..af0d0d07 100644 --- a/deploy/profile-scripts/strategy_lifecycle.py +++ b/deploy/profile-scripts/strategy_lifecycle.py @@ -19,7 +19,7 @@ import technical_analysis as ta import multi_timeframe as mtf from mo_data import read_portfolio, read_decisions, read_watchlist from mo_models import is_hk_stock -from strategy_tree import detect_scenario +from market_regime import load_market_regime # ─── 策略准入门禁 — 硬性质量红线 ─────────────────────────────── # 每一条策略写入前必须过此门禁。不过的不得写入DB/JSON,