Files
lm_code/bitmart/四分之一_新反手策略.py
2026-02-05 16:10:53 +08:00

605 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import random
import time
from tqdm import tqdm
from loguru import logger
from bit_tools import openBrowser
from DrissionPage import ChromiumPage
from DrissionPage import ChromiumOptions
from bitmart.api_contract import APIContract
class BitmartFuturesTransaction:
def __init__(self, bit_id):
self.page: ChromiumPage | None = None
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
self.start = 0 # 持仓状态: -1 空, 0 无, 1 多
self.direction = None
self.pbar = tqdm(total=30, desc="等待K线", ncols=80)
self.last_kline_time = None # 上一次处理的K线时间戳用于判断是否是新K线
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
self.open_type = "cross" # 全仓模式
self.risk_percent = 0.01 # 每次开仓使用可用余额的 1%
self.open_avg_price = None # 开仓价格
self.current_amount = None # 持仓量
self.bit_id = bit_id
# 策略相关变量
self.prev_kline = None # 上一根K线
self.current_kline = None # 当前K线
self.prev_entity = None # 上一根K线实体大小
self.current_open = None # 当前K线开盘价
# 反手信号控制记录当前K线是否已执行过反手每根K线只执行一次
self.reverse_executed_kline_id = None # 已执行反手的K线时间戳
def get_klines(self):
"""获取最近2根K线当前K线和上一根K线"""
try:
end_time = int(time.time())
# 获取足够多的条目确保有最新的K线
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=5, # 5分钟
start_time=end_time - 3600 * 3, # 取最近3小时
end_time=end_time
)[0]["data"]
# 每根: [timestamp, open, high, low, close, volume]
formatted = []
for k in response:
formatted.append({
'id': int(k["timestamp"]),
'open': float(k["open_price"]),
'high': float(k["high_price"]),
'low': float(k["low_price"]),
'close': float(k["close_price"])
})
formatted.sort(key=lambda x: x['id'])
# 返回最近2根K线倒数第二根上一根和最后一根当前
if len(formatted) >= 2:
return formatted[-2], formatted[-1]
return None, None
except Exception as e:
logger.error(f"获取K线异常: {e}")
self.ding(text="获取K线异常", error=True)
return None, None
def get_current_price(self):
"""获取当前最新价格"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1分钟
start_time=end_time - 3600 * 1, # 取最近1小时
end_time=end_time
)[0]
if response['code'] == 1000:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用USDT余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
def get_position_status(self):
"""获取当前持仓方向"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
return True
self.start = 1 if positions[0]['position_type'] == 1 else -1
self.open_avg_price = float(positions[0]['open_avg_price'])
self.current_amount = positions[0]['current_amount']
self.position_cross = positions[0]["position_cross"]
return True
else:
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
def set_leverage(self):
"""程序启动时设置全仓 + 高杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def openBrowser(self):
"""打开 TGE 对应浏览器实例"""
try:
bit_port = openBrowser(id=self.bit_id)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
return True
except:
return False
def click_safe(self, xpath, sleep=0.5):
"""安全点击"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
# ele.scroll.to_see(center=True)
# time.sleep(sleep)
ele.click(by_js=True)
return True
except:
return False
def 平仓(self):
"""平仓操作"""
self.click_safe('x://span[normalize-space(text()) ="市价"]')
def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None):
"""
marketPriceLongOrder 市价做多或者做空1是做多-1是做空
limitPriceShortOrder 限价做多或者做空
"""
if marketPriceLongOrder == -1:
# self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.page.ele('x://*[@id="size_0"]').input(vals=size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif marketPriceLongOrder == 1:
# self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.page.ele('x://*[@id="size_0"]').input(vals=size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
if limitPriceShortOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif limitPriceShortOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
def ding(self, text, error=False):
"""日志通知"""
if error:
logger.error(text)
else:
logger.info(text)
def calculate_entity(self, kline):
"""计算K线实体大小绝对值"""
return abs(kline['close'] - kline['open'])
def calculate_upper_shadow(self, kline):
"""计算上影线涨幅百分比"""
# 上影线 = (最高价 - max(开盘价, 收盘价)) / max(开盘价, 收盘价)
body_top = max(kline['open'], kline['close'])
if body_top == 0:
return 0
return (kline['high'] - body_top) / body_top * 100
def calculate_lower_shadow(self, kline):
"""计算下影线跌幅百分比"""
# 下影线 = (min(开盘价, 收盘价) - 最低价) / min(开盘价, 收盘价)
body_bottom = min(kline['open'], kline['close'])
if body_bottom == 0:
return 0
return (body_bottom - kline['low']) / body_bottom * 100
def get_entity_edge(self, kline):
"""获取K线实体边收盘价或开盘价取决于是阳线还是阴线"""
# 阳线(收盘>开盘):实体上边=收盘价,实体下边=开盘价
# 阴线(收盘<开盘):实体上边=开盘价,实体下边=收盘价
return {
'upper': max(kline['open'], kline['close']), # 实体上边
'lower': min(kline['open'], kline['close']) # 实体下边
}
def check_signal(self, current_price, prev_kline, current_kline):
"""
检查交易信号
返回: ('long', trigger_price) / ('short', trigger_price) / None
"""
# 计算上一根K线实体
prev_entity = self.calculate_entity(prev_kline)
# 实体过小不交易(实体 < 0.1
if prev_entity < 0.1:
logger.info(f"上一根K线实体过小: {prev_entity:.4f},跳过信号检测")
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'] # 实体下边
# 计算触发价基于上一根K线实体位置
long_trigger = prev_entity_lower + prev_entity / 4 # 做多触发价 = 实体下边 + 实体/4下四分之一处
short_trigger = prev_entity_upper - 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
logger.info(f"当前价格: {current_price:.2f}, 上一根实体: {prev_entity:.4f}")
logger.info(f"上一根实体上边: {prev_entity_upper:.2f}, 下边: {prev_entity_lower:.2f}")
logger.info(f"做多触发价(下1/4): {long_trigger:.2f}, 做空触发价(上1/4): {short_trigger:.2f}")
if skip_short_by_upper_third:
logger.info("上一根阴线+当前阳线(做多形态),不按上四分之一做空")
if skip_long_by_lower_third:
logger.info("上一根阳线+当前阴线(做空形态),不按下四分之一做多")
# 无持仓时检查开仓信号(保留原有开仓逻辑)
if self.start == 0:
if current_price >= long_trigger and not skip_long_by_lower_third:
logger.info(f"触发做多信号!价格 {current_price:.2f} >= 触发价(下1/4) {long_trigger:.2f}")
return ('long', long_trigger)
elif current_price <= short_trigger and not skip_short_by_upper_third:
logger.info(f"触发做空信号!价格 {current_price:.2f} <= 触发价(上1/4) {short_trigger:.2f}")
return ('short', short_trigger)
# ========== 止盈/止损逻辑(四分之一触发价)==========
# 计算基于当前K线开盘价的止盈止损触发价
current_open = current_kline['open']
stop_long_trigger = current_open + prev_entity / 4 # 止盈做多触发价 = 当前开盘价 + 上一根实体/4
stop_short_trigger = current_open - prev_entity / 4 # 止损做空触发价 = 当前开盘价 - 上一根实体/4
logger.info(f"止盈止损触发价: 做多止盈={stop_long_trigger:.2f}, 做空止损={stop_short_trigger:.2f}")
# 持多仓时检查止损信号(只平仓,不反手)
if self.start == 1:
if current_price <= stop_short_trigger and not skip_short_by_upper_third:
logger.info(f"【止损平仓】持多仓, 价格 {current_price:.2f} <= 止损价 {stop_short_trigger:.2f}")
return ('close_long', stop_short_trigger)
# 持空仓时检查止盈信号(只平仓,不反手)
elif self.start == -1:
if current_price >= stop_long_trigger and not skip_long_by_lower_third:
logger.info(f"【止盈平仓】持空仓, 价格 {current_price:.2f} >= 止盈价 {stop_long_trigger:.2f}")
return ('close_short', stop_long_trigger)
# ========== 反手逻辑(影线条件)==========
# 检查当前K线是否已执行过反手每根K线只执行一次
current_kline_id = current_kline['id']
if self.reverse_executed_kline_id == current_kline_id:
logger.debug(f"当前K线 {current_kline_id} 已执行过反手,跳过反手检测")
return None
# 持多仓时检查反手做空信号
if self.start == 1:
# 反手条件: 上一根阳线 + 下影线>=0.1% + 当前先涨后跌到上一根最低价
if prev_is_bullish: # 上一根是阳线
lower_shadow_pct = self.calculate_lower_shadow(prev_kline)
current_went_up_first = current_kline['high'] > current_kline['open']
logger.debug(f"反手做空检测: 上一根阳线, 下影线跌幅={lower_shadow_pct:.4f}%, "
f"当前K线先涨过={current_went_up_first}, "
f"当前价格={current_price:.2f}, 上一根最低价={prev_kline['low']:.2f}")
if lower_shadow_pct >= 0.1 and current_went_up_first and current_price <= prev_kline['low']:
logger.info(f"【反手做空】上一根阳线, 下影线跌幅 {lower_shadow_pct:.4f}% >= 0.1%, "
f"当前K线先涨过, 价格 {current_price:.2f} <= 上一根最低价 {prev_kline['low']:.2f}")
return ('reverse_short', prev_kline['low'])
# 持空仓时检查反手做多信号
elif self.start == -1:
# 反手条件: 上一根阴线 + 上影线>=0.1% + 当前先跌后涨到上一根最高价
if prev_is_bearish: # 上一根是阴线
upper_shadow_pct = self.calculate_upper_shadow(prev_kline)
current_went_down_first = current_kline['low'] < current_kline['open']
logger.debug(f"反手做多检测: 上一根阴线, 上影线涨幅={upper_shadow_pct:.4f}%, "
f"当前K线先跌过={current_went_down_first}, "
f"当前价格={current_price:.2f}, 上一根最高价={prev_kline['high']:.2f}")
if upper_shadow_pct >= 0.1 and current_went_down_first and current_price >= prev_kline['high']:
logger.info(f"【反手做多】上一根阴线, 上影线涨幅 {upper_shadow_pct:.4f}% >= 0.1%, "
f"当前K线先跌过, 价格 {current_price:.2f} >= 上一根最高价 {prev_kline['high']:.2f}")
return ('reverse_long', prev_kline['high'])
return None
def verify_no_position(self, max_retries=5, retry_interval=3):
"""
验证当前无持仓
返回: True 表示无持仓可以开仓False 表示有持仓不能开仓
"""
for i in range(max_retries):
if self.get_position_status():
if self.start == 0:
logger.info(f"确认无持仓,可以开仓")
return True
else:
logger.warning(
f"仍有持仓 (方向: {self.start}),等待 {retry_interval} 秒后重试 ({i + 1}/{max_retries})")
time.sleep(retry_interval)
else:
logger.warning(f"查询持仓状态失败,等待 {retry_interval} 秒后重试 ({i + 1}/{max_retries})")
time.sleep(retry_interval)
logger.error(f"经过 {max_retries} 次重试仍有持仓或查询失败,放弃开仓")
return False
def verify_position_direction(self, expected_direction):
"""
验证当前持仓方向是否与预期一致
expected_direction: 1 多仓, -1 空仓
返回: True 表示持仓方向正确False 表示不正确
"""
if self.get_position_status():
if self.start == expected_direction:
logger.info(f"持仓方向验证成功: {self.start}")
return True
else:
logger.warning(f"持仓方向不符: 期望 {expected_direction}, 实际 {self.start}")
return False
else:
logger.error("查询持仓状态失败")
return False
def execute_trade(self, signal, current_kline_id, size=1):
"""执行交易"""
signal_type, trigger_price = signal
size = 25
if signal_type == 'long':
# 开多前先确认无持仓
logger.info(f"准备开多,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
if self.start != 0:
logger.warning(f"开多前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(f"确认无持仓,执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(1):
logger.success("开多成功")
return True
else:
logger.error("开多后持仓验证失败")
return False
elif signal_type == 'short':
# 开空前先确认无持仓
logger.info(f"准备开空,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
if self.start != 0:
logger.warning(f"开空前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(f"确认无持仓,执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(-1):
logger.success("开空成功")
return True
else:
logger.error("开空后持仓验证失败")
return False
elif signal_type == 'close_long':
# 止损平多仓(只平仓,不反手)
logger.info(f"执行止损平多仓,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(3) # 等待订单执行
# 验证平仓是否成功
if self.get_position_status() and self.start == 0:
logger.success("止损平多仓成功")
time.sleep(20) # 额外等待避免频繁交易
return True
else:
logger.error("止损平多仓后验证失败")
return False
elif signal_type == 'close_short':
# 止盈平空仓(只平仓,不反手)
logger.info(f"执行止盈平空仓,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(3) # 等待订单执行
# 验证平仓是否成功
if self.get_position_status() and self.start == 0:
logger.success("止盈平空仓成功")
time.sleep(20) # 额外等待避免频繁交易
return True
else:
logger.error("止盈平空仓后验证失败")
return False
elif signal_type == 'reverse_long':
# 平空 + 开多(反手做多)- 优化:平仓后立即开仓
logger.info(f"执行反手做多,触发价: {trigger_price:.2f}")
self.平仓()
# time.sleep(1) # 等待1秒让平仓订单提交并更新UI
# 立即执行开多,不等待平仓验证完成(市价单通常毫秒级成交)
logger.info("平仓已提交,立即执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(1):
logger.success("反手做多成功")
# 记录当前K线已执行反手
self.reverse_executed_kline_id = current_kline_id
time.sleep(20) # 额外等待避免频繁交易
return True
else:
logger.error("反手做多后持仓验证失败")
return False
elif signal_type == 'reverse_short':
# 平多 + 开空(反手做空)- 优化:平仓后立即开仓
logger.info(f"执行反手做空,触发价: {trigger_price:.2f}")
self.平仓()
# time.sleep(1) # 等待1秒让平仓订单提交并更新UI
# 立即执行开空,不等待平仓验证完成(市价单通常毫秒级成交)
logger.info("平仓已提交,立即执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(-1):
logger.success("反手做空成功")
# 记录当前K线已执行反手
self.reverse_executed_kline_id = current_kline_id
time.sleep(20) # 额外等待避免频繁交易
return True
else:
logger.error("反手做空后持仓验证失败")
return False
return False
def action(self):
"""主循环"""
logger.info("开始运行四分之一策略交易(新反手逻辑)...")
# 启动时设置全仓高杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,程序继续运行但可能下单失败")
return
page_start = True
while True:
if page_start:
# 打开浏览器
for i in range(5):
if self.openBrowser():
logger.info("浏览器打开成功")
break
else:
self.ding("打开浏览器失败!", error=True)
return
# 进入交易页面
self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
self.click_safe('x://button[normalize-space(text()) ="市价"]')
size = 25
self.page.ele('x://*[@id="size_0"]').input(vals=size, clear=True)
page_start = False
try:
# 1. 获取K线数据当前K线和上一根K线
prev_kline, current_kline = self.get_klines()
if not prev_kline or not current_kline:
logger.warning("获取K线失败等待重试...")
time.sleep(5)
continue
# 2. 获取当前价格
current_price = self.get_current_price()
if not current_price:
logger.warning("获取价格失败,等待重试...")
time.sleep(2)
continue
# 3. 每次循环都通过SDK获取真实持仓状态避免状态不同步导致双向持仓
if not self.get_position_status():
logger.warning("获取持仓状态失败,等待重试...")
time.sleep(2)
continue
logger.debug(f"当前持仓状态: {self.start} (0=无, 1=多, -1=空)")
# 4. 检查信号
signal = self.check_signal(current_price, prev_kline, current_kline)
# 5. 有信号则执行交易
if signal:
trade_success = self.execute_trade(signal, current_kline['id'], size=1)
if trade_success:
logger.success(f"交易执行完成: {signal[0]}, 当前持仓状态: {self.start}")
page_start = True
else:
logger.warning(f"交易执行失败或被阻止: {signal[0]}")
# 6. 短暂等待后继续循环同一根K线遇到信号就操作
time.sleep(0.5)
if page_start:
self.page.close()
time.sleep(25)
page_start = True
except KeyboardInterrupt:
logger.info("用户中断,程序退出")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(5)
if __name__ == '__main__':
BitmartFuturesTransaction(bit_id="f2320f57e24c45529a009e1541e25961").action()