PineScript v6 編譯器¶
一個自訂的四階段編譯管線 (Compilation Pipeline),將 PineScript v6 原始碼轉換為向量化 Python 函式。這不是通用的 PineScript 直譯器——它針對視覺化策略建構器所產生的固定、可預測結構進行了最佳化。編譯器處理訊號式策略所需的 PineScript v6 子集:strategy() 宣告、input.*() 參數、ta.* 指標呼叫、布林運算式,以及包含 strategy.entry()/strategy.close() 的 if 區塊。
一句話範圍說明
處理策略宣告、輸入參數、全部 37 個 ta.* 指標、布林表達式(and/or/not)、比較和交叉運算子,以及包含 strategy.entry()/strategy.close() 的 if 區塊。範圍外:迴圈、var/varip 持久狀態、request.security() 多時間框架、使用者自定義函式、以及 array/matrix 類型。此限制是有意為之 — 建構器產生可預測的子集,能乾淨地映射到向量化 NumPy 運算。
編譯管線¶
第一階段:詞法分析器 (Tokenizer)¶
檔案: backtest/pine/tokens.py
詞法分析器將原始 PineScript 原始碼轉換為一串帶有型別的語彙單元 (Token) 流。它處理幾種 PineScript 特有的行為:
| 行為 | 描述 | 範例 |
|---|---|---|
| 註解移除 | 移除 // 行註解,保留字串中的 // |
rsi = ta.rsi(close, 14) // lookback → 移除註解 |
| 續行合併 | 將括號未配對的行合併為單一邏輯行 | [a, b, c] = ta.macd(close,12, 26, 9) → 一行 |
| 縮排追蹤 | 為 PineScript 的 if 區塊結構產生 INDENT/DEDENT 語彙單元 |
if conditionstrategy.entry(...) |
| 關鍵字辨識 | 區分關鍵字(if、and、or、not、true、false)與識別子 |
if → KEYWORD,rsi_val → IDENT |
語彙單元類型¶
NUMBER 12, 3.14, 0.5
STRING "MACD Cross", 'Long'
IDENT rsi_val, macdLine, fast_length
KEYWORD if, and, or, not, true, false, strategy, input
DOT .
COMMA ,
ASSIGN =
LPAREN ( RPAREN )
LBRACKET [ RBRACKET ]
COMPARE >, <, >=, <=, ==, !=
OPERATOR +, -, *, /
INDENT (indentation increase)
DEDENT (indentation decrease)
NEWLINE (logical line boundary)
第二階段:剖析器 (Parser)¶
檔案: backtest/pine/parser.py
遞迴下降剖析器 (Recursive Descent Parser) 消耗語彙單元流並產生抽象語法樹 (AST)。剖析器辨識以下 PineScript 結構:
| AST 節點 | PineScript 結構 | 範例 |
|---|---|---|
StrategyDecl |
strategy() 宣告 |
strategy("Name", overlay=true, initial_capital=10000) |
InputDecl |
input.*() 參數定義 |
fast = input.int(12, "Fast Length") |
Assignment |
變數賦值(單一或元組解構) | rsi = ta.rsi(close, 14) 或 [m, s, h] = ta.macd(...) |
IfBlock |
包含 strategy.entry/close/exit 的 if 區塊 |
if longCondstrategy.entry("Long", strategy.long) |
FunctionCall |
ta.*、math.*、nz()、na() 呼叫 |
ta.crossover(macdLine, signalLine) |
BinaryOp |
布林與算術運算式 | rsi > 70 and macd > 0 |
UnaryOp |
否定 | not condition |
AST 結構範例¶
一個簡單的 RSI 策略:
Program(
strategy=StrategyDecl(
name="RSI Overbought/Oversold",
settings={"overlay": True, "initial_capital": 10000}
),
inputs=[
InputDecl(var="length", type="int", default=14, title="RSI Length"),
InputDecl(var="upper", type="int", default=70, title="Overbought"),
InputDecl(var="lower", type="int", default=30, title="Oversold"),
],
assignments=[
Assignment(var="rsi_val", expr=FunctionCall("ta.rsi", [Ident("close"), Ident("length")])),
],
blocks=[
IfBlock(
condition=BinaryOp(Ident("rsi_val"), "<", Ident("lower")),
body=[StrategyAction("entry", "Long", "strategy.long")]
),
IfBlock(
condition=BinaryOp(Ident("rsi_val"), ">", Ident("upper")),
body=[StrategyAction("entry", "Short", "strategy.short")]
),
]
)
第三階段:程式碼產生器 (Code Generator)¶
檔案: backtest/pine/codegen.py
程式碼產生器走訪 AST 並輸出兩個 Python 函式:
_compute(df, params) — Pandas 路徑¶
完整的 DataFrame 運算。用於標準回測模式,策略在整個資料集上執行一次。
def _compute(df, params):
_open = df['open']
_high = df['high']
_low = df['low']
_close = df['close']
_volume = df['volume']
length = params.get('RSI Length', 14)
upper = params.get('Overbought', 70)
lower = params.get('Oversold', 30)
rsi_val = ta.rsi(_close, length)
long_entry = (rsi_val < lower).fillna(False)
long_exit = pd.Series(False, index=df.index)
short_entry = (rsi_val > upper).fillna(False)
short_exit = pd.Series(False, index=df.index)
return long_entry, long_exit, short_entry, short_exit
_compute_fast(opens, highs, lows, closes, volumes, params) — NumPy 快速路徑¶
僅對原始 NumPy 陣列進行純量運算。僅回傳最後一根 K 棒的 4 個純量布林值。用於放大鏡的內層迴圈,在此 compute 可能每次回測被呼叫數千次——每個子 K 棒一次。
def _compute_fast(opens, highs, lows, closes, volumes, params):
length = params.get('RSI Length', 14)
upper = params.get('Overbought', 70)
lower = params.get('Oversold', 30)
rsi_val = ta_fast.rsi(closes, length)
long_entry = rsi_val < lower
long_exit = False
short_entry = rsi_val > upper
short_exit = False
return long_entry, long_exit, short_entry, short_exit
關鍵轉換¶
程式碼產生器執行數項關鍵翻譯,以橋接 PineScript 語意與向量化 Python:
價格內建值 → DataFrame 欄位¶
| PineScript | 產生的 Python | 原因 |
|---|---|---|
close |
_close(df['close'] 的別名) |
避免遮蔽 Python 內建名稱 |
open |
_open(df['open'] 的別名) |
open 是 Python 內建名稱 |
high |
_high |
一致性 |
low |
_low |
一致性 |
volume |
_volume |
一致性 |
hlc3 |
(_high + _low + _close) / 3 |
衍生價格來源 |
ohlc4 |
(_open + _high + _low + _close) / 4 |
衍生價格來源 |
隱式引數注入 (Implicit Argument Injection)¶
PineScript 的 ta.atr(14) 隱式使用 high、low、close。產生的 Python 必須將這些明確化:
IMPLICIT_ARGS = {
"atr": ("_high", "_low", "_close"),
"supertrend": ("_high", "_low", "_close"),
"sar": ("_high", "_low"),
"dmi": ("_high", "_low", "_close"),
"obv": ("_close", "_volume"),
"mfi": ("_high", "_low", "_close", "_volume"),
"vwap": ("_high", "_low", "_close", "_volume"),
"ad": ("_high", "_low", "_close", "_volume"),
"wad": ("_high", "_low", "_close"),
}
| PineScript | 產生的 Python |
|---|---|
ta.atr(14) |
ta.atr(_high, _low, _close, 14) |
ta.rsi(close, 14) |
ta.rsi(_close, 14) |
ta.obv() |
ta.obv(_close, _volume) |
ta.supertrend(3, 10) |
ta.supertrend(_high, _low, _close, 3, 10) |
布林運算子¶
| PineScript | 產生的 Python | 原因 |
|---|---|---|
a and b |
(a) & (b) |
pandas Series 需要位元 &,非 and |
a or b |
(a) \| (b) |
pandas Series 需要位元 \|,非 or |
not a |
~(a) |
Series 的位元 NOT |
括號化至關重要
若不明確加上括號,a & b | c 會因 Python 運算子優先順序而被評估為 a & (b | c)。程式碼產生器會包裹每個運算元:(a) & (b)、(a) | (b)。
其他轉換¶
| PineScript | 產生的 Python |
|---|---|
math.abs(x) |
np.abs(x) |
math.max(a, b) |
np.maximum(a, b) |
math.min(a, b) |
np.minimum(a, b) |
math.sqrt(x) |
np.sqrt(x) |
nz(x) |
x.fillna(0) |
na(x) |
x.isna() |
[a, b, c] = f() |
(a, b, c) = f() |
true / false |
True / False |
NaN 安全性¶
每個訊號條件都以 .fillna(False) 包裹。指標在暖身期間(例如 RSI-14 的前 14 根 K 棒)會回傳 NaN,而 NaN 絕不能作為 True 訊號傳播。
為何需要兩條運算路徑¶
| 路徑 | 使用者 | 輸入 | 輸出 | 額外負擔 |
|---|---|---|---|---|
_compute() |
標準回測 | pd.DataFrame(完整資料集) |
4 個 pd.Series(布林) |
DataFrame 配置、索引對齊 |
_compute_fast() |
放大鏡內層迴圈 | 5 個 np.ndarray(原始陣列) |
4 個 bool(純量) |
極小——純 NumPy |
放大鏡在每個子 K 棒上重新計算訊號——每次回測可能 10,000+ 次呼叫(1,000 根圖表 K 棒,每根 10 個子 K 棒)。在此規模下,pandas DataFrame 的額外負擔主導了效能:
快速路徑完全排除 pandas——原始 NumPy 陣列輸入,純量布林輸出。當 _compute_fast 不可用時(含有不支援操作的複雜策略),放大鏡會退回使用 _compute,但會有效能損失。
支援的指標¶
技術指標函式庫(backtest/ta.py)在 ta 類別上以靜態方法實作了 37 個指標。所有指標接受並回傳 pd.Series,使用向量化運算(無 Python 迴圈),且已與 TradingView 輸出驗證一致。
趨勢¶
| 指標 | 函式 | 參數 |
|---|---|---|
| 簡單移動平均線 (SMA) | ta.sma(source, length) |
source, period |
| 指數移動平均線 (EMA) | ta.ema(source, length) |
source, period |
| 加權移動平均線 (WMA) | ta.wma(source, length) |
source, period |
| 成交量加權移動平均線 (VWMA) | ta.vwma(source, volume, length) |
source, volume, period |
| Hull 移動平均線 (HMA) | ta.hma(source, length) |
source, period |
| 運行移動平均線 (RMA) | ta.rma(source, length) |
source, period |
| Arnaud Legoux 移動平均線 (ALMA) | ta.alma(source, length, offset, sigma) |
source, period, offset, sigma |
| 對稱加權移動平均線 (SWMA) | ta.swma(source) |
source |
| 超級趨勢 (SuperTrend) | ta.supertrend(high, low, close, factor, period) |
factor, ATR period |
動量¶
| 指標 | 函式 | 參數 |
|---|---|---|
| 相對強弱指數 (RSI) | ta.rsi(source, length) |
source, period |
| MACD | ta.macd(source, fast, slow, signal) |
source, fast/slow/signal periods |
| 隨機指標 (Stochastic) | ta.stoch(high, low, close, k, d, smooth) |
K period, D period, smoothing |
| 商品通道指數 (CCI) | ta.cci(high, low, close, length) |
period |
| 資金流量指數 (MFI) | ta.mfi(high, low, close, volume, length) |
period |
| Chande 動量振盪器 (CMO) | ta.cmo(source, length) |
source, period |
| 變動率 (ROC) | ta.roc(source, length) |
source, period |
| 真實強度指數 (TSI) | ta.tsi(source, long, short) |
source, long/short periods |
| 動量 (Momentum) | ta.mom(source, length) |
source, period |
| 威廉指標 (Williams %R) | ta.wpr(high, low, close, length) |
period |
| 百分比排名 (Percent Rank) | ta.percentrank(source, length) |
source, period |
波動率¶
| 指標 | 函式 | 參數 |
|---|---|---|
| 平均真實範圍 (ATR) | ta.atr(high, low, close, length) |
period |
| 布林通道 (Bollinger Bands) | ta.bb(source, length, mult) |
source, period, multiplier |
| 布林通道寬度 (BBW) | ta.bbw(source, length, mult) |
source, period, multiplier |
| 肯特納通道 (Keltner Channel) | ta.kc(high, low, close, length, mult) |
period, multiplier |
| 肯特納通道寬度 (KCW) | ta.kcw(high, low, close, length, mult) |
period, multiplier |
| 方向移動指數 (DMI) | ta.dmi(high, low, close, length) |
period |
| 標準差 (Standard Deviation) | ta.stdev(source, length) |
source, period |
| 拋物線 SAR (Parabolic SAR) | ta.sar(high, low, start, inc, max) |
start, increment, max |
| 重心 (Center of Gravity) | ta.cog(source, length) |
source, period |
成交量¶
| 指標 | 函式 | 參數 |
|---|---|---|
| 能量潮 (OBV) | ta.obv(close, volume) |
— |
| 累積/分配線 (A/D) | ta.ad(high, low, close, volume) |
— |
| 價量趨勢 (PVT) | ta.pvt(close, volume) |
— |
| 威廉累積/分配 (WAD) | ta.wad(high, low, close) |
— |
| 成交量加權平均價格 (VWAP) | ta.vwap(high, low, close, volume) |
— |
工具¶
| 指標 | 函式 | 參數 |
|---|---|---|
| 最高值 (Highest) | ta.highest(source, length) |
source, period |
| 最低值 (Lowest) | ta.lowest(source, length) |
source, period |
| 變動 (Change) | ta.change(source, length) |
source, period |
| 中位數 (Median) | ta.median(source, length) |
source, period |
| 區間 (Range) | ta.range(high, low) |
— |
| 線性迴歸 (Linear Regression) | ta.linreg(source, length, offset) |
source, period, offset |
| 上升 (Rising) | ta.rising(source, length) |
source, period |
| 下降 (Falling) | ta.falling(source, length) |
source, period |
| 累積和 (Cumulative Sum) | ta.cum(source) |
source |
交叉偵測¶
| 函式 | 回傳 True 的時機 |
|---|---|
ta.crossover(a, b) |
a 上穿 b(a > b 且 a.shift(1) <= b.shift(1)) |
ta.crossunder(a, b) |
a 下穿 b(a < b 且 a.shift(1) >= b.shift(1)) |
ta.cross(a, b) |
上穿或下穿 |
已編譯策略物件¶
編譯管線的最終輸出:
@dataclass
class TransformedStrategy:
name: str # From strategy("name", ...)
inputs: dict[str, InputParam] # {paramTitle: IntInput(default=12, ...), ...}
compute: Callable # (df, params) -> (le, lx, se, sx)
compute_fast: Callable | None # (opens, highs, lows, closes, vols, params) -> 4 bools
warmup: int # max(all_indicator_periods) * 2
source_code: str # Original PineScript source
generated_code: str # Generated Python (for debugging)
settings: dict # {initial_capital, commission, slippage}
暖身計算
編譯器掃描所有指標的週期引數,並設定 warmup = max(periods) * 2。這是保守估計——EMA 理論上需要無限歷史,但最長週期的兩倍在實務上已足夠。回測器會跳過前 warmup 根 K 棒以避免受 NaN 汙染的訊號。
安全性¶
產生的 Python 透過 exec() 在受限的命名空間中執行:
namespace = {"ta": ta, "pd": pd, "np": np}
exec(generated_source, namespace)
compute_fn = namespace["_compute"]
命名空間刻意排除 os、sys、subprocess、importlib 以及所有可能啟用檔案系統或網路存取的模組。編譯器僅從建構器受限的 PineScript 子集產生程式碼——不接受任意使用者程式碼。
不受信任的輸入
若編譯器未來開放接受來自不受信任使用者的任意 PineScript(超出建構器受限輸出的範圍),則需要額外的沙箱機制:RestrictedPython、子程序隔離或 WASM 執行。目前的 exec() 方法之所以安全,僅因為建構器產生的是可預測、可稽核的 PineScript 子集。
PineScript 子集——涵蓋範圍與不涵蓋範圍¶
| 涵蓋範圍 | 不涵蓋範圍 |
|---|---|
strategy() 宣告 |
for / while 迴圈(難以向量化) |
input.int()、input.float()、input.bool()、input.string() |
var / varip(持久狀態) |
| 變數賦值 | request.security()(多時間框架) |
元組解構 [a, b, c] = f() |
plot()、plotshape()(僅視覺化) |
ta.* 指標呼叫(37 個指標) |
使用者自訂函式 |
布林運算式(and、or、not) |
array.* / matrix.* 型別 |
包含 strategy.entry/close/exit 的 if 區塊 |
switch / 三元運算式 |
math.* 函式 |
字串操作 |
nz()、na() |
型別轉換 |
檔案對應表¶
| 概念 | 檔案 |
|---|---|
公開 API(transform_pinescript) |
backtest/pine/__init__.py |
| 詞法分析器 | backtest/pine/tokens.py |
| 遞迴下降剖析器 | backtest/pine/parser.py |
| AST 節點定義 | backtest/pine/ast_nodes.py |
| 程式碼產生器(AST → Python) | backtest/pine/codegen.py |
| 技術指標函式庫(37 個指標) | backtest/ta.py |
| TransformedStrategy 資料類別 | backtest/strategy.py |
| 輸入參數型別 | backtest/strategy.py(IntInput、FloatInput 等) |
範例 .pine 策略 |
backtest/strategies/ |