第一版策略
This commit is contained in:
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user