优化完美代码,优化计算盈亏

This commit is contained in:
27942
2026-02-07 20:55:36 +08:00
parent a9af0d5dfb
commit 6b19439f87

View File

@@ -1,121 +1,13 @@
import sys
import time
from datetime import datetime
from collections import deque
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
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.layout import Layout
from rich.align import Align
from rich.text import Text
from rich.live import Live
# 是否使用 Rich 仪表盘(否则用 loguru 普通输出)
USE_RICH_DASHBOARD = True
console = Console()
# 颜色标签loguru 用,无 Rich 时)
_R = "\033[0m"
_B = "\033[1m"
_C = "\033[36m"
_Y = "\033[33m"
_G = "\033[32m"
_M = "\033[90m"
_W = "\033[97m"
def _tag(t: str, color: str) -> str:
return color + "[" + t + "]" + _R + " "
LOG_PRICE = _tag("价格", _C)
LOG_SIGNAL = _tag("信号", _Y)
LOG_POSITION = _tag("仓位", _G)
LOG_SYSTEM = _tag("系统", _M)
if not USE_RICH_DASHBOARD:
logger.remove()
logger.add(sys.stderr, format="\033[2m│\033[0m {message}", colorize=False, level="INFO")
def log_kline_header(kline_id):
"""新 K 线分块头(仅非 Rich 模式使用)"""
s = f" K 线 {kline_id} "
width = max(52, len(s) + 4)
line = "" * (width - 2)
pad_left = (width - 2 - len(s)) // 2
pad_right = width - 2 - len(s) - pad_left
logger.info(_M + "" + line + "" + _R)
logger.info(_M + "" + _R + " " * pad_left + _B + _W + s + _R + " " * pad_right + _M + "" + _R)
logger.info(_M + "" + line + "" + _R)
# ---------- Rich 仪表盘 ----------
def make_header() -> Panel:
title = Text("四分之一策略 · ETHUSDT 5m", style="bold cyan")
subtitle = Text(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), style="dim")
return Panel(Align.center(title + Text("\n") + subtitle), border_style="cyan")
def make_metrics_panel(state: dict) -> Panel:
t = Table(show_header=True, header_style="bold magenta", expand=True)
t.add_column("指标", style="cyan")
t.add_column("数值", justify="right", style="green")
t.add_row("当前价", f"{state.get('price', 0):.2f}")
pos_map = {0: "", 1: "", -1: ""}
t.add_row("持仓", pos_map.get(state.get("position", 0), "?"))
t.add_row("K线 ID", str(state.get("kline_id", "-")))
t.add_row("做多触发", f"{state.get('long_trigger', 0):.2f}")
t.add_row("做空触发", f"{state.get('short_trigger', 0):.2f}")
t.add_row("突破做多", f"{state.get('long_breakout', 0):.2f}")
t.add_row("突破做空", f"{state.get('short_breakout', 0):.2f}")
ema10 = state.get("ema10")
ema20 = state.get("ema20")
atr14 = state.get("atr14")
t.add_row("EMA10", f"{ema10:.2f}" if ema10 is not None else "-")
t.add_row("EMA20", f"{ema20:.2f}" if ema20 is not None else "-")
t.add_row("ATR14", f"{atr14:.2f}" if atr14 is not None else "-")
pnl = state.get("unrealized_pnl")
t.add_row("未实现盈亏", f"{pnl:.2f}$" if pnl is not None else "-")
return Panel(t, title="[bold]价格 / 指标[/bold]", border_style="magenta")
def make_logs_panel(logs: list) -> Panel:
recent = list(logs)[-16:] if logs else []
body = "\n".join(recent) if recent else "等待日志..."
text = Text.from_ansi(body) if body else Text("等待日志...", style="dim")
return Panel(text, title="[bold]状态 / 日志[/bold]", border_style="green")
def make_footer(msg: str = "Ctrl+C 退出") -> Panel:
return Panel(Align.center(Text(msg, style="bold yellow")), border_style="yellow")
def build_dashboard_layout(state: dict, logs: list) -> Layout:
layout = Layout()
layout.split_column(
Layout(make_header(), name="header", size=5),
Layout(name="body", ratio=1),
Layout(make_footer(), name="footer", size=3),
)
layout["body"].split_row(
Layout(make_metrics_panel(state), name="left", ratio=1),
Layout(make_logs_panel(logs), name="right", ratio=2),
)
return layout
# ---------- 穿越触发(避免“已在阈值下方/上方”的追单) ----------
def cross_up(prev_price: float, curr_price: float, level: float) -> bool:
"""上一轮 < 阈值 且 这一轮 >= 阈值"""
return prev_price < level <= curr_price
def cross_down(prev_price: float, curr_price: float, level: float) -> bool:
"""上一轮 > 阈值 且 这一轮 <= 阈值"""
return prev_price > level >= curr_price
class BitmartFuturesTransaction:
def __init__(self, bit_id):
@@ -136,27 +28,12 @@ class BitmartFuturesTransaction:
self.last_kline_time = None # 上一次处理的K线时间戳用于判断是否是新K线
# 反手过滤(仅价差,无冷却)
self.reverse_min_move_pct = 0.05 # 反手最小价差过滤(百分比)
self.last_open_time = None # 上次开仓时间(用于最短持仓时间判断)
self.last_open_kline_id = None # 上次开仓所在 K 线 id仅记录不限制单 K 线开仓次数)
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
self.open_type = "cross" # 全仓模式
self.risk_percent = 0 # 未使用;若启用则可为每次开仓占可用余额的百分比
# 退出EMA10 快退出 + EMA20 强制平(可独立开关)+ 自开仓以来 ATR 追踪
self.use_ema_atr_exit = True # 是否启用 EMA/ATR 平仓规则(多单+空单)
self.atr_multiplier = 1.1 # 追踪止盈:自开仓以来最高/最低回撤/反弹 ≥ 此倍数×ATR(14) 则平仓
self.use_ema20_entry_filter = True # 开仓/反手方向过滤:价在 EMA20 同侧才允许
self.use_ema20_force_exit = True # 持仓强制平仓:价穿越 EMA20 则平(与入场过滤解耦)
self.min_hold_seconds = 90 # 开仓/反手后至少持仓此时长才允许技术性止盈EMA10/EMA20/ATR
self.exit_use_last_close = True # True=用已收盘K线 last_close 判断 EMA 破位防针洗False=用当前价
# 自开仓以来的最高/最低(真正的 ATR 追踪,不随换线重置)
self._highest_since_entry = None
self._lowest_since_entry = None
self._last_exit_kline_id = None # 当前K线出场后本K线内不再开仓等下一根K线再判断
self._prev_price = None # 上一轮轮询价,用于穿越触发
self.trailing_activation_usd = 2 # 盈利达到此金额后启动移动止损
self.trailing_distance_usd = 1.5 # 从最高盈利回撤此金额则平仓
self.max_unrealized_pnl_seen = None # 持仓期间见过的最大盈利(用于移动止损)
self.open_avg_price = None # 开仓价格
self.current_amount = None # 持仓量
@@ -164,27 +41,11 @@ class BitmartFuturesTransaction:
self.bit_id = bit_id
self.default_order_size = 25 # 开仓/反手张数,统一在此修改
# 实体过滤ETH 上 0.1 几乎无效,改为百分比或 ATR。二选一entity_pct 或 entity_atr_ratio
self.entity_filter_mode = "atr" # "pct" | "atr"
self.min_entity_pct = 0.02 # entity/close < 0.02% 视为实体过小(当 mode=pct 时)
self.entity_atr_ratio = 0.2 # entity < 0.2*ATR(14) 视为实体过小(当 mode=atr 时)
# 上/下影线反手阈值(原 0.01% 太小,几乎根根满足)
self.min_upper_shadow_pct = 0.05 # 上影线比例 > 此值才允许“上影线反手”
self.min_lower_shadow_pct = 0.05 # 下影线比例 > 此值才允许“下影线反手”
# 跳空/开盘后延迟:当前 K 线开始后经过此秒数才允许触发0=不延迟,可设 30 等防开盘即触发)
self.open_trigger_delay_seconds = 0
# 策略相关变量
self.prev_kline = None
self.current_kline = None
self.prev_entity = None
self.current_open = None
# Rich 仪表盘:状态与日志(供 build_dashboard_layout 使用)
self._display_state = {}
self._display_logs = deque(maxlen=120)
self._display_triggers = {}
self._last_signal_log_kline_id = None
self._log_throttle_at = {}
self.prev_kline = None # 上一根K线
self.current_kline = None # 当前K线
self.prev_entity = None # 上一根K线实体大小
self.current_open = None # 当前K线开盘价
def get_klines(self):
"""获取最近2根K线当前K线和上一根K线"""
@@ -219,79 +80,6 @@ class BitmartFuturesTransaction:
self.ding(text="获取K线异常", error=True)
return None, None
def get_klines_series(self, count=35):
"""获取最近 count 根 5 分钟 K 线(用于 EMA/ATR按时间正序。最后一根为当前未收盘 K 线。"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=5,
start_time=end_time - 3600 * 4,
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[-count:] if len(formatted) >= count else formatted
except Exception as e:
logger.error(f"获取K线序列异常: {e}")
return []
@staticmethod
def _ema(series, period):
"""对序列 series从旧到新计算 EMA(period),返回最后一个 EMA 值。"""
if not series or len(series) < period:
return None
alpha = 2.0 / (period + 1)
ema = sum(series[:period]) / period # 用前 period 根收盘的 SMA 做种子
for i in range(period, len(series)):
ema = alpha * series[i] + (1 - alpha) * ema
return ema
@staticmethod
def _atr(klines, period=14):
"""对 klines从旧到新每项含 high/low/close计算 ATR(period),返回最后一个 ATR 值。"""
if not klines or len(klines) < period + 1:
return None
tr_list = []
for i in range(1, len(klines)):
high, low = klines[i]['high'], klines[i]['low']
prev_close = klines[i - 1]['close']
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
tr_list.append(tr)
if len(tr_list) < period:
return None
# ATR = EMA(TR, period)
alpha = 2.0 / (period + 1)
atr = sum(tr_list[:period]) / period
for i in range(period, len(tr_list)):
atr = alpha * tr_list[i] + (1 - alpha) * atr
return atr
def get_ema_atr_for_exit(self, kline_series):
"""
基于已收盘 K 线计算 EMA10、EMA20、ATR(14)、last_close。
kline_series 最后一根可为当前未收盘 K 线,计算时用倒数第 2~N 根作为已收盘。
返回 dict: ema10, ema20, atr14, last_close已收盘的最后一根 close用于防针洗
"""
if not kline_series or len(kline_series) < 21:
return {"ema10": None, "ema20": None, "atr14": None, "last_close": None}
# 用除最后一根外的已收盘 K 线(若只有 21 根则用前 20 根)
closed = kline_series[:-1] if len(kline_series) >= 21 else kline_series[:20]
closes = [k['close'] for k in closed]
ema10 = self._ema(closes, 10)
ema20 = self._ema(closes, 20)
atr14 = self._atr(closed, 14)
last_close = closed[-1]['close'] if closed else None
return {"ema10": ema10, "ema20": ema20, "atr14": atr14, "last_close": last_close}
def get_current_price(self):
"""获取当前最新价格"""
try:
@@ -442,15 +230,6 @@ class BitmartFuturesTransaction:
else:
logger.info(text)
def _log_throttled(self, key, text, interval=1.0, level="info"):
"""同类日志限频,避免 0.1 秒循环刷屏。"""
now = time.time()
last = self._log_throttle_at.get(key, 0)
if now - last < interval:
return
self._log_throttle_at[key] = now
getattr(logger, level)(text)
def calculate_entity(self, kline):
"""计算K线实体大小绝对值"""
return abs(kline['close'] - kline['open'])
@@ -480,189 +259,109 @@ class BitmartFuturesTransaction:
'lower': min(kline['open'], kline['close']) # 实体下边
}
@staticmethod
def quarter_levels(prev_o, prev_h, prev_l, prev_c, curr_open):
"""计算四分之一价位(含跳空修正)。返回 dict: entity, upper, lower, long_trigger, short_trigger, breakout_long, breakout_short"""
entity = abs(prev_c - prev_o)
upper = max(prev_o, prev_c)
lower = min(prev_o, prev_c)
gap_up = (prev_c > prev_o) and (curr_open > prev_c)
gap_down = (prev_c < prev_o) and (curr_open < prev_c)
if gap_up or gap_down:
base = curr_open
long_trigger = base + entity / 4
short_trigger = base - entity / 4
breakout_long = long_trigger
breakout_short = short_trigger
else:
long_trigger = lower + entity / 4
short_trigger = upper - entity / 4
breakout_long = upper + entity / 4
breakout_short = lower - entity / 4
return {
"entity": entity,
"upper": upper,
"lower": lower,
"long_trigger": long_trigger,
"short_trigger": short_trigger,
"breakout_long": breakout_long,
"breakout_short": breakout_short,
}
def _entity_filter_passed(self, entity: float, prev_close: float, atr14: float | None) -> bool:
"""实体过滤False 表示实体过小,不交易。"""
if self.entity_filter_mode == "pct":
if prev_close <= 0:
return False
return (entity / prev_close * 100) >= self.min_entity_pct
# atr
if atr14 is None or atr14 <= 0:
return entity >= 0.1 # 回退:无 ATR 时用固定 0.1
return entity >= self.entity_atr_ratio * atr14
def _allow_entry_by_ema20(self, side: int, curr_price: float, ema20: float | None) -> bool:
"""开仓/反手方向过滤:多单要求价>EMA20空单要求价<EMA20"""
if not self.use_ema20_entry_filter or ema20 is None:
return True
return (curr_price > ema20) if side == 1 else (curr_price < ema20)
def check_signal(self, current_price, prev_kline, current_kline, levels: dict, ind: dict):
def check_signal(self, current_price, prev_kline, current_kline):
"""
检查交易信号(穿越触发 + 形态过滤 + 影线反手阈值提高)。
返回: ('long'|'short'|'reverse_long'|'reverse_short', trigger_price) None
检查交易信号
返回: ('long', trigger_price) / ('short', trigger_price) / None
"""
current_kline_id = current_kline.get('id')
should_log_snapshot = self._last_signal_log_kline_id != current_kline_id
prev_price = self._prev_price
if prev_price is 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
prev_entity = levels["entity"]
prev_entity_upper = levels["upper"]
prev_entity_lower = levels["lower"]
long_trigger = levels["long_trigger"]
short_trigger = levels["short_trigger"]
breakout_long = levels["breakout_long"]
# 获取上一根K线的实体上下边
prev_entity_edge = self.get_entity_edge(prev_kline)
prev_entity_upper = prev_entity_edge['upper'] # 实体上边
prev_entity_lower = prev_entity_edge['lower'] # 实体下边
# 形态过滤
# 优化:以下两种情况以当前这根的开盘价作为计算基准
# 1) 上一根阳线 且 当前开盘价 > 上一根收盘价(跳空高开)
# 2) 上一根阴线 且 当前开盘价 < 上一根收盘价(跳空低开)
prev_is_bullish_for_calc = prev_kline['close'] > prev_kline['open']
prev_is_bearish_for_calc = prev_kline['close'] < prev_kline['open']
current_open_above_prev_close = current_kline['open'] > prev_kline['close']
current_open_below_prev_close = current_kline['open'] < prev_kline['close']
use_current_open_as_base = (prev_is_bullish_for_calc and current_open_above_prev_close) or (prev_is_bearish_for_calc and current_open_below_prev_close)
if use_current_open_as_base:
# 以当前K线开盘价为基准计算跳空时用当前开盘价参与计算
calc_lower = current_kline['open']
calc_upper = current_kline['open'] # 同一基准,上下四分之一对称
long_trigger = calc_lower + prev_entity / 4
short_trigger = calc_upper - prev_entity / 4
long_breakout = calc_upper + prev_entity / 4
short_breakout = calc_lower - prev_entity / 4
else:
# 原有计算方式
long_trigger = prev_entity_lower + prev_entity / 4 # 做多触发价 = 实体下边 + 实体/4下四分之一处
short_trigger = prev_entity_upper - prev_entity / 4 # 做空触发价 = 实体上边 - 实体/4上四分之一处
long_breakout = prev_entity_upper + prev_entity / 4 # 做多突破价 = 实体上边 + 实体/4
short_breakout = prev_entity_lower - 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
if should_log_snapshot:
logger.info(LOG_PRICE + f"当前价: {current_price:.2f} 上一轮价: {prev_price:.2f} | 实体: {prev_entity:.4f} | 上边: {prev_entity_upper:.2f} 下边: {prev_entity_lower:.2f}")
logger.info(LOG_PRICE + f"做多触发: {long_trigger:.2f} | 做空触发: {short_trigger:.2f} | 突破做多: {breakout_long:.2f}")
if skip_short_by_upper_third:
logger.info(LOG_PRICE + "上一根阴+当前阳(做多形态)不按上1/4做空")
if skip_long_by_lower_third:
logger.info(LOG_PRICE + "上一根阳+当前阴(做空形态)不按下1/4做多")
self._last_signal_log_kline_id = current_kline_id
if use_current_open_as_base:
if prev_is_bullish_for_calc and current_open_above_prev_close:
logger.info(f"上一根阳线且当前开盘价({current_kline['open']:.2f})>上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
else:
logger.info(f"上一根阴线且当前开盘价({current_kline['open']:.2f})<上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
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}")
logger.info(f"突破做多价(上1/4外): {long_breakout:.2f}, 突破做空价(下1/4外): {short_breakout:.2f}")
if skip_short_by_upper_third:
logger.info("上一根阴线+当前阳线(做多形态),不按上四分之一做空")
if skip_long_by_lower_third:
logger.info("上一根阳线+当前阴线(做空形态),不按下四分之一做多")
self._display_triggers = {
"long_trigger": long_trigger, "short_trigger": short_trigger,
"long_breakout": breakout_long, "short_breakout": levels["breakout_short"]
}
ema20 = ind.get("ema20")
# 无持仓:穿越触发
# 无持仓时检查开仓信号
if self.start == 0:
if cross_up(prev_price, current_price, breakout_long) and not skip_long_by_lower_third:
if self._allow_entry_by_ema20(1, current_price, ema20):
logger.info(LOG_SIGNAL + f"穿越做多 | 上一轮 {prev_price:.2f} < 突破价 {breakout_long:.2f} <= 当前 {current_price:.2f}")
return ('long', breakout_long)
if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third:
if self._allow_entry_by_ema20(-1, current_price, ema20):
logger.info(LOG_SIGNAL + f"穿越做空 | 上一轮 {prev_price:.2f} > 做空触发 {short_trigger:.2f} >= 当前 {current_price:.2f}")
return ('short', short_trigger)
return None
if current_price >= long_breakout and not skip_long_by_lower_third:
logger.info(f"触发做多信号!价格 {current_price:.2f} >= 突破价(上1/4外) {long_breakout:.2f}")
return ('long', long_breakout)
elif current_price <= short_breakout and not skip_short_by_upper_third:
logger.info(f"触发做空信号!价格 {current_price:.2f} <= 突破价(下1/4外) {short_breakout:.2f}")
return ('short', short_breakout)
# 持多 → 反手空
if self.start == 1:
if cross_down(prev_price, current_price, short_trigger) and not skip_short_by_upper_third:
if self._pct_move(current_price, short_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(-1, current_price, ema20):
logger.info(LOG_SIGNAL + f"持多→穿越反手做空 | 触发价 {short_trigger:.2f}")
return ('reverse_short', short_trigger)
# 持仓时检查反手信号
elif self.start == 1: # 持多仓
# 反手条件1: 价格跌到上一根K线的上三分之一处做空触发价上一根阴线+当前阳线做多时跳过
if current_price <= short_trigger and not skip_short_by_upper_third:
logger.info(f"持多反手做空!价格 {current_price:.2f} <= 触发价(上1/4) {short_trigger:.2f}")
return ('reverse_short', short_trigger)
# 反手条件2: 上一根K线上阴线涨幅>0.01%,当前跌到上一根实体下边
upper_shadow_pct = self.calculate_upper_shadow(prev_kline)
if upper_shadow_pct > self.min_upper_shadow_pct and cross_down(prev_price, current_price, prev_entity_lower):
if self._allow_entry_by_ema20(-1, current_price, ema20):
logger.info(LOG_SIGNAL + f"持多→反手做空 | 上影线 {upper_shadow_pct:.2f}% 穿越实体下边 {prev_entity_lower:.2f}")
return ('reverse_short', prev_entity_lower)
return None
if upper_shadow_pct > 0.01 and current_price <= prev_entity_lower:
logger.info(f"持多反手做空!上阴线涨幅 {upper_shadow_pct:.4f}% > 0.01%"
f"价格 {current_price:.2f} <= 实体下边 {prev_entity_lower:.2f}")
return ('reverse_short', prev_entity_lower)
# 持空 → 反手多
if self.start == -1:
if cross_up(prev_price, current_price, long_trigger) and not skip_long_by_lower_third:
if self._pct_move(current_price, long_trigger) >= self.reverse_min_move_pct and self._allow_entry_by_ema20(1, current_price, ema20):
logger.info(LOG_SIGNAL + f"持空→穿越反手做多 | 触发价 {long_trigger:.2f}")
return ('reverse_long', long_trigger)
elif self.start == -1: # 持空仓
# 反手条件1: 价格涨到上一根K线的下三分之一处做多触发价上一根阳线+当前阴线做空时跳过
if current_price >= long_trigger and not skip_long_by_lower_third:
logger.info(f"持空反手做多!价格 {current_price:.2f} >= 触发价(下1/4) {long_trigger:.2f}")
return ('reverse_long', long_trigger)
# 反手条件2: 上一根K线下阴线跌幅>0.01%,当前涨到上一根实体上边
lower_shadow_pct = self.calculate_lower_shadow(prev_kline)
if lower_shadow_pct > self.min_lower_shadow_pct and cross_up(prev_price, current_price, prev_entity_upper):
if self._allow_entry_by_ema20(1, current_price, ema20):
logger.info(LOG_SIGNAL + f"持空→反手做多 | 下影线 {lower_shadow_pct:.2f}% 穿越实体上边 {prev_entity_upper:.2f}")
return ('reverse_long', prev_entity_upper)
return None
if lower_shadow_pct > 0.01 and current_price >= prev_entity_upper:
logger.info(f"持空反手做多!下阴线跌幅 {lower_shadow_pct:.4f}% > 0.01%"
f"价格 {current_price:.2f} >= 实体上边 {prev_entity_upper:.2f}")
return ('reverse_long', prev_entity_upper)
return None
def _pct_move(self, curr_price: float, trigger_price: float) -> float:
"""当前价相对触发价的变动百分比"""
if not trigger_price or trigger_price <= 0:
return 0.0
return abs(curr_price - trigger_price) / trigger_price * 100.0
def can_open(self, current_kline_id):
"""开仓前过滤(已删除开仓冷却,保留接口便于后续扩展)。"""
return True
def can_reverse(self, current_price, trigger_price):
"""反手前过滤:仅最小价差(与 check_signal 内已做重复校验,保留双保险)"""
if trigger_price and trigger_price > 0:
move_pct = self._pct_move(current_price, trigger_price)
if move_pct < self.reverse_min_move_pct:
self._log_throttled("reverse_move_small", LOG_SYSTEM + f"反手价差不足: {move_pct:.4f}% < {self.reverse_min_move_pct}%", interval=1.0)
return False
return True
def should_exit(self, current_price: float, kline_id: int, ind: dict, now_ts: float):
"""
是否应技术性平仓。使用 last_close 防针洗(可选)。
返回 (True, reason_str) 或 (False, None)。
"""
if self.start == 0 or not self.use_ema_atr_exit:
return False, None
if not self.last_open_time or (now_ts - self.last_open_time) < self.min_hold_seconds:
return False, None
ema10 = ind.get("ema10")
ema20 = ind.get("ema20")
atr14 = ind.get("atr14")
last_close = ind.get("last_close")
ref_price = last_close if (self.exit_use_last_close and last_close is not None) else current_price
if self.start == 1:
if ema10 is not None and ref_price < ema10:
return True, "EXIT_LONG_EMA10"
if self.use_ema20_force_exit and ema20 is not None and ref_price < ema20:
return True, "EXIT_LONG_EMA20"
if atr14 is not None and self._highest_since_entry is not None:
dd = self._highest_since_entry - current_price
if dd >= self.atr_multiplier * atr14:
return True, "EXIT_LONG_ATR"
elif self.start == -1:
if ema10 is not None and ref_price > ema10:
return True, "EXIT_SHORT_EMA10"
if self.use_ema20_force_exit and ema20 is not None and ref_price > ema20:
return True, "EXIT_SHORT_EMA20"
if atr14 is not None and self._lowest_since_entry is not None:
ru = current_price - self._lowest_since_entry
if ru >= self.atr_multiplier * atr14:
return True, "EXIT_SHORT_ATR"
return False, None
def verify_no_position(self, max_retries=5, retry_interval=3):
"""
验证当前无持仓
@@ -671,7 +370,7 @@ class BitmartFuturesTransaction:
for i in range(max_retries):
if self.get_position_status():
if self.start == 0:
logger.info(LOG_POSITION + "确认无持仓,可以开仓")
logger.info(f"确认无持仓,可以开仓")
return True
else:
logger.warning(
@@ -692,7 +391,7 @@ class BitmartFuturesTransaction:
"""
if self.get_position_status():
if self.start == expected_direction:
logger.info(LOG_POSITION + f"持仓方向验证成功: {self.start}")
logger.info(f"持仓方向验证成功: {self.start}")
return True
else:
logger.warning(f"持仓方向不符: 期望 {expected_direction}, 实际 {self.start}")
@@ -708,7 +407,7 @@ class BitmartFuturesTransaction:
if signal_type == 'long':
# 开多前先确认无持仓
logger.info(LOG_POSITION + f"准备开多,触发价: {trigger_price:.2f}")
logger.info(f"准备开多,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
@@ -716,15 +415,14 @@ class BitmartFuturesTransaction:
logger.warning(f"开多前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(LOG_POSITION + "确认无持仓,执行开多")
logger.info(f"确认无持仓,执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(1):
self.last_open_time = time.time()
self.last_open_kline_id = getattr(self, "_current_kline_id_for_open", None)
logger.success(LOG_POSITION + "开多成功")
self.max_unrealized_pnl_seen = None # 新仓位重置移动止损记录
logger.success("开多成功")
return True
else:
logger.error("开多后持仓验证失败")
@@ -732,7 +430,7 @@ class BitmartFuturesTransaction:
elif signal_type == 'short':
# 开空前先确认无持仓
logger.info(LOG_POSITION + f"准备开空,触发价: {trigger_price:.2f}")
logger.info(f"准备开空,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
@@ -740,15 +438,14 @@ class BitmartFuturesTransaction:
logger.warning(f"开空前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(LOG_POSITION + "确认无持仓,执行开空")
logger.info(f"确认无持仓,执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(-1):
self.last_open_time = time.time()
self.last_open_kline_id = getattr(self, "_current_kline_id_for_open", None)
logger.success(LOG_POSITION + "开空成功")
self.max_unrealized_pnl_seen = None # 新仓位重置移动止损记录
logger.success("开空成功")
return True
else:
logger.error("开空后持仓验证失败")
@@ -756,7 +453,7 @@ class BitmartFuturesTransaction:
elif signal_type == 'reverse_long':
# 平空 + 开多(反手做多):先平仓,确认无仓后再开多,避免双向持仓
logger.info(LOG_POSITION + f"执行反手做多,触发价: {trigger_price:.2f}")
logger.info(f"执行反手做多,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(1) # 给交易所处理平仓的时间
# 轮询确认已无持仓再开多(最多等约 10 秒)
@@ -767,13 +464,13 @@ class BitmartFuturesTransaction:
if self.start != 0:
logger.warning("反手做多:平仓后仍有持仓,放弃本次开多")
return False
logger.info(LOG_POSITION + "已确认无持仓,执行开多")
logger.info("已确认无持仓,执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3)
if self.verify_position_direction(1):
self.last_open_time = time.time() # 反手后的新仓位也受最短持仓时间保护
logger.success(LOG_POSITION + "反手做多成功")
self.max_unrealized_pnl_seen = None
logger.success("反手做多成功")
time.sleep(20)
return True
else:
@@ -782,7 +479,7 @@ class BitmartFuturesTransaction:
elif signal_type == 'reverse_short':
# 平多 + 开空(反手做空):先平仓,确认无仓后再开空
logger.info(LOG_POSITION + f"执行反手做空,触发价: {trigger_price:.2f}")
logger.info(f"执行反手做空,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(1)
for _ in range(10):
@@ -792,13 +489,13 @@ class BitmartFuturesTransaction:
if self.start != 0:
logger.warning("反手做空:平仓后仍有持仓,放弃本次开空")
return False
logger.info(LOG_POSITION + "已确认无持仓,执行开空")
logger.info("已确认无持仓,执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3)
if self.verify_position_direction(-1):
self.last_open_time = time.time() # 反手后的新仓位也受最短持仓时间保护
logger.success(LOG_POSITION + "反手做空成功")
self.max_unrealized_pnl_seen = None
logger.success("反手做空成功")
time.sleep(20)
return True
else:
@@ -810,7 +507,7 @@ class BitmartFuturesTransaction:
def action(self):
"""主循环"""
logger.info(LOG_SYSTEM + "开始运行四分之一策略交易...")
logger.info("开始运行四分之一策略交易...")
# 启动时设置全仓高杠杆
if not self.set_leverage():
@@ -818,203 +515,100 @@ class BitmartFuturesTransaction:
return
page_start = True
if USE_RICH_DASHBOARD:
self._display_logs.clear()
logger.remove()
level_color = {
"DEBUG": "\033[2m",
"INFO": "\033[36m",
"SUCCESS": "\033[32m",
"WARNING": "\033[33m",
"ERROR": "\033[31m",
"CRITICAL": "\033[35m",
}
def _sink(msg):
record = msg.record
ts = record["time"].strftime("%H:%M:%S")
level = record["level"].name
color = level_color.get(level, "\033[37m")
line = f"\033[2m{ts}\033[0m {color}{level:<7}\033[0m {record['message']}"
self._display_logs.append(line)
logger.add(_sink, format="{message}", level="INFO")
while True:
live = None
if USE_RICH_DASHBOARD:
live = Live(console=console, refresh_per_second=8, screen=True)
live.start()
try:
while True:
if live is not None:
try:
live.update(build_dashboard_layout(self._display_state, self._display_logs))
except Exception:
pass
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()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(vals=25, 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
# 记录进入新的K线
current_kline_time = current_kline['id']
if self.last_kline_time != current_kline_time:
self.last_kline_time = current_kline_time
logger.info(f"进入新K线: {current_kline_time}")
# 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=空)")
# 3.5 止损/止盈/移动止损
if self.start != 0:
pnl_usd = self.get_unrealized_pnl_usd()
if pnl_usd is not None:
# 更新持仓期间最大盈利(用于移动止损)
if self.max_unrealized_pnl_seen is None:
self.max_unrealized_pnl_seen = pnl_usd
else:
self.max_unrealized_pnl_seen = max(self.max_unrealized_pnl_seen, pnl_usd)
# 移动止损:盈利曾达到 activation 后,从最高盈利回撤 trailing_distance 则平仓
if self.max_unrealized_pnl_seen >= self.trailing_activation_usd:
if pnl_usd < self.max_unrealized_pnl_seen - self.trailing_distance_usd:
logger.info(f"移动止损:当前盈利 {pnl_usd:.2f} 从最高 {self.max_unrealized_pnl_seen:.2f} 回撤 >= {self.trailing_distance_usd} 美元,平仓")
self.平仓()
self.max_unrealized_pnl_seen = None
time.sleep(3)
continue
# 4. 检查信号
signal = self.check_signal(current_price, prev_kline, current_kline)
# 5. 有信号则执行交易
if signal:
trade_success = self.execute_trade(signal)
if trade_success:
logger.success(f"交易执行完成: {signal[0]}, 当前持仓状态: {self.start}")
page_start = True
else:
logger.warning(f"交易执行失败或被阻止: {signal[0]}")
# 短暂等待后继续循环同一根K线遇到信号就操作
time.sleep(0.1)
if page_start:
# 打开浏览器
for i in range(5):
if self.openBrowser():
logger.info(LOG_SYSTEM + "浏览器打开成功")
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()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(vals=25, 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(LOG_SYSTEM + "获取K线失败等待重试...")
time.sleep(5)
continue
# 记录进入新的K线分块分隔便于阅读
current_kline_time = current_kline['id']
if self.last_kline_time != current_kline_time:
self.last_kline_time = current_kline_time
if USE_RICH_DASHBOARD:
logger.info(LOG_SYSTEM + f"进入新K线: {current_kline_time}")
else:
logger.info("")
log_kline_header(current_kline_time)
# 2. 获取当前价格
current_price = self.get_current_price()
if not current_price:
logger.warning(LOG_SYSTEM + "获取价格失败,等待重试...")
time.sleep(2)
continue
# 3. 每次循环都通过SDK获取真实持仓状态避免状态不同步导致双向持仓
if not self.get_position_status():
logger.warning(LOG_SYSTEM + "获取持仓状态失败,等待重试...")
time.sleep(2)
continue
self._log_throttled(
"position_state",
f"当前持仓状态: {self.start} (0=无, 1=多, -1=空)",
interval=2.0,
level="debug",
)
kline_series = self.get_klines_series(35)
ind = self.get_ema_atr_for_exit(kline_series)
# 更新仪表盘左侧数据(供 Rich 展示)
try:
self._display_state["price"] = current_price
self._display_state["position"] = self.start
self._display_state["kline_id"] = current_kline_time
self._display_state["unrealized_pnl"] = self.get_unrealized_pnl_usd()
self._display_state["ema10"] = ind["ema10"]
self._display_state["ema20"] = ind["ema20"]
self._display_state["atr14"] = ind["atr14"]
if getattr(self, "_display_triggers", None):
self._display_state.update(self._display_triggers)
except Exception:
pass
# 3.5 更新自开仓以来最高/最低(真正 ATR 追踪,不随换线重置)
if self.start == 1:
self._highest_since_entry = max(self._highest_since_entry or 0, current_price)
elif self.start == -1:
self._lowest_since_entry = min(self._lowest_since_entry if self._lowest_since_entry is not None else float('inf'), current_price)
# 3.6 平仓EMA10 / EMA20 强制 / ATR 追踪ref 用 last_close 防针洗)
now_ts = time.time()
exit_ok, exit_reason = self.should_exit(current_price, current_kline_time, ind, now_ts)
if exit_ok:
logger.info(LOG_POSITION + f"技术性平仓 | 原因: {exit_reason}")
self.平仓()
self._last_exit_kline_id = current_kline_time
self._highest_since_entry = None
self._lowest_since_entry = None
time.sleep(3)
self._prev_price = current_price
continue
# 4. 四分之一价位与实体过滤
levels = self.quarter_levels(
prev_kline['open'], prev_kline['high'], prev_kline['low'], prev_kline['close'],
current_kline['open']
)
if not self._entity_filter_passed(levels["entity"], prev_kline['close'], ind.get("atr14")):
self._log_throttled("entity_small", LOG_PRICE + f"实体过小(实体={levels['entity']:.4f}),跳过信号", interval=2.0)
self._prev_price = current_price
time.sleep(0.1)
continue
# 开盘后延迟:避免开盘价即触发
if self.open_trigger_delay_seconds > 0:
kline_elapsed = time.time() - current_kline_time
if kline_elapsed < self.open_trigger_delay_seconds:
self._prev_price = current_price
time.sleep(0.1)
continue
# 5. 检查信号(穿越触发 + EMA20 入场过滤已在 check_signal 内)
signal = self.check_signal(current_price, prev_kline, current_kline, levels, ind)
# 5.5 反手过滤:最小价差(双保险)
if signal and signal[0].startswith('reverse_'):
if not self.can_reverse(current_price, signal[1]):
signal = None
# 5.6 开仓过滤当前K线已出场则等下一根K线再开仓
if signal and signal[0] in ('long', 'short'):
if self._last_exit_kline_id == current_kline_time:
self._log_throttled("same_kline_no_open", LOG_SYSTEM + "当前K线已出场等下一根K线再开仓", interval=2.0)
signal = None
elif not self.can_open(current_kline_time):
signal = None
else:
self._current_kline_id_for_open = current_kline_time
# 6. 有信号则执行交易
if signal:
trade_success = self.execute_trade(signal)
if trade_success:
self._highest_since_entry = current_price
self._lowest_since_entry = current_price
logger.success(LOG_POSITION + f"交易执行完成: {signal[0]} | 当前持仓: {self.start}")
page_start = True
else:
logger.warning(f"交易执行失败或被阻止: {signal[0]}")
self._prev_price = current_price
time.sleep(0.1)
if page_start:
self.page.close()
time.sleep(5)
except KeyboardInterrupt:
logger.info(LOG_SYSTEM + "用户中断,程序退出")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
self.page.close()
time.sleep(5)
finally:
if live is not None:
try:
live.stop()
except Exception:
pass
if USE_RICH_DASHBOARD:
logger.remove()
logger.add(sys.stderr, format="{message}", level="INFO")
except KeyboardInterrupt:
logger.info("用户中断,程序退出")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(5)
if __name__ == '__main__':
BitmartFuturesTransaction(bit_id="f2320f57e24c45529a009e1541e25961").action()