#!/usr/bin/env python3 # -*- coding: utf-8 -*- """swap_decision.py — 换仓决策模块(2026-08-18 老莫:资金腾挪时对比预期收益取最低者卖) 核心:需要资金买入新票时,对比【新票预期收益 E_new】vs【持仓不卖预期收益 E_hold】, 按 E_hold 升序选票卖出凑钱(卖预期收益最低的)。能代码算的尽量代码算,LLM 只做定性修正。 数据依据(10y 回测实测,2026-08-18 验证): E_hold 查表(深套等到底均收益): -20~-25%: -3.8% (30d恢复76% 60d85% 120d92%) -25~-30%: -3.7% (30d55% 60d72% 120d85%) -30~-40%: -2.9% (30d29% 60d38% 120d65%) -40%以下: -0.3% (30d18% 60d21% 120d48%) E_new 查表(策略avg_pnl,strategy_regime_perf_by_period 温区级优先,fallback 10y整体): v_weak: +3.34% | b_td1_v3: +8.01% | s2_panic: +16.61% | v_next/v8.1: +9.39% 安全边际: E_new - E_hold >= 2%(覆盖双边交易成本~0.4% + 新票亏损风险) """ import sqlite3, json DB = "/home/hmo/MoFin/data/mofin.db" SAFE_MARGIN = 2.0 # 换仓安全边际(%) # 深套深度 → E_hold(等到底均收益%)— 2026-08-18 10y回测实测 E_HOLD_TABLE = [ # (dd_min, dd_max, e_hold_pct, note) (-100, -40, -0.3, "深套>40%:弹回概率极低,等到底期望≈-0.3%"), (-40, -30, -2.9, "深套30-40%:120天仅65%恢复,期望-2.9%"), (-30, -25, -3.7, "深套25-30%:60天72%恢复,期望-3.7%"), (-25, -20, -3.8, "深套20-25%:30天76%恢复,期望-3.8%"), ] def _get_conn(): conn = sqlite3.connect(DB, timeout=30) conn.execute("PRAGMA busy_timeout=30000") return conn def get_current_regime(market="a"): """当前温区""" try: conn = _get_conn() r = conn.execute("SELECT regime FROM market_regime WHERE market=? ORDER BY date DESC LIMIT 1", (market,)).fetchone() conn.close() return r[0] if r else "unknown" except Exception: return "unknown" def get_strategy_expected(strategy, market="a", regime=None): """E_new:策略预期单笔收益。温区级优先(strategy_regime_perf_by_period),fallback 10y整体(strategy_research)。 返回 (avg_pnl, source_desc)""" conn = _get_conn() try: # 温区级优先 if regime and regime != "unknown": r = conn.execute( "SELECT avg_pnl, trades FROM strategy_regime_perf_by_period " "WHERE strategy=? AND market=? AND regime=? AND period_tag='2y'", (strategy, market, regime)).fetchone() if r and r[0] is not None and r[1] and r[1] >= 5: conn.close() return (float(r[0]), f"温区{regime}2y({r[1]}笔)") # fallback 10y 整体 r2 = conn.execute( "SELECT results_json FROM strategy_research WHERE version=? AND COALESCE(market,'a')=? AND period_tag='10y' " "ORDER BY id DESC LIMIT 1", (strategy, market)).fetchone() if r2 and r2[0]: trades = json.loads(r2[0]).get("trades", []) if trades: avg = sum(t.get("profit_pct", 0) for t in trades) / len(trades) conn.close() return (round(avg, 2), f"10y整体({len(trades)}笔)") except Exception: pass conn.close() return (0.0, "无数据") def get_hold_expected(dd_pct): """E_hold:深套持仓等到底期望。按深套深度查表。非深套(dd>-20)返回 None(不算深套)。""" if dd_pct is None or dd_pct > -20: return None for lo, hi, e_hold, note in E_HOLD_TABLE: if lo <= dd_pct < hi: return {"e_hold": e_hold, "note": note, "dd": round(dd_pct, 1)} return {"e_hold": E_HOLD_TABLE[-1][2], "note": E_HOLD_TABLE[-1][3], "dd": round(dd_pct, 1)} def compute_hold_pnl(cost, price): """持仓浮盈%(成本 vs 现价)""" if not cost or not price: return None return (price - cost) / cost * 100 def decide_swap(need_cash, holdings, new_strategy, new_expected=None, market="a", regime=None): """核心换仓决策。 need_cash: 需要腾出的资金(元) holdings: [{code, name, cost, price, shares, strategy_name, dd_pct(深套深度,可选), pnl_pct}] new_strategy: 新标的的策略名 new_expected: 新标的预期收益(外部已算),None 则查表 返回: {decided, sell_list, need_cash, raised, reason, e_new, e_new_src} """ regime = regime or get_current_regime(market) # E_new if new_expected is None: e_new, e_new_src = get_strategy_expected(new_strategy, market, regime) else: e_new, e_new_src = new_expected, "外部提供" # 每只持仓算 E_hold scored = [] for h in holdings: code = h.get("code", "") name = h.get("name", code) cost = h.get("cost") or 0 price = h.get("price") or 0 shares = h.get("shares") or 0 market_val = price * shares if price and shares else 0 # 深套判定:优先用外部给的 dd_pct,否则用浮盈算(-20% 以下 = 深套) dd = h.get("dd_pct") pnl = h.get("pnl_pct") if dd is None and pnl is None and cost and price: pnl = compute_hold_pnl(cost, price) if dd is None: dd = pnl # 浮盈为负即深套深度近似 eh = get_hold_expected(dd) if (dd is not None and dd <= -20) else None # 非深套持仓:E_hold = 其自身策略的预期(继续持有的期望) h_strategy = h.get("strategy_name") or "" if eh is None: e_self, src_self = get_strategy_expected(h_strategy, market, regime) if h_strategy else (0.0, "无策略") eh = {"e_hold": e_self, "note": f"非深套,按原策略{h_strategy or 'unknown'}期望", "dd": None} scored.append({ "code": code, "name": name, "market_val": market_val, "e_hold": eh["e_hold"], "note": eh["note"], "dd": eh["dd"], "pnl": pnl, }) # 按 E_hold 升序(最低优先卖) scored.sort(key=lambda x: x["e_hold"]) # 累加凑钱 sell_list = [] raised = 0.0 for s in scored: if raised >= need_cash: break if s["market_val"] <= 0: continue sell_list.append(s) raised += s["market_val"] # 决策 if not sell_list: return {"decided": False, "reason": "无可卖持仓", "sell_list": [], "raised": 0, "e_new": e_new} # 安全边际:被卖的最后一只 E_hold vs E_new last_ehold = sell_list[-1]["e_hold"] margin = e_new - last_ehold if margin >= SAFE_MARGIN: decided = True reason = (f"换仓: 新票({new_strategy})E={e_new:.1f}%[{e_new_src}] " f"vs 被卖最后一只E={last_ehold:.1f}%({sell_list[-1]['name']}), 边际{margin:.1f}%≥{SAFE_MARGIN}%") else: decided = False reason = (f"不换: 新票({new_strategy})E={e_new:.1f}% vs 最低E={last_ehold:.1f}%, " f"边际{margin:.1f}%<{SAFE_MARGIN}%(不划算)") return { "decided": decided, "reason": reason, "sell_list": sell_list, "raised": round(raised, 0), "need_cash": need_cash, "e_new": e_new, "e_new_src": e_new_src, "margin": round(margin, 1), "regime": regime, } def format_swap_advice(decision): """格式化换仓建议(供 LLM prompt 注入 / XMPP 推送)""" if not decision.get("decided"): return f"【换仓决策】{decision.get('reason')}" lines = [f"【换仓决策】{decision.get('reason')}"] lines.append(f" 需资金 {decision.get('need_cash',0):.0f}元,卖出 {len(decision['sell_list'])} 只(共{decision.get('raised',0):.0f}元):") for s in decision["sell_list"]: lines.append(f" - {s['code']} {s['name']}: E_hold={s['e_hold']:.1f}% {s['note']}") return "\n".join(lines) if __name__ == "__main__": # 自测 holdings = [ {"code": "000850", "name": "华茂", "cost": 3.84, "price": 4.14, "shares": 30600, "strategy_name": ""}, {"code": "688775", "name": "影石", "cost": 130, "price": 126.27, "shares": 100, "strategy_name": "accumulation"}, {"code": "300750", "name": "宁德", "cost": 500, "price": 393, "shares": 100, "strategy_name": ""}, ] d = decide_swap(need_cash=100000, holdings=holdings, new_strategy="b_td1_v3", market="a") print(format_swap_advice(d)) print(json.dumps(d, ensure_ascii=False, indent=1)[:800])