diff --git a/bitmart/保守模式参数优化/README.md b/bitmart/保守模式参数优化/README.md index b5c7b21..95d6f39 100644 --- a/bitmart/保守模式参数优化/README.md +++ b/bitmart/保守模式参数优化/README.md @@ -95,7 +95,30 @@ pip3 install websocket-client - `BITMART_WS_TOPIC=...`:自定义订阅 topic - `BITMART_WS_PRICE_TTL=2.0`:价格新鲜度阈值(秒) -## 5. 费用模型说明 +## 5. 动态止盈止损 + +保守模式已支持按近期波动率自动调整: +- `take_profit_usd` +- `stop_loss_usd` +- `trailing_activation_usd` +- `trailing_distance_usd` +- `break_even_activation_usd` +- `break_even_floor_usd` + +触发方式: +- 每进入一根新的 5 分钟 K 线,自动计算近 N 根K线平均振幅(trimmed mean) +- 用当前波动率和目标波动率比值计算缩放系数,再更新上述参数 + +可选环境变量: +- `BITMART_DYNAMIC_RISK_ENABLED=1`:开启/关闭动态风控 +- `BITMART_DYNAMIC_RISK_WINDOW_BARS=36`:波动率窗口(5m K线根数) +- `BITMART_DYNAMIC_VOL_TARGET_PCT=0.25`:目标波动率(%) +- `BITMART_DYNAMIC_SCALE_MIN=0.7`:最小缩放倍数 +- `BITMART_DYNAMIC_SCALE_MAX=1.8`:最大缩放倍数 + +这些参数也可写到 `current_params.json` 的 `params` 中。 + +## 6. 费用模型说明 优化器会按下面公式计入手续费返佣: @@ -110,7 +133,7 @@ pip3 install websocket-client - `--raw-fee-rate` - `--rebate-ratio` -## 6. 重要提示 +## 7. 重要提示 - 回测撮合属于简化模型,不等于实盘撮合。 - 参数应周期性重训(例如每天或每周)。 diff --git a/bitmart/保守模式参数优化/current_params.json b/bitmart/保守模式参数优化/current_params.json index 30ae203..732e8f2 100644 --- a/bitmart/保守模式参数优化/current_params.json +++ b/bitmart/保守模式参数优化/current_params.json @@ -29,6 +29,11 @@ "open_cooldown_seconds": 300, "reverse_cooldown_seconds": 300, "reverse_min_move_pct": 0.15, + "dynamic_risk_enabled": true, + "dynamic_risk_window_bars": 36, + "dynamic_vol_target_pct": 0.25, + "dynamic_scale_min": 0.7, + "dynamic_scale_max": 1.8, "order_notional_usd": 100.0, "pnl_per_usd_move": 1.0, "slippage_pct": 0.01 diff --git a/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py b/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py index ead2fb8..5eb30b0 100644 --- a/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py +++ b/bitmart/保守模式参数优化/四分之一,五分钟,反手条件充足_保守模式.py @@ -93,8 +93,18 @@ class BitmartFuturesTransactionConservative: self.ws_stop_event = threading.Event() self.ws_lock = threading.Lock() + # 动态风控:按近期波动率自动调整 TP/SL/移动止损 + self.dynamic_risk_enabled = os.getenv("BITMART_DYNAMIC_RISK_ENABLED", "1").strip().lower() not in {"0", "false", "off", "no"} + self.dynamic_risk_window_bars = int(os.getenv("BITMART_DYNAMIC_RISK_WINDOW_BARS", "36")) # 5m * 36 = 3小时 + self.dynamic_vol_target_pct = float(os.getenv("BITMART_DYNAMIC_VOL_TARGET_PCT", "0.25")) # 目标波动率(%) + self.dynamic_scale_min = float(os.getenv("BITMART_DYNAMIC_SCALE_MIN", "0.7")) + self.dynamic_scale_max = float(os.getenv("BITMART_DYNAMIC_SCALE_MAX", "1.8")) + self.last_dynamic_risk_kline_id = None + self.base_risk_params = {} + # 启动时尝试读取动态参数(可由优化脚本自动生成) self.load_runtime_params() + self.capture_base_risk_params() def load_runtime_params(self): """ @@ -124,6 +134,11 @@ class BitmartFuturesTransactionConservative: "open_cooldown_seconds", "reverse_cooldown_seconds", "reverse_min_move_pct", + "dynamic_risk_enabled", + "dynamic_risk_window_bars", + "dynamic_vol_target_pct", + "dynamic_scale_min", + "dynamic_scale_max", } try: @@ -139,6 +154,8 @@ class BitmartFuturesTransactionConservative: continue if key == "leverage": setattr(self, key, str(value)) + elif key == "dynamic_risk_enabled": + setattr(self, key, self._to_bool(value)) else: setattr(self, key, value) @@ -155,6 +172,111 @@ class BitmartFuturesTransactionConservative: except Exception as e: logger.error(f"加载动态参数文件失败: {e} | path={params_path}") + def capture_base_risk_params(self): + """记录参数基线,动态风控在此基线之上按波动率缩放。""" + self.base_risk_params = { + "take_profit_usd": float(self.take_profit_usd), + "stop_loss_usd": float(self.stop_loss_usd), + "trailing_activation_usd": float(self.trailing_activation_usd), + "trailing_distance_usd": float(self.trailing_distance_usd), + "break_even_activation_usd": float(self.break_even_activation_usd), + "break_even_floor_usd": float(self.break_even_floor_usd), + } + + def get_recent_volatility_pct(self, bars=None): + """ + 计算近 N 根 5m K线的平均振幅百分比(trimmed mean)。 + 返回: 波动率百分比,如 0.25 表示 0.25% + """ + bars = bars or self.dynamic_risk_window_bars + bars = max(12, int(bars)) + now_ts = int(time.time()) + window_seconds = (bars + 10) * 5 * 60 + try: + response = self.contractAPI.get_kline( + contract_symbol=self.contract_symbol, + step=5, + start_time=now_ts - window_seconds, + end_time=now_ts, + )[0] + if response.get("code") != 1000: + return None + data = response.get("data", []) + if len(data) < 12: + return None + + ranges = [] + for k in data[-bars:]: + high = self._safe_float(k.get("high_price")) + low = self._safe_float(k.get("low_price")) + close = self._safe_float(k.get("close_price")) + if high is None or low is None or close is None or close <= 0: + continue + ranges.append((high - low) / close * 100) + + if len(ranges) < 12: + return None + + ranges.sort() + trim = int(len(ranges) * 0.1) + core = ranges[trim: len(ranges) - trim] if len(ranges) > trim * 2 else ranges + if not core: + return None + return sum(core) / len(core) + except Exception: + return None + + def update_dynamic_risk_params(self, current_kline_id): + """ + 每根新K线按近期波动率动态更新风险参数,避免固定 TP/SL 不适配行情。 + """ + if not self.dynamic_risk_enabled: + return + if self.last_dynamic_risk_kline_id == current_kline_id: + return + if not self.base_risk_params: + self.capture_base_risk_params() + if not self.base_risk_params: + return + + vol_pct = self.get_recent_volatility_pct(self.dynamic_risk_window_bars) + if vol_pct is None or vol_pct <= 0: + logger.warning("动态风控: 波动率计算失败,继续使用当前风险参数") + self.last_dynamic_risk_kline_id = current_kline_id + return + + target = max(self.dynamic_vol_target_pct, 0.01) + scale = vol_pct / target + scale = max(self.dynamic_scale_min, min(self.dynamic_scale_max, scale)) + + base = self.base_risk_params + self.take_profit_usd = round(base["take_profit_usd"] * scale, 4) + self.stop_loss_usd = round(-abs(base["stop_loss_usd"]) * scale, 4) + self.trailing_activation_usd = round(base["trailing_activation_usd"] * scale, 4) + self.trailing_distance_usd = round(base["trailing_distance_usd"] * scale, 4) + self.break_even_activation_usd = round(base["break_even_activation_usd"] * scale, 4) + self.break_even_floor_usd = round(base["break_even_floor_usd"] * scale, 4) + + # 保持参数关系合理,防止逻辑冲突 + if self.trailing_activation_usd <= self.break_even_activation_usd: + self.trailing_activation_usd = round(self.break_even_activation_usd + 0.2, 4) + if self.take_profit_usd <= self.trailing_activation_usd: + self.take_profit_usd = round(self.trailing_activation_usd + 0.4, 4) + max_floor = self.break_even_activation_usd * 0.9 + if self.break_even_floor_usd > max_floor: + self.break_even_floor_usd = round(max_floor, 4) + if self.trailing_distance_usd >= self.trailing_activation_usd: + self.trailing_distance_usd = round(max(0.1, self.trailing_activation_usd * 0.6), 4) + + self.last_dynamic_risk_kline_id = current_kline_id + logger.info( + "动态风控更新: " + f"vol={vol_pct:.4f}% scale={scale:.3f} " + f"TP={self.take_profit_usd} SL={self.stop_loss_usd} " + f"TrailAct={self.trailing_activation_usd} TrailDist={self.trailing_distance_usd} " + f"BEAct={self.break_even_activation_usd} BEFloor={self.break_even_floor_usd}" + ) + def _safe_float(self, value): try: f = float(value) @@ -164,6 +286,15 @@ class BitmartFuturesTransactionConservative: except Exception: return None + def _to_bool(self, value): + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return bool(value) + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "y", "on"} + return False + def _extract_price_from_ws_payload(self, payload): """ 尽量从 WS 消息中提取价格,兼容不同字段结构。 @@ -856,6 +987,7 @@ class BitmartFuturesTransactionConservative: if self.last_kline_time != current_kline_time: self.last_kline_time = current_kline_time logger.info(f"进入新K线: {current_kline_time}") + self.update_dynamic_risk_params(current_kline_time) # 2. 获取当前价格 current_price = self.get_current_price()