Skip to content
// 0x
Go back
0x09 // 工具设计

从 Pine 到回测 — 构建本地交易信号系统

在 TradingView 上写 Pine Script 策略很方便,但有两个硬伤:回测只能在 TV 服务器上跑,以及实盘信号无法脱离浏览器。当策略积累到一定程度,你自然想要一个本地的回测和信号监控系统。

本文记录如何从零搭建这样的系统,以及如何将已有的 Pine 策略移植到 Python 回测引擎。

系统架构

Binance API (公开数据)


data/fetcher.py     ← 定时增量同步(cron)


data/historical.db  ← SQLite 本地数据仓库

    ├── core/backtest.py  →  回测报告(HTML + equity curve)

    └── core/monitor.py   →  实盘信号监控(桌面通知)

整个系统三个层次:数据层、引擎层、策略层,单向依赖互不耦合。

数据层:本地 SQLite 仓库

设计

使用 SQLite 单文件存储,无需额外数据库服务。两个核心表:

CREATE TABLE symbols (
    symbol TEXT NOT NULL,
    contract_type TEXT NOT NULL,
    source TEXT NOT NULL,
    name TEXT,
    PRIMARY KEY (symbol, contract_type)
);

CREATE TABLE klines (
    symbol TEXT NOT NULL,
    contract_type TEXT NOT NULL,
    interval TEXT NOT NULL,
    open_time INTEGER NOT NULL,
    open REAL, high REAL, low REAL, close REAL, volume REAL,
    PRIMARY KEY (symbol, contract_type, interval, open_time)
);

标的注册表(symbols)区分 contract_type(spot / perpetual / futures_dated)和 source(binance_spot / binance_futures / yahoo),从设计上支持多交易所、多合约类型。K 线表冗余 contract_type 避免频繁 join。

数据源按接口隔离为独立模块,新增数据源只需实现一个 fetch_klines() 接口。

增量同步

基于 MAX(open_time) 断点续传,无论关机多久都能自动补全:

TZ=UTC
0,15,30,45 * * * *    15m(对齐 K 线收盘边界)
0 * * * *              1h
0 0,4,8,12,16,20 * *  4h
0 0 * * *              1d
@reboot                开机全量更新

关键设计:只在 K 线收盘时刻触发同步,避免未收盘的半截信号干扰策略判断。

引擎层:策略 DSL + 回测

Strategy 基类

没有用 Backtrader 等第三方框架,自研引擎只有一套核心约定——约 30 行的基类:

from core.strategy import Strategy, Signal

class MyStrategy(Strategy):
    symbol = "BTCUSDT"
    contract_type = "perpetual"
    interval = "1h"

    def on_bar(self, ctx):
        rsi = ctx.rsi(14)
        if rsi < 30:
            return Signal("buy", reason=f"RSI 超卖 {rsi}")
        return None

on_bar 每根 K 线回调一次,返回 SignalNone。回测引擎和实盘监控器共用同一套 Context

ctx.ma(20)              # 移动平均
ctx.ema(period)         # 指数移动平均
ctx.rsi(period)         # RSI
ctx.macd()              # (dif, dea, hist)
ctx.boll(period, k)     # (mid, upper, lower)
ctx.atr(period)         # 平均真实波幅
ctx.cci(period)         # CCI(仅收盘价,不传 live_price 时不参与计算)
ctx.trend()             # bull / bear / unknown
ctx.patterns()          # K 线形态列表

# 所有指标支持多周期
ctx.rsi(14, "1d")       # 日线 RSI
ctx.ma(20, "4h")        # 4h MA

Context 内部做了两级优化:预计算数组(回测用)和惰性求值缓存(实盘用),避免重复计算。

回测引擎

核心循环约 150 行,支持:

python3 core/backtest.py \
  --strategy strategies.signal_multi.SignalMulti \
  --symbol BTCUSDT --contract perpetual \
  --from 2024-06-01 --to 2026-04-30 \
  --capital 10000 --report

输出为单页 HTML(Chart.js 绘制 equity curve),无需服务器。

策略层:从 Pine 到 Python

Pine 策略的共同模式

分析 TradingView 中积累的 Pine 文件后,发现它们共享一套设计模式:

3~4 层 EMA 判断趋势方向 (21 / 144 / 288 / 576)
  + CCI(89/144) EMA 交叉作为入场确认
  + 不同趋势强度下启用不同的 CCI 阈值
  + 超买/超卖区平仓

移植实现

三个策略移植为统一的 signal_ 命名系列:

策略文件核心逻辑
SignalMultisignal_multi.py3EMA + CCI EMA 交叉 + 多级阈值,最完整的 Pine 移植
SignalCrosssignal_cross.pyCCI(89) EMA(21/55) 交叉 + 3EMA
SignalTrendsignal_trend.py4EMA(21/144/576) + CCI(144) EMA(13),趋势跟随

以 SignalMulti 为例,它在 Pine 中的核心逻辑转换为 Python 后:

class SignalMulti(Strategy):
    def on_bar(self, ctx):
        e21, e144, e288 = ctx.ema(21), ctx.ema(144), ctx.ema(288)
        bullish = (e144 < e288 and e21 > e288) or (e144 > e288 and e21 > e144)
        f = self._cci_fast  # CCI(89) 的 EMA(21)
        if bullish and f < 100:
            return Signal("buy", reason=f"多头 e21={e21:.0f} cci={f:.0f}")
        return None

关键移植点:Pine 的 cci_calculation 使用 ta.stdev,我们使用平均绝对偏差(Mean Deviation)。两者数学上不同(stdev ≈ 1.25 × MD),但移植时选择保持统一的 CCI 实现,确保所有策略使用相同基准。

回测结果

在 BTCUSDT 永续合约 1h 上的表现(2024-06 ~ 2026-04,初始资金 10,000):

策略交易数胜率盈亏比收益率Sharpe
SignalMulti36648.6%1.13-36.5%1.27
SignalCross7561.3%1.28-4.2%0.57
SignalTrend3746.0%1.88+32.2%0.40

三个策略没有一个能在所有指标上同时胜出——这正是搭建回测系统的价值。手工在 TradingView 上”看起来不错”的策略,放到系统性回测里,缺陷一览无余:SignalMulti 交易太频繁被手续费拖垮,SignalCross 胜率高但盈亏比低,SignalTrend 唯一正收益但回撤大。

经验总结

  1. 数据先行:没有可靠的本地数据,回测和监控都是空中楼阁。SQLite + 增量 cron 是最低成本的方案。开盘自动更新,关机自动续传。

  2. 自研引擎比套框架更可控:Backtrader 等框架学习成本高,策略 DSL 与 Pine 差异大。600 行的自研引擎,策略写起来和 Pine 一样直觉,调试起来比理解框架内部机制容易得多。

  3. Pine 移植的关键不是代码翻译,是理解信号语义:同一个”CCI 超卖”在 Pine 和 Python 中可能因标准差/平均差的计算差异而表现不同。移植时选择统一的计算基准,比逐字逐句复制更重要。

  4. 回测的目的是发现缺陷,不是证明策略有效:三个移植策略中只有一个在回测期取得正收益。这不是策略失效,而是让你在实盘前看到问题出在哪里——没有硬止损、参数照搬 Pine 默认值、单标的表现可能不具普适性。

下一步的方向:硬止损、多标的验证、参数扫描、K 线形态信号增强——但那是下回的内容了。


Share this post on:

Previous Post
Shopify OAuth 跨账号授权:一个调试中的意外发现
Next Post
扩展 AI 编码助手的本地能力 — 浏览器自动化与共享终端