优化交易代码
This commit is contained in:
@@ -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、合约、杠杆等参数。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user