diff --git a/bitmart/保守模式参数优化/README.md b/bitmart/保守模式参数优化/README.md index 36920bb..24f3ac0 100644 --- a/bitmart/保守模式参数优化/README.md +++ b/bitmart/保守模式参数优化/README.md @@ -94,6 +94,18 @@ python3 "/Users/ddrwode/code/lm_code/bitmart/保守模式参数优化/四分之 - 实盘下单数量现在会直接使用 `default_order_size`(不再固定写死 25),与回测/优化口径一致。 - `current_params.json` 新增支持趋势过滤和 AI 门控参数(见第 7 节)。 +### 启动时自动优化与加速(不减少计算数据) + +数据量、试验数、窗口数、压力场景均与配置一致,加速仅通过**代码并行**实现: + +- **并行试验**(Optuna):`BITMART_AUTO_OPT_N_JOBS=4` 或 优化脚本 `--n-jobs 4`,多组试验同时跑。 +- **并行压力场景**:每个窗口内多组 slip×fee 同时回测。环境变量 `BITMART_AUTO_OPT_STRESS_WORKERS=4` 或 优化脚本 `--n-stress-workers 4`。 +- **跳过近期已优化**:`BITMART_AUTO_OPT_SKIP_IF_FRESH_HOURS=24` 表示若 `current_params.json` 24 小时内已更新则跳过本次优化,直接加载。 + +单独运行优化脚本时: +- `--n-jobs 4`:Optuna 并行 4 个试验。 +- `--n-stress-workers 4`:每窗口 4 个进程并行跑压力场景(不减少 slip/fee 组数)。 + ## 4. 实时价格(WebSocket) 保守模式脚本已支持: diff --git a/bitmart/保守模式参数优化/optimize_params.py b/bitmart/保守模式参数优化/optimize_params.py index 7a924eb..962c5ca 100644 --- a/bitmart/保守模式参数优化/optimize_params.py +++ b/bitmart/保守模式参数优化/optimize_params.py @@ -6,6 +6,7 @@ import os import random import re import sys +from concurrent.futures import ProcessPoolExecutor from datetime import datetime, timezone from pathlib import Path from statistics import mean, pstdev @@ -156,6 +157,77 @@ def load_credentials_from_strategy_file(strategy_path: Path) -> Dict[str, str]: return result +def _run_one_stress_scenario( + task: Tuple[ + Dict[str, Any], + float, + float, + List[Dict[str, float]], + List[Dict[str, float]], + float, + float, + float, + float, + int, + float, + ] +) -> Tuple[float, float, float, Any, Any]: + """ + 单压力场景回测(供多进程调用,不减少计算量)。 + 返回: (train_score, valid_score, valid_drawdown, base_train_res, base_valid_res) + base_* 仅当 slip≈1 且 fee≈1 时非 None。 + """ + ( + params, + slip_mult, + fee_mult, + train_data, + valid_data, + raw_fee_rate, + rebate_ratio, + drawdown_weight, + loss_day_weight, + min_trades, + undertrade_penalty, + ) = task + scenario_params = dict(params) + scenario_params["slippage_pct"] = max( + 0.0, + _as_float(params.get("slippage_pct", 0.01), 0.01) * float(slip_mult), + ) + scenario_params = normalize_params(scenario_params) + scenario_raw_fee_rate = max(0.0, raw_fee_rate * float(fee_mult)) + backtester = ConservativeBacktester( + params=scenario_params, + raw_fee_rate=scenario_raw_fee_rate, + rebate_ratio=rebate_ratio, + ) + train_res = backtester.run(train_data) + valid_res = backtester.run(valid_data) + train_score = score_result( + train_res, + drawdown_weight=drawdown_weight, + loss_day_weight=loss_day_weight, + min_trades=min_trades, + undertrade_penalty=undertrade_penalty, + ) + valid_score = score_result( + valid_res, + drawdown_weight=drawdown_weight, + loss_day_weight=loss_day_weight, + min_trades=min_trades, + undertrade_penalty=undertrade_penalty, + ) + is_base = abs(slip_mult - 1.0) < 1e-9 and abs(fee_mult - 1.0) < 1e-9 + return ( + train_score, + valid_score, + valid_res.max_drawdown, + train_res if is_base else None, + valid_res if is_base else None, + ) + + def sample_random_params(rng: random.Random) -> Dict[str, Any]: params = dict(DEFAULT_PARAMS) params.update( @@ -207,10 +279,13 @@ def evaluate_candidate( stability_weight: float, stress_slippage_multipliers: List[float], stress_fee_multipliers: List[float], + n_stress_workers: int = 0, ) -> Tuple[float, Dict[str, float], Dict[str, float]]: train_results = [] valid_results = [] window_scores = [] + num_stress = len(stress_slippage_multipliers) * len(stress_fee_multipliers) + use_parallel = n_stress_workers > 0 and num_stress > 1 for train_data, valid_data in windows: scenario_train_scores: List[float] = [] @@ -219,50 +294,78 @@ def evaluate_candidate( base_train_res = None base_valid_res = None - for slip_mult in stress_slippage_multipliers: - for fee_mult in stress_fee_multipliers: - scenario_params = dict(params) - scenario_params["slippage_pct"] = max( - 0.0, - _as_float(params.get("slippage_pct", 0.01), 0.01) * float(slip_mult), - ) - scenario_params = normalize_params(scenario_params) - scenario_raw_fee_rate = max(0.0, raw_fee_rate * float(fee_mult)) - backtester = ConservativeBacktester( - params=scenario_params, - raw_fee_rate=scenario_raw_fee_rate, - rebate_ratio=rebate_ratio, + if use_parallel: + tasks = [ + ( + params, + slip_mult, + fee_mult, + train_data, + valid_data, + raw_fee_rate, + rebate_ratio, + drawdown_weight, + loss_day_weight, + min_trades, + undertrade_penalty, ) + for slip_mult in stress_slippage_multipliers + for fee_mult in stress_fee_multipliers + ] + max_workers = min(n_stress_workers, len(tasks)) + with ProcessPoolExecutor(max_workers=max_workers) as executor: + for res in executor.map(_run_one_stress_scenario, tasks): + train_score, valid_score, valid_dd, tr, vr = res + scenario_train_scores.append(train_score) + scenario_valid_scores.append(valid_score) + scenario_valid_drawdowns.append(valid_dd) + if tr is not None and vr is not None: + base_train_res, base_valid_res = tr, vr + else: + for slip_mult in stress_slippage_multipliers: + for fee_mult in stress_fee_multipliers: + scenario_params = dict(params) + scenario_params["slippage_pct"] = max( + 0.0, + _as_float(params.get("slippage_pct", 0.01), 0.01) * float(slip_mult), + ) + scenario_params = normalize_params(scenario_params) + scenario_raw_fee_rate = max(0.0, raw_fee_rate * float(fee_mult)) + backtester = ConservativeBacktester( + params=scenario_params, + raw_fee_rate=scenario_raw_fee_rate, + rebate_ratio=rebate_ratio, + ) - train_res = backtester.run(train_data) - valid_res = backtester.run(valid_data) + train_res = backtester.run(train_data) + valid_res = backtester.run(valid_data) - train_score = score_result( - train_res, - drawdown_weight=drawdown_weight, - loss_day_weight=loss_day_weight, - min_trades=min_trades, - undertrade_penalty=undertrade_penalty, - ) - valid_score = score_result( - valid_res, - drawdown_weight=drawdown_weight, - loss_day_weight=loss_day_weight, - min_trades=min_trades, - undertrade_penalty=undertrade_penalty, - ) + train_score = score_result( + train_res, + drawdown_weight=drawdown_weight, + loss_day_weight=loss_day_weight, + min_trades=min_trades, + undertrade_penalty=undertrade_penalty, + ) + valid_score = score_result( + valid_res, + drawdown_weight=drawdown_weight, + loss_day_weight=loss_day_weight, + min_trades=min_trades, + undertrade_penalty=undertrade_penalty, + ) - scenario_train_scores.append(train_score) - scenario_valid_scores.append(valid_score) - scenario_valid_drawdowns.append(valid_res.max_drawdown) + scenario_train_scores.append(train_score) + scenario_valid_scores.append(valid_score) + scenario_valid_drawdowns.append(valid_res.max_drawdown) - if base_train_res is None and abs(slip_mult - 1.0) < 1e-9 and abs(fee_mult - 1.0) < 1e-9: - base_train_res = train_res - base_valid_res = valid_res + if base_train_res is None and abs(slip_mult - 1.0) < 1e-9 and abs(fee_mult - 1.0) < 1e-9: + base_train_res = train_res + base_valid_res = valid_res - if base_train_res is None: - base_train_res = train_res - base_valid_res = valid_res + if base_train_res is None: + base_train_res = train_res + base_valid_res = valid_res if base_train_res is None or base_valid_res is None: fallback_backtester = ConservativeBacktester( @@ -360,6 +463,7 @@ def optimize_with_optuna( stability_weight=args.stability_weight, stress_slippage_multipliers=args.stress_slippage_multipliers, stress_fee_multipliers=args.stress_fee_multipliers, + n_stress_workers=getattr(args, "n_stress_workers", 0), ) trial.set_user_attr("train_agg", train_agg) @@ -375,7 +479,8 @@ def optimize_with_optuna( } return score - study.optimize(objective, n_trials=args.n_trials, show_progress_bar=False) + n_jobs = max(1, int(getattr(args, "n_jobs", 1))) + study.optimize(objective, n_trials=args.n_trials, n_jobs=n_jobs, show_progress_bar=(n_jobs == 1)) if best: return best @@ -411,6 +516,7 @@ def optimize_with_random( stability_weight=args.stability_weight, stress_slippage_multipliers=args.stress_slippage_multipliers, stress_fee_multipliers=args.stress_fee_multipliers, + n_stress_workers=getattr(args, "n_stress_workers", 0), ) if best is None or score > best["score"]: @@ -513,6 +619,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--method", type=str, default="auto", choices=["auto", "optuna", "random"], help="Search method.") parser.add_argument("--n-trials", type=int, default=300, help="Number of optimization trials.") + parser.add_argument("--n-jobs", type=int, default=1, help="Parallel trials (Optuna only). 1=sequential, 2+=parallel.") + parser.add_argument("--n-stress-workers", type=int, default=0, help="Parallel stress scenarios per window (0=sequential). e.g. 4 runs slip×fee in parallel.") + parser.add_argument("--max-windows", type=int, default=0, help="Cap rolling windows (0=no cap). Speeds up when set to 2-3.") parser.add_argument("--seed", type=int, default=42, help="Random seed.") parser.add_argument("--raw-fee-rate", type=float, default=0.0006, help="Raw taker fee rate (e.g. 0.0006).") @@ -568,6 +677,10 @@ def main() -> None: "Could not build rolling windows. Increase data days or reduce train/valid window sizes." ) + max_windows = max(0, int(getattr(args, "max_windows", 0))) + if max_windows > 0 and len(windows) > max_windows: + windows = windows[:max_windows] + print(f"Capped rolling windows to {max_windows} (--max-windows)") print(f"Loaded klines: {len(klines)}, rolling windows: {len(windows)}") use_method = args.method diff --git a/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py b/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py index f1b4967..013f046 100644 --- a/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py +++ b/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py @@ -148,6 +148,11 @@ class BitmartFuturesTransactionConservative: self.auto_optimize_data_file = os.getenv("BITMART_AUTO_OPT_DATA_FILE", "").strip() self.auto_optimize_script_path = Path(__file__).with_name("optimize_params.py") self.auto_optimized_this_run = False + # 若参数文件在 N 小时内更新过则跳过优化(0=不跳过) + self.auto_optimize_skip_if_fresh_hours = float(os.getenv("BITMART_AUTO_OPT_SKIP_IF_FRESH_HOURS", "0")) + # 并行:试验数(Optuna 有效)、压力场景数(每窗口 slip×fee 并行) + self.auto_optimize_n_jobs = int(os.getenv("BITMART_AUTO_OPT_N_JOBS", "0")) + self.auto_optimize_stress_workers = int(os.getenv("BITMART_AUTO_OPT_STRESS_WORKERS", "0")) # 方案B:智能动态模式 self.enable_smart_mode = os.getenv("BITMART_SMART_MODE_ENABLED", "1").strip().lower() not in {"0", "false", "off", "no"} @@ -301,6 +306,7 @@ class BitmartFuturesTransactionConservative: def run_auto_optimization_before_trade(self): """ 启动前自动运行 optimize_params.py,完成后重新加载参数。 + 支持快速启动(少天数/少试验/单压力场景)与“参数文件若为新则跳过优化”。 """ if not self.auto_optimize_before_trade: logger.info("启动前自动优化已禁用(BITMART_AUTO_OPTIMIZE_BEFORE_TRADE=0)") @@ -308,6 +314,21 @@ class BitmartFuturesTransactionConservative: if self.auto_optimized_this_run: return True + output_path = self.get_runtime_params_path() + if self.auto_optimize_skip_if_fresh_hours > 0 and output_path.exists(): + try: + mtime = output_path.stat().st_mtime + age_hours = (time.time() - mtime) / 3600.0 + if age_hours <= self.auto_optimize_skip_if_fresh_hours: + logger.info( + f"参数文件 {output_path.name} {age_hours:.1f}h 内已更新,跳过本次优化(BITMART_AUTO_OPT_SKIP_IF_FRESH_HOURS={self.auto_optimize_skip_if_fresh_hours})" + ) + self.load_runtime_params() + self.capture_base_risk_params() + return True + except Exception as e: + logger.debug(f"检查参数文件新鲜度失败: {e}") + script_path = self.auto_optimize_script_path if not script_path.exists(): msg = f"未找到参数优化脚本: {script_path}" @@ -317,9 +338,17 @@ class BitmartFuturesTransactionConservative: logger.warning(f"{msg},继续使用当前参数") return True - output_path = self.get_runtime_params_path() output_path.parent.mkdir(parents=True, exist_ok=True) + # 使用完整数据与试验数(不缩减);加速依赖优化脚本内部并行(n_jobs / 压力场景并行) + days = self.auto_optimize_days + train_days = self.auto_optimize_train_days + valid_days = self.auto_optimize_valid_days + window_step_days = self.auto_optimize_window_step_days + n_trials = self.auto_optimize_n_trials + stress_slippage = self.auto_optimize_stress_slippage + stress_fee = self.auto_optimize_stress_fee + method = str(self.auto_optimize_method).strip().lower() or "auto" if method not in {"auto", "optuna", "random"}: logger.warning(f"未知优化方法 {method},自动改为 auto") @@ -338,21 +367,21 @@ class BitmartFuturesTransactionConservative: sys.executable, str(script_path), "--days", - str(self.auto_optimize_days), + str(days), "--symbol", self.contract_symbol, "--step", "5", "--train-days", - str(self.auto_optimize_train_days), + str(train_days), "--valid-days", - str(self.auto_optimize_valid_days), + str(valid_days), "--window-step-days", - str(self.auto_optimize_window_step_days), + str(window_step_days), "--method", method, "--n-trials", - str(self.auto_optimize_n_trials), + str(n_trials), "--valid-score-weight", str(self.auto_optimize_valid_score_weight), "--drawdown-guard", @@ -360,12 +389,16 @@ class BitmartFuturesTransactionConservative: "--stability-weight", str(self.auto_optimize_stability_weight), "--stress-slippage-multipliers", - str(self.auto_optimize_stress_slippage), + stress_slippage, "--stress-fee-multipliers", - str(self.auto_optimize_stress_fee), + stress_fee, "--output", str(output_path), ] + if self.auto_optimize_n_jobs > 0: + cmd.extend(["--n-jobs", str(self.auto_optimize_n_jobs)]) + if self.auto_optimize_stress_workers > 0: + cmd.extend(["--n-stress-workers", str(self.auto_optimize_stress_workers)]) if self.auto_optimize_data_file: cmd.extend(["--data-file", self.auto_optimize_data_file])