From a9af0d5dfba021954f807233adbcf68ba9739ed3 Mon Sep 17 00:00:00 2001 From: ddrwode <34234@3来 34> Date: Fri, 6 Feb 2026 18:19:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B1=95=E7=A4=BA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitmart/四分之一策略说明-修改版.md | 192 +++++++++ .../四分之一,五分钟,反手条件充足修改版.py | 403 ++++++++++-------- 2 files changed, 417 insertions(+), 178 deletions(-) create mode 100644 bitmart/四分之一策略说明-修改版.md diff --git a/bitmart/四分之一策略说明-修改版.md b/bitmart/四分之一策略说明-修改版.md new file mode 100644 index 0000000..cda56c7 --- /dev/null +++ b/bitmart/四分之一策略说明-修改版.md @@ -0,0 +1,192 @@ +# 四分之一策略 · ETHUSDT 5 分钟(修改版)— 详细说明 + +## 优化摘要(与初版对比) + +- **触发方式**:由「当前价与阈值比较」改为**穿越触发**(`cross_up` / `cross_down`),即上一轮价在阈值一侧、当前轮在另一侧才触发,避免“已在阈值下方/上方”的追单。 +- **实体过滤**:由固定 `entity < 0.1` 改为**百分比或 ATR**:`entity_filter_mode` 可选 `"pct"`(entity/close ≥ min_entity_pct)或 `"atr"`(entity ≥ entity_atr_ratio × ATR14)。 +- **影线反手**:上/下影线阈值由 0.01% 提高到 **0.05%**(可调),且影线反手也要求**穿越**实体边,减少频繁反手。 +- **EMA20**:拆成 **use_ema20_entry_filter**(开仓/反手方向过滤)与 **use_ema20_force_exit**(持仓强制平仓),解耦便于调参。 +- **平仓防针洗**:**exit_use_last_close = True** 时用已收盘 K 线的 `last_close` 判断 EMA 破位,减少瞬间插针触发平仓。 +- **ATR 追踪**:由「本 K 线内最高/最低」改为**自开仓以来**的 `_highest_since_entry` / `_lowest_since_entry`,换线不重置,真正追踪止盈。 +- **开盘延迟**:可选 **open_trigger_delay_seconds**(默认 0),当前 K 线开始后经过该秒数才允许触发,减轻开盘即触发。 + +--- + +## 一、策略概述 + +- **名称**:四分之一策略(Quarter Strategy) +- **标的**:Bitmart 合约 **ETHUSDT** +- **周期**:**5 分钟 K 线** +- **模式**:全仓(cross)、高杠杆(100x) +- **思路**:用「上一根 K 线实体」的 **1/4 位置** 做突破/回落触发,结合 EMA10/EMA20/ATR 止盈与趋势过滤,支持多空开仓与反手。 + +--- + +## 二、核心价格与 K 线定义 + +### 2.1 上一根 K 线实体 + +- **实体大小**:`entity = |close - open|`(绝对值) +- **实体上边**:`upper = max(open, close)` +- **实体下边**:`lower = min(open, close)` + +即:阳线时上边=收盘、下边=开盘;阴线时上边=开盘、下边=收盘。 + +### 2.2 四个关键价位(常规情况) + +以**上一根 K 线**的实体为基准: + +| 名称 | 计算公式 | 含义 | +|--------------|------------------------------|--------------------------| +| 做多触发 | `lower + entity/4` | 实体下方向上 1/4 处 | +| 做空触发 | `upper - entity/4` | 实体上边向下 1/4 处 | +| 突破做多 | `upper + entity/4` | 实体上边再向上 1/4 处 | +| 突破做空 | `lower - entity/4` | 实体下边再向下 1/4 处 | + +- **无仓时**:做多看「突破做多」价,做空看「做空触发」价。 +- **有仓时**:反手多/空用「做多触发」「做空触发」价。 + +### 2.3 跳空时的基准修正 + +当出现跳空时,用**当前 K 线开盘价**作为计算基准,使 1/4 区间对称于开盘价: + +- **情况 1**:上一根为**阳线**且**当前开盘价 > 上一根收盘价**(跳空高开) +- **情况 2**:上一根为**阴线**且**当前开盘价 < 上一根收盘价**(跳空低开) + +此时: + +- `calc_base = 当前 K 线 open` +- 做多触发 = `calc_base + entity/4` +- 做空触发 = `calc_base - entity/4` +- 突破做多 = `calc_base + entity/4`(与做多触发同) +- 突破做空 = `calc_base - entity/4`(与做空触发同) + +即跳空时以当前开盘为轴心,上下各 1/4 实体对称。 + +--- + +## 三、开仓与反手信号逻辑 + +### 3.1 前置过滤(所有信号共用) + +- **实体过小**:按 `entity_filter_mode` 二选一:`entity/close` 不低于 `min_entity_pct`(如 0.02%),或 `entity` 不低于 `entity_atr_ratio × ATR(14)`(如 0.2×ATR)。不满足则本根不产生信号。 +- **形态过滤**: + - **上一根阴线 + 当前阳线**(视为做多形态):不按「做空触发(上 1/4)」做空。 + - **上一根阳线 + 当前阴线**(视为做空形态):不按「做多触发(下 1/4)」做多。 +- **穿越触发**:所有开仓/反手均要求**上一轮价格在阈值一侧、当前轮穿越到另一侧**(避免追单)。 + +### 3.2 无持仓(start == 0) + +- **做多**:**穿越**突破做多(上一轮 < 突破价 ≤ 当前价),且未被做多形态过滤、且通过 EMA20 入场过滤 → 开多。 +- **做空**:**穿越**做空触发(上一轮 > 做空触发 ≥ 当前价),且未被做空形态过滤、且通过 EMA20 入场过滤 → 开空。 + +### 3.3 持多仓(start == 1)→ 反手做空 + +满足其一即反手空: + +1. **穿越**做空触发,且反手价差 ≥ reverse_min_move_pct,且 EMA20 入场过滤通过。 +2. **上影线反手**:上一根**上影线比例 > min_upper_shadow_pct**(如 0.05%),且**穿越**上一根实体下边。 + +### 3.4 持空仓(start == -1)→ 反手做多 + +满足其一即反手多: + +1. **穿越**做多触发,且反手价差 ≥ reverse_min_move_pct,且 EMA20 入场过滤通过。 +2. **下影线反手**:上一根**下影线比例 > min_lower_shadow_pct**(如 0.05%),且**穿越**上一根实体上边。 + +--- + +## 四、反手前的额外过滤 + +- **反手价差过滤**: + `reverse_min_move_pct = 0.05%` + 反手时要求:`|当前价 - 触发价| / 触发价 * 100 >= 0.05%`,否则不执行反手(避免刚触及就反手)。 + +--- + +## 五、开仓前的过滤 + +1. **同 K 线出场不再开仓**:若本根 K 线内已经因 EMA/ATR/EMA20 平过仓(`_last_exit_kline_id == current_kline_time`),本根 K 线内不再开多/开空,等下一根 K 线再判断。 +2. **EMA20 方向过滤**(`use_ema20_filter = True`): + - 开多 / 反手多:仅当 **当前价 > EMA20** 时允许。 + - 开空 / 反手空:仅当 **当前价 < EMA20** 时允许。 + 即逆势(价在 EMA20 另一侧)时暂停开仓/反手。 + +--- + +## 六、平仓(止盈/风控)规则 + +- **最短持仓时间**:`min_hold_seconds = 90` 秒。开仓或反手后,至少持仓 90 秒才允许下面「技术性平仓」。 +- **EMA/ATR 平仓开关**:`use_ema_atr_exit = True` 时,下面规则才生效。 +- **ATR 倍数**:`atr_multiplier = 1.1`,即 1.1 × ATR(14)。 +- **防针洗**:`exit_use_last_close = True` 时,EMA10/EMA20 破位用**已收盘 K 线的 last_close** 判断,减少插针触发平仓;ATR 仍用当前价与自开仓以来极值比较。 +- **EMA20 强制平**:由独立开关 **use_ema20_force_exit** 控制,与开仓用的 **use_ema20_entry_filter** 解耦。 + +### 6.1 多单平仓(满足其一即平) + +1. **EMA10 快出**:参考价 **< EMA10**(参考价 = last_close 或当前价,见上)→ 平多。 +2. **EMA20 强制平**(`use_ema20_force_exit = True`):参考价 **< EMA20** → 强制平多。 +3. **ATR 追踪止盈**:从**自开仓以来最高价**回撤 **≥ 1.1×ATR(14)**(用当前价)→ 平多。极值不随换线重置。 + +### 6.2 空单平仓(满足其一即平) + +1. **EMA10 快出**:参考价 **> EMA10** → 平空。 +2. **EMA20 强制平**:参考价 **> EMA20** → 强制平空。 +3. **ATR 追踪止盈**:从**自开仓以来最低价**反弹 **≥ 1.1×ATR(14)** → 平空。 + +### 6.3 指标计算说明 + +- **EMA10 / EMA20 / ATR(14) / last_close**:均基于「已收盘 K 线」计算(最近约 35 根 5m,排除当前未收盘一根);`get_ema_atr_for_exit` 返回 dict 含上述四项。 +- **自开仓以来最高/最低**:仅在持仓期间用当前价更新,用于 ATR 追踪;开仓/反手时重置为当次入场价,换线不重置。 + +--- + +## 七、交易执行与风控细节 + +- **下单方式**:市价单;开仓/反手统一张数 `default_order_size = 25`(可在代码中修改)。 +- **反手流程**:先市价平仓 → 轮询确认无持仓(最多约 10 秒)→ 再市价开新方向;反手成功后多等约 20 秒再继续循环,避免界面/接口延迟。 +- **开仓前校验**:开多/开空前会查持仓,若已有反向或同向持仓则放弃本次开仓,避免双向持仓。 +- **杠杆与账户**:启动时设置全仓、100x 杠杆;未使用 `risk_percent`,仓位由固定张数控制。 + +--- + +## 八、主循环流程概览 + +1. 拉取最近 2 根 5 分钟 K 线(上一根 + 当前根)及当前价。 +2. 每次循环通过 API 获取真实持仓状态(避免与交易所不同步)。 +3. **若已有持仓**: + 先判断是否满足「最短持仓 90 秒」+ EMA10/EMA20/ATR 平仓条件,满足则平仓并本 K 线内不再开仓。 +4. **信号检测**: + 根据当前价与上一根实体 1/4 计算做多/做空/突破价,结合形态过滤与持仓状态,得到开多、开空、反手多、反手空之一或无信号。 +5. **反手过滤**:若为反手信号,再检查反手价差 ≥ 0.05%。 +6. **开仓过滤**:若为开多/开空,检查「本 K 线未因技术性平仓」及 EMA20 方向过滤。 +7. **执行**:有信号则执行对应开仓或反手,成功后刷新页面/状态并短暂等待再进入下一轮。 + +--- + +## 九、关键参数汇总 + +| 参数 | 值 | 说明 | +|------------------------------|-----------|----------------------------------------| +| contract_symbol | ETHUSDT | 合约品种 | +| 周期 | 5 分钟 | K 线步长 | +| leverage | 100 | 杠杆倍数 | +| open_type | cross | 全仓 | +| default_order_size | 25 | 开仓/反手张数 | +| reverse_min_move_pct | 0.05 | 反手最小价差(%) | +| min_hold_seconds | 90 | 最短持仓时间(秒) | +| use_ema_atr_exit | True | 是否启用 EMA/ATR 平仓 | +| atr_multiplier | 1.1 | ATR 追踪止盈倍数(自开仓以来极值) | +| use_ema20_entry_filter | True | 开仓/反手方向过滤(价在 EMA20 同侧) | +| use_ema20_force_exit | True | 持仓强制平仓(价穿越 EMA20 则平) | +| exit_use_last_close | True | 用已收盘 last_close 判断 EMA 破位防针洗| +| entity_filter_mode | atr | 实体过滤:pct 或 atr | +| min_entity_pct | 0.02 | 实体/close ≥ 此%才交易(mode=pct) | +| entity_atr_ratio | 0.2 | 实体 ≥ 此倍数×ATR 才交易(mode=atr) | +| min_upper_shadow_pct | 0.05 | 上影线比例 > 此值才允许上影线反手 | +| min_lower_shadow_pct | 0.05 | 下影线比例 > 此值才允许下影线反手 | +| open_trigger_delay_seconds | 0 | 当前 K 线开始后延迟 N 秒再允许触发 | + +--- + +以上即为「四分之一,五分钟,反手条件充足修改版」中的完整策略逻辑说明;实际运行以代码为准,本文档便于理解和后续调参。 diff --git a/bitmart/四分之一,五分钟,反手条件充足修改版.py b/bitmart/四分之一,五分钟,反手条件充足修改版.py index f75b132..0b4d114 100644 --- a/bitmart/四分之一,五分钟,反手条件充足修改版.py +++ b/bitmart/四分之一,五分钟,反手条件充足修改版.py @@ -106,6 +106,17 @@ def build_dashboard_layout(state: dict, logs: list) -> Layout: return layout +# ---------- 穿越触发(避免“已在阈值下方/上方”的追单) ---------- +def cross_up(prev_price: float, curr_price: float, level: float) -> bool: + """上一轮 < 阈值 且 这一轮 >= 阈值""" + return prev_price < level <= curr_price + + +def cross_down(prev_price: float, curr_price: float, level: float) -> bool: + """上一轮 > 阈值 且 这一轮 <= 阈值""" + return prev_price > level >= curr_price + + class BitmartFuturesTransaction: def __init__(self, bit_id): @@ -134,15 +145,18 @@ class BitmartFuturesTransaction: self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位) self.open_type = "cross" # 全仓模式 self.risk_percent = 0 # 未使用;若启用则可为每次开仓占可用余额的百分比 - # 退出:仅 EMA10(快退出)+ 1.1×ATR(14) 追踪。EMA20 不做先平,只做趋势/方向过滤(见下) + # 退出:EMA10 快退出 + EMA20 强制平(可独立开关)+ 自开仓以来 ATR 追踪 self.use_ema_atr_exit = True # 是否启用 EMA/ATR 平仓规则(多单+空单) - self.atr_multiplier = 1.1 # 追踪止盈:从当前K线极值回撤/反弹 ≥ 此倍数×ATR(14) 则平仓 - self.use_ema20_filter = True # 是否用 EMA20 做开仓/反手方向过滤 + 持仓强制减仓(趋势过滤) + self.atr_multiplier = 1.1 # 追踪止盈:自开仓以来最高/最低回撤/反弹 ≥ 此倍数×ATR(14) 则平仓 + self.use_ema20_entry_filter = True # 开仓/反手方向过滤:价在 EMA20 同侧才允许 + self.use_ema20_force_exit = True # 持仓强制平仓:价穿越 EMA20 则平(与入场过滤解耦) self.min_hold_seconds = 90 # 开仓/反手后至少持仓此时长才允许技术性止盈(EMA10/EMA20/ATR) - self._candle_high_seen = None # 当前K线内见过的最高价(多头用) - self._candle_low_seen = None # 当前K线内见过的最低价(空头用) - self._candle_id_for_high_low = None # 记录高低对应的K线 id,换线则重置 + self.exit_use_last_close = True # True=用已收盘K线 last_close 判断 EMA 破位(防针洗),False=用当前价 + # 自开仓以来的最高/最低(真正的 ATR 追踪,不随换线重置) + self._highest_since_entry = None + self._lowest_since_entry = None self._last_exit_kline_id = None # 当前K线出场后,本K线内不再开仓,等下一根K线再判断 + self._prev_price = None # 上一轮轮询价,用于穿越触发 self.open_avg_price = None # 开仓价格 self.current_amount = None # 持仓量 @@ -150,6 +164,16 @@ class BitmartFuturesTransaction: self.bit_id = bit_id self.default_order_size = 25 # 开仓/反手张数,统一在此修改 + # 实体过滤:ETH 上 0.1 几乎无效,改为百分比或 ATR。二选一:entity_pct 或 entity_atr_ratio + self.entity_filter_mode = "atr" # "pct" | "atr" + self.min_entity_pct = 0.02 # entity/close < 0.02% 视为实体过小(当 mode=pct 时) + self.entity_atr_ratio = 0.2 # entity < 0.2*ATR(14) 视为实体过小(当 mode=atr 时) + # 上/下影线反手阈值(原 0.01% 太小,几乎根根满足) + self.min_upper_shadow_pct = 0.05 # 上影线比例 > 此值才允许“上影线反手” + self.min_lower_shadow_pct = 0.05 # 下影线比例 > 此值才允许“下影线反手” + # 跳空/开盘后延迟:当前 K 线开始后经过此秒数才允许触发(0=不延迟,可设 30 等防开盘即触发) + self.open_trigger_delay_seconds = 0 + # 策略相关变量 self.prev_kline = None self.current_kline = None @@ -253,19 +277,20 @@ class BitmartFuturesTransaction: def get_ema_atr_for_exit(self, kline_series): """ - 基于已收盘 K 线计算 EMA10、EMA20、ATR(14)。 + 基于已收盘 K 线计算 EMA10、EMA20、ATR(14)、last_close。 kline_series 最后一根可为当前未收盘 K 线,计算时用倒数第 2~N 根作为已收盘。 - 返回 (ema10, ema20, atr14),任一不足则对应为 None。 + 返回 dict: ema10, ema20, atr14, last_close(已收盘的最后一根 close,用于防针洗)。 """ if not kline_series or len(kline_series) < 21: - return None, None, None + return {"ema10": None, "ema20": None, "atr14": None, "last_close": None} # 用除最后一根外的已收盘 K 线(若只有 21 根则用前 20 根) closed = kline_series[:-1] if len(kline_series) >= 21 else kline_series[:20] closes = [k['close'] for k in closed] ema10 = self._ema(closes, 10) ema20 = self._ema(closes, 20) atr14 = self._atr(closed, 14) - return ema10, ema20, atr14 + last_close = closed[-1]['close'] if closed else None + return {"ema10": ema10, "ema20": ema20, "atr14": atr14, "last_close": last_close} def get_current_price(self): """获取当前最新价格""" @@ -455,122 +480,189 @@ class BitmartFuturesTransaction: 'lower': min(kline['open'], kline['close']) # 实体下边 } - def check_signal(self, current_price, prev_kline, current_kline): + @staticmethod + def quarter_levels(prev_o, prev_h, prev_l, prev_c, curr_open): + """计算四分之一价位(含跳空修正)。返回 dict: entity, upper, lower, long_trigger, short_trigger, breakout_long, breakout_short""" + entity = abs(prev_c - prev_o) + upper = max(prev_o, prev_c) + lower = min(prev_o, prev_c) + gap_up = (prev_c > prev_o) and (curr_open > prev_c) + gap_down = (prev_c < prev_o) and (curr_open < prev_c) + if gap_up or gap_down: + base = curr_open + long_trigger = base + entity / 4 + short_trigger = base - entity / 4 + breakout_long = long_trigger + breakout_short = short_trigger + else: + long_trigger = lower + entity / 4 + short_trigger = upper - entity / 4 + breakout_long = upper + entity / 4 + breakout_short = lower - entity / 4 + return { + "entity": entity, + "upper": upper, + "lower": lower, + "long_trigger": long_trigger, + "short_trigger": short_trigger, + "breakout_long": breakout_long, + "breakout_short": breakout_short, + } + + def _entity_filter_passed(self, entity: float, prev_close: float, atr14: float | None) -> bool: + """实体过滤:False 表示实体过小,不交易。""" + if self.entity_filter_mode == "pct": + if prev_close <= 0: + return False + return (entity / prev_close * 100) >= self.min_entity_pct + # atr + if atr14 is None or atr14 <= 0: + return entity >= 0.1 # 回退:无 ATR 时用固定 0.1 + return entity >= self.entity_atr_ratio * atr14 + + def _allow_entry_by_ema20(self, side: int, curr_price: float, ema20: float | None) -> bool: + """开仓/反手方向过滤:多单要求价>EMA20,空单要求价 ema20) if side == 1 else (curr_price < ema20) + + def check_signal(self, current_price, prev_kline, current_kline, levels: dict, ind: dict): """ - 检查交易信号 - 返回: ('long', trigger_price) / ('short', trigger_price) / None + 检查交易信号(穿越触发 + 形态过滤 + 影线反手阈值提高)。 + 返回: ('long'|'short'|'reverse_long'|'reverse_short', trigger_price) 或 None。 """ current_kline_id = current_kline.get('id') should_log_snapshot = self._last_signal_log_kline_id != current_kline_id - - # 计算上一根K线实体 - prev_entity = self.calculate_entity(prev_kline) - - # 实体过小不交易(实体 < 0.1) - if prev_entity < 0.1: - if should_log_snapshot: - logger.info(LOG_PRICE + f"上一根K线实体过小: {prev_entity:.4f},跳过信号检测") - self._last_signal_log_kline_id = current_kline_id + prev_price = self._prev_price + if prev_price is None: + # 第一轮无上一轮价,无法判断穿越,不产生开仓/反手 return None - # 获取上一根K线的实体上下边 - prev_entity_edge = self.get_entity_edge(prev_kline) - prev_entity_upper = prev_entity_edge['upper'] # 实体上边 - prev_entity_lower = prev_entity_edge['lower'] # 实体下边 + prev_entity = levels["entity"] + prev_entity_upper = levels["upper"] + prev_entity_lower = levels["lower"] + long_trigger = levels["long_trigger"] + short_trigger = levels["short_trigger"] + breakout_long = levels["breakout_long"] - # 优化:以下两种情况以当前这根的开盘价作为计算基准 - # 1) 上一根阳线 且 当前开盘价 > 上一根收盘价(跳空高开) - # 2) 上一根阴线 且 当前开盘价 < 上一根收盘价(跳空低开) - prev_is_bullish_for_calc = prev_kline['close'] > prev_kline['open'] - prev_is_bearish_for_calc = prev_kline['close'] < prev_kline['open'] - current_open_above_prev_close = current_kline['open'] > prev_kline['close'] - current_open_below_prev_close = current_kline['open'] < prev_kline['close'] - use_current_open_as_base = (prev_is_bullish_for_calc and current_open_above_prev_close) or (prev_is_bearish_for_calc and current_open_below_prev_close) - - if use_current_open_as_base: - # 以当前K线开盘价为基准计算(跳空时用当前开盘价参与计算) - calc_lower = current_kline['open'] - calc_upper = current_kline['open'] # 同一基准,上下四分之一对称 - long_trigger = calc_lower + prev_entity / 4 - short_trigger = calc_upper - prev_entity / 4 - long_breakout = calc_upper + prev_entity / 4 - short_breakout = calc_lower - prev_entity / 4 - else: - # 原有计算方式 - long_trigger = prev_entity_lower + prev_entity / 4 # 做多触发价 = 实体下边 + 实体/4(下四分之一处) - short_trigger = prev_entity_upper - prev_entity / 4 # 做空触发价 = 实体上边 - 实体/4(上四分之一处) - long_breakout = prev_entity_upper + prev_entity / 4 # 做多突破价 = 实体上边 + 实体/4 - short_breakout = prev_entity_lower - prev_entity / 4 # 做空突破价 = 实体下边 - 实体/4 - - # 上一根阴线 + 当前阳线:做多形态,不按上一根K线上三分之一做空 + # 形态过滤 prev_is_bearish = prev_kline['close'] < prev_kline['open'] current_is_bullish = current_kline['close'] > current_kline['open'] skip_short_by_upper_third = prev_is_bearish and current_is_bullish - # 上一根阳线 + 当前阴线:做空形态,不按上一根K线下三分之一做多 prev_is_bullish = prev_kline['close'] > prev_kline['open'] current_is_bearish = current_kline['close'] < current_kline['open'] skip_long_by_lower_third = prev_is_bullish and current_is_bearish if should_log_snapshot: - if use_current_open_as_base: - if prev_is_bullish_for_calc and current_open_above_prev_close: - logger.info(LOG_PRICE + f"上一根阳线且当前开盘价({current_kline['open']:.2f})>上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算") - else: - logger.info(LOG_PRICE + f"上一根阴线且当前开盘价({current_kline['open']:.2f})<上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算") - logger.info(LOG_PRICE + f"当前价: {current_price:.2f} | 上一根实体: {prev_entity:.4f} | 实体上边: {prev_entity_upper:.2f} 下边: {prev_entity_lower:.2f}") - logger.info(LOG_PRICE + f"做多触发(下1/4): {long_trigger:.2f} | 做空触发(上1/4): {short_trigger:.2f} | 突破做多: {long_breakout:.2f} | 突破做空: {short_breakout:.2f}") + logger.info(LOG_PRICE + f"当前价: {current_price:.2f} 上一轮价: {prev_price:.2f} | 实体: {prev_entity:.4f} | 上边: {prev_entity_upper:.2f} 下边: {prev_entity_lower:.2f}") + logger.info(LOG_PRICE + f"做多触发: {long_trigger:.2f} | 做空触发: {short_trigger:.2f} | 突破做多: {breakout_long:.2f}") if skip_short_by_upper_third: logger.info(LOG_PRICE + "上一根阴+当前阳(做多形态),不按上1/4做空") if skip_long_by_lower_third: logger.info(LOG_PRICE + "上一根阳+当前阴(做空形态),不按下1/4做多") self._last_signal_log_kline_id = current_kline_id - self._display_triggers = {"long_trigger": long_trigger, "short_trigger": short_trigger, "long_breakout": long_breakout, "short_breakout": short_breakout} + self._display_triggers = { + "long_trigger": long_trigger, "short_trigger": short_trigger, + "long_breakout": breakout_long, "short_breakout": levels["breakout_short"] + } - # 无持仓时检查开仓信号(做多看突破价,做空看做空触发价,与反手阈值一致便于回落即进空) + ema20 = ind.get("ema20") + + # 无持仓:穿越触发 if self.start == 0: - if current_price >= long_breakout and not skip_long_by_lower_third: - logger.info(LOG_SIGNAL + f"触发做多 | 价 {current_price:.2f} >= 突破价 {long_breakout:.2f}") - return ('long', long_breakout) - elif current_price <= short_trigger and not skip_short_by_upper_third: - logger.info(LOG_SIGNAL + f"触发做空 | 价 {current_price:.2f} <= 做空触发价 {short_trigger:.2f}") - return ('short', short_trigger) + if cross_up(prev_price, current_price, breakout_long) and not skip_long_by_lower_third: + if self._allow_entry_by_ema20(1, current_price, ema20): + logger.info(LOG_SIGNAL + f"穿越做多 | 上一轮 {prev_price:.2f} < 突破价 {breakout_long:.2f} <= 当前 {current_price:.2f}") + return ('long', breakout_long) + if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third: + if self._allow_entry_by_ema20(-1, current_price, ema20): + logger.info(LOG_SIGNAL + f"穿越做空 | 上一轮 {prev_price:.2f} > 做空触发 {short_trigger:.2f} >= 当前 {current_price:.2f}") + return ('short', short_trigger) + return None - # 持仓时检查反手信号 - elif self.start == 1: # 持多仓 - if current_price <= short_trigger and not skip_short_by_upper_third: - logger.info(LOG_SIGNAL + f"持多→反手做空 | 价 {current_price:.2f} <= 触发价 {short_trigger:.2f}") - return ('reverse_short', short_trigger) + # 持多 → 反手空 + if self.start == 1: + if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third: + if self._pct_move(current_price, short_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(-1, current_price, ema20): + logger.info(LOG_SIGNAL + f"持多→穿越反手做空 | 触发价 {short_trigger:.2f}") + return ('reverse_short', short_trigger) upper_shadow_pct = self.calculate_upper_shadow(prev_kline) - if upper_shadow_pct > 0.01 and current_price <= prev_entity_lower: - logger.info(LOG_SIGNAL + f"持多→反手做空 | 上阴线 {upper_shadow_pct:.4f}% 价<=实体下边 {prev_entity_lower:.2f}") - return ('reverse_short', prev_entity_lower) + if upper_shadow_pct > self.min_upper_shadow_pct and cross_down(prev_price, current_price, prev_entity_lower): + if self._allow_entry_by_ema20(-1, current_price, ema20): + logger.info(LOG_SIGNAL + f"持多→反手做空 | 上影线 {upper_shadow_pct:.2f}% 穿越实体下边 {prev_entity_lower:.2f}") + return ('reverse_short', prev_entity_lower) + return None - elif self.start == -1: # 持空仓 - if current_price >= long_trigger and not skip_long_by_lower_third: - logger.info(LOG_SIGNAL + f"持空→反手做多 | 价 {current_price:.2f} >= 触发价 {long_trigger:.2f}") - return ('reverse_long', long_trigger) + # 持空 → 反手多 + if self.start == -1: + if cross_up(prev_price, current_price, long_trigger) and not skip_long_by_lower_third: + if self._pct_move(current_price, long_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(1, current_price, ema20): + logger.info(LOG_SIGNAL + f"持空→穿越反手做多 | 触发价 {long_trigger:.2f}") + return ('reverse_long', long_trigger) lower_shadow_pct = self.calculate_lower_shadow(prev_kline) - if lower_shadow_pct > 0.01 and current_price >= prev_entity_upper: - logger.info(LOG_SIGNAL + f"持空→反手做多 | 下阴线 {lower_shadow_pct:.4f}% 价>=实体上边 {prev_entity_upper:.2f}") - return ('reverse_long', prev_entity_upper) + if lower_shadow_pct > self.min_lower_shadow_pct and cross_up(prev_price, current_price, prev_entity_upper): + if self._allow_entry_by_ema20(1, current_price, ema20): + logger.info(LOG_SIGNAL + f"持空→反手做多 | 下影线 {lower_shadow_pct:.2f}% 穿越实体上边 {prev_entity_upper:.2f}") + return ('reverse_long', prev_entity_upper) + return None return None + def _pct_move(self, curr_price: float, trigger_price: float) -> float: + """当前价相对触发价的变动百分比""" + if not trigger_price or trigger_price <= 0: + return 0.0 + return abs(curr_price - trigger_price) / trigger_price * 100.0 + def can_open(self, current_kline_id): """开仓前过滤(已删除开仓冷却,保留接口便于后续扩展)。""" return True def can_reverse(self, current_price, trigger_price): - """反手前过滤:仅最小价差""" + """反手前过滤:仅最小价差(与 check_signal 内已做重复校验,保留双保险)""" if trigger_price and trigger_price > 0: - move_pct = abs(current_price - trigger_price) / trigger_price * 100 + move_pct = self._pct_move(current_price, trigger_price) if move_pct < self.reverse_min_move_pct: self._log_throttled("reverse_move_small", LOG_SYSTEM + f"反手价差不足: {move_pct:.4f}% < {self.reverse_min_move_pct}%", interval=1.0) return False - return True + def should_exit(self, current_price: float, kline_id: int, ind: dict, now_ts: float): + """ + 是否应技术性平仓。使用 last_close 防针洗(可选)。 + 返回 (True, reason_str) 或 (False, None)。 + """ + if self.start == 0 or not self.use_ema_atr_exit: + return False, None + if not self.last_open_time or (now_ts - self.last_open_time) < self.min_hold_seconds: + return False, None + ema10 = ind.get("ema10") + ema20 = ind.get("ema20") + atr14 = ind.get("atr14") + last_close = ind.get("last_close") + ref_price = last_close if (self.exit_use_last_close and last_close is not None) else current_price + + if self.start == 1: + if ema10 is not None and ref_price < ema10: + return True, "EXIT_LONG_EMA10" + if self.use_ema20_force_exit and ema20 is not None and ref_price < ema20: + return True, "EXIT_LONG_EMA20" + if atr14 is not None and self._highest_since_entry is not None: + dd = self._highest_since_entry - current_price + if dd >= self.atr_multiplier * atr14: + return True, "EXIT_LONG_ATR" + elif self.start == -1: + if ema10 is not None and ref_price > ema10: + return True, "EXIT_SHORT_EMA10" + if self.use_ema20_force_exit and ema20 is not None and ref_price > ema20: + return True, "EXIT_SHORT_EMA20" + if atr14 is not None and self._lowest_since_entry is not None: + ru = current_price - self._lowest_since_entry + if ru >= self.atr_multiplier * atr14: + return True, "EXIT_SHORT_ATR" + return False, None + def verify_no_position(self, max_retries=5, retry_interval=3): """ 验证当前无持仓 @@ -815,104 +907,69 @@ class BitmartFuturesTransaction: level="debug", ) + kline_series = self.get_klines_series(35) + ind = self.get_ema_atr_for_exit(kline_series) # 更新仪表盘左侧数据(供 Rich 展示) try: self._display_state["price"] = current_price self._display_state["position"] = self.start self._display_state["kline_id"] = current_kline_time self._display_state["unrealized_pnl"] = self.get_unrealized_pnl_usd() - kline_series = self.get_klines_series(35) - ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - self._display_state["ema10"] = ema10 - self._display_state["ema20"] = ema20 - self._display_state["atr14"] = atr14 + self._display_state["ema10"] = ind["ema10"] + self._display_state["ema20"] = ind["ema20"] + self._display_state["atr14"] = ind["atr14"] if getattr(self, "_display_triggers", None): self._display_state.update(self._display_triggers) except Exception: pass - # 3.5 平仓:EMA10 快退出、EMA20 趋势过滤强制减仓、1.1×ATR 追踪 - if self.start != 0: - # 换线重置跟踪,有持仓时更新本K线内最高/最低价 - if self._candle_id_for_high_low != current_kline_time: - self._candle_high_seen = None - self._candle_low_seen = None - self._candle_id_for_high_low = current_kline_time - # 多头:先更新本 K 线最高价(供 ATR 追踪与后续回落逻辑用) - if self.start == 1: - self._candle_high_seen = max(self._candle_high_seen or 0, current_price) - elif self.start == -1: - self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price) + # 3.5 更新自开仓以来最高/最低(真正 ATR 追踪,不随换线重置) + if self.start == 1: + self._highest_since_entry = max(self._highest_since_entry or 0, current_price) + elif self.start == -1: + self._lowest_since_entry = min(self._lowest_since_entry if self._lowest_since_entry is not None else float('inf'), current_price) - hold_sec = (time.time() - self.last_open_time) if self.last_open_time else 999999 - allow_technical_exit = hold_sec >= self.min_hold_seconds + # 3.6 平仓:EMA10 / EMA20 强制 / ATR 追踪(ref 用 last_close 防针洗) + now_ts = time.time() + exit_ok, exit_reason = self.should_exit(current_price, current_kline_time, ind, now_ts) + if exit_ok: + logger.info(LOG_POSITION + f"技术性平仓 | 原因: {exit_reason}") + self.平仓() + self._last_exit_kline_id = current_kline_time + self._highest_since_entry = None + self._lowest_since_entry = None + time.sleep(3) + self._prev_price = current_price + continue - # 多单平仓:EMA10 快退出 / EMA20 趋势过滤强制减仓 / 1.1×ATR 追踪(组合1:能赚点是点) - if allow_technical_exit and self.start == 1 and self.use_ema_atr_exit: - kline_series = self.get_klines_series(35) - ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - # 1) 价格跌破 EMA10 → 先平(能赚点是点) - if ema10 is not None and current_price < ema10: - logger.info(LOG_POSITION + f"多单 EMA10 平仓 | 价 {current_price:.2f} 跌破 EMA10 {ema10:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_high_seen = None - time.sleep(3) - continue - # 2) EMA20 趋势过滤:价格跌破 EMA20 → 强制减仓(方向过滤,不做先平触发) - if self.use_ema20_filter and ema20 is not None and current_price < ema20: - logger.info(LOG_POSITION + f"多单 EMA20 强制减仓 | 价 {current_price:.2f} 跌破 EMA20 {ema20:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_high_seen = None - time.sleep(3) - continue - # 3) 从最高价回撤 ≥ 1.1×ATR(14) → 平仓 - if atr14 is not None and self._candle_high_seen and (self._candle_high_seen - current_price) >= self.atr_multiplier * atr14: - logger.info(LOG_POSITION + f"多单 ATR 追踪止盈 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} 回撤≥{self.atr_multiplier}×ATR={atr14:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_high_seen = None - time.sleep(3) - continue + # 4. 四分之一价位与实体过滤 + levels = self.quarter_levels( + prev_kline['open'], prev_kline['high'], prev_kline['low'], prev_kline['close'], + current_kline['open'] + ) + if not self._entity_filter_passed(levels["entity"], prev_kline['close'], ind.get("atr14")): + self._log_throttled("entity_small", LOG_PRICE + f"实体过小(实体={levels['entity']:.4f}),跳过信号", interval=2.0) + self._prev_price = current_price + time.sleep(0.1) + continue - # 空单平仓:EMA10 快退出 / EMA20 趋势过滤强制减仓 / 1.1×ATR 追踪 - if allow_technical_exit and self.start == -1 and self.use_ema_atr_exit: - kline_series = self.get_klines_series(35) - ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - # 1) 价格涨破 EMA10 → 先平 - if ema10 is not None and current_price > ema10: - logger.info(LOG_POSITION + f"空单 EMA10 平仓 | 价 {current_price:.2f} 涨破 EMA10 {ema10:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_low_seen = None - time.sleep(3) - continue - # 2) EMA20 趋势过滤:价格涨破 EMA20 → 强制减仓 - if self.use_ema20_filter and ema20 is not None and current_price > ema20: - logger.info(LOG_POSITION + f"空单 EMA20 强制减仓 | 价 {current_price:.2f} 涨破 EMA20 {ema20:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_low_seen = None - time.sleep(3) - continue - # 3) 从最低价反弹 ≥ 1.1×ATR(14) → 平仓 - if atr14 is not None and self._candle_low_seen and (current_price - self._candle_low_seen) >= self.atr_multiplier * atr14: - logger.info(LOG_POSITION + f"空单 ATR 追踪止盈 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} 反弹≥{self.atr_multiplier}×ATR={atr14:.2f}") - self.平仓() - self._last_exit_kline_id = current_kline_time - self._candle_low_seen = None - time.sleep(3) - continue - # 4. 检查信号 - signal = self.check_signal(current_price, prev_kline, current_kline) + # 开盘后延迟:避免开盘价即触发 + if self.open_trigger_delay_seconds > 0: + kline_elapsed = time.time() - current_kline_time + if kline_elapsed < self.open_trigger_delay_seconds: + self._prev_price = current_price + time.sleep(0.1) + continue - # 5. 反手过滤:最小价差 + # 5. 检查信号(穿越触发 + EMA20 入场过滤已在 check_signal 内) + signal = self.check_signal(current_price, prev_kline, current_kline, levels, ind) + + # 5.5 反手过滤:最小价差(双保险) if signal and signal[0].startswith('reverse_'): if not self.can_reverse(current_price, signal[1]): signal = None - # 5.5 开仓过滤:当前K线已出场则等下一根K线再开仓 + # 5.6 开仓过滤:当前K线已出场则等下一根K线再开仓 if signal and signal[0] in ('long', 'short'): if self._last_exit_kline_id == current_kline_time: self._log_throttled("same_kline_no_open", LOG_SYSTEM + "当前K线已出场,等下一根K线再开仓", interval=2.0) @@ -920,30 +977,20 @@ class BitmartFuturesTransaction: elif not self.can_open(current_kline_time): signal = None else: - self._current_kline_id_for_open = current_kline_time # 供 execute_trade 成功后记录 - - # 5.6 EMA20 方向过滤:价在 EMA20 上方才开多,下方才开空(趋势过滤,暂停逆势开仓) - if signal and self.use_ema20_filter: - kline_series = self.get_klines_series(35) - _, ema20, _ = self.get_ema_atr_for_exit(kline_series) - if ema20 is not None: - if signal[0] in ('long', 'reverse_long') and current_price <= ema20: - self._log_throttled("ema20_no_long", LOG_SYSTEM + f"EMA20 方向过滤:价 {current_price:.2f} 未在 EMA20 {ema20:.2f} 上方,暂停开多", interval=2.0) - signal = None - elif signal[0] in ('short', 'reverse_short') and current_price >= ema20: - self._log_throttled("ema20_no_short", LOG_SYSTEM + f"EMA20 方向过滤:价 {current_price:.2f} 未在 EMA20 {ema20:.2f} 下方,暂停开空", interval=2.0) - signal = None + self._current_kline_id_for_open = current_kline_time # 6. 有信号则执行交易 if signal: trade_success = self.execute_trade(signal) if trade_success: + self._highest_since_entry = current_price + self._lowest_since_entry = current_price logger.success(LOG_POSITION + f"交易执行完成: {signal[0]} | 当前持仓: {self.start}") page_start = True else: logger.warning(f"交易执行失败或被阻止: {signal[0]}") - # 短暂等待后继续循环(同一根K线遇到信号就操作) + self._prev_price = current_price time.sleep(0.1) if page_start: