feat(盯盘): 推荐操作精选层——72h新鲜度+RR降序+现金预算贪心装入(上限5只)

老爸: 太多推荐=没有推荐。机制:
- 候选: tag非空 且 reassessed_at 72h内(陈旧信号不算推荐)
- 排序: rr_ratio 降序
- 预算: 从 portfolio_summary 读现金/总资产, 每只建议仓位
  (position_advice解析%或默认8%) 贪心累加, 耗尽即止, 最多5只
- 落选者降回持仓/自选自然组
- /api/watch 返回 cash/budget_pct/rec_used_pct, 前端显示预算条
- 推荐行仓位列显示'建议X%'
This commit is contained in:
hmo
2026-07-22 00:16:35 +08:00
parent 93f0b24007
commit c1b38d3d7b
2 changed files with 62 additions and 3 deletions
+55 -1
View File
@@ -191,6 +191,57 @@ def get_watch():
results.append(d)
# ── 推荐操作精选层(2026-07-21 老爸:太多推荐=没有推荐)──
# 候选 = tag 非空 且 72h 内有新鲜重评;按 RR 降序;贪心装入现金预算;最多 5 只。
# 落选者降回其自然分组(持仓/自选)。
import re as _re
from datetime import datetime as _dt, timedelta as _td
conn2 = sqlite3.connect("/home/hmo/MoFin/data/mofin.db")
try:
_pr = conn2.execute("SELECT cash, total_assets FROM portfolio_summary WHERE id=1").fetchone()
_cash = float(_pr[0] or 0)
_total = float(_pr[1] or 0)
finally:
conn2.close()
_budget_pct = (_cash / _total * 100) if _total > 0 else 0
_fresh_cutoff = _dt.now() - _td(hours=72)
def _is_fresh(d):
ra = d.get('reassessed_at') or ''
if not ra:
return False
try:
return _dt.fromisoformat(str(ra)[:19]) >= _fresh_cutoff
except Exception:
return False
def _sugg_pct(d):
# 从 position_advice 解析百分比(如 "8%(理由...)"),失败默认 8%
m = _re.search(r'(\d+(?:\.\d+)?)\s*%', d.get('position_advice') or '')
if m:
try:
v = float(m.group(1))
if 0 < v <= 30:
return v
except Exception:
pass
return 8.0
_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)
_selected, _cum = [], 0.0
for d in _cands:
pct = _sugg_pct(d)
if len(_selected) < 5 and _cum + pct <= _budget_pct + 1e-9:
d['suggested_position_pct'] = pct
_cum += pct
_selected.append(d)
_sel_codes = {d['code'] for d in _selected}
for d in results:
if d['sort_group'] == 0 and d['code'] not in _sel_codes:
# 落选:降回自然分组
d['sort_group'] = 1 if d['decision_type'] == '持仓策略' else 2
# 排序:group → signal_rank → group-internal (持仓按position_pct desc, 自选按rr desc)
def skey(x):
g = x['sort_group']
@@ -207,7 +258,10 @@ def get_watch():
d.pop('_pos', None)
d.pop('_rr', None)
return json.dumps({"stocks": results, "count": len(results)}, ensure_ascii=False)
return json.dumps({"stocks": results, "count": len(results),
"cash": _cash, "total_assets": _total,
"budget_pct": round(_budget_pct, 2),
"rec_used_pct": round(_cum, 2)}, ensure_ascii=False)
@app.route("/api/strategy_history/<code>")