加入一个回测,

This commit is contained in:
ddrwode
2026-03-05 12:51:12 +08:00
parent b74449989b
commit 01b6a0fdcb
17 changed files with 189157 additions and 29751 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
178.5,305.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,305.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,306.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,306.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,307.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,307.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,308.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,308.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,309.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,309.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,310.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,310.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,311.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,311.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,312.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,312.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,313.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,313.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,314.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,314.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,315.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,315.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,316.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,316.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,317.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,317.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,318.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,318.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,319.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,319.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,320.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,320.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,321.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,321.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,322.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,322.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,323.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,323.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,324.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,324.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,325.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,325.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,326.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
1 178.5 305.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
2 178.5 305.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
3 178.5 306.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
4 178.5 306.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
5 178.5 307.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
6 178.5 307.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
7 178.5 308.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
8 178.5 308.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
9 178.5 309.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
10 178.5 309.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
11 178.5 310.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
12 178.5 310.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
13 178.5 311.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
14 178.5 311.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
15 178.5 312.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
16 178.5 312.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
17 178.5 313.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
18 178.5 313.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
19 178.5 314.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
20 178.5 314.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
21 178.5 315.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
22 178.5 315.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
23 178.5 316.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
24 178.5 316.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
25 178.5 317.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
26 178.5 317.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
27 178.5 318.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
28 178.5 318.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
29 178.5 319.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
30 178.5 319.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
31 178.5 320.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
32 178.5 320.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
33 178.5 321.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
34 178.5 321.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
35 178.5 322.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
36 178.5 322.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
37 178.5 323.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
38 178.5 323.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
39 178.5 324.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
40 178.5 324.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
41 178.5 325.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
42 178.5 325.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
43 178.5 326.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19

File diff suppressed because it is too large Load Diff

View File

@@ -87,12 +87,12 @@ class BollingerBandBacktest:
self.current_run_label = "default" self.current_run_label = "default"
def calculate_bollinger_bands(self, df): def calculate_bollinger_bands(self, df):
"""计算布林带""" """计算布林带整体右移1根避免使用当前K收盘价"""
df['sma'] = df['close'].rolling(window=self.bb_period).mean() df['sma'] = df['close'].rolling(window=self.bb_period).mean()
df['std'] = df['close'].rolling(window=self.bb_period).std() df['std'] = df['close'].rolling(window=self.bb_period).std()
df['upper'] = df['sma'] + self.bb_std * df['std'] df['upper'] = (df['sma'] + self.bb_std * df['std']).shift(1)
df['lower'] = df['sma'] - self.bb_std * df['std'] df['lower'] = (df['sma'] - self.bb_std * df['std']).shift(1)
df['middle'] = df['sma'] df['middle'] = df['sma'].shift(1)
return df return df
def get_rebate_amount(self, fee): def get_rebate_amount(self, fee):
@@ -288,52 +288,54 @@ class BollingerBandBacktest:
self.clear_delay_reversal() self.clear_delay_reversal()
return True return True
def check_and_execute_delay_reversal(self, i, row, prev_row, timestamp): def check_delay_reversal_signal(self, i, row, prev_row):
"""执行延迟反转三段判断逻辑""" """检查延迟反转是否在当前收盘K成立仅返回信号不直接执行"""
if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None: if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None:
return False return None
offset = i - self.delay_reverse_kline_index offset = i - self.delay_reverse_kline_index
# 禁止同K确认最早次K确认
if offset <= 0:
return None
high = row['high'] high = row['high']
low = row['low'] low = row['low']
if self.delay_reverse_type == 'long_to_short': if self.delay_reverse_type == 'long_to_short':
# 情况1/2:触上轨后,同K或下一K回调到记录上轨价 # 情况1触上轨后K回调到记录上轨价
if offset <= 1 and low <= self.delay_reverse_price: if offset == 1 and low <= self.delay_reverse_price:
tag = "K回调确认" if offset == 0 else "次K回调确认" return 'short', "延迟反转-次K回调确认", self.delay_reverse_price
return self.reverse_position(self.delay_reverse_price, 'short', timestamp, f"延迟反转-{tag}")
# 情况3持续等待动态追踪上一根K线条件 # 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None: if offset >= 2 and prev_row is not None:
prev_touch_upper = prev_row['high'] >= prev_row['upper'] prev_upper = prev_row['upper']
prev_touch_upper = pd.notna(prev_upper) and prev_row['high'] >= prev_upper
if prev_touch_upper: if prev_touch_upper:
ref_price = prev_row['upper'] if low <= prev_upper:
if low <= ref_price: return 'short', "延迟反转-上一根触上轨后回调确认", prev_upper
return self.reverse_position(ref_price, 'short', timestamp, "延迟反转-上一根触上轨后回调确认")
else: else:
prev_body_low = min(prev_row['open'], prev_row['close']) prev_body_low = min(prev_row['open'], prev_row['close'])
if low <= prev_body_low: if low <= prev_body_low:
return self.reverse_position(prev_body_low, 'short', timestamp, "延迟反转-跌破上一根实体确认") return 'short', "延迟反转-跌破上一根实体确认", prev_body_low
elif self.delay_reverse_type == 'short_to_long': elif self.delay_reverse_type == 'short_to_long':
# 情况1/2:触下轨后,同K或下一K反弹到记录下轨价 # 情况1触下轨后K反弹到记录下轨价
if offset <= 1 and high >= self.delay_reverse_price: if offset == 1 and high >= self.delay_reverse_price:
tag = "K反弹确认" if offset == 0 else "次K反弹确认" return 'long', "延迟反转-次K反弹确认", self.delay_reverse_price
return self.reverse_position(self.delay_reverse_price, 'long', timestamp, f"延迟反转-{tag}")
# 情况3持续等待动态追踪上一根K线条件 # 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None: if offset >= 2 and prev_row is not None:
prev_touch_lower = prev_row['low'] <= prev_row['lower'] prev_lower = prev_row['lower']
prev_touch_lower = pd.notna(prev_lower) and prev_row['low'] <= prev_lower
if prev_touch_lower: if prev_touch_lower:
ref_price = prev_row['lower'] if high >= prev_lower:
if high >= ref_price: return 'long', "延迟反转-上一根触下轨后反弹确认", prev_lower
return self.reverse_position(ref_price, 'long', timestamp, "延迟反转-上一根触下轨后反弹确认")
else: else:
prev_body_high = max(prev_row['open'], prev_row['close']) prev_body_high = max(prev_row['open'], prev_row['close'])
if high >= prev_body_high: if high >= prev_body_high:
return self.reverse_position(prev_body_high, 'long', timestamp, "延迟反转-突破上一根实体确认") return 'long', "延迟反转-突破上一根实体确认", prev_body_high
return False return None
def open_position(self, price, direction, timestamp, reason): def open_position(self, price, direction, timestamp, reason):
"""开仓或加仓""" """开仓或加仓"""
@@ -459,8 +461,8 @@ class BollingerBandBacktest:
f"可用资金: {self.capital:.4f}U | {reason}") f"可用资金: {self.capital:.4f}U | {reason}")
return True return True
def check_stop_loss(self, high, low, timestamp): def check_stop_loss(self, high, low):
"""检查止损""" """检查当前收盘K是否触发止损信号"""
if self.position == 0: if self.position == 0:
return False return False
@@ -470,16 +472,10 @@ class BollingerBandBacktest:
else: else:
unrealized_pnl = abs(self.position) * (self.entry_price - stop_price) unrealized_pnl = abs(self.position) * (self.entry_price - stop_price)
# 止损条件亏损达到保证金的50% return unrealized_pnl <= -self.total_margin * self.stop_loss_ratio
if unrealized_pnl <= -self.total_margin * self.stop_loss_ratio:
logger.warning(f"触发止损!浮亏: {unrealized_pnl:.2f}U | 保证金: {self.total_margin:.2f}U")
self.close_position(stop_price, 1.0, timestamp, "止损")
return True
return False
def run_backtest(self, start_date, end_date): def run_backtest(self, start_date, end_date):
"""运行回测""" """运行回测开仓当K触发平仓/止损仍按下一根K开盘执行"""
# 重置状态,支持同一实例重复回测 # 重置状态,支持同一实例重复回测
self.capital = self.initial_capital self.capital = self.initial_capital
self.position = 0 self.position = 0
@@ -533,66 +529,101 @@ class BollingerBandBacktest:
logger.info(f"加载数据: {len(df)} 根K线") logger.info(f"加载数据: {len(df)} 根K线")
logger.info(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}") logger.info(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}")
# 加载对应时间范围的1分钟数据用于中轨平半确认
one_min_start = int(df['timestamp'].min())
one_min_end = int(df['timestamp'].max()) + 300000
self.load_one_minute_cache(one_min_start, one_min_end)
# 计算布林带 # 计算布林带
df = self.calculate_bollinger_bands(df) df = self.calculate_bollinger_bands(df)
# 逐根K线回测 if len(df) <= self.bb_period + 1:
for i in range(self.bb_period, len(df)): logger.error("数据不足,无法执行回测")
return None
# 逐根K线回测开仓当K触发平仓/止损下一K开盘执行
for i in range(self.bb_period, len(df) - 1):
row = df.iloc[i] row = df.iloc[i]
prev_row = df.iloc[i-1] if i > 0 else None prev_row = df.iloc[i-1] if i > 0 else None
next_row = df.iloc[i + 1]
timestamp = row['datetime'].strftime('%Y-%m-%d %H:%M') signal_dt = row['datetime']
signal_ts = signal_dt.strftime('%Y-%m-%d %H:%M')
five_min_ts = int(row['timestamp']) five_min_ts = int(row['timestamp'])
execute_ts = next_row['datetime'].strftime('%Y-%m-%d %H:%M')
next_open = float(next_row['open']) if pd.notna(next_row['open']) else None
high = row['high'] high = row['high']
low = row['low'] low = row['low']
upper = row['upper'] upper = row['upper']
lower = row['lower'] lower = row['lower']
middle = row['middle'] middle = row['middle']
# 先处理当前时刻返佣到账 # 先处理当前收盘时刻返佣到账
self.apply_pending_rebates(row['datetime']) self.apply_pending_rebates(signal_dt)
# 检查止损 if pd.isna(upper) or pd.isna(lower) or pd.isna(middle):
if self.check_stop_loss(high, low, timestamp): continue
if next_open is None:
continue continue
# 已处于延迟反转状态时,先执行确认逻辑 # 检查止损收盘确认下一K开盘平仓
if self.check_stop_loss(high, low):
logger.warning(f"[{signal_ts}] 触发止损信号下一K开盘执行")
self.close_position(next_open, 1.0, execute_ts, f"止损-收盘确认({signal_ts})")
continue
# 已处于延迟反转状态时,先检查确认逻辑
if self.delay_reverse_price is not None: if self.delay_reverse_price is not None:
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp): reversal_signal = self.check_delay_reversal_signal(i, row, prev_row)
if reversal_signal is not None and self.position != 0:
new_direction, reason, reversal_price = reversal_signal
if reversal_price is None or pd.isna(reversal_price):
reversal_price = float(row['close'])
close_side = "" if self.position > 0 else ""
open_side = "" if new_direction == 'long' else ""
self.close_position(
reversal_price,
1.0,
signal_ts,
f"{reason}-当K确认({signal_ts})-平{close_side}"
)
open_price = self.apply_entry_slippage(reversal_price, new_direction)
self.open_position(
open_price,
new_direction,
signal_ts,
f"{reason}-当K确认({signal_ts})-开{open_side}"
)
self.mid_closed_half = False
self.clear_delay_reversal()
continue continue
# === 中轨平仓逻辑 === # === 中轨平仓逻辑 ===
if self.position != 0: if self.position != 0:
had_mid_closed_half = self.mid_closed_half
if self.position > 0: # 多仓 if self.position > 0: # 多仓
# 1m顺序确认先上方再回踩中轨平50% # 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if not self.mid_closed_half and low <= middle <= high: if had_mid_closed_half and low <= self.entry_price:
should_close_half, reason_1m = self.should_close_half_by_1m_trend( self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
five_min_ts, middle, 'long' entry_price = self.apply_entry_slippage(next_open, 'short')
) self.open_position(entry_price, 'short', execute_ts, f"回开仓价反手开空-收盘确认({signal_ts})")
if should_close_half: self.mid_closed_half = False
self.close_position(middle, 0.5, timestamp, f"触中轨平50%-{reason_1m}") continue
self.mid_closed_half = True # 触中轨平半(收盘确认)
# 继续回落到开仓均价全平 if not had_mid_closed_half and low <= middle <= high:
if self.position != 0 and self.mid_closed_half and low <= self.entry_price: self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.close_position(self.entry_price, 1.0, timestamp, "回开仓价全平") self.mid_closed_half = True
continue
else: # 空仓 else: # 空仓
# 1m顺序确认先下方再反抽中轨平50% # 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if not self.mid_closed_half and low <= middle <= high: if had_mid_closed_half and high >= self.entry_price:
should_close_half, reason_1m = self.should_close_half_by_1m_trend( self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
five_min_ts, middle, 'short' entry_price = self.apply_entry_slippage(next_open, 'long')
) self.open_position(entry_price, 'long', execute_ts, f"回开仓价反手开多-收盘确认({signal_ts})")
if should_close_half: self.mid_closed_half = False
self.close_position(middle, 0.5, timestamp, f"触中轨平50%-{reason_1m}") continue
self.mid_closed_half = True # 触中轨平半(收盘确认)
# 继续反弹到开仓均价全平 if not had_mid_closed_half and low <= middle <= high:
if self.position != 0 and self.mid_closed_half and high >= self.entry_price: self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.close_position(self.entry_price, 1.0, timestamp, "回开仓价全平") self.mid_closed_half = True
continue
# === 开仓与加仓逻辑 === # === 开仓与加仓逻辑 ===
if self.position == 0: # 空仓 if self.position == 0: # 空仓
@@ -601,24 +632,22 @@ class BollingerBandBacktest:
# 触上轨开空 # 触上轨开空
if high >= upper: if high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper) entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', timestamp, "触上轨开空") self.open_position(entry_price, 'short', signal_ts, f"触上轨开空-当K触发({signal_ts})")
# 触下轨开多 # 触下轨开多
elif low <= lower: elif low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower) entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', timestamp, "触下轨开多") self.open_position(entry_price, 'long', signal_ts, f"触下轨开多-当K触发({signal_ts})")
continue continue
# 有持仓:先检查是否触发延迟反转(核心) # 有持仓:先检查是否触发延迟反转(核心)
if self.position > 0 and high >= upper: if self.position > 0 and high >= upper:
self.mark_delay_reversal('long_to_short', upper, i, timestamp) self.mark_delay_reversal('long_to_short', upper, i, signal_ts)
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp): continue
continue
elif self.position < 0 and low <= lower: elif self.position < 0 and low <= lower:
self.mark_delay_reversal('short_to_long', lower, i, timestamp) self.mark_delay_reversal('short_to_long', lower, i, signal_ts)
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp): continue
continue
# 进入延迟反转等待后,不再执行加仓 # 进入延迟反转等待后,不再执行加仓
if self.delay_reverse_price is not None: if self.delay_reverse_price is not None:
@@ -628,10 +657,13 @@ class BollingerBandBacktest:
if self.position_count == 1: if self.position_count == 1:
if self.position > 0 and low <= lower: if self.position > 0 and low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower) entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', timestamp, "触下轨加多") self.open_position(entry_price, 'long', signal_ts, f"触下轨加多-当K触发({signal_ts})")
elif self.position < 0 and high >= upper: elif self.position < 0 and high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper) entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', timestamp, "触上轨加空") self.open_position(entry_price, 'short', signal_ts, f"触上轨加空-当K触发({signal_ts})")
# 回测末尾再处理一次返佣到账
self.apply_pending_rebates(df.iloc[-1]['datetime'])
# 最后平仓 # 最后平仓
if self.position != 0: if self.position != 0:
@@ -680,7 +712,9 @@ class BollingerBandBacktest:
# 保存交易记录 # 保存交易记录
trades_df = pd.DataFrame(self.trades) trades_df = pd.DataFrame(self.trades)
output_file = f'bb_backtest_{self.current_run_label}_trades.csv' output_dir = Path(__file__).parent / 'backtest_outputs' / 'trades'
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f'bb_backtest_{self.current_run_label}_trades.csv'
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig') trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
logger.info(f"\n交易记录已保存到: {output_file}") logger.info(f"\n交易记录已保存到: {output_file}")
@@ -697,7 +731,7 @@ class BollingerBandBacktest:
'total_trades': total_trades, 'total_trades': total_trades,
'win_trades': win_trades, 'win_trades': win_trades,
'loss_trades': loss_trades, 'loss_trades': loss_trades,
'trades_file': output_file, 'trades_file': str(output_file),
'trades': self.trades 'trades': self.trades
} }
@@ -710,8 +744,8 @@ if __name__ == '__main__':
# 创建回测实例 # 创建回测实例
backtest = BollingerBandBacktest() backtest = BollingerBandBacktest()
# 运行回测2026年3月 # 运行回测2026年全年
result = backtest.run_backtest('2026-03-01', '2026-03-31') result = backtest.run_backtest('2026-01-01', '2026-12-31')
if result: if result:
logger.success(f"\n回测完成!最终收益率: {result['roi']:.2f}%") logger.success(f"\n回测完成!最终收益率: {result['roi']:.2f}%")

601
bb_delay_reversal_trade.py Normal file
View File

@@ -0,0 +1,601 @@
"""
布林带延迟反转策略 - 实盘交易
基于回测策略 bb_backtest_march_2026.py
使用API查询 + 浏览器自动化交易
"""
import time
import numpy as np
from datetime import datetime, timezone
from pathlib import Path
from loguru import logger
from bitmart.api_contract import APIContract
from bit_tools import openBrowser
from DrissionPage import ChromiumPage, ChromiumOptions
class BBDelayReversalConfig:
"""策略配置"""
# API凭证
API_KEY = "6104088c65a68d7e53df5d9395b67d78e555293a"
SECRET_KEY = "a8b14312330d8e6b9b09acfd972b34e32022fdfa9f2b06f0a0a31723b873fd01"
MEMO = "me"
# 合约
CONTRACT_SYMBOL = "ETHUSDT"
TRADE_URL = "https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT"
# 浏览器ID
BIT_ID = "62f9107d0c674925972084e282df55b3"
# 布林带参数
BB_PERIOD = 10
BB_STD = 2.5
# 仓位管理
LEVERAGE = 50
OPEN_TYPE = "isolated"
MARGIN_PCT = 0.01 # 首次开仓1%
# 运行参数
POLL_INTERVAL = 5
KLINE_STEP = 5
KLINE_HOURS = 2
class BBDelayReversalTrader:
"""布林带延迟反转交易器"""
def __init__(self, cfg: BBDelayReversalConfig = None):
self.cfg = cfg or BBDelayReversalConfig()
self.api = APIContract(
self.cfg.API_KEY, self.cfg.SECRET_KEY, self.cfg.MEMO,
timeout=(5, 15)
)
# 浏览器
self.page: ChromiumPage | None = None
self.page_start = True
self.last_page_open_time = 0.0
self.PAGE_REFRESH_INTERVAL = 1800
# 持仓状态
self.position = 0 # -1空, 0无, 1多
self.position_count = 0 # 0空仓, 1首次, 2加仓
self.entry_price = 0
self.current_amount = 0
self.total_margin = 0
# 延迟反转状态
self.delay_reverse_price = None
self.delay_reverse_type = None # 'long_to_short' 或 'short_to_long'
self.delay_reverse_kline_id = None
# 中轨平仓
self.mid_closed_half = False
# 交易控制
self.last_trade_time = 0.0
self.last_kline_id = None
# 日志
self.log_dir = Path(__file__).resolve().parent
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}"
)
# ========== API查询方法 ==========
def get_klines(self) -> list | None:
"""获取5分钟K线"""
try:
end_time = int(time.time())
start_time = end_time - 3600 * self.cfg.KLINE_HOURS
resp = self.api.get_kline(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
step=self.cfg.KLINE_STEP,
start_time=start_time,
end_time=end_time
)[0]
if resp.get("code") != 1000:
logger.error(f"获取K线失败: {resp}")
return None
data = resp["data"]
klines = []
for k in data:
klines.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"]),
})
klines.sort(key=lambda x: x["id"])
return klines
except Exception as e:
logger.error(f"获取K线异常: {e}")
return None
def get_current_price(self) -> float | None:
"""获取当前价格"""
try:
end_time = int(time.time())
resp = self.api.get_kline(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
step=1,
start_time=end_time - 300,
end_time=end_time
)[0]
if resp.get("code") == 1000 and resp["data"]:
return float(resp["data"][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_balance(self) -> float | None:
"""获取可用余额"""
try:
resp = self.api.get_assets_detail()[0]
if resp.get("code") == 1000:
data = resp["data"]
if isinstance(data, dict):
return float(data.get("available_balance", 0))
elif isinstance(data, list):
for asset in data:
if asset.get("currency") == "USDT":
return float(asset.get("available_balance", 0))
return None
except Exception as e:
logger.error(f"查询余额异常: {e}")
return None
def get_position_status(self) -> bool:
"""查询持仓状态"""
try:
resp = self.api.get_position(contract_symbol=self.cfg.CONTRACT_SYMBOL)[0]
if resp.get("code") != 1000:
logger.error(f"查询持仓失败: {resp}")
return False
positions = resp["data"]
if not positions:
self.position = 0
self.position_count = 0
self.entry_price = 0
self.current_amount = 0
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"])
logger.debug(f"持仓: {'' if self.position > 0 else ''} | "
f"价格={self.entry_price:.2f} | 数量={self.current_amount:.4f}")
return True
except Exception as e:
logger.error(f"查询持仓异常: {e}")
return False
def set_leverage(self) -> bool:
"""设置杠杆"""
try:
resp = self.api.post_submit_leverage(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
leverage=str(self.cfg.LEVERAGE),
open_type=self.cfg.OPEN_TYPE
)[0]
if resp.get("code") == 1000:
logger.success(f"杠杆设置成功: {self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}")
return True
else:
logger.error(f"杠杆设置失败: {resp}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
# ========== 布林带计算 ==========
def calc_bollinger(self, closes: list):
"""计算布林带"""
if len(closes) < self.cfg.BB_PERIOD:
return None
arr = np.array(closes[-self.cfg.BB_PERIOD:], dtype=float)
mid = arr.mean()
std = arr.std(ddof=0)
upper = mid + self.cfg.BB_STD * std
lower = mid - self.cfg.BB_STD * std
return mid, upper, lower
# ========== 浏览器自动化 ==========
def open_browser(self) -> bool:
"""打开浏览器"""
try:
bit_port = openBrowser(id=self.cfg.BIT_ID)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
self.last_page_open_time = time.time()
return True
except Exception as e:
logger.error(f"打开浏览器失败: {e}")
return False
def click_safe(self, xpath, sleep=0.5) -> bool:
"""安全点击"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
ele.click(by_js=True)
time.sleep(sleep)
return True
except Exception as e:
logger.warning(f"点击失败: {e}")
return False
def browser_open_position(self, direction: str, usdt_amount: float) -> bool:
"""浏览器开仓"""
try:
logger.info(f"浏览器操作: 开{'' if direction == 'long' else ''} {usdt_amount}U")
# 点击市价
self.click_safe('x://button[normalize-space(text()) ="市价"]')
# 输入金额
self.page.ele('x://*[@id="size_0"]').input(vals=usdt_amount, clear=True)
time.sleep(0.5)
# 点击开仓按钮
if direction == 'long':
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
else:
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(2)
return True
except Exception as e:
logger.error(f"浏览器开仓失败: {e}")
return False
def browser_close_position(self, ratio: float = 1.0) -> bool:
"""浏览器平仓"""
try:
logger.info(f"浏览器操作: 平仓{int(ratio*100)}%")
if ratio >= 0.99:
# 全平
self.click_safe('x://span[normalize-space(text()) ="市价"]')
else:
# 平半
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
if ratio == 0.5:
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
if self.position > 0:
self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
else:
self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
time.sleep(2)
return True
except Exception as e:
logger.error(f"浏览器平仓失败: {e}")
return False
# ========== 延迟反转逻辑 ==========
def mark_delay_reversal(self, reverse_type: str, trigger_price: float, kline_id: int):
"""标记延迟反转"""
self.delay_reverse_type = reverse_type
self.delay_reverse_price = trigger_price
self.delay_reverse_kline_id = kline_id
logger.info(f"触发延迟反转: {reverse_type} @ {trigger_price:.2f}")
def clear_delay_reversal(self):
"""清除延迟反转状态"""
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_id = None
def check_delay_reversal(self, current_kline, prev_kline, kline_index) -> tuple | None:
"""检查延迟反转确认"""
if self.position == 0 or self.delay_reverse_price is None:
return None
if self.delay_reverse_kline_id is None:
return None
offset = kline_index - self.delay_reverse_kline_id
if offset <= 0:
return None
high = current_kline['high']
low = current_kline['low']
if self.delay_reverse_type == 'long_to_short':
# 多转空: 回调到记录价格
if offset == 1 and low <= self.delay_reverse_price:
return 'short', self.delay_reverse_price, "次K回调确认"
if offset >= 2 and prev_kline:
prev_body_low = min(prev_kline['open'], prev_kline['close'])
if low <= prev_body_low:
return 'short', prev_body_low, "跌破上一根实体"
elif self.delay_reverse_type == 'short_to_long':
# 空转多: 反弹到记录价格
if offset == 1 and high >= self.delay_reverse_price:
return 'long', self.delay_reverse_price, "次K反弹确认"
if offset >= 2 and prev_kline:
prev_body_high = max(prev_kline['open'], prev_kline['close'])
if high >= prev_body_high:
return 'long', prev_body_high, "突破上一根实体"
return None
# ========== 主循环 ==========
def run(self):
"""策略主循环"""
logger.info("=" * 60)
logger.info(f"布林带延迟反转策略启动")
logger.info(f"BB({self.cfg.BB_PERIOD}, {self.cfg.BB_STD}) | "
f"{self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}")
logger.info("=" * 60)
# 设置杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,退出")
return
# 初始持仓
if not self.get_position_status():
logger.error("初始持仓查询失败,退出")
return
logger.info(f"初始持仓: {self.position}")
page_start = True
kline_history = []
while True:
try:
# ===== 浏览器管理 =====
if page_start:
for i in range(5):
if self.open_browser():
logger.info("浏览器打开成功")
break
else:
logger.error("打开浏览器失败")
return
self.page.get(self.cfg.TRADE_URL)
time.sleep(2)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
page_start = False
# 定期刷新浏览器
if time.time() - self.last_page_open_time >= self.PAGE_REFRESH_INTERVAL:
logger.info("浏览器刷新")
try:
self.page.close()
except:
pass
page_start = True
time.sleep(3)
continue
# ===== 获取K线 =====
klines = self.get_klines()
if not klines or len(klines) < self.cfg.BB_PERIOD + 1:
logger.warning(f"K线数据不足: {len(klines) if klines else 0}")
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 使用已收盘K线计算BB
closed_klines = klines[:-1]
current_kline = klines[-1]
if len(closed_klines) < self.cfg.BB_PERIOD:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 计算布林带
closes = [k['close'] for k in closed_klines]
bb = self.calc_bollinger(closes)
if bb is None:
time.sleep(self.cfg.POLL_INTERVAL)
continue
bb_mid, bb_upper, bb_lower = bb
# 获取当前价格
current_price = self.get_current_price()
if current_price is None:
time.sleep(self.cfg.POLL_INTERVAL)
continue
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
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}"
)
# 同步持仓
if not self.get_position_status():
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 避免同一K线重复触发
if kline_id == self.last_kline_id:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# ===== 延迟反转确认 =====
if self.delay_reverse_price is not None and len(kline_history) > 0:
prev_kline = kline_history[-1] if len(kline_history) > 0 else None
reversal = self.check_delay_reversal(
current_kline, prev_kline, len(kline_history)
)
if reversal:
new_direction, reversal_price, reason = reversal
logger.info(f"延迟反转确认: {reason} @ {reversal_price:.2f}")
# 平仓
self.browser_close_position(1.0)
time.sleep(2)
self.get_position_status()
if self.position == 0:
# 反向开仓
balance = self.get_balance()
if balance:
usdt_amount = round(balance * self.cfg.MARGIN_PCT, 2)
self.browser_open_position(new_direction, usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position != 0:
self.position_count = 1
self.mid_closed_half = False
self.clear_delay_reversal()
self.last_kline_id = kline_id
continue
# ===== 中轨平仓 =====
if self.position != 0 and touched_middle:
if not self.mid_closed_half:
logger.info("触中轨平50%")
self.browser_close_position(0.5)
time.sleep(2)
self.mid_closed_half = True
self.last_kline_id = kline_id
continue
elif self.mid_closed_half:
# 回到开仓价全平+反手
if (self.position > 0 and cur_low <= self.entry_price) or \
(self.position < 0 and cur_high >= self.entry_price):
logger.info("回到开仓价,全平+反手")
old_direction = 'long' if self.position > 0 else 'short'
new_direction = 'short' if old_direction == 'long' else 'long'
self.browser_close_position(1.0)
time.sleep(2)
self.get_position_status()
if self.position == 0:
balance = self.get_balance()
if balance:
usdt_amount = round(balance * self.cfg.MARGIN_PCT, 2)
self.browser_open_position(new_direction, usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position != 0:
self.position_count = 1
self.mid_closed_half = False
self.last_kline_id = kline_id
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 touched_upper:
logger.info(f"空仓触上轨,开空 {usdt_amount}U")
self.browser_open_position('short', usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position == -1:
self.position_count = 1
self.last_kline_id = kline_id
elif touched_lower:
logger.info(f"空仓触下轨,开多 {usdt_amount}U")
self.browser_open_position('long', usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position == 1:
self.position_count = 1
self.last_kline_id = kline_id
# ===== 延迟反转触发 =====
elif self.position > 0 and touched_upper:
logger.info("多仓触上轨,标记延迟反转")
self.mark_delay_reversal('long_to_short', bb_upper, len(kline_history))
self.last_kline_id = kline_id
elif self.position < 0 and touched_lower:
logger.info("空仓触下轨,标记延迟反转")
self.mark_delay_reversal('short_to_long', bb_lower, len(kline_history))
self.last_kline_id = kline_id
# ===== 加仓 =====
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")
self.browser_open_position('long', usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position == 1:
self.position_count = 2
self.last_kline_id = kline_id
elif self.position < 0 and touched_upper:
logger.info(f"空仓触上轨,加仓 {usdt_amount}U")
self.browser_open_position('short', usdt_amount)
time.sleep(2)
self.get_position_status()
if self.position == -1:
self.position_count = 2
self.last_kline_id = kline_id
# 更新K线历史
kline_history.append(current_kline)
if len(kline_history) > 100:
kline_history = kline_history[-100:]
time.sleep(self.cfg.POLL_INTERVAL)
except KeyboardInterrupt:
logger.info("用户中断")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
page_start = True
time.sleep(10)
if __name__ == "__main__":
trader = BBDelayReversalTrader()
trader.run()

File diff suppressed because it is too large Load Diff

267
bitmart/框架.py Normal file
View File

@@ -0,0 +1,267 @@
import time
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
class BitmartFuturesTransaction:
def __init__(self, bit_id):
self.page: ChromiumPage | None = None
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
self.start = 0 # 持仓状态: -1 空, 0 无, 1 多
self.direction = None
self.pbar = tqdm(total=30, desc="等待K线", ncols=80)
self.last_kline_time = None
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
self.open_type = "cross" # 全仓模式(你的“成本开仓”需求)
self.risk_percent = 0.01 # 每次开仓使用可用余额的 1%
self.open_avg_price = None # 开仓价格
self.current_amount = None # 持仓量
self.bit_id = bit_id
def get_klines(self):
"""获取最近3根30分钟K线step=30"""
try:
end_time = int(time.time())
# 获取足够多的条目确保有最新3根
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=30, # 30分钟
start_time=end_time - 3600 * 10, # 取最近10小时
end_time=end_time
)[0]["data"]
# 每根: [timestamp, open, high, low, close, volume]
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 # 最近3根: kline_1 (最老), kline_2, kline_3 (最新)
except Exception as e:
logger.error(f"获取K线异常: {e}")
self.ding(error=True, msg="获取K线异常")
return None
def get_current_price(self):
"""获取当前最新价格,用于计算张数"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1分钟
start_time=end_time - 3600 * 3, # 取最近10小时
end_time=end_time
)[0]
if response['code'] == 1000:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用USDT余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
# 获取当前持仓方向
def get_position_status(self):
"""获取当前持仓方向"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
return True
self.start = 1 if positions[0]['position_type'] == 1 else -1
self.open_avg_price = positions[0]['open_avg_price']
self.current_amount = positions[0]['current_amount']
self.position_cross = positions[0]["position_cross"]
return True
else:
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
# 设置杠杆和全仓
def set_leverage(self):
"""程序启动时设置全仓 + 高杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def openBrowser(self):
"""打开 TGE 对应浏览器实例"""
try:
bit_port = openBrowser(id=self.bit_id)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
return True
except:
return False
def take_over_browser(self):
"""接管浏览器"""
try:
co = ChromiumOptions()
co.set_local_port(self.tge_port)
self.page = ChromiumPage(addr_or_opts=co)
self.page.set.window.max()
return True
except:
return False
def close_extra_tabs(self):
"""关闭多余 tab"""
try:
for idx, tab in enumerate(self.page.get_tabs()):
if idx > 0:
tab.close()
return True
except:
return False
def click_safe(self, xpath, sleep=0.5):
"""安全点击"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
ele.scroll.to_see(center=True)
time.sleep(sleep)
ele.click()
return True
except:
return False
def 全平仓(self):
self.click_safe('x://span[normalize-space(text()) ="市价"]')
def 平一半多仓(self):
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
def 平一半空仓(self):
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None):
"""
marketPriceLongOrder 市价最多或者做空1是做多-1是做空
limitPriceShortOrder 限价最多或者做空
"""
if marketPriceLongOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif marketPriceLongOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
if limitPriceShortOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif limitPriceShortOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
def ding(self, text, error=False):
logger.info(text)
def action(self):
# 启动时设置全仓高杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,程序继续运行但可能下单失败")
return
# 1. 打开浏览器
if not self.openBrowser():
self.ding("打开 TGE 失败!", error=True)
return
logger.info("TGE 端口获取成功")
self.get_klines()
# self.close_extra_tabs()
# self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
#
self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.click_safe('x://button[normalize-space(text()) ="限价"]')
#
# self.page.ele('x://*[@id="price_0"]').input(vals=3000, clear=True)
# self.page.ele('x://*[@id="size_0"]').input(1)
# self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
# self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
if __name__ == '__main__':
BitmartFuturesTransaction(bit_id="f2320f57e24c45529a009e1541e25961").action()

View File

@@ -28,12 +28,12 @@ class BitMartETH5M(Model):
def calculate_bollinger_bands(df, period=10, std_dev=2.5): def calculate_bollinger_bands(df, period=10, std_dev=2.5):
"""计算布林带""" """计算布林带右移1根与回测口径一致"""
df['sma'] = df['close'].rolling(window=period).mean() df['sma'] = df['close'].rolling(window=period).mean()
df['std'] = df['close'].rolling(window=period).std() df['std'] = df['close'].rolling(window=period).std()
df['bb_upper'] = df['sma'] + std_dev * df['std'] df['bb_upper'] = (df['sma'] + std_dev * df['std']).shift(1)
df['bb_mid'] = df['sma'] df['bb_mid'] = df['sma'].shift(1)
df['bb_lower'] = df['sma'] - std_dev * df['std'] df['bb_lower'] = (df['sma'] - std_dev * df['std']).shift(1)
return df return df
@@ -504,20 +504,24 @@ if __name__ == '__main__':
try: try:
print("正在生成回测可视化图表...") print("正在生成回测可视化图表...")
output_dir = Path(__file__).parent / 'backtest_outputs' / 'charts'
output_dir.mkdir(parents=True, exist_ok=True)
# 生成图表数据 # 生成图表数据
chart_data, trades_markers = generate_chart_data( chart_data, trades_markers = generate_chart_data(
start_date='2026-03-01', start_date='2026-03-01',
end_date='2026-03-03', end_date='2026-03-03',
trades_file='bb_backtest_march_2026_trades.csv' trades_file=str(Path(__file__).parent / 'backtest_outputs' / 'trades' / 'bb_backtest_march_2026_trades.csv')
) )
print(f"📊 K线数据: {len(chart_data)}") print(f"📊 K线数据: {len(chart_data)}")
print(f"📍 交易标记: {len(trades_markers)}") print(f"📍 交易标记: {len(trades_markers)}")
# 生成HTML # 生成HTML
generate_html(chart_data, trades_markers, 'bb_backtest_visualization.html') output_file = output_dir / 'bb_backtest_visualization.html'
generate_html(chart_data, trades_markers, str(output_file))
print("\n🎉 完成!请在浏览器中打开 bb_backtest_visualization.html 查看图表") print(f"\n🎉 完成!请在浏览器中打开 {output_file} 查看图表")
finally: finally:
db.close() db.close()