Files
lm_code/open_fifth_strategy/main.py

537 lines
19 KiB
Python
Raw Normal View History

2026-02-02 10:55:54 +08:00
# -*- coding: utf-8 -*-
"""
BitMart 基于开盘价的五分之一策略交易
2026-02-02 13:11:09 +08:00
策略规则1111
- 做多触发价 = 当前K线开盘价 + 实体/5
- 做空触发价 = 当前K线开盘价 - 实体/5
- 基于前一根有效K线实体 >= 0.1
执行触及做多/做空触发价则开仓或反手同根K线只交易一次
运行python open_fifth_strategy/main.py在项目根目录 lm_code
2026-02-02 10:55:54 +08:00
"""
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
2026-02-02 13:11:09 +08:00
# ==================== 策略核心 ====================
2026-02-02 10:55:54 +08:00
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
2026-02-02 13:11:09 +08:00
def get_trigger_levels(self, prev, curr_open):
2026-02-02 10:55:54 +08:00
"""
2026-02-02 13:11:09 +08:00
基于当前K线开盘价计算触发价1111
2026-02-02 10:55:54 +08:00
做多触发 = 当前K线开盘价 + 实体/5
做空触发 = 当前K线开盘价 - 实体/5
"""
body = self.get_body_size(prev)
if body < 0.001:
return None, None
curr_o = float(curr_open)
2026-02-02 13:11:09 +08:00
return curr_o + body / 5, curr_o - body / 5
2026-02-02 10:55:54 +08:00
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):
2026-02-02 13:11:09 +08:00
"""同根K线多空都触及时用1分钟K线判断先后"""
2026-02-02 10:55:54 +08:00
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
2026-02-02 13:11:09 +08:00
def check_trigger(self, kline_data):
2026-02-02 10:55:54 +08:00
"""
2026-02-02 13:11:09 +08:00
检测当前K线是否触发信号1111当前开盘价±实体/5
2026-02-02 10:55:54 +08:00
返回(方向, 触发价, 有效前一根, 当前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"]
2026-02-02 13:11:09 +08:00
_, prev = self.find_valid_prev_bar(kline_data, len(kline_data) - 1)
2026-02-02 10:55:54 +08:00
if prev is None:
return None, None, None, None
2026-02-02 13:11:09 +08:00
long_trigger, short_trigger = self.get_trigger_levels(prev, curr_open)
2026-02-02 10:55:54 +08:00
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
2026-02-02 13:11:09 +08:00
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
2026-02-02 10:55:54 +08:00
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}")
2026-02-02 13:11:09 +08:00
size = max(1, min(25, int(size)))
2026-02-02 10:55:54 +08:00
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):
2026-02-02 13:11:09 +08:00
prefix = "❌五分之一:" if error else "🔔五分之一:"
2026-02-02 10:55:54 +08:00
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)
2026-02-02 13:11:09 +08:00
position_cross = float(getattr(self, "position_cross", 0) or 0)
2026-02-02 10:55:54 +08:00
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 = (
2026-02-02 13:11:09 +08:00
(current_price - open_avg_price) / open_avg_price * 100
2026-02-02 10:55:54 +08:00
if self.start == 1
2026-02-02 13:11:09 +08:00
else (open_avg_price - current_price) / open_avg_price * 100
2026-02-02 10:55:54 +08:00
)
direction_str = "" if self.start == -1 else ""
msg = (
2026-02-02 13:11:09 +08:00
f"【五分之一 {self.contract_symbol}\n"
2026-02-02 10:55:54 +08:00
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 = (
2026-02-02 13:11:09 +08:00
f"【五分之一 {self.contract_symbol}\n"
2026-02-02 10:55:54 +08:00
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(
2026-02-02 13:11:09 +08:00
f"五分之一策略3分钟K线开始监测间隔: {self.check_interval}"
2026-02-02 10:55:54 +08:00
)
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 = (
2026-02-02 13:11:09 +08:00
self.check_trigger(kline_data)
2026-02-02 10:55:54 +08:00
)
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("已退出")