校验
清单结构、入口解析、hook 签名、import 允许列表、禁用语法。静态检查,几秒钟,不收费——和 ot check 跑的是同一份。
// 策略 SDK · v1.6.7
一个策略就是一个类,加一份 outcometick.json。执行器在密闭容器里按事件时间顺序逐 tick 调你的 hook。本地对着免费样例文件用的是同一套 SDK,我们机器上对着完整归档用的也是同一套,所以在你本地上能跑的策略,到这里原样能跑。
循环由执行器控制。你的类只实现 hook,不开文件、不读时钟、不连网络。事件按这个顺序交给你:
for each market-day in your scope (sharded across workers)
on_market_open(ctx, market) ← state resets here
for each event in event-time order:
on_tick / on_book / on_trade ← you may return an Order
on_settle(ctx, market, outcome) ← official label arrives last每个市场彼此独立:你的实例状态在每次 on_market_open 时被丢弃,正因如此我们才能把成百上千个市场日分片到多个 worker 上。
实线框是你要写的方法,虚线框是执行器在你两次调用之间做的事。
on_tick / on_book / on_trade 里,你至少实现一个就行——想对哪条流做反应就实现哪个;on_market_open 和 on_settle 都是可选的。循环归执行器管:你不轮询、不 sleep、也不去要下一个事件,而且下一行还没被读进来,压根没有可偷看的东西。
你的 hook 返回 一个 Order,执行器拿它去撮合那一毫秒真实挂在盘口上的深度。成交、部分成交、吃穿几档,都由归档决定,不由你的假设决定。
ot 命令行工具只在 npm 包里,要装成全局的,否则它不在你的 PATH 上。pip 包是给编辑器和类型检查用的 SDK 表面,不含 ot——写 Python 策略也需要上面那行才有 ot。版本按语言锁死,执行器用的是同样的版本;对不上是拒绝,不是警告。
# the ot CLI — check / run / submit (node 24, ESM only)
npm i -g outcometick@1.6.7
# writing in Python? add this for type hints in your editor.
# it does NOT contain ot — the line above is what gives you the command.
pip install outcometick==1.6.7两个文件就是一次完整的提交。这里用 Python 演示;Node.js 语义完全一致,只是名字按各自习惯写,见下面的命名对照表。
from outcometick import Strategy, Order
class MeanReversion(Strategy):
def on_market_open(self, ctx, market):
self.entered = False
def on_tick(self, ctx, tick):
# tick.value is the settlement feed's price, as a float
z = ctx.zscore(tick.value, window=180)
if self.entered or abs(z) < ctx.p.entry_z:
return None
side = "DOWN" if z > 0 else "UP"
book = ctx.book(tick.market_id)
if book.depth(side) < ctx.p.min_depth:
return None
self.entered = True
return Order(side=side, notional=ctx.p.notional,
limit=book.best(side),
hold_s=ctx.p.hold_minutes * 60){
"schema": 1,
"language": "python@3.14",
"entry": "strategy.py:MeanReversion",
"hooks": ["on_market_open", "on_tick"],
"datasets": ["settlement", "book"],
"deps": ["numpy"],
"params": {
"entry_z": 2.0,
"hold_minutes": 20,
"notional": 200,
"min_depth": 2000
}
}没有一份合法的 outcometick.json,提交不会进队列。报价也是按它算出来的。
| 字段 | 必填 | 含义 |
|---|---|---|
| schema | Yes | 清单版本,当前是 1。同一个版本里,字段的含义不会改。 |
| language | Yes | python@3.14 · nodejs@24。是钉死的运行时版本,不是版本范围。 |
| entry | Yes | file:ClassName。类名必须完全一致,我们不做自动发现,也没有注册用的装饰器。 |
| hooks | Yes | 你实现了哪些 hook。没列出来的永远不会被调用;列了却没实现的,提交会被拒掉。 |
| datasets | Yes | settlement · prices · twap30s · twap60s · book · bbo · trades · markets。只有你要的那些会被喂进来,要得越少,你的 handler 要处理的就越少。价格不变:一个市场日一个 credit,不管读了多少。优先用 settlement。 |
| intervals | No | 默认 ["5m"]。归档里的预测市场只有 5m 和 15m 两种周期。一个 credit 买的是「一个币种、一个 UTC 日、一个周期」——两个周期都要就是两倍的市场日、两倍的价,因为那是两倍的市场。 |
| latency | No | 这次运行使用的成交延迟(毫秒),例如 250。不填即为 0。每一笔成交都比触发它的信号晚这么久落地,报告里的每个数字都测在那个世界里——它不是额外的一趟,也不额外花时间。 |
| reference | No | 外部行情,按同一个时钟回放,比如 binance:btcusdt:spot:1s。 |
| series | No | 你自己的 CSV 序列,按事件时间对齐,可以声明发布延迟。 |
| deps | No | 只写名字,且必须在该语言的允许列表里。版本由我们定,你钉不了,也没有安装步骤。 |
| params | No | 默认参数,在策略里用 ctx.p 拿。参数扫描变的就是这些;除此之外每个格子的条件完全相同。 |
| mode | No | market(默认,每个市场重置状态)或 session(整个区间一条有序流)。两者同价。 |
平台改过这些市场的结算依据,而且不是一次性全改的。声明 settlement,每个市场就会被喂给它真正结算所依据的那条流,读的是这个市场自己的配置,绝不按日期去推。你不需要知道切换点在哪,也不可能弄错。
| settlement | 按市场解析到它真正结算所依据的那条流。推荐用这个。 |
| prices | Chainlink 的 1 Hz 报价流。 |
| twap30s | 30 秒回看窗口的 TWAP。 |
| twap60s | 60 秒回看窗口的 TWAP。 |
| book | 盘口快照和增量。 |
| bbo | 未限流的盘口顶部:只有价格,没有挂单量。它删除市场已经越过的档位,绝不新增——维护盘口的增量流按平台的采集频率抽稀,所以最陈旧的档位恰好是订单最先吃到的那些,而且陈旧的方向对你有利。要用就得自己写上它:它不在起始 manifest 里,因为打开它会改变哪些档位可以成交,也就改变每一份没用它写出来的报告。Polymarket 自 2026-09-03 起采集(归档里也有 2026-09-02,但那天缺开头 42 分钟)。更早的日期和 Predict.fun 不会被拒绝——它们就按这条流出现之前的方式跑,coverage 里会列出哪些天用上了。 |
| trades | 平台广播的成交流。 |
| markets | 逐市场的元数据、行权价和结算结果。 |
| twap60s:derived | 对于开始采集之前的日期,由 prices 重新算出来——它是我们确实持有的数据的确定性函数,每一行都标了 derived。 |
| twap30s:derived | 对于开始采集之前的日期,由 prices 重新算出来——它是我们确实持有的数据的确定性函数,每一行都标了 derived。 |
声明一条我们实际并未在那个窗口采集过的流,会得到 E_COVERAGE,绝不会悄悄换一条给你。每份归档都会报出每条结算流各自支撑了多少个市场,这一次重放用的是什么组合,你看得到。
需要哪些就实现哪些,并在清单里精确声明。没声明的钩子永远不会被调用;声明了却没实现的,是拒绝。
| hook | 触发时机 | 返回 |
|---|---|---|
| on_market_open | 每个市场一次,在任何事件之前。实例状态就在这次调用之前被清空。 | nothing |
| on_tick | 结算流的每一条,按事件时间顺序。这条流就是该市场实际用来结算的那条。 | Order | list | None |
| on_book | 盘口每次变化时触发,前提是你声明了 book 数据集。传入的是这次变化本身(快照或单个档位),变化后的盘口用 ctx.book() 读。 | Order | list | None |
| on_trade | 归档里的每条成交,包括别人的。 | Order | list | None |
| on_settle | 每个市场一次,在最后一个事件之后,带上官方结算结果("UP"、"DOWN",或 "TIE"——50:50 结算,两边每份都兑付 $0.50)和行权价。 | nothing |
| python | on_tick(ctx, tick) |
| nodejs | onTick(ctx, tick) |
执行器允许你碰的一切都挂在这个对象上。没列在这里的,这个进程里就没有。
| ctx.p | 这次运行的参数,已经按清单做过类型校验。 |
| ctx.now | 事件时间,毫秒。进程里唯一的时间。 |
| ctx.book(market_id) | 当前这一毫秒的盘口。.best(side) 是卖一,.best_bid(side) 是买一,另有 .depth(side) 和 .levels(n)。看不到之后的状态。 |
| ctx.history(n) | 你已经见过的最近 n 个 tick。结构上不可能更多。 |
| ctx.zscore(v, window) | 滚动指标:zscore、ema、sma、stdev。两种语言算出来的值完全一致。 |
| ctx.position() | 你在这个市场上的持仓:方向、数量、平均成本、浮盈浮亏。 |
| ctx.random(seed) | 带种子的随机数生成器。这是唯一的随机来源,种子会记进报告。 |
| ctx.ref(name) | 已声明的参考流在当前时点的视图:.last、.window(n)、.at(ts)。永远看不到时间戳晚于 ctx.now 的行。 |
| ctx.ext(name) | 你自己声明的序列,同样是时点数据,声明的延迟已经替你扣掉了。 |
| ctx.log(msg) | 每行最多 512 个字符,整次运行合计 2 MB,会留在归档里。它是用来看你自己代码的窗口,不是导出格式——超出预算后剩下的会被丢弃,日志里会写明。 |
| ctx.assert_outcome(m, o) | 把你自己算的结果和官方结算对一下,结果出现在交叉校验面板里。 |
运行中拉不了 Binance。沙盒没有网络,而且实时拉取会让同一份代码在不同日子跑出不同的报告。外部数据在开跑之前就被解析成一个数据集,和其它一切按同一个时钟回放。
| binance:*:spot:1s | spot 的 1s K 线,来自币安自己的公开日归档——用的是币安的时间戳,不是我们的。我们提前下载好,回放时和结算流按事件时间对齐。 BTC ETH SOL XRP BNB DOGE |
| binance:*:spot:1m | spot 的 1m K 线,来自币安自己的公开日归档——用的是币安的时间戳,不是我们的。我们提前下载好,回放时和结算流按事件时间对齐。 BTC ETH SOL XRP BNB DOGE |
| series(你自己的) | 每条序列一个 CSV,最大 4 MB。要有表头、一列时间戳(ts_ms / ts / time / timestamp / date / datetime,或者第一列直接放 epoch / ISO),其余列随你写。当数据读,CSV 里的东西不会被解包或执行。 |
| lag_ms | 每条序列可选。每一行会推迟到 ts_ms + lag_ms 才给你,用来模拟一个你当时不可能立刻拿到的信号。 |
| 不提供 | 运行中对任何 URL、以任何理由发起的实时请求。沙盒里没有网络。 |
"reference": ["binance:btcusdt:spot:1s"],
"series": [{ "name": "my_signal", "file": "signal.csv" }]
# both are read-only and aligned to event time
spot = ctx.ref("binance:btcusdt:spot:1s").last
basis = tick.value - spot.close
mine = ctx.ext("my_signal").at(ctx.now)当市场和币安现货动量方向相反时反着做。给的是完整的一份,能看清外部数据是从哪里进来的。
{
"schema": 1,
"language": "python@3.14",
"entry": "strategy.py:Basis",
"hooks": ["on_market_open", "on_tick"],
"datasets": ["settlement", "book"],
"reference": ["binance:btcusdt:spot:1s"],
"params": { "lookback_s": 30, "min_drift": 0.0004, "max_px": 0.6, "notional": 80 }
}from outcometick import Strategy, Order
class Basis(Strategy):
"""Fade the market when it disagrees with Binance spot momentum."""
def on_market_open(self, ctx, market):
self.done = False
def on_tick(self, ctx, tick):
if self.done:
return None
spot = ctx.ref("binance:btcusdt:spot:1s")
# Enough history to measure momentum over. Early in a market-day there
# is not any, and None is what you get -- not a zero, and not a bar
# borrowed from after ctx.now.
bars = spot.window(ctx.p.lookback_s)
if len(bars) < ctx.p.lookback_s:
return None
drift = (bars[-1].close - bars[0].close) / bars[0].close
if abs(drift) < ctx.p.min_drift:
return None
# Spot is going up, so the market saying "down" is the side to take.
side = "UP" if drift > 0 else "DOWN"
book = ctx.book(tick.market_id)
px = book.best(side)
if px is None or px > ctx.p.max_px:
return None
self.done = True
ctx.log(f"drift {drift:+.4%} over {ctx.p.lookback_s}s -> {side} at {px}")
return Order(side=side, notional=ctx.p.notional, limit=px)
window(n) 只返回截至 ctx.now 已经收盘的那些——市场日刚开始时拿到的会少于你要的数量,所以这里是检查长度而不是假设。另外:覆盖 [t-1s, t] 的 1 秒 K 线在 t 时刻才出现,因为它在 t-1s 时还不可知。
在编辑器里用「+ 外部数据」把文件挂上去,manifest 会自动声明。它和其它一切按同一个时钟读:在 ctx.now,你看到的是时间戳不晚于它的那些行,之后的一行都没有。
ts_ms,score,confidence
1786536000000,0.62,0.91
1786536060000,0.58,0.88
1786536120000,0.71,0.95{
"schema": 1,
"language": "python@3.14",
"entry": "strategy.py:Signal",
"hooks": ["on_tick"],
"datasets": ["settlement", "prices", "twap30s", "twap60s", "book", "bbo", "trades", "markets"],
"series": [{ "name": "my_signal", "file": "signal.csv", "lag_ms": 300000 }],
"params": { "min_score": 0.6, "notional": 80 }
}from outcometick import Strategy, Order
class Signal(Strategy):
def on_tick(self, ctx, tick):
row = ctx.ext("my_signal").last
# None until the first row is BOTH stamped and past its lag_ms. The
# column names are your CSV's own -- ts_ms is ours, the rest is yours.
if row is None or row.score < ctx.p.min_score:
return None
book = ctx.book(tick.market_id)
px = book.best("UP")
if px is None:
return None
return Order(side="UP", notional=ctx.p.notional, limit=px)
如果你的信号是延迟算出来的——比如一个要五分钟数据才能给这一分钟打分的模型——用 lag_ms 声明出来,每一行都会被推迟那么久。不声明的话,回测会在一个它还不可能存在的时刻读到这个信号。
两者在结构上都是时点数据:.at() 和 .last 看不到时间戳晚于 ctx.now 的行,所以一个粗心构造的信号文件也没法把未来漏进你的回测。如果你的序列有发布延迟,用 "lag_ms" 声明出来,我们会把每一行推迟那么久再给你。
想要的参考数据来自我们还没接的交易所?说一声,接一条新的通常很快。
Order 是 hook 的返回值,不是你发出去的请求。执行器拿它撮合那一毫秒挂着的深度;超过可见量的订单会部分成交并吃上盘口,缺口出现在滑点报告里。
| side | "UP" 或 "DOWN",你买的是哪一边的结果代币。 |
| size | 订单份额。和平台上一样,保证金全额占用。和 notional 二选一,不能同时给。 |
| notional | 最多花这么多钱,按你的 limit 折算成份额:floor(notional / limit)。必须配 limit——一张合约的成本是它实际成交的价格,而吃单会沿盘口往上走,所以用当前最优价折算,一有滑点就会超预算。 |
| limit | 价格,0 到 1 之间。开仓时是上限,高于它不成交;减仓时是下限。没成交的部分会如实报出来,不会当成成交。 |
| hold_s | 持有多少秒后平掉,从成交那一刻算起;不填就持有到结算。 |
| reduce_only | 只减不加,数量会被截到你的现有持仓,并且是卖给买一。 |
| tif | 只能是 "ioc"。订单要么在那一毫秒吃到挂着的深度,要么记为未成交。 |
| tag | 任意字符串,会带到 trades.csv 的每一行。 |
三种形态,覆盖了人们在这些市场上实际会跑的大部分东西。每个阈值都是参数,所以每一个都离"调好"只差一次扫描。
在结算前 N 秒买一次,且只在价格落在某个区间时才买。整个策略就四个参数,死活全看滑点页和延迟页,因为所有人都在同一秒做同一件事。
def on_market_open(self, ctx, market):
self.close_ts = market.close_ts_ms
self.done = False
def on_tick(self, ctx, tick):
secs_left = (self.close_ts - ctx.now) / 1000
if self.done or secs_left > ctx.p.enter_at_s:
return None
ask = ctx.book(tick.market_id).best(ctx.p.side)
if not ctx.p.px_min <= ask <= ctx.p.px_max:
return None
self.done = True
return Order(side=ctx.p.side, notional=ctx.p.notional, limit=ask)入场窗口一样,但方向来自币种自己的动量,而不是这个市场的价格。结算流就是这个市场用来结算的那条 Chainlink 流,而它本来就在喂给你,所以这个例子不需要任何外部数据,也没有时间戳要对齐。想改用交易所的动量,就在清单里声明一条参考流,见「外部数据」。
def on_tick(self, ctx, tick):
secs_left = (self.close_ts - ctx.now) / 1000
if self.done or secs_left > ctx.p.enter_at_s:
return None
# Momentum on the underlying, straight off the settlement stream —
# the same Chainlink prints this market settles on. No outside feed,
# and nothing to align: it is the series you are already being fed.
past = ctx.history(ctx.p.lookback_s + 1)
if len(past) <= ctx.p.lookback_s:
return None
mom = tick.value / past[0].value - 1
if abs(mom) < ctx.p.mom_threshold:
return None
side = "UP" if mom > 0 else "DOWN"
ask = ctx.book(tick.market_id).best(side)
if ask is None or not ctx.p.px_min <= ask <= ctx.p.px_max:
return None
self.done = True
return Order(side=side, notional=ctx.p.notional, limit=ask)激进,信号翻转就进出同一个市场。用 on_book 而不是 on_tick 来驱动,这样每次盘口变化你都能反应;用 reduce_only 平仓;想在一个事件里翻方向就 return 一个列表。这是吃单策略:它主动穿过价差。
def on_book(self, ctx, event):
book = ctx.book() # the event is the change; this is the book it produced
edge = ctx.p.fair - book.best("UP") # your signal
pos = ctx.position()
# flat, and the edge is worth crossing for
if pos.size == 0 and edge > ctx.p.entry_edge:
return Order(side="UP", size=ctx.p.clip, limit=book.best("UP"))
# edge gone — flatten
if pos.size > 0 and edge < ctx.p.exit_edge:
return Order(side="UP", size=pos.size, reduce_only=True,
limit=book.best_bid("UP"), tag="exit")
# signal flipped — close and reverse in one event
if pos.size > 0 and edge < -ctx.p.entry_edge:
return [
Order(side="UP", size=pos.size, reduce_only=True, limit=book.best_bid("UP")),
Order(side="DOWN", size=ctx.p.clip, limit=book.best("DOWN")),
]
return None| 挂单(tif: "gtc") | 一直挂到市场收盘的单子,和做市报价是同一个难题:没有排队位置模型,我们只能猜你排在队伍的什么位置,而往乐观里猜会把收益放大好几倍。所以提交这种单会被拒,而不是悄悄按一次性成交处理。 |
| 做市报价 | 挂在盘口等着被吃的被动单。要如实模拟,就得有排队位置模型——你在那个价位上排第几——而乐观假设会把做市收益放大好几倍。在这块模型做扎实之前不支持。主动吃价差的策略现在就能跑。 |
| 撤单改单 | 同一件事没做完之前不支持。现在订单要么在那一毫秒吃到挂着的深度,要么记为未成交。 |
| 跨市场状态(默认模式) | 实例状态每个市场重置一次。要做组合层面的逻辑就用 session 模式:同一个实例按时间顺序看完整个区间。价格相同。 |
| 编译型语言 | Go 和 Rust 要先编译不受信任的代码,而编译本身就是在执行不受信任的代码——build script、过程宏——还需要一整套离线的依赖缓存。这是另一个安全问题,不是这个问题的放大版。 |
| 实盘或前向测试 | 这是一个跑归档的回放引擎。它不碰任何实盘,也不会真的下单。 |
同一份代码在同一区间上跑两次,必须产出逐字节一致的报告,否则我们给你的数字就没有意义。这些规则在导入时静态检查、在运行时结构性保证:那些 API 根本不存在,而不仅仅是被拦住。
| 文件数 | 6 | 只收文本,不收压缩包和仓库地址 |
| 源码总量 | 256 KB | 含清单文件 |
| 每事件预算 | 400 µs sustained | 持续超标会杀掉这个分片 |
| 内存 | 2 GB | 每个 worker、每个市场日 |
| CPU | 1 vCPU | 你自己开不了线程;并行发生在市场之间 |
| 超时时间 | up to 108 min | 按扫描的市场日数给,超时就杀掉并退款 |
| 区间上限 | 90 days max | 单次任务;要更长找我们 |
| ctx.log 输出 | 2 MB / run | 整次运行合计,会留在归档里 |
| 状态重置 | 每个市场 | session 模式取消这条 |
session 模式取消每个市场的状态重置,让一个实例以单条有序流看到区间内的每一个市场:组合层面的限额、跨市场对冲、资金分配。它无法分片,所以跑得更慢,但收费和 market 模式一样:一个市场日就是一个 credit,哪种模式读它都一样。用 "mode": "session" 声明。
一个目录。清单必须叫 outcometick.json、放在目录根上;其余文件就是你的入口文件 import 到的那些。网页编辑器把这两个文件名当标签显示给你了,命令行这边要自己建,所以写在这里。
my-strategy/
outcometick.json # the manifest: language, entry, hooks, datasets, params
strategy.py # the file `entry` points at, holding the `entry` class
helpers.py # split it up if you like
signal.csv # your own series, declared under `series`
# python: import your own files RELATIVELY, or the validator rejects them —
# from .helpers import cheaper ✓
# from helpers import cheaper ✗ E_IMPORT: not on the allowlist
# node: import { cheaper } from "./helpers.mjs";
# the directory IS the submission — that is what the "." refers to
ot check ./my-strategy
ot submit ./my-strategy --assets btc --from 2026-07-14 --to 2026-08-12 --venue polymarket最多 6 个文件,其中源码总量不超过 256 KB;清单里声明的 CSV 序列占文件数,但字节数单独计,每个上限 4 MB,而且是直接传到存储、不经过 API。
SDK 里带的就是我们用的那个执行器。先指向免费的样例仓库,把契约调对,再花 credits 去跑完整归档。
# one day of real files, free, no key needed
curl -L https://github.com/Ligengxin96/polymarket-data-samples/releases/latest/download/polymarket-data-samples.tar.gz | tar xz
# validate the manifest and hook signatures — no data touched, no key needed
ot check .
# replay locally, same runner, same report format
ot run . --data ./polymarket-data-samplesot check 跑的就是队列里那个一模一样的校验器。本地通过了,提交时就不会被拒。
可以从命令行提交,也可以在 /backtest 页面提交——进的是同一个队列。`ot check` 跑的就是队列那个校验器,所以本地通过的提交不会在这里被拒。
# the key travels in the environment, never in a flag:
# a flag lands in shell history and in the process list, and this key spends money
export OT_BACKTEST_KEY=bt_...
# submit to the queue against the full archive
ot submit . --assets btc,eth --from 2026-07-14 --to 2026-08-12
# where it got to, and what it actually cost
ot status run_...
# download the archive once it is done
ot fetch run_...清单结构、入口解析、hook 签名、import 允许列表、禁用语法。静态检查,几秒钟,不收费——和 ot check 跑的是同一份。
拿一个市场日的样例数据免费跑一次,必须产出有效报告。在这里就崩的策略,进不了队列,也不会动你的余额。
按你的范围数出市场日、冻结额度,然后把任务分片给多个 worker。参数扫描按市场日计费,不按格子。
一个 zip:CSV、JSON 报告、覆盖情况和 sha256sums.txt。报告里带着源码的 sha256,能对上是哪一版策略跑的。签名链接通过邮件发给你,保留 7 天。
credits 只在产出了报告的任务上扣。被拒、编译错误、超时、崩溃都不花钱,你会拿到校验器输出或 stderr,归档根本不会被创建。
除 E_RUNTIME 之外,这里列出的每一条,ot check 在你花任何钱之前就会告诉你。只有 E_RUNTIME 不行:它意味着任务已经在跑了,所以它是全额退款,而不是被提前拦下。
| E_MANIFEST | 没有 outcometick.json,或者它格式不对,又或者 schema 版本我们不认识。 |
| E_ENTRY | entry 在指定文件里找不到对应的类,或者这个类没有继承 SDK 的基类。 |
| E_HOOK_SIG | 声明了的 hook 参数个数不对,或者返回了 Order 和「什么都不返回」以外的类型。 |
| E_IMPORT | import 了允许列表之外的东西,间接引入的也算。会把出问题的引用链打给你。 |
| E_FORBIDDEN | 导入时发现了线程、子进程、eval、动态 import、反射或原生扩展。 |
| E_NONDETERMINISM | 用了不带种子的随机,或者读了系统时间。请改用 ctx.random 和 ctx.now。 |
| E_STATE | 实例状态不能序列化,这个市场日就没法在 worker 之间搬。 |
| E_BUDGET | 冒烟跑时超了每事件预算。这次没有计费。 |
| E_COVERAGE | 要的采集流超出了它实际被采集的时间窗。 |
| E_LIMIT | 超了提交限额——文件数、源码总量,或者序列文件大小。 |
| E_SCOPE | 要的平台、币种或日期范围我们提供不了。 |
| E_RUNTIME | 任务已经开始跑,但没能跑完——沙箱崩了、喂给它的数据中途断了,或者回放提前结束。这次没有计费。 |
策略准备好了?贴进编辑器,或者从 CLI 提交。
跑一次回测 →