第一版策略

This commit is contained in:
ddrwode
2026-02-26 16:34:30 +08:00
parent 0edf741849
commit cf499863a3
19 changed files with 6067 additions and 21 deletions

View File

@@ -36,8 +36,24 @@ class BBConfig:
# Risk management
max_daily_loss: float = 150.0 # stop trading after this daily loss
max_daily_loss_pct: float = 0.0 # if >0, daily loss limit = equity * pct (overrides fixed)
stop_loss_pct: float = 0.0 # 0 = disabled; e.g. 0.02 = 2% SL from entry
# Liquidation
liq_enabled: bool = True # enable liquidation simulation
cross_margin: bool = True # 全仓模式: 仅当 equity<=0 爆仓; False=逐仓: 按仓位保证金算强平价
maint_margin_rate: float = 0.005 # 逐仓时用: 0.5% 维持保证金率
# Slippage: applied to each trade execution price
slippage_pct: float = 0.0005 # 0.05% slippage per trade
# 成交价模式: False=理想(在触轨的极价成交), True=真实(在K线收盘价成交)
# 实盘检测到触及布林带后以市价单成交,通常接近收盘价
fill_at_close: bool = False
# Max single order notional (market capacity limit, USDT)
max_notional: float = 0.0 # 0 = unlimited; e.g. 500000 = 50万U max per order
# Dynamic sizing: if > 0, margin = equity * margin_pct (overrides margin_per_trade)
margin_pct: float = 0.0 # e.g. 0.01 = 1% of equity per trade
@@ -124,6 +140,7 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
day_pnl = 0.0
day_stopped = False
current_day = None
day_start_equity = cfg.initial_capital # equity at start of each day
# Delayed rebate tracking
pending_rebate = 0.0 # fees from previous day to be rebated
@@ -151,6 +168,12 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
if position == 0:
return
# Apply slippage: closing long sells lower, closing short buys higher
if position == 1:
exit_price = exit_price * (1 - cfg.slippage_pct)
else:
exit_price = exit_price * (1 + cfg.slippage_pct)
if position == 1:
gross = entry_qty * (exit_price - entry_price)
else:
@@ -193,6 +216,12 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
nonlocal balance, total_fee, day_pnl, today_fees
nonlocal pyramid_count, last_add_margin
# Apply slippage: buy higher, sell lower
if side == "long" or (is_add and position == 1):
price = price * (1 + cfg.slippage_pct)
else:
price = price * (1 - cfg.slippage_pct)
if is_add and cfg.pyramid_step > 0:
# 递增加仓: margin = equity * (margin_pct + step * (count+1))
equity = balance + unrealised(price)
@@ -210,6 +239,12 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
if margin <= 0:
return
notional = margin * cfg.leverage
# Cap notional to market capacity limit
if cfg.max_notional > 0 and notional > cfg.max_notional:
notional = cfg.max_notional
margin = notional / cfg.leverage
qty = notional / price
fee = notional * cfg.fee_rate
@@ -245,9 +280,10 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
if bar_day is not None and bar_day != current_day:
# New day: move today's fees to pending, reset
if cfg.rebate_pct > 0:
pending_rebate = today_fees * cfg.rebate_pct
pending_rebate += today_fees * cfg.rebate_pct
today_fees = 0.0
rebate_applied_today = False
day_start_equity = balance + unrealised(arr_close[i])
day_pnl = 0.0
day_stopped = False
current_day = bar_day
@@ -266,7 +302,7 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
out_position[i] = position
continue
# Daily loss check
# Daily loss check (percentage-based if configured, else fixed)
if day_stopped:
out_equity[i] = balance + unrealised(arr_close[i])
out_balance[i] = balance
@@ -274,7 +310,12 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
continue
cur_equity = balance + unrealised(arr_close[i])
if day_pnl + unrealised(arr_close[i]) <= -cfg.max_daily_loss:
if cfg.max_daily_loss_pct > 0:
# percentage-based: use start-of-day equity
daily_loss_limit = day_start_equity * cfg.max_daily_loss_pct
else:
daily_loss_limit = cfg.max_daily_loss
if day_pnl + unrealised(arr_close[i]) <= -daily_loss_limit:
close_position(arr_close[i], i)
day_stopped = True
out_equity[i] = balance
@@ -291,39 +332,124 @@ def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
sl_price = entry_price * (1 + cfg.stop_loss_pct)
close_position(sl_price, i)
# Liquidation check
# 全仓(cross_margin): 仅当 账户权益<=0 时爆仓,整仓担保
# 逐仓: 按仓位保证金算强平价
if position != 0 and cfg.liq_enabled and entry_margin > 0:
cur_equity = balance + unrealised(arr_close[i])
upnl = unrealised(arr_close[i])
if cfg.cross_margin:
# 全仓: 只有权益归零才爆仓
if cur_equity <= 0:
balance += upnl # 实现亏损
balance = max(0.0, balance)
day_pnl += upnl
trades.append(BBTrade(
side="long" if position == 1 else "short",
entry_price=entry_price, exit_price=arr_close[i],
entry_time=entry_time, exit_time=ts_index[i],
margin=entry_margin, leverage=cfg.leverage, qty=entry_qty,
gross_pnl=upnl, fee=0.0, net_pnl=upnl,
))
position = 0
entry_price = 0.0
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
pyramid_count = 0
last_add_margin = 0.0
out_equity[i] = balance
out_balance[i] = balance
out_position[i] = 0
if balance <= 0:
out_equity[i:] = 0.0
out_balance[i:] = 0.0
break
else:
# 逐仓: 按强平价
liq_threshold = 1.0 / cfg.leverage * (1 - cfg.maint_margin_rate)
if position == 1:
liq_price = entry_price * (1 - liq_threshold)
if arr_low[i] <= liq_price:
balance -= entry_margin
day_pnl -= entry_margin
trades.append(BBTrade(
side="long", entry_price=entry_price, exit_price=liq_price,
entry_time=entry_time, exit_time=ts_index[i],
margin=entry_margin, leverage=cfg.leverage, qty=entry_qty,
gross_pnl=-entry_margin, fee=0.0, net_pnl=-entry_margin,
))
position = 0
entry_price = 0.0
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
pyramid_count = 0
last_add_margin = 0.0
if balance <= 0:
balance = 0.0
out_equity[i] = 0.0
out_balance[i] = 0.0
out_position[i] = 0
out_equity[i:] = 0.0
out_balance[i:] = 0.0
break
elif position == -1:
liq_price = entry_price * (1 + liq_threshold)
if arr_high[i] >= liq_price:
balance -= entry_margin
day_pnl -= entry_margin
trades.append(BBTrade(
side="short", entry_price=entry_price, exit_price=liq_price,
entry_time=entry_time, exit_time=ts_index[i],
margin=entry_margin, leverage=cfg.leverage, qty=entry_qty,
gross_pnl=-entry_margin, fee=0.0, net_pnl=-entry_margin,
))
position = 0
entry_price = 0.0
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
pyramid_count = 0
last_add_margin = 0.0
if balance <= 0:
balance = 0.0
out_equity[i:] = 0.0
out_balance[i:] = 0.0
break
# Signal detection: use high/low to check if price touched BB
touched_upper = arr_high[i] >= arr_upper[i]
touched_lower = arr_low[i] <= arr_lower[i]
# 成交价: fill_at_close=True 时用收盘价(模拟实盘市价单),否则用触轨价(理想化)
fill_price = arr_close[i] if cfg.fill_at_close else None
if touched_upper and touched_lower:
# Both touched in same bar (wide bar) — skip, too volatile
pass
elif touched_upper:
# Price touched upper BB → go short
exec_price = fill_price if fill_price is not None else arr_upper[i]
if position == 1:
# Close long at upper BB price
close_position(arr_upper[i], i)
close_position(exec_price, i)
if position == 0:
# Open short
open_position("short", arr_upper[i], i)
open_position("short", exec_price, 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)
open_position("short", exec_price, i, is_add=True)
elif touched_lower:
# Price touched lower BB → go long
exec_price = fill_price if fill_price is not None else arr_lower[i]
if position == -1:
# Close short at lower BB price
close_position(arr_lower[i], i)
close_position(exec_price, i)
if position == 0:
# Open long
open_position("long", arr_lower[i], i)
open_position("long", exec_price, 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)
open_position("long", exec_price, i, is_add=True)
# Record equity
out_equity[i] = balance + unrealised(arr_close[i])