功能

Rust 引擎

在原生 Rust 引擎上模拟,并用 Rust 编写策略回调

为 Python 研究引入 Rust 速度,或用 Rust 编写整个策略。VBT PRO 提供原生 Rust 回测引擎,支持这两种工作流。Python 用户通过可选扩展获得预编译计算;Rust 用户还可以编写有状态策略回调,并跨 CPU 核心运行独立模拟。

选择使用 Rust 的方式

您想做什么从哪里开始
加速已有 Python notebook 或脚本中受支持的计算安装可选扩展,继续使用 Python API。
保留用 Python 编写的自定义交易规则使用 Numba 回调,流程中其他受支持的计算仍可用 Rust。
编写根据成交、现金或持仓作出反应的 Rust 策略使用原生模拟器,让策略拥有自己的状态。
在 Rust 程序中逐根处理新到达的 K 线使用流式步进器,延续投资组合与策略状态。

Python 扩展和原生 Rust 库共享计算代码。您可以无须学习 Rust 就采用扩展,也可以用原生库构建独立 Rust 策略。原生 Rust 程序不依赖 Python 运行时即可使用该库。

Python 与 Rust 中的有状态策略

下面同一策略运行两次:一次通过 Python API 使用 Numba 回调,一次作为 Rust 策略。它在 50,000 根小时 K 线上交易 100 个合成资产,均线上穿时入场,反向交叉、3% 止损或 6% 止盈时离场;任一止损或止盈触发后,该资产等待 24 根 K 线再允许入场。是否需要等待取决于模拟实际产生的成交,因此无法预先计算为数组。

Write the cooldown as Numba callbacks and run it through the Python API
from collections import namedtuple

COOLDOWN_BARS = 24
np.random.seed(42)
returns = np.random.normal(0, 0.01, size=(50_000, 100))
close = pd.DataFrame(100 * np.exp(returns.cumsum(axis=0)))
fast = close.rolling(20).mean()
slow = close.rolling(100).mean()
entries = fast.vbt.crossed_above(slow)
exits = fast.vbt.crossed_below(slow)
Memory = namedtuple("Memory", ["blocked_until"])

@njit
def cooldown_signal_func_nb(c, entries, exits, memory):
    cooling = c.i < memory.blocked_until[c.col]
    entry = vbt.nb.flex_select_nb(entries, c.i, c.col) and not cooling
    exit = vbt.nb.flex_select_nb(exits, c.i, c.col)
    return entry, exit, False, False

@njit
def cooldown_post_order_func_nb(c, memory):
    if vbt.pf_nb.order_closed_position_nb(c):  
        if vbt.pf_nb.get_last_order_nb(c)["stop_type"] >= 0:
            memory.blocked_until[c.col] = c.i + 1 + COOLDOWN_BARS

memory = Memory(blocked_until=np.zeros(close.shape[1], dtype=int_))
pf = vbt.PF.from_signals(
    close,
    signal_func_nb=cooldown_signal_func_nb,
    signal_args=(vbt.Rep("entries"), vbt.Rep("exits"), memory),
    broadcast_named_args=dict(entries=entries, exits=exits),
    post_order_func_nb=cooldown_post_order_func_nb,
    post_order_args=(memory,),
    size=1.0,
    fees=0.001,
    sl_stop=0.03,
    tp_stop=0.06,
)
Write the same cooldown as a Rust strategy
const COOLDOWN_BARS: usize = 24;

struct Cooldown<'a> {
    entries: ArrayView2<'a, bool>,
    exits: ArrayView2<'a, bool>,
    blocked_until: Vec<usize>, 
}

impl SignalStrategy for Cooldown<'_> {
    fn signals(&mut self, ctx: &mut SignalContext<'_, '_>) -> VbtResult<Signals> {
        let cell = [ctx.i(), ctx.col()];
        let cooling = ctx.i() < self.blocked_until[ctx.local_col()];
        Ok(Signals {
            long_entry: self.entries[cell] && !cooling,
            long_exit: self.exits[cell],
            ..Signals::default()
        })
    }

    fn on_order_end(
        &mut self,
        ctx: &mut OrderEndContext<'_, '_, SignalOrderRecord>,
        report: &ExecutionReport,
    ) -> VbtResult<()> {
        if report.before.position != 0.0 && report.after.position == 0.0 {
            let record = ctx.order_record_now().expect("a close emits a record");
            if record.stop_type >= 0 {
                self.blocked_until[ctx.local_col()] = ctx.i() + 1 + COOLDOWN_BARS;
            }
        }
        Ok(())
    }
}
Run it on one core and on all cores
let simulator = SignalSimulator::builder()
    .config(
        SimulationConfig::builder()
            .target_shape(close.dim())
            .group_lens(group_lens.view())
            .close(close.view())
            .build()?,
    )
    .signal_config(
        SignalConfig::builder().size(1.0).fees(0.001).sl_stop(0.03).tp_stop(0.06).build(),
    )
    .build()?;
let new_cooldown = |info: GroupInfo| {
    Ok(Cooldown {
        entries: entries.view(),
        exits: exits.view(),
        blocked_until: vec![0; info.to_col - info.from_col],
    })
};
let serial = simulator.run(new_cooldown)?; 
let parallel = simulator.run_parallel(new_cooldown)?;

两个版本都生成相同的 65,278 个订单,每列、每根 K 线、数量和价格均一致。在 8 核 Apple M3 上测量,取五次中的最短耗时:

运行方式毫秒
通过 vbt.PF.from_signals 运行 Numba 回调1,129
Rust 策略,单核538
Rust 策略,全部核心201

Python 时间包括参数准备和投资组合构建,Rust 时间只包括模拟。因此,表中测量的是两个工作流的不同层级。生成结果的研究脚本会构建 Rust 程序,运行两个版本,并比较订单。

可直接替换的 Rust 内核

Python 用户无须编写 Rust 即可使用 Rust 引擎。安装可选 vectorbtpro-rust 包后,滚动统计、信号辅助函数、收益度量,以及 from_signals 和 from_orders 模拟器等受支持函数,作为预编译 Rust 内核运行;Rust 未覆盖的部分回退到 Numba。按照安装指南(英文),先安装对应版本的平台 wheel,再安装 vectorbtpro。只有版本完全匹配时,VBT 才会使用它。jitted=dict(jitter="rs", parallel=True) 可按列或独立投资组合组,将受支持计算分配到多个核心。

内核已提前编译,因此会话中的首次调用无须等待 Numba。在构建本页的机器上,新进程首次计算滚动标准差,Rust 约耗时 0.08 秒。Numba 首次接触该输入时需编译,耗时 1.3 秒;后续进程从磁盘加载已编译版本,约耗时 0.17 秒。同一进程中的后续调用,两者均不到一毫秒,因此短脚本、定时任务和新工作进程受益最大。计算后端页展示 Rust 何时领先及如何逐次选择后端,下方 Rust 后端功能介绍展示具体控制方式。

原生模拟器

同一套模拟器也作为 Rust 库提供。Rust 程序可用 run_single、run 或 run_parallel 一次运行完整历史,也可向 SignalStepper 等步进器逐根输入 K 线,并即时接收每根的成交,使用检查点保存和恢复状态。可以先回放历史,再随着新 K 线到达继续处理,保留现金、持仓和上述冷却期等策略记忆。此工作流详见实时模拟。

ensure_simulation_eq 可比较批量与流式运行,检查记录及最终投资组合状态,并报告首个不一致处。因此可以验证,策略从完整历史运行迁移至逐根 K 线输入后,模拟交易是否保持一致。crate 的测试套件将其模拟器与 Numba 版本比较,如同上例对订单进行比较。

Rust 回调

Rust 策略是实现某个 trait 的类型,例如信号使用 SignalStrategy,每个元素一个订单使用 OrderStrategy,每根 K 线任意数量订单使用 FlexOrderStrategy。其方法接收包含当前 K 线、列、现金和持仓的上下文,并在每个订单后收到执行报告。其他原生引擎也接受信号生成、应用与归约操作以及投资组合配置回调。

每个并行模拟组各有独立策略实例。一个组可包含多个共享现金的资产,因此既能将投资组合层面的决策放在一起,也能并行运行独立投资组合。各组内部的行和回调仍按顺序执行。

Rust 回调运行于 Rust 程序中。在 Python 中,策略逻辑使用上例那样的 Numba 回调,并在 Numba 模拟器上运行。Rust 错误带有类别,例如无效值或订单被拒绝,在 Python 中表现为对应异常类型。

将 Rust 结果带回 Python

原生 Rust 模拟可用 save_npz 将输出保存为兼容 NumPy 的归档。通过 vbt.load_simulation_npz 加载,并使用与模拟相同的价格、标签、初始现金和分组构建 Portfolio。随后可对 Rust 结果使用 VBT 的绩效分析和交易分析。这样,模拟在 Rust 中运行,Python 分析与报告工作流则得以保留。

教程

会员专享的从 Python 到 Rust(英文)教程分别使用高层 API、Numba 内核、Python 绑定及原生 Rust 构建同一策略,再迁移至流式执行。

原生 Rust 模拟器

✅ 将策略直接带到 Rust。VBT 原生模拟器可让同一策略处理完整价格数组,也可逐根处理新到达的 K 线。策略回调能在执行过程中检查现金、持仓与订单,让交易逻辑随时访问投资组合状态。

说明

下方示例共用策略定义,需要 vectorbtpro-rust 和 ndarray = "0.16"。安装方式参见 Rust 设置指南。

Define a strategy with price and position conditions
use vectorbtpro_rust::error::VbtResult;
use vectorbtpro_rust::portfolio::enums::Order;
use vectorbtpro_rust::portfolio::simulator::{
    FnOrderStrategy, OrderContext, OrderStrategy,
};

fn buy_the_dip() -> impl OrderStrategy {
    FnOrderStrategy::new(|ctx: &OrderContext<'_, '_>| {
        let price = ctx.close(ctx.col());
        let position = ctx.position(ctx.col());
        let size = if price <= 100.0 && position == 0.0 { 
            1.0
        } else if price >= 110.0 && position > 0.0 {
            -position
        } else {
            return Ok(None);
        };
        Ok(Some(Order::builder().size(size).build()))
    })
}
Run a batch simulation
use ndarray::array;
use vectorbtpro_rust::portfolio::simulator::{OrderSimulator, SimulationConfig};

fn main() -> VbtResult<()> {
    let close = array![[100.0], [98.0], [105.0], [112.0]];
    let groups = array![1];
    let config = SimulationConfig::builder()
        .target_shape(close.dim())
        .group_lens(groups.view())
        .close(close.view())
        .init_cash(1000.0)
        .build()?;
    let simulator = OrderSimulator::builder().config(config).build()?;
    let output = simulator.run_single(&mut buy_the_dip())?;
    for order in &output.order_records {
        println!("row {}: {:.0} share at ${:.0}", order.idx, order.size, order.price);
    }
    Ok(())
}
row 0: 1 share at $100
row 3: 1 share at $112

教程

更多内容参见从 Python 到 Rust教程。

Rust 后端

✅ 安装可选 vectorbtpro-rust 扩展后,兼容的 jitted 调用便可自动使用 Rust 加速。VBT 通过 vbt.rs 提供扩展,将 Rust 内核注册为 jitted="rs",并在 Rust 不可用或不受支持时回退到普通实现,通常是 Numba。

Keep your workflow, get the Rust lane
data = vbt.YFData.pull("BTC-USD", start="2024")

fast_ma = data.close.vbt.rolling_mean(20)  
slow_ma = data.close.vbt.rolling_mean(50)
entries = fast_ma.vbt.crossed_above(slow_ma)  
exits = fast_ma.vbt.crossed_below(slow_ma)

pf = vbt.Portfolio.from_signals(  
    data,
    entries=entries,
    exits=exits,
    sl_stop=0.05,
    tp_stop=0.15,
    fees=0.001,
)

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。