Files
lm_code/open_fifth_strategy/main.py
2026-02-02 13:11:09 +08:00

537 lines
19 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.

# -*- coding: utf-8 -*-
"""
BitMart 基于开盘价的五分之一策略交易
策略规则1111
- 做多触发价 = 当前K线开盘价 + 实体/5
- 做空触发价 = 当前K线开盘价 - 实体/5
- 基于前一根有效K线实体 >= 0.1
执行:触及做多/做空触发价则开仓或反手同根K线只交易一次
运行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))
import random
import time
from concurrent.futures import ThreadPoolExecutor
from loguru import logger
from bit_tools import openBrowser
from DrissionPage import ChromiumPage
from DrissionPage import ChromiumOptions
from bitmart.api_contract import APIContract
from 交易.tools import send_dingtalk_message
from open_fifth_strategy.config import (
API_KEY,
SECRET_KEY,
MEMO,
CONTRACT_SYMBOL,
KLINE_STEP,
MIN_BODY_SIZE,
CHECK_INTERVAL,
LEVERAGE,
OPEN_TYPE,
RISK_PERCENT,
BIT_ID,
)
ding_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="dingtalk")
class OpenBasedFifthStrategy:
"""基于开盘价的五分之一策略"""
def __init__(self, bit_id=None):
self.page = None
self.api_key = API_KEY
self.secret_key = SECRET_KEY
self.memo = MEMO
self.contract_symbol = CONTRACT_SYMBOL
self.contractAPI = APIContract(
self.api_key, self.secret_key, self.memo, timeout=(5, 15)
)
self.start = 0 # 持仓: -1空, 0无, 1多
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
self.bit_id = bit_id or BIT_ID
self.min_body_size = MIN_BODY_SIZE
self.kline_step = KLINE_STEP
self.check_interval = CHECK_INTERVAL
self.leverage = LEVERAGE
self.open_type = OPEN_TYPE
self.risk_percent = RISK_PERCENT
self.last_trigger_kline_id = None
self.last_trigger_direction = None
self.last_trade_kline_id = None
# ==================== 策略核心 ====================
def get_body_size(self, candle):
return abs(float(candle["open"]) - float(candle["close"]))
def find_valid_prev_bar(self, all_data, current_idx):
"""找前一根有效K线实体>=min_body_size"""
if current_idx <= 0:
return None, None
for i in range(current_idx - 1, -1, -1):
prev = all_data[i]
if self.get_body_size(prev) >= self.min_body_size:
return i, prev
return None, None
def get_trigger_levels(self, prev, curr_open):
"""
基于当前K线开盘价计算触发价1111
做多触发 = 当前K线开盘价 + 实体/5
做空触发 = 当前K线开盘价 - 实体/5
"""
body = self.get_body_size(prev)
if body < 0.001:
return None, None
curr_o = float(curr_open)
return curr_o + body / 5, curr_o - body / 5
def get_1m_bars_for_3m_bar(self, bar_3m):
"""获取当前3分钟K线对应的3根1分钟K线"""
try:
start_ts = int(bar_3m["id"])
end_ts = start_ts + 3 * 60
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1,
start_time=start_ts,
end_time=end_ts,
)[0]
if response.get("code") != 1000:
return []
data = response.get("data", [])
out = []
for k in data:
out.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"]),
}
)
out.sort(key=lambda x: x["id"])
return out
except Exception as e:
logger.warning(f"获取1分钟K线失败: {e}")
return []
def determine_trigger_order_by_1m(self, bars_1m, long_trigger, short_trigger):
"""同根K线多空都触及时用1分钟K线判断先后"""
if not bars_1m:
return None
for bar in bars_1m:
high = float(bar["high"])
low = float(bar["low"])
open_price = float(bar["open"])
long_ok = high >= long_trigger
short_ok = low <= short_trigger
if long_ok and not short_ok:
return "long"
if short_ok and not long_ok:
return "short"
if long_ok and short_ok:
d_long = abs(long_trigger - open_price)
d_short = abs(short_trigger - open_price)
return "short" if d_short < d_long else "long"
return None
def check_trigger(self, kline_data):
"""
检测当前K线是否触发信号1111当前开盘价±实体/5
返回:(方向, 触发价, 有效前一根, 当前K线) 或 (None,...)
"""
if len(kline_data) < 2:
return None, None, None, None
curr = kline_data[-1]
curr_kline_id = curr["id"]
curr_open = curr["open"]
_, prev = self.find_valid_prev_bar(kline_data, len(kline_data) - 1)
if prev is None:
return None, None, None, None
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"])
long_triggered = c_high >= long_trigger
short_triggered = c_low <= short_trigger
direction = None
trigger_price = None
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
if (
self.last_trigger_kline_id == curr_kline_id
and self.last_trigger_direction == direction
):
return None, None, None, None
return direction, trigger_price, prev, curr
# ==================== BitMart API ====================
def get_klines(self):
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=self.kline_step,
start_time=end_time - 3600 * 3,
end_time=end_time,
)[0]["data"]
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"])
return formatted
except Exception as e:
if "429" in str(e) or "too many requests" in str(e).lower():
logger.warning(f"API限流等待60秒: {e}")
time.sleep(60)
else:
logger.error(f"获取K线异常: {e}")
self.ding("获取K线异常", error=True)
return None
def get_available_balance(self):
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))
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
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
return True
self.start = 1 if positions[0]["position_type"] == 1 else -1
self.open_avg_price = positions[0]["open_avg_price"]
self.current_amount = positions[0]["current_amount"]
self.position_cross = positions[0]["position_cross"]
return True
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
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
# ==================== 浏览器 ====================
def _open_browser(self):
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 Exception:
return False
def close_extra_tabs(self):
try:
for idx, tab in enumerate(self.page.get_tabs()):
if idx > 0:
tab.close()
return True
except Exception:
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 Exception:
return False
def 平仓(self):
logger.info("执行平仓...")
self.click_safe('x://span[normalize-space(text()) ="市价"]')
time.sleep(0.5)
self.ding("执行平仓操作")
def 开单(self, marketPriceLongOrder=0, size=None):
if size is None or size <= 0:
logger.warning("开单金额无效")
return False
direction_str = "做多" if marketPriceLongOrder == 1 else "做空"
logger.info(f"执行{direction_str},金额: {size}")
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))
if marketPriceLongOrder == -1:
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
else:
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
self.ding(f"执行{direction_str},金额: {size}")
return True
except Exception as e:
logger.error(f"开单异常: {e}")
return False
def ding(self, msg, error=False):
prefix = "❌五分之一:" if error else "🔔五分之一:"
full_msg = f"{prefix}{msg}"
if error:
logger.error(msg)
for _ in range(3):
ding_executor.submit(self._send_ding_safe, full_msg)
else:
logger.info(msg)
ding_executor.submit(self._send_ding_safe, full_msg)
def _send_ding_safe(self, msg):
try:
send_dingtalk_message(msg)
except Exception as e:
logger.warning(f"消息发送失败: {e}")
def _send_position_message(self, latest_kline):
current_price = float(latest_kline["close"])
balance = self.get_available_balance()
self.balance = balance if balance is not None else 0.0
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)
if self.start == 1:
unrealized_pnl = current_amount * 0.001 * (
current_price - open_avg_price
)
else:
unrealized_pnl = current_amount * 0.001 * (
open_avg_price - current_price
)
pnl_rate = (
(current_price - open_avg_price) / open_avg_price * 100
if self.start == 1
else (open_avg_price - current_price) / open_avg_price * 100
)
direction_str = "" if self.start == -1 else ""
msg = (
f"【五分之一 {self.contract_symbol}\n"
f"方向:{direction_str}\n"
f"现价:{current_price:.2f}\n"
f"开仓均价:{open_avg_price:.2f}\n"
f"浮动盈亏:{unrealized_pnl:+.2f} USDT ({pnl_rate:+.2f}%)\n"
f"余额:{self.balance:.2f}"
)
else:
msg = (
f"【五分之一 {self.contract_symbol}\n"
f"方向:无\n"
f"现价:{current_price:.2f}\n"
f"余额:{self.balance:.2f}"
)
self.ding(msg)
# ==================== 主循环 ====================
def action(self):
if not self.set_leverage():
logger.error("杠杆设置失败")
return
if not self._open_browser():
self.ding("打开浏览器失败!", error=True)
return
logger.info("浏览器打开成功")
self.close_extra_tabs()
self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
time.sleep(2)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
logger.info(
f"五分之一策略3分钟K线开始监测间隔: {self.check_interval}"
)
last_report_time = 0
report_interval = 300
while True:
for _ in range(5):
if self._open_browser():
break
time.sleep(5)
else:
self.ding("打开浏览器失败!", error=True)
return
self.close_extra_tabs()
self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
time.sleep(2)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
try:
kline_data = self.get_klines()
if not kline_data or len(kline_data) < 3:
logger.warning("K线数据不足...")
time.sleep(self.check_interval)
continue
curr = kline_data[-1]
if not self.get_position_status():
logger.warning("获取仓位失败,使用缓存")
direction, trigger_price, valid_prev, curr_kline = (
self.check_trigger(kline_data)
)
if direction:
curr_kline_id = curr_kline["id"]
if self.last_trade_kline_id == curr_kline_id:
self.last_trigger_kline_id = curr_kline_id
self.last_trigger_direction = direction
time.sleep(self.check_interval)
continue
if (direction == "long" and self.start == 1) or (
direction == "short" and self.start == -1
):
self.last_trigger_kline_id = curr_kline_id
self.last_trigger_direction = direction
time.sleep(self.check_interval)
continue
balance = self.get_available_balance()
trade_size = (balance or 0) * self.risk_percent
executed = False
if direction == "long":
if self.start == -1:
self.平仓()
time.sleep(1)
self.开单(marketPriceLongOrder=1, size=trade_size)
executed = True
elif self.start == 0:
self.开单(marketPriceLongOrder=1, size=trade_size)
executed = True
elif direction == "short":
if self.start == 1:
self.平仓()
time.sleep(1)
self.开单(marketPriceLongOrder=-1, size=trade_size)
executed = True
elif self.start == 0:
self.开单(marketPriceLongOrder=-1, size=trade_size)
executed = True
self.last_trigger_kline_id = curr_kline_id
self.last_trigger_direction = direction
if executed:
self.last_trade_kline_id = curr_kline_id
self.get_position_status()
self._send_position_message(curr_kline)
last_report_time = time.time()
if time.time() - last_report_time >= report_interval:
if self.get_position_status():
self._send_position_message(kline_data[-1])
last_report_time = time.time()
time.sleep(self.check_interval)
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(self.check_interval)
time.sleep(3)
if random.randint(1, 10) > 7:
self.page.close()
time.sleep(15)
if __name__ == "__main__":
try:
OpenBasedFifthStrategy(bit_id=BIT_ID).action()
except KeyboardInterrupt:
logger.info("程序被用户中断")
finally:
ding_executor.shutdown(wait=True)
logger.info("已退出")