加入一个回测,

This commit is contained in:
ddrwode
2026-03-09 10:29:29 +08:00
parent d1deb97843
commit e81119031d
2 changed files with 443344 additions and 10 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -48,9 +48,10 @@ class BBDelayReversalConfig:
# 递增加仓 (D方案) # 递增加仓 (D方案)
PYRAMID_STEP = 0.01 # 每次加仓增加1%权益比例 (1%→2%→3%→4%) PYRAMID_STEP = 0.01 # 每次加仓增加1%权益比例 (1%→2%→3%→4%)
PYRAMID_MAX = 3 # 最多加仓3次 (首次开仓不算) PYRAMID_MAX = 1 # 最多加仓1次 (首次开仓不算)
# 风控 # 风控
STOP_LOSS_PCT = 0.50 # 亏损百分之50就平仓
MAX_DAILY_LOSS = 50.0 # 日最大亏损(U),达到后停止交易 MAX_DAILY_LOSS = 50.0 # 日最大亏损(U),达到后停止交易
COOLDOWN_SECONDS = 30 # 两次交易之间最小间隔(秒) COOLDOWN_SECONDS = 30 # 两次交易之间最小间隔(秒)
@@ -98,6 +99,12 @@ class BBDelayReversalTrader:
self.position = 0 self.position = 0
self.open_avg_price = None self.open_avg_price = None
self.current_amount = None self.current_amount = None
self.unrealized_value = 0.0
# 半仓折返逻辑状态
self.has_half_closed = False # 是否已经触发过“平一半”逻辑
self.highest_since_open = None # 持有多仓以来的最高价
self.lowest_since_open = None # 持有空仓以来的最低价
# 加仓状态 # 加仓状态
self.pyramid_count = 0 # 当前已加仓次数 (0=仅首次开仓) self.pyramid_count = 0 # 当前已加仓次数 (0=仅首次开仓)
@@ -195,14 +202,18 @@ class BBDelayReversalTrader:
self.position = 0 self.position = 0
self.open_avg_price = None self.open_avg_price = None
self.current_amount = None self.current_amount = None
self.unrealized_value = 0.0
self.has_half_closed = False
self.highest_since_open = None
self.lowest_since_open = None
return True return True
pos = positions[0] pos = positions[0]
self.position = 1 if pos["position_type"] == 1 else -1 self.position = 1 if pos["position_type"] == 1 else -1
self.open_avg_price = float(pos["open_avg_price"]) self.open_avg_price = float(pos["open_avg_price"])
self.current_amount = float(pos["current_amount"]) self.current_amount = float(pos["current_amount"])
unrealized = float(pos.get("unrealized_value", 0)) self.unrealized_value = float(pos.get("unrealized_value", 0))
logger.debug(f"持仓: dir={self.position} price={self.open_avg_price} " logger.debug(f"持仓: dir={self.position} price={self.open_avg_price} "
f"amt={self.current_amount} upnl={unrealized:.2f}") f"amt={self.current_amount} upnl={self.unrealized_value:.2f}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"查询持仓异常: {e}") logger.error(f"查询持仓异常: {e}")
@@ -258,8 +269,37 @@ class BBDelayReversalTrader:
def browser_close_position(self) -> bool: def browser_close_position(self) -> bool:
"""浏览器点击市价全平""" """浏览器点击市价全平"""
logger.info("浏览器操作: 市价平仓") logger.info("浏览器操作: 市价平仓")
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
time.sleep(0.5)
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[5]') # 100%
time.sleep(0.5)
return self.click_safe('x://span[normalize-space(text()) ="市价"]') return self.click_safe('x://span[normalize-space(text()) ="市价"]')
def browser_half_close_long(self) -> bool:
"""平一半多仓"""
logger.info("浏览器操作: 平一半多仓")
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
time.sleep(0.5)
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]') # 50%
time.sleep(0.5)
return self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
def browser_half_close_short(self) -> bool:
"""平一半空仓"""
logger.info("浏览器操作: 平一半空仓")
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
time.sleep(0.5)
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]') # 50%
time.sleep(0.5)
return self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
def reset_position_state(self):
"""重置所有关于持仓的变量"""
self.pyramid_count = 0
self.has_half_closed = False
self.highest_since_open = None
self.lowest_since_open = None
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 仓位操作 # 仓位操作
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -469,7 +509,127 @@ class BBDelayReversalTrader:
time.sleep(self.cfg.POLL_INTERVAL) time.sleep(self.cfg.POLL_INTERVAL)
continue continue
# 5. 信号判断 # 4.5 止损检查
if self.position != 0 and self.current_amount and self.open_avg_price:
position_margin = (self.open_avg_price * self.current_amount) / self.cfg.LEVERAGE
if position_margin > 0:
roe = self.unrealized_value / position_margin
if roe <= -self.cfg.STOP_LOSS_PCT:
logger.warning(f"触发止损!当前亏损比例: {roe:.2%} (<= -{self.cfg.STOP_LOSS_PCT:.0%}),开始平仓")
action = "止损平仓"
reason = f"亏损比例 {roe:.2%} 达到 {self.cfg.STOP_LOSS_PCT:.0%} 止损线"
self.browser_close_position()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position == 0:
self.reset_position_state()
self.write_trade_log(action, current_price, bb_upper, bb_mid, bb_lower, reason)
logger.success("止损平仓成功")
page_start = True
try:
self.page.close()
except Exception:
pass
self.page = None
time.sleep(5)
continue
else:
logger.error(f"止损平仓失败,当前仍有持仓({self.position})")
# 5. 极值更新与半仓/全平反手逻辑 (需求: 如果当前做多,涨破中轨后回落碰中轨平一半;再跌到开仓价平全仓反手)
if self.position == 1 and self.open_avg_price:
if self.highest_since_open is None or current_price > self.highest_since_open:
self.highest_since_open = current_price
# 规则 A1: 涨过中轨
if self.highest_since_open >= bb_mid:
# 从最高点回落触碰中轨,且未平过一半
if not self.has_half_closed and (cur_low <= bb_mid or current_price <= bb_mid):
if self.can_trade():
logger.info("做多回落触碰中轨,执行平一半多仓")
self.browser_half_close_long()
time.sleep(2)
self.get_position_status()
self.has_half_closed = True
self.write_trade_log("平半仓(多)", current_price, bb_upper, bb_mid, bb_lower, "多单涨破中轨后回落触碰中轨")
# 切换回开仓面板以防后续逻辑出错
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
# 规则 A2: 平过一半后,跌回开仓价 -> 全平反手开空
if self.has_half_closed and (cur_low <= self.open_avg_price or current_price <= self.open_avg_price):
if self.can_trade():
logger.info("平半仓后跌回多单开仓价,全平反手开空")
self.browser_close_position()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
self.reset_position_state()
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
self.get_position_status()
self.write_trade_log("全平反手(多转空)", current_price, bb_upper, bb_mid, bb_lower, "多单平半后跌回开仓价")
page_start = True
try: self.page.close()
except: pass
self.page = None
time.sleep(5)
continue
elif self.position == -1 and self.open_avg_price:
if self.lowest_since_open is None or current_price < self.lowest_since_open:
self.lowest_since_open = current_price
# 规则 B1: 跌破过中轨
if self.lowest_since_open <= bb_mid:
# 从最低点回升触碰中轨,且未平过一半
if not self.has_half_closed and (cur_high >= bb_mid or current_price >= bb_mid):
if self.can_trade():
logger.info("做空反弹触碰中轨,执行平一半空仓")
self.browser_half_close_short()
time.sleep(2)
self.get_position_status()
self.has_half_closed = True
self.write_trade_log("平半仓(空)", current_price, bb_upper, bb_mid, bb_lower, "空单跌破中轨后反弹触碰中轨")
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
# 规则 B2: 平过一半后,涨回开仓价 -> 全平反手开多
if self.has_half_closed and (cur_high >= self.open_avg_price or current_price >= self.open_avg_price):
if self.can_trade():
logger.info("平半仓后涨回空单开仓价,全平反手开多")
self.browser_close_position()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
self.reset_position_state()
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
self.get_position_status()
self.write_trade_log("全平反手(空转多)", current_price, bb_upper, bb_mid, bb_lower, "空单平半后涨回开仓价")
page_start = True
try: self.page.close()
except: pass
self.page = None
time.sleep(5)
continue
# 6. 基本信号判断 (上轨空/下轨多)
kline_id = current_kline["id"] kline_id = current_kline["id"]
if kline_id == last_kline_id: if kline_id == last_kline_id:
time.sleep(self.cfg.POLL_INTERVAL) time.sleep(self.cfg.POLL_INTERVAL)
@@ -507,17 +667,22 @@ class BBDelayReversalTrader:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开空") logger.warning(f"平仓后仍有持仓({self.position}),放弃开空")
time.sleep(self.cfg.POLL_INTERVAL) time.sleep(self.cfg.POLL_INTERVAL)
continue continue
# 翻转时重置加仓计数 self.reset_position_state()
self.pyramid_count = 0
# 平仓后在同一页面直接点卖出/做空 # 平仓后在同一页面直接点卖出/做空
logger.info("平仓完成,直接开空") logger.info("平仓完成,直接开空")
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3) time.sleep(3)
if self.verify_position(-1): if self.verify_position(-1):
success = True success = True
elif self.position == 0: elif self.position == 0:
action = "开空" action = "开空"
self.pyramid_count = 0 self.reset_position_state()
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3) time.sleep(3)
if self.verify_position(-1): if self.verify_position(-1):
@@ -529,6 +694,9 @@ class BBDelayReversalTrader:
# 重新计算加仓金额并输入 # 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True) add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0: if add_usdt > 0:
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True) self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5) time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
@@ -560,16 +728,21 @@ class BBDelayReversalTrader:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开多") logger.warning(f"平仓后仍有持仓({self.position}),放弃开多")
time.sleep(self.cfg.POLL_INTERVAL) time.sleep(self.cfg.POLL_INTERVAL)
continue continue
# 翻转时重置加仓计数 self.reset_position_state()
self.pyramid_count = 0
logger.info("平仓完成,直接开多") logger.info("平仓完成,直接开多")
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3) time.sleep(3)
if self.verify_position(1): if self.verify_position(1):
success = True success = True
elif self.position == 0: elif self.position == 0:
action = "开多" action = "开多"
self.pyramid_count = 0 self.reset_position_state()
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3) time.sleep(3)
if self.verify_position(1): if self.verify_position(1):
@@ -581,6 +754,9 @@ class BBDelayReversalTrader:
# 重新计算加仓金额并输入 # 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True) add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0: if add_usdt > 0:
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
time.sleep(0.5)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True) self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5) time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')