回測引擎¶
在較高時間框架上的標準回測有一個根本性缺陷:訊號在已完成的 K 棒上計算,而成交則在 K 棒收盤時執行。4 小時 K 棒上的 MACD 交叉可能在 K 棒開始 2 小時後於 $49,800 觸發,但回測卻在 K 棒收盤時記錄成交——$51,200。誤差隨時間框架增大:1 分鐘資料可忽略不計,4 小時可達 $1,000+,日線可達 $5,000+(以 BTC 級別資產而言)。放大鏡技術 (Magnifier Technique) 透過在子 K 棒解析度模擬盤中執行來解決此問題,在訊號實際觸發的時刻成交。
為什麼這對交易者重要
誇大的回測報酬導致過度自信的倉位配置和意外的實盤回撤。一個以收盤價成交顯示 +40% 年報酬的策略,實際上可能只有 +25% — 更糟的情況是,將虧損策略偽裝成盈利策略。Magnifier 技術在子 K 棒解析度內產生成交價格(例如 4h 圖表的 15 分鐘刻度),讓交易者在投入資金前獲得真實的損益預期。
兩種執行模式¶
| 標準模式 | 放大鏡模式 | |
|---|---|---|
| 旗標 | --no-magnify |
預設(時間框架 > 1 分鐘) |
| 訊號計算 | 一次性對完整 DataFrame 計算 | 逐子 K 棒漸進式計算 |
| 成交價格 | K 棒收盤價 | 觸發時刻的子 K 棒收盤價 |
| 速度 | 非常快(單次向量化呼叫) | 中等(約 10K 次 compute 呼叫 / 1,000 根 K 棒) |
| 成交真實性 | 較高時間框架上較差 | 良好(子 K 棒解析度成交) |
| 使用場景 | 1 分鐘資料、參數掃描、快速篩選 | 1 小時以上時間框架的正式回測 |
| 運算路徑 | _compute(df, params) — pandas |
優先使用 _compute_fast(),退回使用 _compute() |
放大鏡技術¶
概念¶
對於每根圖表時間框架的 K 棒,放大鏡以更細的解析度逐一迭代子 K 棒,漸進式建構一根「形成中」的 OHLCV K 棒。在每個子 K 棒 tick,它以一組已完成的 K 棒加上形成中的 K 棒來執行策略。一旦訊號觸發,進出場就記錄在該子 K 棒的收盤價——而非圖表 K 棒的收盤價。
圖表 K 棒(4h): [════════════════════════════════════]
開盤 $50,000 收盤 $51,200
放大鏡(15m): [══|══|══|══|══|══|══|══|══|══|══|══|══|══|══|══]
↑
Tick 4:MACD 交叉觸發
以 $49,800 成交(子 K 棒收盤價)
跳過剩餘 12 個 tick
詳細流程¶
演算法¶
for bar_idx in range(warmup, len(df_chart)):
bar_start = df_chart.index[bar_idx]
bar_end = bar_start + chart_timedelta
# Magnifier ticks within this chart bar
mag_start = df_mag.index.searchsorted(bar_start, side="left")
mag_end = df_mag.index.searchsorted(bar_end, side="left")
# Completed chart-TF bars as indicator context
completed = df_chart.iloc[max(0, bar_idx - warmup*3) : bar_idx]
# Progressive forming bar
forming_open = df_mag["open"].iloc[mag_start]
forming_high, forming_low, forming_vol = -inf, inf, 0.0
for pos in range(mag_start, mag_end):
forming_high = max(forming_high, df_mag["high"].iloc[pos])
forming_low = min(forming_low, df_mag["low"].iloc[pos])
forming_close = df_mag["close"].iloc[pos]
forming_vol += df_mag["volume"].iloc[pos]
window = concat([completed, forming_bar])
le, lx, se, sx = strategy.compute(window, params)
if signal_triggered(le, lx, se, sx):
record_signal_at(pos)
break # One signal per chart bar
關鍵:訊號後 break
每根圖表 K 棒僅允許一個訊號。記錄訊號後,放大鏡會跳出子 K 棒迴圈。若沒有此 break,同一根 K 棒可能產生多個相互矛盾的進出場。
動態解析度¶
放大鏡會選擇一個能整除圖表時間框架的子 K 棒解析度,目標約為每根 K 棒 10 個 tick(最多約 16 個):
| 圖表時間框架 | 放大鏡時間框架 | 每根 K 棒的 Tick 數 |
|---|---|---|
| 5m | 1m | 5 |
| 15m | 1m | 15 |
| 30m | 3m | 10 |
| 1h | 5m | 12 |
| 4h | 15m | 16 |
| 1d | 1h | 24 |
VALID_RESOLUTIONS = [1, 3, 5, 15, 30, 60, 240] # minutes
def pick_magnifier_resolution(chart_tf, target_ticks=10):
chart_min = TF_MINUTES[chart_tf]
if chart_min <= 1:
return "1m"
max_ticks = int(target_ticks * 1.6) # ~16
best = "1m"
for res in VALID_RESOLUTIONS:
if res >= chart_min or chart_min % res != 0:
continue
ticks = chart_min // res
if ticks > max_ticks:
continue
if abs(ticks - target_ticks) < best_dist:
best = minutes_to_tf(res)
return best
需求:1 分鐘基礎資料
放大鏡需要 1 分鐘 K 線資料作為基礎解析度。所有較高時間框架均從 1 分鐘資料重新取樣。若 1 分鐘資料不可用,回測器會自動退回標準模式。
效能最佳化¶
快速路徑 vs. 慢速路徑¶
| 路徑 | 使用時機 | 每次呼叫額外負擔 |
|---|---|---|
快速路徑(_compute_fast) |
策略具有純 NumPy 實作 | ~0.1ms |
慢速路徑(_compute) |
複雜策略、退回機制 | ~2ms |
對於 4 小時回測,跨越 1,000 根圖表 K 棒,每根有 16 個放大鏡 tick:
- 快速路徑: 16,000 次呼叫 x 0.1ms = 1.6 秒
- 慢速路徑: 16,000 次呼叫 x 2ms = 32 秒
PineScript 編譯器會在可能時產生兩條路徑。回測器優先嘗試 _compute_fast,若不可用則透明地退回 _compute。
效能矩陣¶
| 組態 | 速度 | 成交真實性 | 適合用途 |
|---|---|---|---|
| 標準,1m | 快 | 優秀 | 正式環境(1m 已具有 K 棒級精度) |
| 標準,4h | 非常快 | 差 | 參數掃描、篩選 |
| 放大鏡,1h(快速路徑) | ~2 秒 | 良好(5m 成交) | 正式回測 |
| 放大鏡,4h(快速路徑) | ~4 秒 | 良好(15m 成交) | 正式回測 |
| 放大鏡,1d(快速路徑) | ~6 秒 | 良好(1h 成交) | 日線策略 |
回測結果模型 (BacktestResult)¶
回測器將所有結果萃取至結構化、可 JSON 序列化的模型中:
中繼資料¶
| 欄位 | 型別 | 描述 |
|---|---|---|
strategy_name |
str |
來自 strategy("name", ...) |
symbol |
str |
交易標的(如 BTCUSDT) |
exchange |
str |
資料來源交易所 |
timeframe |
str |
圖表時間框架(1h、4h、1d) |
period |
str |
日期範圍(2024-01-01 to 2025-01-01) |
mode |
str |
standard 或 magnifier |
params |
dict |
使用的策略輸入參數 |
績效指標¶
| 欄位 | 型別 | 範例 |
|---|---|---|
total_return_pct |
float |
+28.82 |
annualized_return_pct |
float |
+34.15 |
sharpe_ratio |
float \| null |
2.15 |
sortino_ratio |
float \| null |
3.20 |
calmar_ratio |
float \| null |
1.85 |
max_drawdown_pct |
float |
-12.50 |
交易統計¶
| 欄位 | 型別 | 範例 |
|---|---|---|
total_trades |
int |
28 |
win_rate_pct |
float |
57.14 |
profit_factor |
float |
1.85 |
expectancy |
float |
106.75 |
best_trade_pct |
float |
+8.32 |
worst_trade_pct |
float |
-4.15 |
avg_trade_pct |
float |
+1.03 |
時間序列(取樣後)¶
| 欄位 | 資料點數 | 格式 |
|---|---|---|
equity_curve |
~1,000 | [{timestamp, value}, ...] |
returns |
~1,000 | [{timestamp, return}, ...] |
drawdown_curve |
~1,000 | [{timestamp, drawdown_pct}, ...] |
明細記錄¶
| 欄位 | 型別 | 關鍵欄位 |
|---|---|---|
trades |
list[TradeRecord] |
方向、進出場時間+價格、損益、報酬率、持有時間 |
orders |
list[OrderRecord] |
時間戳、方向、價格、數量、手續費 |
drawdowns |
list[DrawdownRecord] |
高點時間、低點時間、回撤百分比、持續時間 |
圖表資料¶
| 欄位 | 資料點數 | 用途 |
|---|---|---|
ohlcv_bars |
~5,000 | 用於圖表渲染的 OHLCV 資料 |
trade_markers |
每筆交易 | 價格圖表上的進出場疊加標記 |
NaN → null
許多統計在未定義時回傳 NaN(如零變異數下的 Sharpe、無虧損交易時的 Profit Factor)。結果萃取器在 JSON 序列化前將所有 NaN 和 Inf 值轉換為 null。前端將 null 指標顯示為 —,而非 0。
資料來源¶
回測器透過 DataSource 協定 (Protocol) 載入資料:
class DataSource(Protocol):
def load_1m(self, symbol: str, exchange: str,
start: datetime, end: datetime) -> pd.DataFrame:
"""Load 1-minute OHLCV candles."""
...
| 來源 | 狀態 | 儲存方式 | 備註 |
|---|---|---|---|
DiskSource |
啟用中 | .npz 檔案(透過 CCXT) |
壓縮 NumPy 存檔,本地快速讀取 |
TimescaleSource |
規劃中 | PostgreSQL / TimescaleDB | 使用連續聚合的超表 (Hypertable),跨使用者共享 |
所有較高時間框架均從 1 分鐘基礎資料重新取樣:
def resample_ohlcv(df_1m: pd.DataFrame, target_tf: str) -> pd.DataFrame:
rule = TF_RESAMPLE_RULES[target_tf] # "5T", "1H", "4H", "1D"
return df_1m.resample(rule).agg({
"open": "first",
"high": "max",
"low": "min",
"close": "last",
"volume": "sum",
}).dropna()
使用範例¶
# Standard magnifier mode (default for TF > 1m)
python -m backtest --script strategies/macd_crossover.pine --timeframe 4h
# Disable magnifier for faster parameter sweeps
python -m backtest --script strategies/supertrend.pine --timeframe 1h --no-magnify
# JSON output for programmatic consumption
python -m backtest --script strategies/rsi_overbought.pine --timeframe 1d --json
# Specify date range and initial capital
python -m backtest --script strategies/bb_squeeze.pine --timeframe 4h \
--start 2024-01-01 --end 2025-01-01 --capital 50000
# Override strategy parameters
python -m backtest --script strategies/macd_crossover.pine --timeframe 1h \
--param "Fast Length=8" --param "Slow Length=21"
檔案對應表¶
| 概念 | 檔案 |
|---|---|
| 回測器類別 | backtest/backtester.py(Backtester) |
| 標準模式執行 | backtest/backtester.py(_run_standard) |
| 放大鏡模式執行 | backtest/backtester.py(_run_magnified) |
| 結果萃取 | backtest/backtester.py(_extract_result) |
| BacktestResult 資料類別 | backtest/strategy.py(BacktestResult) |
| TradeRecord / OrderRecord / DrawdownRecord | backtest/strategy.py |
| 動態解析度選擇器 | backtest/backtester.py(pick_magnifier_resolution) |
| DataSource 協定 | backtest/backtester.py |
| CLI 進入點 | backtest/__main__.py |