diff --git a/server.py b/server.py index 776eb759..ea095f8f 100644 --- a/server.py +++ b/server.py @@ -574,7 +574,65 @@ def api_research_strategies(): _c.close() except Exception: pass - # 2026-08-15 证伪标记(数据驱动):strategy_research.deprecated + # 温区级普适前先建立日期→温区映射(A股 market_regime) + _regime_map = {} + try: + import sqlite3 as _sq5 + _c5 = _sq5.connect(str(DATA_DIR / "mofin.db"), timeout=10) + for _r in _c5.execute("SELECT date, regime FROM market_regime WHERE market='a'"): + _regime_map[_r[0]] = _r[1] + _c5.close() + except Exception: + pass + # 2026-08-15 温区级普适:策略在适应温区内发出信号的月份分散度 + 温区总月份占比 + # 温区总月份数(A股: choppy35.4%/trend_down30.3%/trend_up34.3%;按市场 regime 时长统计) + try: + import sqlite3 as _sq3 + _c3 = _sq3.connect(str(DATA_DIR / "mofin.db"), timeout=10) + _total_months = _c3.execute( + "SELECT COUNT(DISTINCT substr(date,1,7)) FROM market_regime WHERE market='a'").fetchone()[0] + _regime_months = {r[0]: r[1] for r in _c3.execute( + "SELECT regime, COUNT(DISTINCT substr(date,1,7)) FROM market_regime WHERE market='a' GROUP BY regime").fetchall()} + _c3.close() + except Exception: + _total_months = 0 + _regime_months = {} + _regime_universality = {} # (version, regime) -> {'months': N, 'score': 分散度} + try: + import sqlite3 as _sq4, json as _json4, collections as _coll + _c4 = _sq4.connect(str(DATA_DIR / "mofin.db"), timeout=10) + for _r in _c4.execute( + "SELECT version, results_json FROM strategy_research WHERE results_json IS NOT NULL"): + try: + _res = _json4.loads(_r[1]) + _trades = _res.get("trades", []) + if not _trades: + continue + # 按温区过滤该策略 trades(regime_winrates 已有温区归类) + for _rg in ("trend_up", "choppy", "trend_down"): + _rg_months = _coll.Counter() + for _t in _trades: + _ed = _t.get("entry_date", "") + if _ed and _regime_map.get(_ed) == _rg: + _rg_months[_ed[:7]] += 1 + if _rg_months: + _n = len(_rg_months) + _tot = sum(_rg_months.values()) + _score = 0.0 + if _n > 1: + for _c in _rg_months.values(): + _p = _c / _tot + _score -= _p * (0.0 if _p == 0 else __import__('math').log(_p)) + _score = _score / __import__('math').log(_n) * 100 + _regime_universality.setdefault(_r[0], {})[_rg] = { + "months": _n, "score": round(_score, 0), + "regime_total_months": _regime_months.get(_rg, 0), + } + except Exception: + continue + _c4.close() + except Exception: + _regime_universality = {} _deprecated = {} try: import sqlite3 as _sq2 diff --git a/static/index.html b/static/index.html index 9ced2856..c5257ac4 100644 --- a/static/index.html +++ b/static/index.html @@ -2442,8 +2442,9 @@ function renderStrategyTable(strategies) { const holdShow = rw && rw.avg_hold_days != null ? rw.avg_hold_days : (isOverall ? st.avg_hold_days : null); const sharpeShow = rw && rw.sharpe_ratio != null ? rw.sharpe_ratio : (isOverall ? st.sharpe_ratio : null); const pfShow = rw && rw.profit_factor != null ? rw.profit_factor : (isOverall ? st.profit_factor : null); - // 组合级列:整体行用整体组合;温区行用温区级组合(rw.portfolio)+ 温区级 composite + 整体普适近似 + // 组合级列:整体行用整体组合;温区行用温区级组合(rw.portfolio)+ 温区级 composite + 温区级 universality const rwPf = rw && rw.portfolio ? rw.portfolio : null; + const rwUniv = rw && rw.universality ? rw.universality : null; // 温区级 composite(用温区 portfolio 的 cagr/total_return + 温区胜率/夏普/盈亏比/回撤,对齐整体综合分逻辑) let rgComposite = null; if (rwPf) { @@ -2455,7 +2456,8 @@ function renderStrategyTable(strategies) { rgComposite = Math.round((rgRet + rgWr + rgSh + rgPfc + rgDd) * (s._confidence || 1)); } const compositeShow = isOverall ? s._composite : rgComposite; - const universalityShow = isOverall ? (st.universality || {}).score : (st.universality || {}).score; + // 温区级 universality(温区内信号月份分散度 + 温区总月份占比) + const universalityShow = isOverall ? (st.universality || {}).score : (rwUniv ? rwUniv.score : null); const retShow = isOverall ? (st.portfolio_full||{}).total_return_pct : (rwPf ? rwPf.total_return_pct : null); const capShow = isOverall ? (st.portfolio_full||pf).capital_final : (rwPf ? rwPf.capital_final : null); const cagrShow = isOverall ? (st.portfolio_full||pf).cagr_pct : (rwPf ? rwPf.cagr_pct : null); @@ -2474,7 +2476,7 @@ function renderStrategyTable(strategies) { '' + (st.sizing_slots ? st.sizing_slots + '仓' : '10仓') + (st.conviction_model ? '' : '') + '' + '' + ((s.description && s.description.title) ? '' : '') + '' + cell('composite', compositeShow, v => v + (s._confidence != null && s._confidence < 1 ? '×' + s._confidence.toFixed(2) + '' : ''), 'font-bold text-amber-300') + - cell('universality_score', universalityShow, v => v + '/' + (st.universality || {}).months + '月') + + cell('universality_score', universalityShow, v => v + '/' + (isOverall ? (st.universality || {}).months : (rwUniv ? rwUniv.months + '/' + (rwUniv.regime_total_months || '?') : '?')) + '月') + cell('full_return_pct', retShow, v => v + '%', 'font-bold text-amber-300') + cell('capital_final', capShow, v => '¥' + (v/10000).toFixed(0) + '万') + cell('cagr_pct', cagrShow, v => v + '%', hl(cagrShow, best.cagr_pct)) +