This commit is contained in:
27942
2026-02-25 06:21:49 +08:00
parent c8fb43a700
commit 905ce34aa7
7 changed files with 1445 additions and 162 deletions

View File

@@ -49,6 +49,14 @@ class BBConfig:
rebate_pct: float = 0.0 # e.g. 0.70 = 70% rebate
rebate_hour_utc: int = 0 # hour in UTC when rebate arrives (0 = 8am UTC+8)
# Pyramid (加仓): add to position on repeated same-direction BB touch
pyramid_enabled: bool = False
pyramid_decay: float = 0.99 # each add uses margin * decay^n (n = add count)
pyramid_max: int = 10 # max number of adds (0 = unlimited)
# Increment mode: each add uses equity * (margin_pct + pyramid_step * n)
# e.g. step=0.01 → 1st open=1%, 1st add=2%, 2nd add=3% ...
pyramid_step: float = 0.0 # 0 = use decay mode; >0 = use increment mode
@dataclass
class BBTrade:
@@ -105,6 +113,8 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
pyramid_count = 0 # number of adds so far
last_add_margin = 0.0 # margin used in last open/add
trades: List[BBTrade] = []
total_fee = 0.0
@@ -136,6 +146,7 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
def close_position(exit_price, exit_idx):
nonlocal balance, position, entry_price, entry_time, entry_margin, entry_qty
nonlocal total_fee, total_rebate, day_pnl, today_fees
nonlocal pyramid_count, last_add_margin
if position == 0:
return
@@ -174,12 +185,23 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
pyramid_count = 0
last_add_margin = 0.0
def open_position(side, price, idx):
def open_position(side, price, idx, is_add=False):
nonlocal position, entry_price, entry_time, entry_margin, entry_qty
nonlocal balance, total_fee, day_pnl, today_fees
nonlocal pyramid_count, last_add_margin
if cfg.margin_pct > 0:
if is_add and cfg.pyramid_step > 0:
# 递增加仓: margin = equity * (margin_pct + step * (count+1))
equity = balance + unrealised(price)
pct = cfg.margin_pct + cfg.pyramid_step * (pyramid_count + 1)
margin = equity * pct
elif is_add:
# 衰减加仓: margin = last_add_margin * decay
margin = last_add_margin * cfg.pyramid_decay
elif cfg.margin_pct > 0:
equity = balance + unrealised(price) if position != 0 else balance
margin = equity * cfg.margin_pct
else:
@@ -196,11 +218,24 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
today_fees += fee
day_pnl -= fee
position = 1 if side == "long" else -1
entry_price = price
entry_time = ts_index[idx]
entry_margin = margin
entry_qty = qty
if is_add and position != 0:
# 加仓: weighted average entry price
old_notional = entry_qty * entry_price
new_notional = qty * price
entry_qty += qty
entry_price = (old_notional + new_notional) / entry_qty
entry_margin += margin
pyramid_count += 1
last_add_margin = margin
else:
# 新开仓
position = 1 if side == "long" else -1
entry_price = price
entry_time = ts_index[idx]
entry_margin = margin
entry_qty = qty
pyramid_count = 0
last_add_margin = margin
# Main loop
for i in range(n):
@@ -268,17 +303,27 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
if position == 1:
# Close long at upper BB price
close_position(arr_upper[i], i)
if position != -1:
if position == 0:
# Open short
open_position("short", arr_upper[i], i)
elif position == -1 and cfg.pyramid_enabled:
# Already short → add to short (pyramid)
can_add = cfg.pyramid_max <= 0 or pyramid_count < cfg.pyramid_max
if can_add:
open_position("short", arr_upper[i], i, is_add=True)
elif touched_lower:
# Price touched lower BB → go long
if position == -1:
# Close short at lower BB price
close_position(arr_lower[i], i)
if position != 1:
if position == 0:
# Open long
open_position("long", arr_lower[i], i)
elif position == 1 and cfg.pyramid_enabled:
# Already long → add to long (pyramid)
can_add = cfg.pyramid_max <= 0 or pyramid_count < cfg.pyramid_max
if can_add:
open_position("long", arr_lower[i], i, is_add=True)
# Record equity
out_equity[i] = balance + unrealised(arr_close[i])