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:
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user