diff --git a/static/index.html b/static/index.html
index d4561dd1..a8c8cfbd 100644
--- a/static/index.html
+++ b/static/index.html
@@ -2252,6 +2252,47 @@ async function loadEvolutionBox() {
new MutationObserver(()=>{if(!document.getElementById('tab-research')?.classList?.contains('hidden'))loadEvolutionBox();}).observe(document.body,{attributes:true,subtree:true});
+function sortBy(key) {
+ if (window._sortKey === key) {
+ window._sortDir = window._sortDir === 'desc' ? 'asc' : 'desc';
+ } else {
+ window._sortKey = key;
+ window._sortDir = 'desc';
+ }
+ renderStrategyTable(window._strats || []);
+}
+
+function sortStrategies(strategies, key, dir) {
+ const get = (s) => {
+ const st = s.summary_stats || {}; const pf = st.portfolio || {};
+ switch(key) {
+ case 'version': return s.version || '';
+ case 'name': return s.name || '';
+ case 'sizing_slots': return st.sizing_slots || 10;
+ case 'composite': return s._composite != null ? s._composite : -999;
+ case 'universality': return (st.universality || {}).score || 0;
+ case 'total_return': return pf.total_return_pct != null ? pf.total_return_pct : -999;
+ case 'capital_final': return pf.capital_final || 0;
+ case 'cagr': return pf.cagr_pct != null ? pf.cagr_pct : -999;
+ case 'trades': return st.total_trades || 0;
+ case 'hold': return st.avg_hold_days || 0;
+ case 'win_rate': return st.win_rate != null ? st.win_rate : -999;
+ case 'avg_profit': return st.avg_profit_pct != null ? st.avg_profit_pct : -999;
+ case 'sharpe': return st.sharpe_ratio != null ? st.sharpe_ratio : -999;
+ case 'profit_factor': return st.profit_factor != null ? st.profit_factor : -999;
+ case 'max_dd': return pf.portfolio_max_dd_pct != null ? pf.portfolio_max_dd_pct : 999;
+ default: return 0;
+ }
+ };
+ const arr = [...strategies];
+ arr.sort((a, b) => {
+ const va = get(a), vb = get(b);
+ let cmp = (typeof va === 'string') ? va.localeCompare(vb) : va - vb;
+ return dir === 'desc' ? -cmp : cmp;
+ });
+ return arr;
+}
+
function renderStrategyTable(strategies) {
const el = document.getElementById('strategyList');
if (!strategies.length) { el.innerHTML = '
暂无策略版本
'; return; }
@@ -2482,9 +2523,49 @@ function renderStrategyTable(strategies) {
};
const renderRegimeTable = (strats, rg, isCur, marketLabel, tempHtml) => {
// 2026-08-15 方案A:策略只在适应温区(best_regime)表显示一行——所有列都是该策略在适应温区的数据
+ // 2026-08-16 温区感知排序:尊重 window._sortKey(点列头排序),字段从 regime_winrates[rg] 取
const metas = strats
- .filter(s => s.best_regime === rg && (s.regime_winrates || {})[rg] && (s.regime_winrates || {})[rg].trades != null)
- .sort((x, y) => ((y.regime_winrates || {})[rg].win_rate || -1) - ((x.regime_winrates || {})[rg].win_rate || -1));
+ .filter(s => s.best_regime === rg && (s.regime_winrates || {})[rg] && (s.regime_winrates || {})[rg].trades != null);
+ const regimeGet = (s, key) => {
+ const rw = (s.regime_winrates || {})[rg] || {};
+ const pf = rw.portfolio || {};
+ switch (key) {
+ case 'version': return s.version || '';
+ case 'name': return s.name || '';
+ case 'win_rate': return rw.win_rate != null ? rw.win_rate : -999;
+ case 'avg_profit': return rw.avg_pnl != null ? rw.avg_pnl : -999;
+ case 'trades': return rw.trades || 0;
+ case 'hold': return rw.avg_hold_days != null ? rw.avg_hold_days : 0;
+ case 'cagr': return pf.cagr_pct != null ? pf.cagr_pct : -999;
+ case 'sharpe': return rw.sharpe_ratio != null ? rw.sharpe_ratio : -999;
+ case 'profit_factor': return rw.profit_factor != null ? rw.profit_factor : -999;
+ case 'composite': {
+ // 复用温区行综合分公式(与 renderRegimeRow 一致)
+ const ret = Math.min(pf.total_return_pct || 0, 100) / 100 * 30;
+ const w = (rw.win_rate || 0) / 100 * 20;
+ const sh = Math.min(Math.max((pf.sharpe_ratio || 0), 0), 20) / 20 * 20;
+ const pfc = Math.min((pf.profit_factor || 0), 5) / 5 * 15;
+ const dd = (1 - Math.min(pf.portfolio_max_dd_pct || 0, 50) / 50) * 15;
+ const rn = rw.trades || 0;
+ const rm = rw.universality ? (rw.universality.months || 0) : 0;
+ const rconf = Math.min(1, rn / 40) * (rm > 0 ? Math.min(1, rm / 6) : 1);
+ return Math.round((ret + w + sh + pfc + dd) * rconf);
+ }
+ case 'universality': return (rw.universality || {}).score || 0;
+ case 'max_dd': return pf.portfolio_max_dd_pct != null ? pf.portfolio_max_dd_pct : 999;
+ default: return 0;
+ }
+ };
+ if (window._sortKey) {
+ const key = window._sortKey, dir = window._sortDir;
+ metas.sort((a, b) => {
+ const va = regimeGet(a, key), vb = regimeGet(b, key);
+ let cmp = (typeof va === 'string') ? va.localeCompare(vb) : va - vb;
+ return dir === 'desc' ? -cmp : cmp;
+ });
+ } else {
+ metas.sort((x, y) => ((y.regime_winrates || {})[rg].win_rate || -1) - ((x.regime_winrates || {})[rg].win_rate || -1));
+ }
if (!metas.length) return '';
const meta = REGIME_META[rg] || {icon: '🌐', name: rg};
return '' +