作 者:老余捞鱼
原创不易,转载请标明出处及原作者。

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。
本文约 4800 字 | 预计阅读 12分钟,后有代码和开源仓库使用路径,建议先收藏。
NautilusTrader 不是一个策略库,也不是一个回测工具。它是一套完整的交易引擎,交易系统的地基。
GitHub 上 29740 个 star、3949 个 fork,LGPL-3.0 协议,2018 年立项,nautechsystems 维护,到今天还在高频提交。
它要解决的是这么一件事。市面上大多数量化框架,把研究阶段和交易阶段分成了两套东西。研究用 pandas 一把梭,策略逻辑验证完,等真要上市场,行情怎么接、订单怎么报、仓位怎么算、断线怎么重连、对不上账怎么办,全部推倒重写一遍。这套重写的活儿往往比写策略本身还费时间,而且改一个参数要记得两边都改,忘一边就等着实盘给你惊喜吧。
它的思路是把地基先铺好。研究和实盘踩在同一块地基上。
换句话说,你写策略的那部分代码,跑回测和跑实盘是同一份。差的是接哪个交易所。
一、它的结构长什么样
这一点是理解它的关键,不然后面全看不懂。
它是两层结构,底下那层用 Rust 写,上面那层用 Python。

底下那层 Rust 内核负责所有”不能慢”和”不能错”的东西。行情数据进来、订单撮合、持仓与资金计算、风控校验、执行算法、跟交易所的通信,全在这一层。它跑在 tokio 异步运行时上,内存分配器用的是 mimalloc,时间精度是纳秒级。
上面那层 Python 只负责一件事,写策略。你写一个类继承 Strategy,订阅 K 线或者盘口,在回调里下判断要不要下单。这部分是 Python 舒服的那部分,不用跟内存和线程打交道。
中间靠 PyO3 粘起来。这也是为什么装完包要 60MB,里面塞的是完整的 Rust 运行时。
它内部还有几个核心组件,值得知道名字,因为文档里到处是它们。
- `Cache` 数据缓存。行情和账户状态都存在这里,策略查询走它
- `MessageBus` 消息总线。组件之间靠它通信,不直接互相调用
- `Portfolio` 持仓与账户。算仓位、算盈亏、算保证金
- `RiskEngine` 风控。下单前校验,拒不合规的订单
- `ExecutionEngine` 执行。把订单送到交易所,收回执
- `Actors` 参与者。不止策略能做 Actor,行情订阅、对账逻辑都可以做成 Actor 挂进去
还有一点它的设计取向要留意,它的 API 分高低两档。低层 `BacktestEngine` 你可以随便拆开组装,控制最细,但没有通往实盘的路;高层 `BacktestNode` 配置化、有数据目录、能批量跑,官方推荐用这个,它才是能过渡到实盘的那条路。所以别一上来就用低层 API 写策略,写完发现上不了实盘。
资产类别上它是资产无关的,不绑定某一类品种。股票、期货、期权、外汇、加密货币、博彩投注,都能塞进同一套系统研究。
二、作者自己怎么说
它的自我介绍里有一句话我盯着看了很久。它说:回测和实盘用同一套架构,策略代码验证完可以接着往真实市场走。紧接着又补了一句,说实盘依然会引入场所、传输、时序、持久化、外部活动和对账这些行为,模拟未必能复现。
先承认一样再承认一句,这种写法在我看过的项目文档里不多。
我这次就想搞清楚的是,前半句到底成不成立,后半句在实操里会撞到什么样的墙。
下面按顺序来。先看它怎么装,再验卖点,然后是我踩的坑和跑出来的数。
它的 pip 包 60MB,因为里面塞了完整的 Rust 运行时(mimalloc 内存分配器、tokio 异步网络、rustls 加密、PyO3 绑定)。前置条件是 Python 3.13 或 3.14,装完不需要你碰 Rust 工具链,因为官方直接给了预编译轮子。
我是在一台 64GB 内存的 Mac mini 上装的。
终端里跑 `pip install` 时加上预发布开关(文档里写作双连字符加 pre 那个参数),一条命令搞定。装完我第一件事是去核实它到底装成了什么版本,而不是急着写策略。装完顺手看了一眼它自报的环境。
nautilus_trader 2.0.0rc6,nautilus_core 0.65.0,rustc 1.98.1,numpy 2.5.3,pandas 3.0.6,precision high(128-bit, 16 dp),profile release。
precision 那一行我盯着看了半天。128-bit,16 位小数。
你平时写 Python float 是 15 到 17 位有效数字,数字上看着差不多,但它内部用的是十进制定点数算钱。你在代码里写 0.1,它不会给你算出 0.1000000000000000055 这种惊喜。
这不是炫技,是交易所级别系统的基本要求。但它确实带来一个副作用。我写代码时得先想一遍这个数走的是哪条路径,不能像平时那样随手一算就信。
三、怎么装
安装这件事本身不难,难在你要知道自己装的是什么。
第一步是装 Python 3.13。注意不是 3.12,官方明确写了 3.13 起。3.14 也支持。
第二步是加预发布开关装 rc 版。
pip install -U nautilus_trader --pre
那个 pre 参数必须加。截至写稿,PyPI 上没有正式的 2.0.0,只有 `2.0.0rcN` 系列,我用的是 rc6。
第三步是验一下版本。
python -c "import nautilus_trader; print(nautilus_trader.__version__)"
打出 `2.0.0rc6` 就算成了。60MB 的包,几十秒下完,比装个带 CUDA 的 PyTorch 轻。
装完第一件事别急着写策略,先去看它自带的那句警告。文档里明确写着,不要在生产环境用开发轮子,特别是实盘控制真金白银的时候。这句话不是客套,因为它同时还提醒你 TRACE 级别的日志会把 WebSocket 原始报文打出来,里面可能夹着某些场所的认证信息,分享日志之前要脱敏。
一个交易引擎主动提醒你别乱开日志,这个细节比星标数更能说明团队的成熟度。
以上是装上就能用的部分,往下是动手部分。
四、核心卖点我验证了,是真的
我不想只读文档就下判断,所以写了个脚本,把同一个策略类分别挂到回测引擎和模拟实盘节点上跑。
这里要先讲一下它有两层 API。低层是 `BacktestEngine`,你可以直接往里塞组件,控制最细,但没有通往实盘的路。高层是 `BacktestNode`,配置对象、数据目录、批量跑都归它管,而且能平滑过渡到实盘,官方推荐用这个。
我要验证的是低层和高层能不能共用同一套东西。说实话,动手前我心里有点怀疑,因为这类承诺通常在第二个环节就露馅。官方 examples 目录里有个 `live/sandbox` 的例子,看完我就照着写了一段。
from nautilus_trader.live import LiveNode
from nautilus_trader.common import Environment
# 模拟实盘(真实行情 + 本地撮合引擎)
node = (
LiveNode.builder("SANDBOX-001", trader_id, Environment.SANDBOX)
.with_reconciliation(reconciliation=False)
.with_risk_engine_config(LiveRiskEngineConfig(bypass=True))
.add_simulated_exec_client(
"SIM",
SandboxExecutionClientFactory(),
SandboxExecutionClientConfig(
venue=venue,
account_id=Account_id,
starting_balances=[Money(1_000_000, USD)],
fee_model=MakerTakerFeeModel(
maker_rate=Decimal("0.00002"),
taker_rate=Decimal("0.00002"),
),
),
)
.build()
)
跑出来两个节点都建成功了。同一份 instrument 定义、同一套费用模型、同一套账户类型,从回测走到模拟实盘,策略代码一个字没改。
我把这个结果反复看了两遍才敢下结论。
这就是它真正值钱的地方。别的框架里,回测和实盘是两套代码,你改一个参数要记得两边都改,忘了就等着实盘给你惊喜吧。它把这个坑填了。
五、然后我踩到了第一个坑
跑通不代表能用。我喂数据的时候卡了十几分钟,日志刷了一屏。
SubmitOrder ... DENIED: MARKET_PRICE_UNAVAILABLE: order_type=MARKET, instrument_id=AUD/USD.SIM
市价单全部被拒。我一开始以为是自己代码写错了,去翻文档,越查越慌。
后来想明白了,问题出在我只喂了 1 分钟 K 线,没喂报价。它的撮合引擎要撮合市价单,得有一个人家能拿来成交的价格;你只给 K 线,它就不肯凭空捏一个出来,于是每一单都拒。
我说实话,我当时心里咯噔了一下。这要是换个框架,很可能悄悄拿 K线的收盘价给我撮合了,回测跑出来一条漂亮曲线,实盘一单都成交不了。
这个坑最怕的不是 MARKET_PRICE_UNAVAILABLE 报错,是连报都不报。
它不。这是加分项。
我把逐笔报价喂进去,问题解决,两百多笔订单全部成交。改动的核心就一行数据准备,策略逻辑没碰。
六、同一份策略,结果差 19 万
数据跑通之后我做了五组对照。策略是最普通的 EMA 交叉,行情是固定随机种子的合成数据,5000 根 1 分钟 K 线,起始资金 100 万美金,每次单笔 100 万。
我把手续费当唯一变量往上加,然后顺手试了两组均线参数。
| 组别 | 手续费(单边) | 均线 | 订单/成交 | 净利润 | 最大回撤 |
|---|---|---|---|---|---|
| A 基准 | 0.002% | EMA20/50 | 172 / 172 | +87,492.49 | 1.42% |
| B 费率 ×20 | 0.04% | EMA20/50 | 172 / 172 | +14,397.62 | 2.61% |
| C 费率 ×50 | 0.1% | EMA20/50 | 172 / 172 | -101,017.45 | 10.43% |
| D 慢线拉长 | 0.002% | EMA20/100 | 112 / 112 | +79,492.79 | 1.96% |
| E 快线绞紧 | 0.002% | EMA10/20 | 378 / 378 | +92,276.92 | 1.90% |

先看A 到 C。策略代码一个字没改,订单数都是 172,撮合次数都是 172,唯一变的是我填进去的费率假设。净利润从 +87,492 走到 -101,017,中间隔了 188,509 美元将近 19 万。
换算成一句大白话。这份策略在 0.002% 的假设下赚钱,在 0.1% 的假设下亏钱。而这两个数字都是我自己填的,引擎不知道哪个是真的。
它忠实地执行了我的假设,没有替我兜着。
说实话我有点意外。我以为这类框架多少会替我兜一点底,比如自动填一个 0.1% 的滑点。结果它一点都没有。这就是”回测等于实盘”这句话里最容易被跳过的部分。代码确实共用,但成交价、费率、延迟、拒单概率,全是你的假设。
再看 D 和 E,这两组更有意思。均线参数一改,订单数从 172 变成 112 或者 378,成交次数同步跟着变。E 组交易最频繁,净利润最高,但它的收益是建立在更高换手上的,手续费一旦放大,最先死的就是它。
我在做这组对比时顺手算了最大回撤。C 组的 10.43% 是 A 组 1.42% 的七倍多。也就是说,费率假设不仅改变收益的符号,还成倍地改变了风险。这比我预想的更严重。
七、它报 Sharpe 16,我算出 0.0296
这节是我最想跟读者交代的。
跑完 A 组,引擎给我报了个 Sharpe 16.04,Sortino 1526.63,Profit Factor 216。这个数字太漂亮了。我一开始以为是它算错了,核完才知道不是。
我去查它的收益序列,发现 `returns_series` 里只有 5 个点。5 个点算出来的年化 Sharpe 能报 16 并不奇怪,分母太小,除出来自然是个大数。
我自己按每根 bar 的权益变化重算了一遍,4999 个逐 bar 收益点,算出来的 Sharpe 是 0.0296。

差 540 倍。
这里我要说清楚。引擎没算错,它只是严格按它的定义在算。我一开始是把它当成 bug 的。是我的合成行情只覆盖了 3.5 天(5000 根 1 分钟 K 线),年化系数一乘就炸了。换成一年真实数据,这点差异会消失。
但这件事本身是个提醒。它默认给你输出的那套漂亮的绩效统计,样本量不足的时候是不可信的。这个坑不是我踩的,是任何人拿短周期数据跑一遍都会踩的。而一个交易框架把年化 Sharpe 直接打在回测结果里,也不提醒你”收益点只有 5 个”,这是设计上可以更谨慎的地方。
这个问题是我自己踩出来的。我起初以为是公式错了,核完发现公式没错,是我的样本太短。
后来我的处理方式变了。现在拿到回测结果,第一件事先看 returns_series 的长度,再看 Sharpe。这个顺序反过来,就是自欺欺人。
八、它适合谁,不适合谁
说点实际的。
先说不适合的。
只想快速验一个简单策略的话,它太重了。想测个均线交叉,pandas 加 matplotlib 十几行就完了,装一个 60MB 的 Rust 引擎、引入事件驱动的编程模型,收益不成正比。
做 A 股的话,它帮不上直接的忙。我把官方 integrations 页面整个过了一遍,17 个 adapter 里有 Binance、Bybit、OKX、Coinbase、Kraken、Deribit、dYdX、Hyperliquid、Polygon、Polymarket、Betfair、Databento、Tardis,还有 Interactive Brokers。券商只有一个 IB。
我把 17 个 adapter 的名字一个一个读完,结论是没有一家 A 股券商,也没有 CTP、期货公司或者券商柜台接口。
读到最后一个的时候,我确认自己没漏。这个结论对做 A 股的人有点扫兴,但它比让人以为「支持多资产」等于「支持你的资产」要诚实。想做A股,得自己按它的 adapter 规范写一个,这活儿不小。它自己在文档里也留了口子,任何有 REST API 或 WebSocket 行情的场所都能接,但”能接”和”有人接”之间还有很长的路。
数据量很小的话,它的速度优势用不上。它快是因为 Rust 内核加纳秒级时间戳,那是给盘口、高频、多场所套利准备的。你拿日线数据跑一个年线策略,瓶颈在Python 层的策略代码上,不在内核。
然后说适合的。
策略逻辑本身已经稳定、只是不想在实盘环节重复劳动的话,它值得看。假设你有一套跑通的 alpha,现在卡在接行情、对账、断线重连这些纯工程环节上,它能把你从这些活儿里捞出来。
做加密货币的话,这是它的主场。这它的主场,主流交易所基本都在,CEX 和 DEX 都有,数据商也配齐了。
需要多场所同时跑的话,它支持同时对多个场所做市和跨场所策略,单进程多场所这个能力在开源里不算多见。
九、两个必须知道的风险
第一件事是项目正在大版本切换,现在上手要选边。
最新正式版是 v1.231.0,标签后面写着 Beta,发布于 2026-08-02。它的 release note 里写得很清楚,这是 legacy Cython v1 核心的最后一个 1.x 版本;之后 `develop` 分支会变成 v2 专用,v1 核心挪到 `develop_v1` 分支,只收关键安全补丁,收大约三个月。
而我装的是 2.0.0rc6。也就是说现在 PyPI 上的正式版和一个Beta 级旧核心的继承者并存,功能都在往前推,但文档、示例、issue 里有一部分还在讲v1 的写法。
要选边就得想清楚。想跟新功能和长期维护走 v2,注意它列了几条不兼容的变更(Catalog 旧订单事件数据不读、CustomData 语义变了、`Bar.is_revision` 没了),迁移得另开一个虚拟环境测;想稳就留在 1.231.0,但那是安全补丁期,不会有新东西。
第二件事,这个项目的长期风险不来自技术,来自人。
我去查贡献者分布的时候,是带着「看看是不是个松散社区」的预期点的。
结果 GitHub 列出 210 个人,提交量却极度集中。第一位 19,216 次,第二位 339次,第三位 334 次。前一位一个人吃掉绝大部分,后面两百多人分剩下的零头。
我盯着这个数看了好几遍才反应过来不是我读错。
210 这个数字挺唬人的。我认为真正在维护的是一个人。
这个结构的含义很直接。LGPL-3.0 协议允许你商用、允许你改,但核心维护基本压在一个人身上。他哪天不干了,这个项目的迭代速度会断崖式下跌,而你要么自己接手维护,要么回到自己搭一套的状态。
不是说一个人主导的项目一定不靠谱,很多优秀项目都是这样起来的。但你得知道自己在依赖什么。用它做实盘基础设施之前,把这一点想清楚比把回测跑通更重要。
十、我的判断
回到开头那个问题。它宣传的”回测到实盘不用重写”,成立吗?
成立,但有前提。
成立的部分是真的。同一套 Strategy、订单逻辑、执行算法,回测和实盘共用,前提是你自己填的那些假设和实盘一致。
不成立的部分是,它保证不了假设和实盘一致,那是你自己的活儿。它给的是一把好尺子,量出来的数准不准,取决于你怎么用尺子。
我自己的取舍是这样的。
如果我现在有一个已经验证过、只是在工程环节卡住的策略,我会考虑用它,因为它替我省掉的正是最烦的那部分工作。
如果我只是想做研究阶段的快速验证,我不用它,pandas 加 polars 更快。
不管用不用它,有一条纪律我这篇是带走的。看到回测结果,先问收益点够不够,再看收益好不好。这个习惯在哪个框架里都成立。
最后一段代码在下面,我把它拆成了几段带注释的版本,可以直接跑。它只依赖 nautilus_trader 本身,不下载外部数据,所以你跑出来的数字和我文里的一致。
附录,完整脚本
脚本分成五段,都在同一个文件里,依次是行情生成、策略、回测装配、指标自查和主流程。
开头这几行 import 五段都要用,先放在最前面。整份脚本只依赖 nautilus_trader 本身。
import math
import random
from decimal import Decimal
from nautilus_trader.backtest import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.execution import MakerTakerFeeModel
from nautilus_trader.indicators import ExponentialMovingAverage
from nautilus_trader.model import (
BarType, Currency, Money, OrderSide, Price, Quantity, QuoteTick, TraderId, Venue,
)
from nautilus_trader.testkit.providers import TestInstrumentProvider
from nautilus_trader.trading import Strategy, StrategyConfig
SYMBOL = "AUD/USD"
BAR_SPEC = "AUD/USD.SIM-1-MINUTE-MID-EXTERNAL"
第一段,行情。用固定随机种子,不联网,保证可复现。
import math
import random
def synth_closes(n: int = 5000, seed: int = 7) -> list[float]:
"""确定性行情。不下载外部数据,保证谁跑都一样。"""
rnd = random.Random(seed)
px = 1.1000
out = []
for i in range(n):
# 缓慢的正弦漂移 + 高斯噪声,构造一段有趋势但不平滑的行情
drift = 0.00004 * math.sin(i / 260.0)
px *= 1.0 + drift + rnd.gauss(0.0, 0.00045)
out.append(round(px, 5))
return out
第二段,报价。这一段是我踩坑之后补的。撮合市价单必须有报价,不然会被 MARKET_PRICE_UNAVAILABLE 拒掉。
写这段的时候我试了四遍才把参数顺序搞错再搞对。Bar 和 QuoteTick 都是一串位置参数,Price 和 Quantity 长得又不像,猜错一次报一个类型错误。第一遍我把成交量放在价格前面,直接被拒。
K 线那段用的是 Bar,签名不一样,顺序是 bar_type, open, high, low, close, volume, ts_event, ts_init。
from nautilus_trader.model import Bar, Quantity
def make_bars(closes: list[float], bar_spec: str = BAR_SPEC) -> list[Bar]:
"""把收盘价序列包成 1 分钟 K 线。"""
bt = BarType.from_str(bar_spec)
bars = []
for i, c in enumerate(closes):
prev = closes[i - 1] if i else c
hi = max(prev, c) * 1.00012
lo = min(prev, c) * 0.99988
ts = 1_700_000_000_000_000_000 + i * 60_000_000_000
bars.append(
Bar(
bt,
Price.from_str(f"{prev:.5f}"),
Price.from_str(f"{hi:.5f}"),
Price.from_str(f"{lo:.5f}"),
Price.from_str(f"{c:.5f}"),
Quantity.from_int(1000),
ts,
ts,
),
)
return bars
def make_quotes(closes: list[float], spread: float = 0.00002):
"""给每个时间戳造一条双边报价。撮合必须有报价,否则市价单被拒。"""
iid = TestInstrumentProvider.default_fx_ccy(SYMBOL, Venue("SIM")).id
out = []
for i, c in enumerate(closes):
half = spread / 2
ts = 1_700_000_000_000_000_000 + i * 60_000_000_000
out.append(
QuoteTick(
iid,
Price.from_str(f"{c - half:.5f}"),
Price.from_str(f"{c + half:.5f}"),
Quantity.from_int(1000),
Quantity.from_int(1000),
ts,
ts,
),
)
return out
第三段,策略。EMA 交叉,用它自带的指标类。配置类要跟着策略一起给,漏了会 NameError。
class EmaXConfig(StrategyConfig):
def __init__(self, instrument_id, bar_type, trade_size,
fast=20, slow=50, **kw):
super().__init__()
self.instrument_id = instrument_id
self.bar_type = bar_type
self.trade_size = trade_size
self.fast = fast
self.slow = slow
class EmaX(Strategy):
def __init__(self, config: EmaXConfig) -> None:
super().__init__(config)
self.fast_ema = ExponentialMovingAverage(config.fast)
self.slow_ema = ExponentialMovingAverage(config.slow)
self.equity = [] # (ts, 权益),用来自己算指标
def on_start(self) -> None:
self.register_indicator_for_bars(self.config.bar_type, self.fast_ema)
self.register_indicator_for_bars(self.config.bar_type, self.slow_ema)
self.subscribe_bars(self.config.bar_type)
def on_bar(self, bar) -> None:
# 每根 bar 记一次权益,用来自己算指标(引擎自报的那套样本量不足)
acct = self.portfolio.account(self.config.instrument_id.venue)
if acct is not None:
self.equity.append(
(int(bar.ts_event), float(acct.balance_total().as_decimal())),
)
if not self.indicators_initialized():
return
iid = self.config.instrument_id
if self.fast_ema.value >= self.slow_ema.value:
if self.portfolio.is_net_flat(iid):
self._market(1)
elif self.portfolio.is_net_short(iid):
self.close_all_positions(iid)
self._market(1)
elif self.fast_ema.value < self.slow_ema.value:
if self.portfolio.is_net_flat(iid):
self._market(-1)
elif self.portfolio.is_net_long(iid):
self.close_all_positions(iid)
self._market(-1)
def _market(self, sign: int) -> None:
"""市价单。sign 为 1 买、-1 卖。"""
inst = self.cache.instrument(self.config.instrument_id)
side = OrderSide.BUY if sign > 0 else OrderSide.SELL
order = self.order_factory.market(
self.config.instrument_id,
side,
inst.make_qty(Decimal(self.config.trade_size)),
)
self.submit_order(order)
def on_stop(self) -> None:
self.close_all_positions(self.config.instrument_id)
第四段,装配引擎。这段在一个 `run_case` 函数里,手续费是参数传进去的,五组实验唯一变的就是它。
from nautilus_trader.model import AccountType, OmsType
def run_case(label, taker_rate="0.00002", maker_rate="0.00002",
fast=20, slow=50, starting=1_000_000):
venue = Venue("SIM")
usd = Currency.from_str("USD")
engine = BacktestEngine(
BacktestEngineConfig(trader_id=TraderId.from_str("BACKTESTER-001"), logging=None),
)
engine.add_venue(
venue=venue,
oms_type=OmsType.HEDGING,
account_type=AccountType.MARGIN,
base_currency=usd,
starting_balances=[Money(starting, usd)],
fee_model=MakerTakerFeeModel(
maker_rate=Decimal(maker_rate),
taker_rate=Decimal(taker_rate),
),
)
inst = TestInstrumentProvider.default_fx_ccy(SYMBOL, venue)
engine.add_instrument(inst)
closes = synth_closes()
engine.add_data(make_quotes(closes))
engine.add_data(make_bars(closes))
strat = EmaX(EmaXConfig(
instrument_id=inst.id,
bar_type=BarType.from_str(BAR_SPEC),
trade_size=Decimal("1000000"),
fast=fast, slow=slow,
))
engine.add_strategy(strat)
engine.run()
end_bal = float(engine.portfolio.account(venue).balance_total().as_decimal())
out = {
"label": label,
"taker_rate": taker_rate,
"pnl": round(end_bal - starting, 2),
"pnl_pct": round((end_bal - starting) / starting * 100, 4),
"n_orders": int(len(engine.generate_orders_report())),
"n_fills": int(len(engine.generate_order_fills_report())),
}
out.update(metrics_from_equity(strat.equity, starting))
engine.dispose()
return out
第五段,自己算 Sharpe。这一点是我建议每个用这个框架的人都抄走的。
def metrics_from_equity(points, starting):
"""自己算最大回撤与逐点收益,避免直接引用引擎的年化指标。"""
bals = [b for _, b in points]
peak, mdd = bals[0], 0.0
for b in bals:
peak = max(peak, b)
if peak:
mdd = max(mdd, (peak - b) / peak)
rets = []
for a, b in zip(bals, bals[1:]):
if a:
rets.append(b / a - 1.0)
out = {"eq_points": len(bals), "mdd_pct": round(mdd * 100, 4)}
if len(rets) > 1:
mean = sum(rets) / len(rets)
sd = (sum((r - mean) ** 2 for r in rets) / (len(rets) - 1)) ** 0.5
if sd > 0:
out["sharpe_unannualized"] = round(mean / sd, 4)
out["n_periods"] = len(rets)
return out
跑完之后你会得到一张表,长这样。
case orders fills PnL PnL% mdd%
A基准 EMA20/50 taker 0.002% 172 172 87,492.49 8.7492 1.4226
B 费率x20 taker 0.04% 172 172 14,397.62 1.4398 2.6085
C 费率x50 taker 0.1% 172 172 -101,017.45 -10.1017 10.4297
D EMA20/100 112 112 79,492.79 7.9493 1.9640
E EMA10/20 378 378 92,276.92 9.2277 1.9034
想改手续费假设,就改主流程里的第二个参数。想验证自己那份策略能不能扛住成本,把 taker 往上调 50 倍看看还剩多少,就知道它是真的有Alpha 还是刚好盖住了手续费。
项目地址 https://github.com/nautechsystems/nautilus_trader
#NautilusTrader #量化工具 #开源项目 #回测框架 #实盘交易 #事件驱动 #交易系统 #量化工程 #Rust #A股量化
风险提示:本文所有数据来自公开仓库信息与本人本地实测,合成行情不代表任何真实市场,不构成任何操作建议。市场有风险,文中提到的任何工具都不构成荐股。开源框架的技术指标不能替代策略本身的逻辑验证。
Be First to Comment