加入一个回测,

This commit is contained in:
ddrwode
2026-03-06 10:36:18 +08:00
parent c473c738a3
commit b2515bb7ce
2 changed files with 106891 additions and 127 deletions

View File

@@ -3,6 +3,7 @@
基于回测策略 bb_backtest_march_2026.py
使用框架的API查询 + 浏览器自动化交易
"""
import json
import time
import numpy as np
from datetime import datetime, timezone
@@ -37,7 +38,8 @@ class BBDelayReversalConfig:
LEVERAGE = "50"
OPEN_TYPE = "isolated" # 逐仓模式
MARGIN_PCT = 0.01 # 首次开仓1%
STOP_LOSS_RATIO = 0.5 # 浮亏达到总保证金50%止损
# 运行参数
POLL_INTERVAL = 5
KLINE_STEP = 5
@@ -84,15 +86,69 @@ class BBDelayReversalTrader:
# 交易控制
self.last_trade_time = 0.0
self.last_kline_id = None
self.last_closed_kline_id = None
self.cooldown_seconds = 10 # 交易冷却时间
# 日志
self.log_dir = Path(__file__).resolve().parent
self.state_file = self.log_dir / "bb_delay_reversal_state.json"
logger.add(
self.log_dir / "bb_delay_trade_{time:YYYY-MM-DD}.log",
rotation="1 day", retention="30 days",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
self.load_state()
def load_state(self):
"""加载本地策略状态,便于重启后延续运行。"""
if not self.state_file.exists():
return
try:
data = json.loads(self.state_file.read_text(encoding="utf-8"))
except Exception as e:
logger.warning(f"读取本地状态失败,忽略旧状态: {e}")
return
self.position_count = int(data.get("position_count", self.position_count))
self.total_margin = float(data.get("total_margin", self.total_margin))
self.mid_closed_half = bool(data.get("mid_closed_half", self.mid_closed_half))
self.delay_reverse_price = data.get("delay_reverse_price")
self.delay_reverse_type = data.get("delay_reverse_type")
self.delay_reverse_kline_id = data.get("delay_reverse_kline_id")
self.last_kline_id = data.get("last_kline_id")
self.last_closed_kline_id = data.get("last_closed_kline_id")
if self.delay_reverse_price is not None:
self.delay_reverse_price = float(self.delay_reverse_price)
if self.delay_reverse_kline_id is not None:
self.delay_reverse_kline_id = int(self.delay_reverse_kline_id)
if self.last_kline_id is not None:
self.last_kline_id = int(self.last_kline_id)
if self.last_closed_kline_id is not None:
self.last_closed_kline_id = int(self.last_closed_kline_id)
logger.info("已加载本地策略状态")
def save_state(self):
"""保存关键状态,降低重启造成的状态丢失。"""
data = {
"position_count": self.position_count,
"total_margin": self.total_margin,
"mid_closed_half": self.mid_closed_half,
"delay_reverse_price": self.delay_reverse_price,
"delay_reverse_type": self.delay_reverse_type,
"delay_reverse_kline_id": self.delay_reverse_kline_id,
"last_kline_id": self.last_kline_id,
"last_closed_kline_id": self.last_closed_kline_id,
}
try:
self.state_file.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except Exception as e:
logger.warning(f"写入本地状态失败: {e}")
# ========== API查询方法 ==========
@@ -160,6 +216,18 @@ class BBDelayReversalTrader:
def get_position_status(self) -> bool:
"""查询持仓状态(使用框架方法)"""
try:
old_state = (
self.position,
self.position_count,
self.entry_price,
self.current_amount,
self.total_margin,
self.mid_closed_half,
self.delay_reverse_price,
self.delay_reverse_type,
self.delay_reverse_kline_id,
)
old_position = self.position
response = self.contractAPI.get_position(contract_symbol=self.cfg.CONTRACT_SYMBOL)[0]
if response['code'] == 1000:
positions = response['data']
@@ -168,12 +236,55 @@ class BBDelayReversalTrader:
self.position_count = 0
self.entry_price = 0
self.current_amount = 0
self.total_margin = 0
self.mid_closed_half = False
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_id = None
if (
self.position,
self.position_count,
self.entry_price,
self.current_amount,
self.total_margin,
self.mid_closed_half,
self.delay_reverse_price,
self.delay_reverse_type,
self.delay_reverse_kline_id,
) != old_state:
self.save_state()
return True
pos = positions[0]
self.position = 1 if pos['position_type'] == 1 else -1
self.entry_price = float(pos['open_avg_price'])
self.current_amount = float(pos['current_amount'])
if old_position not in (0, self.position):
logger.warning("检测到持仓方向变化,重置本地策略状态")
self.position_count = 1
self.mid_closed_half = False
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_id = None
elif self.position_count == 0:
self.position_count = 1
if self.total_margin <= 0 and self.current_amount > 0:
leverage = float(self.cfg.LEVERAGE)
self.total_margin = self.current_amount * self.entry_price / leverage
if (
self.position,
self.position_count,
self.entry_price,
self.current_amount,
self.total_margin,
self.mid_closed_half,
self.delay_reverse_price,
self.delay_reverse_type,
self.delay_reverse_kline_id,
) != old_state:
self.save_state()
logger.debug(f"持仓: {'' if self.position > 0 else ''} | "
f"价格={self.entry_price:.2f} | 数量={self.current_amount:.4f}")
return True
@@ -213,6 +324,17 @@ class BBDelayReversalTrader:
upper = mid + self.cfg.BB_STD * std
lower = mid - self.cfg.BB_STD * std
return mid, upper, lower
def calc_shifted_bollinger_for_index(self, closed_klines: list, target_index: int):
"""计算某根已收盘K线对应的右移一根布林带。"""
if target_index < self.cfg.BB_PERIOD:
return None
closes = [
k['close']
for k in closed_klines[target_index - self.cfg.BB_PERIOD:target_index]
]
return self.calc_bollinger(closes)
# ========== 浏览器自动化 ==========
@@ -346,8 +468,8 @@ class BBDelayReversalTrader:
logger.error(f"全平仓确认超时,当前仓位={self.position}")
return False
def open_with_balance_and_confirm(self, direction: str) -> bool:
"""按余额比例开仓并确认方向正确"""
def open_with_balance_and_confirm(self, direction: str, previous_amount: float = 0.0) -> bool:
"""按余额比例开仓/加仓,并确认方向与数量变化正确"""
balance = self.get_balance()
if not balance:
logger.error("余额获取失败,无法开仓")
@@ -372,8 +494,57 @@ class BBDelayReversalTrader:
logger.error(f"开仓结果不一致: 期望={expected_pos}, 实际={self.position}")
return False
if previous_amount > 0:
min_increase = max(previous_amount * 0.05, 1e-8)
if self.current_amount <= previous_amount + min_increase:
logger.error(
f"开仓后数量未明显增加: 之前={previous_amount:.4f}, 当前={self.current_amount:.4f}"
)
return False
self.total_margin += usdt_amount
else:
self.total_margin = usdt_amount
self.save_state()
logger.success(f"✓ 开{'' if direction == 'long' else ''}成功")
return True
def close_partial_and_confirm(
self,
ratio: float,
previous_amount: float,
timeout_seconds: int = 15,
poll_seconds: float = 1.0,
) -> bool:
"""执行部分平仓,并确认仓位数量明显下降。"""
if self.position == 0 or previous_amount <= 0:
return False
expected_pos = self.position
if not self.browser_close_position(ratio):
logger.error("部分平仓指令发送失败")
return False
deadline = time.time() + timeout_seconds
while time.time() < deadline:
time.sleep(poll_seconds)
if not self.get_position_status():
continue
if self.position != expected_pos:
continue
if self.current_amount <= previous_amount * 0.75:
self.total_margin *= (1 - ratio)
self.save_state()
logger.success(
f"✓ 平仓{int(ratio*100)}%确认完成 | 之前={previous_amount:.4f}, 当前={self.current_amount:.4f}"
)
return True
logger.error(
f"平仓{int(ratio*100)}%确认超时,数量未明显下降 | 之前={previous_amount:.4f}, 当前={self.current_amount:.4f}"
)
return False
# ========== 延迟反转逻辑 ==========
@@ -382,13 +553,34 @@ class BBDelayReversalTrader:
self.delay_reverse_type = reverse_type
self.delay_reverse_price = trigger_price
self.delay_reverse_kline_id = kline_id
self.save_state()
logger.warning(f"⚠️ 延迟反转触发: {reverse_type} @ {trigger_price:.2f} | K线ID: {kline_id}")
def clear_delay_reversal(self):
"""清除延迟反转状态"""
had_state = (
self.delay_reverse_price is not None
or self.delay_reverse_type is not None
or self.delay_reverse_kline_id is not None
)
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_id = None
if had_state:
self.save_state()
def check_stop_loss(self, high: float, low: float) -> bool:
"""检查是否达到总保证金50%的止损阈值。"""
if self.position == 0 or self.current_amount <= 0 or self.total_margin <= 0:
return False
stop_price = low if self.position > 0 else high
if self.position > 0:
unrealized_pnl = self.current_amount * (stop_price - self.entry_price)
else:
unrealized_pnl = self.current_amount * (self.entry_price - stop_price)
return unrealized_pnl <= -self.total_margin * self.cfg.STOP_LOSS_RATIO
def check_delay_reversal(self, current_kline, prev_kline, prev_upper, prev_lower) -> tuple | None:
"""
@@ -496,9 +688,6 @@ class BBDelayReversalTrader:
logger.info(f"初始持仓: {self.position}")
page_start = True
kline_history = [] # 保存历史K线
prev_bb_upper = None # 保存上一根K线的上轨
prev_bb_lower = None # 保存上一根K线的下轨
while True:
try:
@@ -540,7 +729,6 @@ class BBDelayReversalTrader:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 使用已收盘K线计算BB
closed_klines = klines[:-1]
current_kline = klines[-1]
@@ -557,35 +745,128 @@ class BBDelayReversalTrader:
continue
bb_mid, bb_upper, bb_lower = bb
signal_kline = closed_klines[-1]
signal_bb = self.calc_shifted_bollinger_for_index(
closed_klines,
len(closed_klines) - 1,
)
# 当前价优先使用当前5m实时K线close避免1m收盘价滞后导致观感偏差
current_price = float(current_kline['close'])
cur_high = current_kline['high']
cur_low = current_kline['low']
kline_id = current_kline['id']
# 触轨判断
touched_upper = cur_high >= bb_upper
touched_lower = cur_low <= bb_lower
touched_middle = cur_low <= bb_mid <= cur_high
# 延迟反转状态显示
delay_status = ""
if self.delay_reverse_price is not None:
delay_status = f" | 🔄延迟反转中: {self.delay_reverse_type} @ {self.delay_reverse_price:.2f}"
signal_status = ""
if signal_bb is not None:
signal_mid, signal_upper, signal_lower = signal_bb
signal_status = (
f" | 收盘K H/L={signal_kline['high']:.2f}/{signal_kline['low']:.2f}"
f" BB={signal_lower:.2f}/{signal_mid:.2f}/{signal_upper:.2f}"
)
logger.info(
f"价格={current_price:.2f} | "
f"BB: {bb_lower:.2f}/{bb_mid:.2f}/{bb_upper:.2f} | "
f"触上={touched_upper} 触下={touched_lower} 触中={touched_middle} | "
f"仓位={self.position}{delay_status}"
f"仓位={self.position}{delay_status}{signal_status}"
)
# 同步持仓
if not self.get_position_status():
time.sleep(self.cfg.POLL_INTERVAL)
continue
# ===== 收盘K确认逻辑止损 / 中轨平半 / 回开仓价反手 =====
if signal_bb is not None and signal_kline['id'] != self.last_closed_kline_id:
signal_mid, _, _ = signal_bb
signal_high = signal_kline['high']
signal_low = signal_kline['low']
signal_ts = datetime.fromtimestamp(signal_kline['id'] / 1000).strftime('%Y-%m-%d %H:%M')
closed_processed = True
if self.check_stop_loss(signal_high, signal_low):
logger.warning(f"🛑 收盘K触发止损: {signal_ts}")
if self.close_all_and_confirm():
self.last_closed_kline_id = signal_kline['id']
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_processed = False
elif self.position != 0 and self.delay_reverse_price is None:
had_mid_closed_half = self.mid_closed_half
if self.position > 0:
if had_mid_closed_half and signal_low <= self.entry_price:
logger.info(f"💰 收盘K回到开仓价 {self.entry_price:.2f},全平并反手开空")
if self.close_all_and_confirm() and self.open_with_balance_and_confirm('short'):
self.position_count = 1
self.mid_closed_half = False
self.last_closed_kline_id = signal_kline['id']
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success("✓ 收盘K反手完成")
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_processed = False
elif not had_mid_closed_half and signal_low <= signal_mid <= signal_high:
logger.info(f"📊 收盘K触中轨 {signal_mid:.2f}平50%")
previous_amount = self.current_amount
if self.close_partial_and_confirm(0.5, previous_amount):
self.mid_closed_half = True
self.last_closed_kline_id = signal_kline['id']
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success("✓ 收盘K平半完成")
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_processed = False
else:
if had_mid_closed_half and signal_high >= self.entry_price:
logger.info(f"💰 收盘K回到开仓价 {self.entry_price:.2f},全平并反手开多")
if self.close_all_and_confirm() and self.open_with_balance_and_confirm('long'):
self.position_count = 1
self.mid_closed_half = False
self.last_closed_kline_id = signal_kline['id']
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success("✓ 收盘K反手完成")
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_processed = False
elif not had_mid_closed_half and signal_low <= signal_mid <= signal_high:
logger.info(f"📊 收盘K触中轨 {signal_mid:.2f}平50%")
previous_amount = self.current_amount
if self.close_partial_and_confirm(0.5, previous_amount):
self.mid_closed_half = True
self.last_closed_kline_id = signal_kline['id']
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success("✓ 收盘K平半完成")
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_processed = False
if not closed_processed:
time.sleep(self.cfg.POLL_INTERVAL)
continue
self.last_closed_kline_id = signal_kline['id']
self.save_state()
# 避免同一K线重复触发
if kline_id == self.last_kline_id:
@@ -594,9 +875,11 @@ class BBDelayReversalTrader:
# ===== 延迟反转确认(优先级最高)=====
if self.delay_reverse_price is not None:
prev_kline = kline_history[-1] if len(kline_history) > 0 else None
prev_kline = signal_kline if signal_bb is not None else None
prev_upper = signal_bb[1] if signal_bb is not None else None
prev_lower = signal_bb[2] if signal_bb is not None else None
reversal = self.check_delay_reversal(
current_kline, prev_kline, prev_bb_upper, prev_bb_lower
current_kline, prev_kline, prev_upper, prev_lower
)
if reversal:
@@ -612,102 +895,42 @@ class BBDelayReversalTrader:
self.clear_delay_reversal()
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success("✓ 延迟反转完成!")
else:
logger.error("延迟反转取消:全平仓未确认")
# 更新历史
kline_history.append(current_kline)
prev_bb_upper = bb_upper
prev_bb_lower = bb_lower
continue
# ===== 中轨平仓(延迟反转期间不执行)=====
if self.position != 0 and touched_middle and self.delay_reverse_price is None:
if not self.mid_closed_half:
logger.info("📊 触中轨平50%")
if not self.can_trade():
continue
self.browser_close_position(0.5)
time.sleep(3)
self.get_position_status()
self.mid_closed_half = True
self.last_kline_id = kline_id
self.update_trade_time()
logger.success(f"✓ 平半成功")
# 更新历史
kline_history.append(current_kline)
prev_bb_upper = bb_upper
prev_bb_lower = bb_lower
continue
# ===== 回到开仓价全平+反手(仅在已平半的情况下)=====
if self.position != 0 and self.mid_closed_half and self.delay_reverse_price is None:
should_close = False
if self.position > 0 and cur_low <= self.entry_price:
should_close = True
elif self.position < 0 and cur_high >= self.entry_price:
should_close = True
if should_close:
logger.info(f"💰 回到开仓价 {self.entry_price:.2f},全平+反手")
if not self.can_trade():
continue
old_direction = 'long' if self.position > 0 else 'short'
new_direction = 'short' if old_direction == 'long' else 'long'
if self.close_all_and_confirm():
if self.open_with_balance_and_confirm(new_direction):
self.position_count = 1
self.mid_closed_half = False
self.last_kline_id = kline_id
self.update_trade_time()
logger.success("✓ 反手完成!")
else:
logger.error("反手取消:全平仓未确认")
# 更新历史
kline_history.append(current_kline)
prev_bb_upper = bb_upper
prev_bb_lower = bb_lower
time.sleep(self.cfg.POLL_INTERVAL)
continue
# ===== 开仓(空仓时)=====
if self.position == 0:
self.clear_delay_reversal()
balance = self.get_balance()
if not balance:
time.sleep(self.cfg.POLL_INTERVAL)
continue
usdt_amount = round(balance * self.cfg.MARGIN_PCT, 2)
if self.delay_reverse_price is not None:
self.clear_delay_reversal()
if touched_upper:
logger.info(f"🔴 空仓触上轨,开空 {usdt_amount}U")
logger.info("🔴 空仓触上轨,开空")
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
self.browser_open_position('short', usdt_amount)
time.sleep(3)
self.get_position_status()
if self.position == -1:
if self.open_with_balance_and_confirm('short'):
self.position_count = 1
self.mid_closed_half = False
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success(f"✓ 开空成功")
elif touched_lower:
logger.info(f"🟢 空仓触下轨,开多 {usdt_amount}U")
logger.info("🟢 空仓触下轨,开多")
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
self.browser_open_position('long', usdt_amount)
time.sleep(3)
self.get_position_status()
if self.position == 1:
if self.open_with_balance_and_confirm('long'):
self.position_count = 1
self.mid_closed_half = False
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success(f"✓ 开多成功")
# ===== 延迟反转触发(有持仓且未在延迟反转中)=====
@@ -715,51 +938,41 @@ class BBDelayReversalTrader:
logger.warning("⚠️ 多仓触上轨,标记延迟反转")
self.mark_delay_reversal('long_to_short', bb_upper, kline_id)
self.last_kline_id = kline_id
self.save_state()
elif self.position < 0 and touched_lower and self.delay_reverse_price is None:
logger.warning("⚠️ 空仓触下轨,标记延迟反转")
self.mark_delay_reversal('short_to_long', bb_lower, kline_id)
self.last_kline_id = kline_id
self.save_state()
# ===== 加仓(仅首次开仓后,且未在延迟反转中)=====
elif self.position_count == 1 and self.delay_reverse_price is None:
balance = self.get_balance()
if balance:
usdt_amount = round(balance * self.cfg.MARGIN_PCT, 2)
if self.position > 0 and touched_lower:
logger.info(f" 多仓触下轨,加仓 {usdt_amount}U")
if not self.can_trade():
continue
self.browser_open_position('long', usdt_amount)
time.sleep(3)
self.get_position_status()
if self.position == 1:
self.position_count = 2
self.last_kline_id = kline_id
self.update_trade_time()
logger.success(f"✓ 加仓成功")
elif self.position < 0 and touched_upper:
logger.info(f" 空仓触上轨,加仓 {usdt_amount}U")
if not self.can_trade():
continue
self.browser_open_position('short', usdt_amount)
time.sleep(3)
self.get_position_status()
if self.position == -1:
self.position_count = 2
self.last_kline_id = kline_id
self.update_trade_time()
logger.success(f"✓ 加仓成功")
# 更新K线历史和布林带历史
kline_history.append(current_kline)
if len(kline_history) > 100:
kline_history = kline_history[-100:]
prev_bb_upper = bb_upper
prev_bb_lower = bb_lower
previous_amount = self.current_amount
if self.position > 0 and touched_lower:
logger.info(" 多仓触下轨,加仓")
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
if self.open_with_balance_and_confirm('long', previous_amount=previous_amount):
self.position_count = 2
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success(f"✓ 加仓成功")
elif self.position < 0 and touched_upper:
logger.info(" 空仓触上轨,加仓")
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
if self.open_with_balance_and_confirm('short', previous_amount=previous_amount):
self.position_count = 2
self.last_kline_id = kline_id
self.update_trade_time()
self.save_state()
logger.success(f"✓ 加仓成功")
time.sleep(self.cfg.POLL_INTERVAL)

File diff suppressed because it is too large Load Diff