fix(rec): RR系统自算落库+卖出不占买入预算+RR>=1.5门槛
- mofin_db.recompute_rr: 用现价+止损止盈重算RR, sync_recommend_tag入口统一调用 (根治: prompt没让LLM输出RR, parse不解析, save不落库 → rr_ratio永远0) - flush_rec_digest: 卖出/止盈单独一组排最前, 不占现金预算; 买入加RR>=1.5可执行门槛 - server.py/api/watch: 卖出永远可执行; 买入RR>=1.5才给可执行徽章 (根治: 卖出默认吃8%预算被排队, RR=0的5%仓位反而挤进预算)
This commit is contained in:
+44
-4
@@ -1144,11 +1144,46 @@ def reconcile_signal_from_analysis(conn, code: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def recompute_rr(conn, code: str) -> float:
|
||||
"""用现价+已存止损/止盈重算 RR 并写回 rr_ratio。
|
||||
根治"LLM 不输出 RR → rr_ratio 永远 0"的断链(红线:RR 由系统算,不信 LLM)。
|
||||
公式: RR = (止盈 - 基准价) / (基准价 - 止损);基准价=现价,兜底区间上沿。
|
||||
损/盈缺失或基准价<=止损 → RR=0(不达标,不参与排序)。"""
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT entry_high, stop_loss, take_profit FROM holding_strategies WHERE code=? AND status='active'",
|
||||
(code,)).fetchone()
|
||||
if not row:
|
||||
return 0.0
|
||||
eh, sl, tp = (row[0] or 0), (row[1] or 0), (row[2] or 0)
|
||||
ref = 0.0
|
||||
try:
|
||||
pr = conn.execute("SELECT price FROM live_prices WHERE code=?", (code,)).fetchone()
|
||||
if pr and (pr[0] or 0) > 0:
|
||||
ref = float(pr[0])
|
||||
except Exception:
|
||||
pass
|
||||
if ref <= 0:
|
||||
ref = float(eh or 0)
|
||||
rr = 0.0
|
||||
if sl > 0 and tp > 0 and ref > sl:
|
||||
rr = round((tp - ref) / (ref - sl), 2)
|
||||
if rr < 0:
|
||||
rr = 0.0
|
||||
conn.execute("UPDATE holding_strategies SET rr_ratio=? WHERE code=? AND status='active'", (rr, code))
|
||||
conn.commit()
|
||||
return rr
|
||||
except Exception as e:
|
||||
print(f" [RR] {code} 重算失败: {e}", flush=True)
|
||||
return 0.0
|
||||
|
||||
|
||||
def sync_recommend_tag(conn, code: str, timing_signal: str):
|
||||
"""裸 SQL 调用方(batch_reassess / per_stock_reassess)的推荐 tag 同步。
|
||||
动作级信号 → current_recommend;信号降级 → 清除 current_recommend;
|
||||
active_manual(人工标记)永不动。与 XMPP 动作级告警同源(红线#12)。"""
|
||||
try:
|
||||
recompute_rr(conn, code) # 先入先算:保证 tag/入队/盯盘排序拿到新鲜 RR
|
||||
_old_tag_row = conn.execute(
|
||||
"SELECT tag FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||||
_old_tag = (_old_tag_row[0] or '') if _old_tag_row else ''
|
||||
@@ -1234,10 +1269,15 @@ def flush_rec_digest(max_items=5):
|
||||
if not items:
|
||||
return 0
|
||||
_os.remove(qf)
|
||||
items.sort(key=lambda x: x.get('rr') or 0, reverse=True)
|
||||
_SELL_SIGS = ("卖出", "止盈")
|
||||
# 卖出/止盈是释放现金的操作,不占买入预算,单独一组排最前
|
||||
sells = [x for x in items if x.get('signal') in _SELL_SIGS]
|
||||
buys_all = [x for x in items if x.get('signal') not in _SELL_SIGS]
|
||||
buys_all.sort(key=lambda x: x.get('rr') or 0, reverse=True)
|
||||
items = sells + buys_all
|
||||
top = items[:max_items]
|
||||
|
||||
# ── 现金预算(决定操盘建议 + 换仓策略)──
|
||||
# ── 现金预算(决定操盘建议 + 换仓策略):只对买入项计算,卖出不占预算 ──
|
||||
cash_note = ""
|
||||
rotation_note = ""
|
||||
try:
|
||||
@@ -1250,11 +1290,11 @@ def flush_rec_digest(max_items=5):
|
||||
cum = 0.0
|
||||
buys = []
|
||||
queued = []
|
||||
for it in top:
|
||||
for it in buys_all: # 只遍历买入项;sells 永远可执行
|
||||
import re as _re
|
||||
m = _re.search(r'(\d+(?:\.\d+)?)\s*%', it.get('position') or '')
|
||||
pct = float(m.group(1)) if m else 8.0
|
||||
if cum + pct <= budget_pct + 1e-9:
|
||||
if (it.get('rr') or 0) >= 1.5 and cum + pct <= budget_pct + 1e-9:
|
||||
buys.append((it, pct))
|
||||
cum += pct
|
||||
else:
|
||||
|
||||
@@ -227,18 +227,27 @@ def get_watch():
|
||||
pass
|
||||
return 8.0
|
||||
|
||||
_SELL_SIGS = ("卖出", "止盈")
|
||||
_cands = [d for d in results if d['sort_group'] == 0 and _is_fresh(d)]
|
||||
_cands.sort(key=lambda x: x.get('rr_ratio') or 0, reverse=True)
|
||||
# 现金预算内标记"可执行",预算外标记"排队"(不再降级隐藏)
|
||||
# 卖出类永远可执行(释放现金,不占买入预算),排最前;买入类按 RR 降序
|
||||
_sells = [d for d in _cands if (d.get('timing_signal') or '') in _SELL_SIGS]
|
||||
_buys = [d for d in _cands if (d.get('timing_signal') or '') not in _SELL_SIGS]
|
||||
_buys.sort(key=lambda x: x.get('rr_ratio') or 0, reverse=True)
|
||||
for d in _sells:
|
||||
d['rec_exec'] = True # 卖出不需要现金,永远可执行
|
||||
d['suggested_position_pct'] = 0.0
|
||||
# 买入:RR≥1.5 才有可执行资格(prompt 自己的纪律:RR<1.5→不推荐);
|
||||
# 达标者贪心装入现金预算,预算外/不达标标记"排队"(不再降级隐藏)
|
||||
_cum = 0.0
|
||||
for d in _cands:
|
||||
for d in _buys:
|
||||
pct = _sugg_pct(d)
|
||||
d['suggested_position_pct'] = pct
|
||||
if _cum + pct <= _budget_pct + 1e-9:
|
||||
rr = d.get('rr_ratio') or 0
|
||||
if rr >= 1.5 and _cum + pct <= _budget_pct + 1e-9:
|
||||
d['rec_exec'] = True # 可执行
|
||||
_cum += pct
|
||||
else:
|
||||
d['rec_exec'] = False # 排队(现金不足)
|
||||
d['rec_exec'] = False # 排队(现金不足或RR不达标)
|
||||
# 落选(tag 但非新鲜)仍降回自然分组;新鲜者全部留在推荐区
|
||||
for d in results:
|
||||
if d['sort_group'] == 0 and not _is_fresh(d):
|
||||
|
||||
Reference in New Issue
Block a user