feat: 换仓决策模块——资金不足时E_new(新票预期)vs E_hold(持仓深套查表)对比,卖E最低的凑钱(老莫:能代码算的不让LLM拍脑袋)。重评注入换仓方案

This commit is contained in:
xxm
2026-08-19 00:06:02 +08:00
parent 6dd4373898
commit 47e6890bba
2 changed files with 229 additions and 0 deletions
+45
View File
@@ -412,6 +412,51 @@ def build_prompt(data):
except Exception:
pass
_position_context += _rotation_context
# 2026-08-18 换仓决策注入(老莫:需要资金时对比预期收益,卖E_hold最低的)
# 触发:本票信号为买入/加仓 + 现金不足(cash < 建议仓位金额估算)
try:
from swap_decision import decide_swap, format_swap_advice, get_strategy_expected
_sig_now = data.get("timing_signal") or ""
_need_fund = _sig_now in ("买入", "可买入", "可加仓")
if _need_fund:
# 估算本票建议仓位金额(按 RR 5%-15%,取中 10%
_est_need = total * 0.10
if cash < _est_need:
# 拉当前持仓(深套判定:cost vs price
try:
import sqlite3 as _sq3
_c3 = _sq3.connect(DB, timeout=30)
_c3.row_factory = _sq3.Row
_holds = _c3.execute("SELECT code, name, shares, cost, price FROM holdings WHERE is_active=1 AND shares>0").fetchall()
_c3.close()
_hold_list = []
for _h in _holds:
_cost_v = _h["cost"] or 0; _price_v = _h["price"] or 0
_pnl_v = (_price_v - _cost_v) / _cost_v * 100 if _cost_v > 0 and _price_v > 0 else None
# 查该持仓的 strategy_name
_sn = ""
try:
_c4 = _sq3.connect(DB, timeout=30)
_sn_r = _c4.execute("SELECT strategy_name FROM holding_strategies WHERE code=? AND status='active'", (_h["code"],)).fetchone()
_c4.close()
_sn = _sn_r[0] if _sn_r else ""
except Exception:
pass
_hold_list.append({
"code": _h["code"], "name": _h["name"] or _h["code"],
"cost": _cost_v, "price": _price_v, "shares": _h["shares"] or 0,
"strategy_name": _sn, "pnl_pct": _pnl_v,
})
_sd = data.get("strategy_name") or data.get("strategy_attributed") or ""
_dec = decide_swap(need_cash=_est_need - cash, holdings=_hold_list,
new_strategy=_sd, market="a")
_swap_advice = format_swap_advice(_dec)
_position_context += f"\n\n{_swap_advice}\n⚠️ 若你给出买入建议但现金不足,以上为系统算好的换仓方案(卖预期收益最低的持仓凑钱);你只需确认是否采纳(可结合消息面/基本面修正),不要重新设计换仓逻辑。"
print(f" [SWAP] 换仓决策已注入: {_dec.get('reason', '')[:80]}", flush=True)
except Exception as _se:
print(f" [SWAP] 换仓决策注入失败: {_se}", flush=True)
except Exception:
pass
# ── 策略语义注入(2026-08-18 老莫:重评按策略定义,不是裸标签)──
_sd = data.get("strategy_def")
+184
View File
@@ -0,0 +1,184 @@
#!/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_pnlstrategy_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])