diff --git a/open_fifth_strategy/README.md b/open_fifth_strategy/README.md index 04fd439..d691d5b 100644 --- a/open_fifth_strategy/README.md +++ b/open_fifth_strategy/README.md @@ -1,54 +1,25 @@ # 基于开盘价的五分之一策略 -根据 1111 中的策略规则实现的 BitMart 合约交易策略。 +策略规则(1111): -## 策略规则 +- **做多触发价** = 当前K线开盘价 + 前一根实体/5 +- **做空触发价** = 当前K线开盘价 - 前一根实体/5 +- **前一根有效K线**:实体 ≥ 0.1 -### 触发价计算(基于前一根有效 K 线,实体 ≥ 0.1) +## 执行逻辑 -- **做多触发价** = 当前 K 线开盘价 + 实体/5 -- **做空触发价** = 当前 K 线开盘价 - 实体/5 +- 当前K线最高价 ≥ 做多触发价 → 做多信号 +- 当前K线最低价 ≤ 做空触发价 → 做空信号 +- 同根K线多空都触及时,用1分钟K线判断先后 +- 触及信号则开仓或反手,同根3分钟K线只交易一次 -### 信号触发条件 - -- 当前 K 线最高价 ≥ 做多触发价 → 做多信号 -- 当前 K 线最低价 ≤ 做空触发价 → 做空信号 - -### 第一分钟反手(若已有持仓) - -- 3分钟K线的**第一分钟**内若出现反手信号 → 平仓开反手 -- **持空反手做多**:价格涨到 开仓价 + 前一根实体/5 -- **持多反手做空**:价格跌到 开仓价 - 前一根实体/5 -- 检测窗口:只使用第1根1分钟K线(0:00~1:00) - -### 与原始五分之一策略的区别 - -| 项目 | 原始策略 | 本策略(基于开盘价) | -|------------|----------------|--------------------------| -| 做多触发基 | 前一根收盘价 | 当前 K 线开盘价 | -| 做空触发基 | 前一根收盘价 | 当前 K 线开盘价 | -| 反手逻辑 | 同左 | 相同 | - -## 运行方式 - -在项目根目录 `lm_code` 下执行: - -```bash -python open_fifth_strategy/main.py -``` - -或使用模块方式: +## 运行 ```bash cd /path/to/lm_code -python -m open_fifth_strategy.main +python open_fifth_strategy/main.py ``` ## 配置 -在 `config.py` 中修改: - -- API 密钥 -- 合约交易对(默认 ETHUSDT) -- K 线周期(默认 3 分钟) -- 杠杆、风险比例等 +在 `config.py` 中修改 API、合约、杠杆等参数。 diff --git a/open_fifth_strategy/config.py b/open_fifth_strategy/config.py index bdaf26e..0260e4f 100644 --- a/open_fifth_strategy/config.py +++ b/open_fifth_strategy/config.py @@ -2,14 +2,10 @@ """ 基于开盘价的五分之一策略 - 配置文件 -策略规则(来自 1111): +策略规则(1111): - 做多触发价 = 当前K线开盘价 + 前一根实体/5 - 做空触发价 = 当前K线开盘价 - 前一根实体/5 - 前一根有效K线:实体 >= 0.1 - -第一分钟反手: -- 持空反手做多:价格涨到 开仓价 + 前一根实体/5 -- 持多反手做空:价格跌到 开仓价 - 前一根实体/5 """ # BitMart API(请勿提交敏感信息到版本库) @@ -26,13 +22,5 @@ LEVERAGE = "100" OPEN_TYPE = "cross" # 全仓 RISK_PERCENT = 0.01 # 每次开仓占用可用余额的比例 -# 反手信号价格容差(美元):K线触及触发价后,收盘价需在触发价±容差内才执行 -# 避免“先涨后跌/先跌后涨”追单,适当增大可减少漏单 -REVERSE_PRICE_TOLERANCE = 5.0 - -# 反手信号检测窗口:3分钟K线的「第一分钟」内出现反手信号则平仓反手 -# 使用前1根1分钟K线(只检测 0:00~1:00) -REVERSE_WINDOW_1M_BARS = 1 - # 比特浏览器ID(用于网页下单) BIT_ID = "f2320f57e24c45529a009e1541e25961" diff --git a/open_fifth_strategy/main.py b/open_fifth_strategy/main.py index d7a2b54..358ba83 100644 --- a/open_fifth_strategy/main.py +++ b/open_fifth_strategy/main.py @@ -2,27 +2,18 @@ """ BitMart 基于开盘价的五分之一策略交易 -策略规则(与 1111 一致): -1. 触发价格计算(基于前一根有效K线,实体>=0.1): - - 做多触发价 = 当前K线开盘价 + 实体/5 - - 做空触发价 = 当前K线开盘价 - 实体/5 +策略规则(1111): +- 做多触发价 = 当前K线开盘价 + 实体/5 +- 做空触发价 = 当前K线开盘价 - 实体/5 +- 基于前一根有效K线(实体 >= 0.1) -2. 信号触发条件: - - 当前K线最高价 >= 做多触发价 → 做多信号 - - 当前K线最低价 <= 做空触发价 → 做空信号 +执行:触及做多/做空触发价则开仓或反手,同根K线只交易一次 -3. 第一分钟反手(若已有持仓): - - 3分钟K线的第一分钟内若出现反手信号,则平仓开反手 - - 持空反手做多:价格涨到 开仓价 + 前一根实体/5 - - 持多反手做空:价格跌到 开仓价 - 前一根实体/5 - -运行方式(在项目根目录 lm_code 下): - python open_fifth_strategy/main.py +运行:python open_fifth_strategy/main.py(在项目根目录 lm_code 下) """ import sys from pathlib import Path -# 确保项目根目录在路径中 _root = Path(__file__).resolve().parent.parent if str(_root) not in sys.path: sys.path.insert(0, str(_root)) @@ -50,8 +41,6 @@ from open_fifth_strategy.config import ( LEVERAGE, OPEN_TYPE, RISK_PERCENT, - REVERSE_PRICE_TOLERANCE, - REVERSE_WINDOW_1M_BARS, BIT_ID, ) @@ -83,18 +72,12 @@ class OpenBasedFifthStrategy: self.leverage = LEVERAGE self.open_type = OPEN_TYPE self.risk_percent = RISK_PERCENT - self.reverse_price_tolerance = REVERSE_PRICE_TOLERANCE - self.reverse_window_1m_bars = REVERSE_WINDOW_1M_BARS self.last_trigger_kline_id = None self.last_trigger_direction = None self.last_trade_kline_id = None - self.entry_prev_body = None - self.entry_price = None - self.entry_kline_id = None - self.early_reverse_executed = False - # ==================== 策略核心(基于开盘价)==================== + # ==================== 策略核心 ==================== def get_body_size(self, candle): return abs(float(candle["open"]) - float(candle["close"])) @@ -109,9 +92,9 @@ class OpenBasedFifthStrategy: return i, prev return None, None - def get_open_based_levels(self, prev, curr_open): + def get_trigger_levels(self, prev, curr_open): """ - 基于当前K线开盘价计算触发价(与1111一致) + 基于当前K线开盘价计算触发价(1111) 做多触发 = 当前K线开盘价 + 实体/5 做空触发 = 当前K线开盘价 - 实体/5 """ @@ -119,9 +102,7 @@ class OpenBasedFifthStrategy: if body < 0.001: return None, None curr_o = float(curr_open) - long_trigger = curr_o + body / 5 - short_trigger = curr_o - body / 5 - return long_trigger, short_trigger + return curr_o + body / 5, curr_o - body / 5 def get_1m_bars_for_3m_bar(self, bar_3m): """获取当前3分钟K线对应的3根1分钟K线""" @@ -155,7 +136,7 @@ class OpenBasedFifthStrategy: return [] def determine_trigger_order_by_1m(self, bars_1m, long_trigger, short_trigger): - """用1分钟K线判断先触发做多还是做空""" + """同根K线多空都触及时,用1分钟K线判断先后""" if not bars_1m: return None for bar in bars_1m: @@ -174,70 +155,9 @@ class OpenBasedFifthStrategy: return "short" if d_short < d_long else "long" return None - def check_early_reverse_signal(self, curr_kline, kline_data): + def check_trigger(self, kline_data): """ - 第一分钟反手检测: - - 3分钟K线的「第一分钟」内若出现反手信号 → 平仓开反手 - - 持空反手做多:价格涨到 开仓价 + 前一根实体/5 - - 持多反手做空:价格跌到 开仓价 - 前一根实体/5 - - 使用前 N 根1分钟K线(REVERSE_WINDOW_1M_BARS=1 只检测第一分钟) - """ - if self.start == 0: - return None, None - - curr_kline_id = curr_kline["id"] - if self.entry_kline_id != curr_kline_id: - self.early_reverse_executed = False - self.entry_kline_id = curr_kline_id - - if self.early_reverse_executed: - return None, None - - entry_price = self.entry_price - if entry_price is None and self.open_avg_price: - entry_price = float(self.open_avg_price) - if entry_price is None: - return None, None - - _, valid_prev = self.find_valid_prev_bar( - kline_data, len(kline_data) - 1 - ) - if valid_prev is None: - return None, None - prev_body = self.get_body_size(valid_prev) - reverse_offset = prev_body / 5 - - bars_1m = self.get_1m_bars_for_3m_bar(curr_kline) - n_bars = min(self.reverse_window_1m_bars, len(bars_1m)) - if n_bars < 1: - return None, None - - # 遍历前 N 根1分钟K线(覆盖前1分30秒),任一出现反手信号则触发 - for i in range(n_bars): - bar = bars_1m[i] - bar_high = float(bar["high"]) - bar_low = float(bar["low"]) - bar_close = float(bar["close"]) - - if self.start == -1: - reverse_long_trigger = entry_price + reverse_offset - if bar_high >= reverse_long_trigger: - if bar_close >= reverse_long_trigger - self.reverse_price_tolerance: - return "long", reverse_long_trigger - - elif self.start == 1: - reverse_short_trigger = entry_price - reverse_offset - if bar_low <= reverse_short_trigger: - if bar_close <= reverse_short_trigger + self.reverse_price_tolerance: - return "short", reverse_short_trigger - - return None, None - - def check_realtime_trigger(self, kline_data, current_position=0): - """ - 实时检测信号 - - 无仓位:基于当前K线开盘价计算触发价 - - 有仓位(反手):基于开仓价计算触发价(1111:开仓价±前一根实体/5) + 检测当前K线是否触发信号(1111:当前开盘价±实体/5) 返回:(方向, 触发价, 有效前一根, 当前K线) 或 (None,...) """ if len(kline_data) < 2: @@ -246,70 +166,41 @@ class OpenBasedFifthStrategy: curr = kline_data[-1] curr_kline_id = curr["id"] curr_open = curr["open"] - valid_prev_idx, prev = self.find_valid_prev_bar( - kline_data, len(kline_data) - 1 - ) + _, prev = self.find_valid_prev_bar(kline_data, len(kline_data) - 1) if prev is None: return None, None, None, None - prev_body = self.get_body_size(prev) - reverse_offset = prev_body / 5 - - # 有仓位时反手用开仓价(1111),无仓位用当前K线开盘价 - if current_position != 0: - entry = self.entry_price or (float(self.open_avg_price) if self.open_avg_price else None) - if entry is not None: - long_trigger = entry + reverse_offset - short_trigger = entry - reverse_offset - else: - long_trigger, short_trigger = self.get_open_based_levels(prev, curr_open) - else: - long_trigger, short_trigger = self.get_open_based_levels(prev, curr_open) - + long_trigger, short_trigger = self.get_trigger_levels(prev, curr_open) if long_trigger is None: return None, None, None, None c_high = float(curr["high"]) c_low = float(curr["low"]) - c_close = float(curr["close"]) long_triggered = c_high >= long_trigger short_triggered = c_low <= short_trigger direction = None trigger_price = None - if current_position == 1: - if short_triggered and c_close <= short_trigger + self.reverse_price_tolerance: - direction = "short" - trigger_price = short_trigger - elif current_position == -1: - if long_triggered and c_close >= long_trigger - self.reverse_price_tolerance: - direction = "long" - trigger_price = long_trigger - else: - if long_triggered and short_triggered: - bars_1m = self.get_1m_bars_for_3m_bar(curr) - if bars_1m: - direction = self.determine_trigger_order_by_1m( - bars_1m, long_trigger, short_trigger - ) - trigger_price = ( - long_trigger if direction == "long" else short_trigger - ) - if direction is None: - c_open_f = float(curr["open"]) - d_long = abs(long_trigger - c_open_f) - d_short = abs(short_trigger - c_open_f) - direction = "short" if d_short <= d_long else "long" - trigger_price = ( - long_trigger if direction == "long" else short_trigger - ) - elif short_triggered: - direction = "short" - trigger_price = short_trigger - elif long_triggered: - direction = "long" - trigger_price = long_trigger + if long_triggered and short_triggered: + bars_1m = self.get_1m_bars_for_3m_bar(curr) + if bars_1m: + direction = self.determine_trigger_order_by_1m( + bars_1m, long_trigger, short_trigger + ) + trigger_price = long_trigger if direction == "long" else short_trigger + if direction is None: + c_open_f = float(curr["open"]) + d_long = abs(long_trigger - c_open_f) + d_short = abs(short_trigger - c_open_f) + direction = "short" if d_short <= d_long else "long" + trigger_price = long_trigger if direction == "long" else short_trigger + elif short_triggered: + direction = "short" + trigger_price = short_trigger + elif long_triggered: + direction = "long" + trigger_price = long_trigger if direction is None: return None, None, None, None @@ -453,7 +344,7 @@ class OpenBasedFifthStrategy: return False direction_str = "做多" if marketPriceLongOrder == 1 else "做空" logger.info(f"执行{direction_str},金额: {size}") - size = max(1, min(25, int(size))) # 限制单次下单金额 1~25 + size = max(1, min(25, int(size))) try: self.click_safe('x://button[normalize-space(text()) ="市价"]') self.page.ele('x://*[@id="size_0"]').input(str(size)) @@ -468,7 +359,7 @@ class OpenBasedFifthStrategy: return False def ding(self, msg, error=False): - prefix = "❌开盘价五分之一:" if error else "🔔开盘价五分之一:" + prefix = "❌五分之一:" if error else "🔔五分之一:" full_msg = f"{prefix}{msg}" if error: logger.error(msg) @@ -491,9 +382,7 @@ class OpenBasedFifthStrategy: if self.start != 0: open_avg_price = float(self.open_avg_price) current_amount = float(self.current_amount) - position_cross = float( - getattr(self, "position_cross", 0) or 0 - ) + position_cross = float(getattr(self, "position_cross", 0) or 0) if self.start == 1: unrealized_pnl = current_amount * 0.001 * ( current_price - open_avg_price @@ -503,17 +392,13 @@ class OpenBasedFifthStrategy: open_avg_price - current_price ) pnl_rate = ( - (current_price - open_avg_price) - / open_avg_price - * 100 + (current_price - open_avg_price) / open_avg_price * 100 if self.start == 1 - else (open_avg_price - current_price) - / open_avg_price - * 100 + else (open_avg_price - current_price) / open_avg_price * 100 ) direction_str = "空" if self.start == -1 else "多" msg = ( - f"【开盘价五分之一 {self.contract_symbol}】\n" + f"【五分之一 {self.contract_symbol}】\n" f"方向:{direction_str}\n" f"现价:{current_price:.2f}\n" f"开仓均价:{open_avg_price:.2f}\n" @@ -522,7 +407,7 @@ class OpenBasedFifthStrategy: ) else: msg = ( - f"【开盘价五分之一 {self.contract_symbol}】\n" + f"【五分之一 {self.contract_symbol}】\n" f"方向:无\n" f"现价:{current_price:.2f}\n" f"余额:{self.balance:.2f}" @@ -544,7 +429,7 @@ class OpenBasedFifthStrategy: time.sleep(2) self.click_safe('x://button[normalize-space(text()) ="市价"]') logger.info( - f"开盘价五分之一策略(3分钟K线)开始监测,间隔: {self.check_interval}秒" + f"五分之一策略(3分钟K线)开始监测,间隔: {self.check_interval}秒" ) last_report_time = 0 @@ -573,46 +458,9 @@ class OpenBasedFifthStrategy: curr = kline_data[-1] if not self.get_position_status(): logger.warning("获取仓位失败,使用缓存") - # 有仓位但 entry_price 未设置时(如程序重启),用开仓均价补全 - if self.start != 0 and self.entry_price is None and self.open_avg_price: - self.entry_price = float(self.open_avg_price) - logger.info(f"从API恢复 entry_price={self.entry_price:.2f}") - # 前1分30秒反手 - if self.start != 0: - first_dir, first_trigger = self.check_early_reverse_signal( - curr, kline_data - ) - if first_dir: - curr_kline_id = curr["id"] - if self.last_trade_kline_id != curr_kline_id: - balance = self.get_available_balance() - trade_size = (balance or 0) * self.risk_percent - if first_dir == "long" and self.start == -1: - self.平仓() - time.sleep(1) - self.开单(marketPriceLongOrder=1, size=trade_size) - elif first_dir == "short" and self.start == 1: - self.平仓() - time.sleep(1) - self.开单(marketPriceLongOrder=-1, size=trade_size) - self.early_reverse_executed = True - self.last_trade_kline_id = curr_kline_id - _, valid_prev = self.find_valid_prev_bar( - kline_data, len(kline_data) - 1 - ) - if valid_prev: - self.entry_prev_body = self.get_body_size(valid_prev) - self.entry_price = float(curr["close"]) - self.entry_kline_id = curr_kline_id - self.get_position_status() - self._send_position_message(curr) - time.sleep(self.check_interval) - continue - - # 常规信号检测 direction, trigger_price, valid_prev, curr_kline = ( - self.check_realtime_trigger(kline_data, self.start) + self.check_trigger(kline_data) ) if direction: @@ -658,10 +506,6 @@ class OpenBasedFifthStrategy: self.last_trigger_direction = direction if executed: self.last_trade_kline_id = curr_kline_id - self.entry_price = trigger_price - self.entry_prev_body = self.get_body_size(valid_prev) - self.entry_kline_id = curr_kline_id - self.early_reverse_executed = False self.get_position_status() self._send_position_message(curr_kline) last_report_time = time.time()