优化完美代码,优化计算盈亏
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user