From c19e2c55c6ce2435e0af913948ee5f9df8de790f Mon Sep 17 00:00:00 2001 From: xxm Date: Sun, 16 Aug 2026 07:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=84=E6=A0=BC=E8=AF=84=E4=BC=B0?= =?UTF-8?q?=E6=8E=A5=E5=85=A5API=E2=80=94=E2=80=94=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E5=8A=A0qualification(=E9=95=BF=E6=9C=9F/=E8=BF=91=E6=9C=9F/?= =?UTF-8?q?=E5=BD=93=E4=B8=8B=E8=BE=BE=E6=A0=87)+manual=5Favailable+state?= =?UTF-8?q?=E9=87=8D=E5=86=99(=E6=BF=80=E6=B4=BB=3D=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=B8=A9=E5=8C=BA+=E6=89=8B=E5=8A=A8=E5=8F=AF=E7=94=A8+?= =?UTF-8?q?=E4=B8=89=E9=A1=B9=E8=BE=BE=E6=A0=87)+=E8=AD=A6=E5=91=8A?= =?UTF-8?q?=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.py | 87 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/server.py b/server.py index 85be48e2..c17c7d6f 100644 --- a/server.py +++ b/server.py @@ -645,6 +645,13 @@ def api_research_strategies(): return {"months": 0, "score": 0, "regime_total_months": 0} try: from strategy_lab import list_strategies, STRATEGY_DESCRIPTIONS + # 2026-08-16 资格评估 + 手动可用性(标准A:长期10y/近期2y/当下1y 适应温区年化>大盘) + try: + sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + from strategy_qualify import get_benchmarks as _qbench, evaluate_strategy as _qeval, is_available as _qavail, load_availability as _qload + except Exception: + _qbench = _qeval = _qavail = _qload = None + _qbench_data = _qbench() if _qbench else {} pt = request.args.get('period_tag') strats = list_strategies(period_tag=pt) # 2026-08-13 温区自适应:为每个策略附加各温区表现(strategy_regime_perf) @@ -788,41 +795,77 @@ def api_research_strategies(): s['current'] = s['version'] in _active_set # 2026-08-15 适应温区(前端方案A:策略只在适应温区表显示一行) s['best_regime'] = _best_regime_map.get(s['version']) + # 2026-08-16 资格评估(标准A:长期10y/近期2y/当下1y 适应温区年化>大盘) + _qual = _qeval(s['version'], (s.get('market') or 'a') if (s.get('market') or 'a') != 'all' else 'a', + best_regime=s.get('best_regime'), bench=_qbench_data) if _qeval else None + if _qual: + s['qualification'] = { + 'long_ok': _qual.get('long_ok'), 'mid_ok': _qual.get('mid_ok'), + 'short_ok': _qual.get('short_ok'), + 'cagr_10y': _qual.get('cagr_10y'), 'cagr_2y': _qual.get('cagr_2y'), + 'cagr_1y': _qual.get('cagr_1y'), + 'bench_10y': _qbench_data.get('10y'), 'bench_2y': _qbench_data.get('2y'), + 'bench_1y': _qbench_data.get('1y'), + } + else: + s['qualification'] = None + # 2026-08-16 手动可用性(老莫把关):激活只能是可用的策略 + s['manual_available'] = _qavail(s['version']) if _qavail else True # 2026-08-15 证伪数据驱动:从 strategy_research.deprecated 读(替代前端硬编码) s['deprecated'] = _deprecated.get(s['version']) - # 2026-08-15 三状态(派生,不存储):激活/可用未激活/不可用 + # ── 2026-08-16 三状态(派生):激活=当前温区+手动可用+三项达标;可用=手动可用+长期近期过 ── + _l_ok = bool(_qual and _qual.get('long_ok')) + _m_ok = bool(_qual and _qual.get('mid_ok')) + _s_ok = bool(_qual and _qual.get('short_ok')) + _qual_ok_3 = _l_ok and _m_ok and _s_ok + _qual_ok_2 = _l_ok and _m_ok if s['version'] in _active_set: - s['state'] = 'active' - s['state_reason'] = '当前温区路由激活' + if s['manual_available'] and _qual_ok_3: + s['state'] = 'active' + s['state_reason'] = '当前温区+手动可用+三项达标' + else: + # 路由激活但不够格 → 降级展示(不真跑),reason 说明原因 + s['state'] = 'available' if (s['manual_available'] and _qual_ok_2) else 'unavailable' + _why = [] + if not s['manual_available']: + _why.append('手动不可用') + if not _qual_ok_3: + _why.append('未三项达标(长期%s/近期%s/当下%s)' % ( + '✓' if _l_ok else '✗', '✓' if _m_ok else '✗', '✓' if _s_ok else '✗')) + s['state_reason'] = '路由激活但' + ';'.join(_why) + s['current'] = False # 激活降级,前端不高亮 elif s.get('deprecated'): s['state'] = 'unavailable' s['state_reason'] = '已证伪: ' + str(s['deprecated'])[:80] elif s['version'] in _candidate_pool: - # ── 2026-08-15 无价值淘汰:适应温区(best_regime) 2y+10y 年化都<大盘 → 历史 ── - _elim_reason = None - try: - _br_v = s.get('best_regime') - if _br_v: - _rw2 = (_elim_r2.get(s['version']) or {}).get(_br_v) - _rw10 = (_elim_r10.get(s['version']) or {}).get(_br_v) - if _rw2 and _rw10: - _c2 = _rw2.get('cagr_pct') - _c10 = _rw10.get('cagr_pct') - if _c2 is not None and _c10 is not None \ - and _c2 < _elim_b2 and _c10 < _elim_b10: - _elim_reason = (f'适应温区[{_br_v}] 2y年化{round(_c2,1)}%<大盘{_elim_b2}% ' - f'且 10y年化{round(_c10,1)}%<大盘{_elim_b10}%') - except Exception: - _elim_reason = None - if _elim_reason: + if not s['manual_available']: s['state'] = 'unavailable' - s['state_reason'] = '适应温区跑不赢大盘已淘汰: ' + _elim_reason + s['state_reason'] = '手动不可用(可手动改回)' + elif not _l_ok: + s['state'] = 'unavailable' + s['state_reason'] = '长期不合格(10y适应温区年化%s%%<大盘%s%%)' % ( + round(_qual['cagr_10y'], 1) if _qual and _qual.get('cagr_10y') is not None else '?', + _qbench_data.get('10y')) + elif _qual_ok_2: + s['state'] = 'available' + s['state_reason'] = '长期+近期达标,待当下确认' else: s['state'] = 'available' - s['state_reason'] = '候选池内,当前温区未激活(温区切换即可激活)' + s['state_reason'] = '候选池内,长期达标' else: s['state'] = 'unavailable' s['state_reason'] = '不在候选池(历史版本,路由不会选中)' + # 2026-08-16 警告标记:可用性下降(近期转差)/ 回升(不可用但近期转好) + s['qual_warning'] = None + if _qual and s.get('manual_available'): + if not _m_ok and _l_ok: + s['qual_warning'] = '⚠️近期转差: 2y年化%s%%<大盘%s%%' % ( + round(_qual['cagr_2y'], 1) if _qual.get('cagr_2y') is not None else '?', + _qbench_data.get('2y')) + if _qual and not s.get('manual_available') and _s_ok: + s['qual_warning'] = '🔄近期回升: 1y年化%s%%>大盘%s%%,可考虑重新启用' % ( + round(_qual['cagr_1y'], 1) if _qual.get('cagr_1y') is not None else '?', + _qbench_data.get('1y')) # 2026-08-13 关键修复:激活策略必须始终在列表中(即使该 period 无回测记录) # 否则 2y 等默认周期下激活策略缺失(只显示 v_oversold)。