feat: 全参与可行仓位模型(单仓≥5万地板,消除上车运气)+排序刷新丢失修复(综合分先算后排)

This commit is contained in:
hmo
2026-07-29 15:26:57 +08:00
parent c90ffe78cc
commit 260354bdd3
2 changed files with 51 additions and 32 deletions
+11 -11
View File
@@ -2003,6 +2003,17 @@ function sortStrategies(strategies, key, dir) {
function renderStrategyTable(strategies) { function renderStrategyTable(strategies) {
const el = document.getElementById('strategyList'); const el = document.getElementById('strategyList');
if (!strategies.length) { el.innerHTML = '<div class="text-slate-500">暂无策略版本</div>'; return; } if (!strategies.length) { el.innerHTML = '<div class="text-slate-500">暂无策略版本</div>'; return; }
// 先算综合分(排序要用),再排序
for (const s of strategies) {
const st = s.summary_stats || {}; const pf = st.portfolio || {};
if (st.total_trades == null) { s._composite = null; continue; }
const ret = Math.min(pf.total_return_pct || 0, 100) / 100 * 30;
const wr = (st.win_rate || 0) / 100 * 20;
const sh = Math.min(Math.max(st.sharpe_ratio || 0, 0), 20) / 20 * 20;
const pfc = Math.min(st.profit_factor || 0, 5) / 5 * 15;
const dd = (1 - Math.min(pf.portfolio_max_dd_pct || 0, 50) / 50) * 15;
s._composite = Math.round(ret + wr + sh + pfc + dd);
}
// 保存数据用于列排序 // 保存数据用于列排序
window._strats = strategies; window._strats = strategies;
if (window._sortKey) { if (window._sortKey) {
@@ -2031,17 +2042,6 @@ function renderStrategyTable(strategies) {
if (pf.cagr_pct != null) best.cagr_pct = Math.max(best.cagr_pct, pf.cagr_pct); if (pf.cagr_pct != null) best.cagr_pct = Math.max(best.cagr_pct, pf.cagr_pct);
if (pf.portfolio_max_dd_pct != null) best.portfolio_max_dd_pct = Math.min(best.portfolio_max_dd_pct, pf.portfolio_max_dd_pct); if (pf.portfolio_max_dd_pct != null) best.portfolio_max_dd_pct = Math.min(best.portfolio_max_dd_pct, pf.portfolio_max_dd_pct);
} }
// 综合评分:总收益30% + 胜率20% + 夏普20% + 盈亏比15% + 资产回撤15%(反向)
for (const s of strategies) {
const st = s.summary_stats || {}; const pf = st.portfolio || {};
if (st.total_trades == null) { s._composite = null; continue; }
const ret = Math.min(pf.total_return_pct || 0, 100) / 100 * 30;
const wr = (st.win_rate || 0) / 100 * 20;
const sh = Math.min(Math.max(st.sharpe_ratio || 0, 0), 20) / 20 * 20;
const pfc = Math.min(st.profit_factor || 0, 5) / 5 * 15;
const dd = (1 - Math.min(pf.portfolio_max_dd_pct || 0, 50) / 50) * 15;
s._composite = Math.round(ret + wr + sh + pfc + dd);
}
const hl = (val, bestVal, invert) => { const hl = (val, bestVal, invert) => {
if (val == null) return ''; if (val == null) return '';
const isBest = invert ? (val === bestVal && bestVal !== 999) : (val === bestVal && bestVal !== -999); const isBest = invert ? (val === bestVal && bestVal !== 999) : (val === bestVal && bestVal !== -999);
+40 -21
View File
@@ -276,19 +276,7 @@ STRATEGIES.update({
"v11.0的'枢轴波段'实为纯硬扛(弱撑从不触发),v8.1才是真波段(44次先出10次再进);用枢轴弱撑做初始止损、强压减半落袋改善风险结构", "v11.0的'枢轴波段'实为纯硬扛(弱撑从不触发),v8.1才是真波段(44次先出10次再进);用枢轴弱撑做初始止损、强压减半落袋改善风险结构",
entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0, entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0,
"hl_only": True, "rsi_delta_min": 6}, "hl_only": True, "rsi_delta_min": 6},
exit_overrides={"tp_pct": None, "exit_mode": "swing_ptp", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10, "stop_mode": "pivot_ws"}), exit_overrides={"tp_pct": None, "exit_mode": "swing_ptp", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10}),
"v11.3": _v40_branch("v11.3", "真波段+强撑止损+减半",
"v11.2的弱支撑止损太紧(77%被洗)——改强支撑止损+强压减半",
"v11.2实证:弱支撑是日内贴身位,当止损77%出场率无法接受;改用更宽的强支撑",
entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0,
"hl_only": True, "rsi_delta_min": 6},
exit_overrides={"tp_pct": None, "exit_mode": "swing_ptp", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10, "stop_mode": "pivot_ss"}),
"v11.4": _v40_branch("v11.4", "v8.1+强压减半",
"v8.1原样(ATR止损+MA10波段) + 仅加强压减半落袋——隔离减半特征",
"v11.2证明弱撑止损有毒,v11.3测强撑,v11.4回到v8.1的ATR止损只保留强压减半这一个枢轴特征",
entry_overrides={"vol_ratio_min": 0.9, "vol_ratio_max": 2.0, "sector_slope_max": 1.0,
"hl_only": True, "rsi_delta_min": 6},
exit_overrides={"tp_pct": None, "exit_mode": "swing_ptp", "sl_atr": 1.5, "max_hold_days": 60, "reentry_days": 10, "stop_mode": "atr"}),
# ── 港股专用版本(港股通宇宙归因推导,2026-07-29)── # ── 港股专用版本(港股通宇宙归因推导,2026-07-29)──
"h1.0": { "h1.0": {
"version": "h1.0", "version": "h1.0",
@@ -934,16 +922,9 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T
elif exit_cfg.get('exit_mode') == 'swing_ptp': elif exit_cfg.get('exit_mode') == 'swing_ptp':
# ── v11.2: MA10真波段 + 弱支撑止损 + 强压减半仓 ── # ── v11.2: MA10真波段 + 弱支撑止损 + 强压减半仓 ──
reentry_window = exit_cfg.get('reentry_days', 10) reentry_window = exit_cfg.get('reentry_days', 10)
_stop_mode = exit_cfg.get('stop_mode', 'atr')
ws0 = last.get('weak_support') or 0 ws0 = last.get('weak_support') or 0
ss0 = last.get('strong_support') or 0
r2_0 = last.get('strong_resist') or 0 r2_0 = last.get('strong_resist') or 0
if _stop_mode == 'pivot_ws' and 0 < ws0 < ep: stop_cur = ws0 if 0 < ws0 < ep else stop
stop_cur = ws0
elif _stop_mode == 'pivot_ss' and 0 < ss0 < ep:
stop_cur = ss0
else:
stop_cur = stop
remaining = 1.0 remaining = 1.0
realized_pnl = 0.0 realized_pnl = 0.0
in_pos = True in_pos = True
@@ -1052,6 +1033,44 @@ def run_backtest(strategy_version, start_date, end_date, capital=1000000, save=T
return result return result
def max_concurrency(trades):
"""信号流的最大并发持仓数(用于全参与组合模拟)。按交易日×1.45≈自然日推算退出点。"""
from datetime import datetime, timedelta
ev = []
for t in trades:
try:
d0 = datetime.strptime(t['entry_date'], '%Y-%m-%d')
d1 = d0 + timedelta(days=max(1, int(t.get('hold_days', 1))) * 1.45)
ev.append((d0, 1))
ev.append((d1, -1))
except Exception:
pass
ev.sort(key=lambda x: (x[0], x[1]))
cur = peak = 0
for _, d in ev:
cur += d
peak = max(peak, cur)
return max(peak, 1)
MIN_POSITION = 50000 # 单仓可行性下限(A股一手+手续费,约5万)
def portfolio_sim_full(trades, capital=1000000):
"""全参与组合模拟(可行版):仓位槽 = min(信号流最大并发, 总资产/单仓下限)。
消除"选哪几笔上车"的运气成分,同时保证单仓金额实际可操作。"""
if not trades:
return {}
natural = max_concurrency(trades)
affordable = max(1, int(capital / MIN_POSITION))
slots = min(natural, affordable)
r = portfolio_sim(trades, capital, slots)
r['slots'] = slots
r['natural_concurrency'] = natural
r['min_position'] = MIN_POSITION
r['mode'] = 'full_feasible'
return r
def portfolio_sim(trades, capital=1000000, max_positions=10): def portfolio_sim(trades, capital=1000000, max_positions=10):
"""组合级模拟:固定等分仓位,按交易日历执行,返回最终资产/总收益/资产曲线回撤 """组合级模拟:固定等分仓位,按交易日历执行,返回最终资产/总收益/资产曲线回撤
规则:每日先结算到期仓位 → 再执行当日入场(仓位满跳过)→ 持仓按成本估值""" 规则:每日先结算到期仓位 → 再执行当日入场(仓位满跳过)→ 持仓按成本估值"""