diff --git a/strategy_lab.py b/strategy_lab.py index 7a1bdabb..8bb6e1e9 100644 --- a/strategy_lab.py +++ b/strategy_lab.py @@ -1057,20 +1057,52 @@ def max_concurrency(trades): MIN_POSITION = 50000 # 单仓可行性下限(A股一手+手续费,约5万) -def portfolio_sim_full(trades, capital=1000000): - """全参与组合模拟(可行版):仓位槽 = min(信号流最大并发, 总资产/单仓下限)。 - 消除"选哪几笔上车"的运气成分,同时保证单仓金额实际可操作。""" +def portfolio_sim_full(trades, capital=1000000, runs=50): + """全参与组合模拟(Monte Carlo 随机选装版): + 仓位槽 = min(信号流最大并发, 总资产/单仓下限); + 同日竞争时随机洗牌选装,重复 runs 次取均值±方差。 + 彻底消灭评分挑选偏差(2026-07-29 老爸:v1.0高分信号被偏爱=虚高收益)""" 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 + # 交易少时直接单跑(无需MC) + if len(trades) <= slots * 2: + r = portfolio_sim(trades, capital, slots) + r.update({'slots': slots, 'natural_concurrency': natural, 'mode': 'full_single', + 'std': 0.0, 'p10': r['total_return_pct'], 'p90': r['total_return_pct']}) + return r + rets = [] + finals = [] + dds = [] + taken = [] + for k in range(runs): + r = portfolio_sim(trades, capital, slots, random_seed=42 + k) + rets.append(r['total_return_pct']) + finals.append(r['capital_final']) + dds.append(r['portfolio_max_dd_pct']) + taken.append(r['positions_taken']) + rets_s = sorted(rets) + mean_ret = sum(rets) / len(rets) + mean_dd = sum(dds) / len(dds) + std = (sum((x - mean_ret) ** 2 for x in rets) / len(rets)) ** 0.5 + return { + 'capital_final': round(sum(finals) / len(finals), 0), + 'total_return_pct': round(mean_ret, 1), + 'cagr_pct': round((((1 + mean_ret / 100) ** 0.5) - 1) * 100, 1), + 'portfolio_max_dd_pct': round(mean_dd, 1), + 'positions_taken': round(sum(taken) / len(taken)), + 'positions_skipped': len(trades) - round(sum(taken) / len(taken)), + 'slots': slots, + 'natural_concurrency': natural, + 'min_position': MIN_POSITION, + 'mode': 'full_mc', + 'mc_runs': runs, + 'std': round(std, 1), + 'p10': round(rets_s[int(len(rets_s) * 0.1)], 1), + 'p90': round(rets_s[int(len(rets_s) * 0.9)], 1), + } COST_RATE = 0.002 # 往返交易费用率(佣金+印花税+滑点≈0.2%) @@ -1079,7 +1111,7 @@ def _trade_legs(t): """波段类出场按2次往返计费""" return 2 if t.get('exit_reason') in ('swing_re', 'swing_ptp') else 1 -def portfolio_sim(trades, capital=1000000, max_positions=10, cost=True): +def portfolio_sim(trades, capital=1000000, max_positions=10, cost=True, random_seed=None): """组合级模拟:固定等分仓位,按交易日历执行,返回最终资产/总收益/资产曲线回撤 规则:每日先结算到期仓位 → 再执行当日入场(仓位满跳过)→ 持仓按成本估值 含交易费用:每笔往返扣 COST_RATE(2026-07-29 老爸:高频策略必须上费用天平)""" @@ -1096,12 +1128,20 @@ def portfolio_sim(trades, capital=1000000, max_positions=10, cost=True): return d return cal[min(i + n, len(cal) - 1)] - # 按入场日组织(同日高分优先) + # 按入场日组织 + # random_seed 设置时:同日信号随机洗牌(消灭评分挑选偏差, Monte Carlo用) + # 否则:同日高分优先(原行为, 集中仓位模拟用) entries = {} for t in trades: entries.setdefault(t['entry_date'], []).append(t) - for d in entries: - entries[d].sort(key=lambda x: -x.get('score', 0)) + if random_seed is not None: + import random as _rnd + rng = _rnd.Random(random_seed) + for d in entries: + rng.shuffle(entries[d]) + else: + for d in entries: + entries[d].sort(key=lambda x: -x.get('score', 0)) cash = capital open_pos = [] # {'exit_date','alloc','pnl'}