From c1b38d3d7bd8e28c257d564404651cd7fe034612 Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 22 Jul 2026 00:16:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=9B=AF=E7=9B=98):=20=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=B2=BE=E9=80=89=E5=B1=82=E2=80=94=E2=80=94?= =?UTF-8?q?72h=E6=96=B0=E9=B2=9C=E5=BA=A6+RR=E9=99=8D=E5=BA=8F+=E7=8E=B0?= =?UTF-8?q?=E9=87=91=E9=A2=84=E7=AE=97=E8=B4=AA=E5=BF=83=E8=A3=85=E5=85=A5?= =?UTF-8?q?(=E4=B8=8A=E9=99=905=E5=8F=AA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老爸: 太多推荐=没有推荐。机制: - 候选: tag非空 且 reassessed_at 72h内(陈旧信号不算推荐) - 排序: rr_ratio 降序 - 预算: 从 portfolio_summary 读现金/总资产, 每只建议仓位 (position_advice解析%或默认8%) 贪心累加, 耗尽即止, 最多5只 - 落选者降回持仓/自选自然组 - /api/watch 返回 cash/budget_pct/rec_used_pct, 前端显示预算条 - 推荐行仓位列显示'建议X%' --- server.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++- static/index.html | 9 ++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index b76e5666..d9432395 100644 --- a/server.py +++ b/server.py @@ -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/") diff --git a/static/index.html b/static/index.html index cf08847b..7e541632 100644 --- a/static/index.html +++ b/static/index.html @@ -1434,7 +1434,9 @@ async function renderWatch() { const buyZone = (el_ && eh_) ? el_.toFixed(1) + '~' + eh_.toFixed(1) : '—'; const sltp = (sl_ || tp_) ? '损' + (sl_||'—') + '/盈' + (tp_||'—') : '—'; - const posDisplay = posPct ? posPct.toFixed(1) + '%' : '—'; + const posDisplay = (isRec && s.suggested_position_pct) + ? '建议' + s.suggested_position_pct + '%' + : (posPct ? posPct.toFixed(1) + '%' : '—'); // 当前操作策略摘要 const strat = fmtStrategy(s); @@ -1462,8 +1464,11 @@ async function renderWatch() { } html += ''; - // 更新时间 + // 更新时间 + 推荐预算 html += '
' + (data.updated_at ? '数据: ' + data.updated_at : '') + '
'; + if (data.budget_pct !== undefined) { + html += '
💰 现金 ' + (data.cash/10000).toFixed(1) + '万 / 总资产 ' + (data.total_assets/10000).toFixed(1) + '万 — 推荐操作预算 ' + data.budget_pct + '%(已用 ' + data.rec_used_pct + '%)
'; + } el.innerHTML = html; // 自动刷新:每30秒