// 策略 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_market_open全新实例 · 在这里初始化状态你声明的数据集里的每个事件 · 按事件时间顺序on_tick / on_book / on_tradereturn Order撮合那一毫秒真实挂着的深度↺ 下一个事件事件放完on_settle结算结果 · 只在这里report.zip权益 · 成交 · 滑点 · 交叉校验

on_tick / on_book / on_trade 里,你至少实现一个就行——想对哪条流做反应就实现哪个;on_market_open 和 on_settle 都是可选的。循环归执行器管:你不轮询、不 sleep、也不去要下一个事件,而且下一行还没被读进来,压根没有可偷看的东西。

你的 hook 返回 一个 Order,执行器拿它去撮合那一毫秒真实挂在盘口上的深度。成交、部分成交、吃穿几档,都由归档决定,不由你的假设决定。

安装

ot 命令行工具只在 npm 包里,要装成全局的,否则它不在你的 PATH 上。pip 包是给编辑器和类型检查用的 SDK 表面,不含 ot——写 Python 策略也需要上面那行才有 ot。版本按语言锁死,执行器用的是同样的版本;对不上是拒绝,不是警告。

SHELL
# 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 语义完全一致,只是名字按各自习惯写,见下面的命名对照表。

STRATEGY.PY
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)
OUTCOMETICK.JSON
{
  "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,提交不会进队列。报价也是按它算出来的。

字段必填含义
schemaYes清单版本,当前是 1。同一个版本里,字段的含义不会改。
languageYespython@3.14 · nodejs@24。是钉死的运行时版本,不是版本范围。
entryYesfile:ClassName。类名必须完全一致,我们不做自动发现,也没有注册用的装饰器。
hooksYes你实现了哪些 hook。没列出来的永远不会被调用;列了却没实现的,提交会被拒掉。
datasetsYessettlement · prices · twap30s · twap60s · book · bbo · trades · markets。只有你要的那些会被喂进来,要得越少,你的 handler 要处理的就越少。价格不变:一个市场日一个 credit,不管读了多少。优先用 settlement。
intervalsNo默认 ["5m"]。归档里的预测市场只有 5m 和 15m 两种周期。一个 credit 买的是「一个币种、一个 UTC 日、一个周期」——两个周期都要就是两倍的市场日、两倍的价,因为那是两倍的市场。
latencyNo这次运行使用的成交延迟(毫秒),例如 250。不填即为 0。每一笔成交都比触发它的信号晚这么久落地,报告里的每个数字都测在那个世界里——它不是额外的一趟,也不额外花时间。
referenceNo外部行情,按同一个时钟回放,比如 binance:btcusdt:spot:1s。
seriesNo你自己的 CSV 序列,按事件时间对齐,可以声明发布延迟。
depsNo只写名字,且必须在该语言的允许列表里。版本由我们定,你钉不了,也没有安装步骤。
paramsNo默认参数,在策略里用 ctx.p 拿。参数扫描变的就是这些;除此之外每个格子的条件完全相同。
modeNomarket(默认,每个市场重置状态)或 session(整个区间一条有序流)。两者同价。

该用哪条结算流

平台改过这些市场的结算依据,而且不是一次性全改的。声明 settlement,每个市场就会被喂给它真正结算所依据的那条流,读的是这个市场自己的配置,绝不按日期去推。你不需要知道切换点在哪,也不可能弄错。

settlement按市场解析到它真正结算所依据的那条流。推荐用这个。
pricesChainlink 的 1 Hz 报价流。
twap30s30 秒回看窗口的 TWAP。
twap60s60 秒回看窗口的 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
命名对照
pythonon_tick(ctx, tick)
nodejsonTick(ctx, tick)

ctx

执行器允许你碰的一切都挂在这个对象上。没列在这里的,这个进程里就没有。

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:1sspot 的 1s K 线,来自币安自己的公开日归档——用的是币安的时间戳,不是我们的。我们提前下载好,回放时和结算流按事件时间对齐。 BTC ETH SOL XRP BNB DOGE
binance:*:spot:1mspot 的 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、以任何理由发起的实时请求。沙盒里没有网络。
MANIFEST + USAGE
  "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)

一个真的用到它的策略

当市场和币安现货动量方向相反时反着做。给的是完整的一份,能看清外部数据是从哪里进来的。

OUTCOMETICK.JSON
{
  "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 }
}
STRATEGY.PY
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,你看到的是时间戳不晚于它的那些行,之后的一行都没有。

SIGNAL.CSV
ts_ms,score,confidence
1786536000000,0.62,0.91
1786536060000,0.58,0.88
1786536120000,0.71,0.95
OUTCOMETICK.JSON
{
  "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 }
}
STRATEGY.PY
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

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 的每一行。

完整示例

三种形态,覆盖了人们在这些市场上实际会跑的大部分东西。每个阈值都是参数,所以每一个都离"调好"只差一次扫描。

1 · 临近结算入场

在结算前 N 秒买一次,且只在价格落在某个区间时才买。整个策略就四个参数,死活全看滑点页和延迟页,因为所有人都在同一秒做同一件事。

LATE ENTRY · PYTHON
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)

2 · 用结算流本身的趋势

入场窗口一样,但方向来自币种自己的动量,而不是这个市场的价格。结算流就是这个市场用来结算的那条 Chainlink 流,而它本来就在喂给你,所以这个例子不需要任何外部数据,也没有时间戳要对齐。想改用交易所的动量,就在清单里声明一条参考流,见「外部数据」。

TREND · PYTHON · "datasets": ["settlement", "book"]
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)

3 · 在同一个市场里反复交易

激进,信号翻转就进出同一个市场。用 on_book 而不是 on_tick 来驱动,这样每次盘口变化你都能反应;用 reduce_only 平仓;想在一个事件里翻方向就 return 一个列表。这是吃单策略:它主动穿过价差。

HIGH FREQUENCY · PYTHON · "hooks": ["on_market_open", "on_book"]
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、每个市场日
CPU1 vCPU你自己开不了线程;并行发生在市场之间
超时时间up to 108 min按扫描的市场日数给,超时就杀掉并退款
区间上限90 days max单次任务;要更长找我们
ctx.log 输出2 MB / run整次运行合计,会留在归档里
状态重置每个市场session 模式取消这条

session 模式取消每个市场的状态重置,让一个实例以单条有序流看到区间内的每一个市场:组合层面的限额、跨市场对冲、资金分配。它无法分片,所以跑得更慢,但收费和 market 模式一样:一个市场日就是一个 credit,哪种模式读它都一样。用 "mode": "session" 声明。

提交的是什么

一个目录。清单必须叫 outcometick.json、放在目录根上;其余文件就是你的入口文件 import 到的那些。网页编辑器把这两个文件名当标签显示给你了,命令行这边要自己建,所以写在这里。

SHELL
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 去跑完整归档。

SHELL
# 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-samples

ot check 跑的就是队列里那个一模一样的校验器。本地通过了,提交时就不会被拒。

提交与计费

可以从命令行提交,也可以在 /backtest 页面提交——进的是同一个队列。`ot check` 跑的就是队列那个校验器,所以本地通过的提交不会在这里被拒。

SHELL
# 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_...
01

校验

清单结构、入口解析、hook 签名、import 允许列表、禁用语法。静态检查,几秒钟,不收费——和 ot check 跑的是同一份。

02

冒烟跑

拿一个市场日的样例数据免费跑一次,必须产出有效报告。在这里就崩的策略,进不了队列,也不会动你的余额。

03

报价与排队

按你的范围数出市场日、冻结额度,然后把任务分片给多个 worker。参数扫描按市场日计费,不按格子。

04

归档

一个 zip:CSV、JSON 报告、覆盖情况和 sha256sums.txt。报告里带着源码的 sha256,能对上是哪一版策略跑的。签名链接通过邮件发给你,保留 7 天。

credits 只在产出了报告的任务上扣。被拒、编译错误、超时、崩溃都不花钱,你会拿到校验器输出或 stderr,归档根本不会被创建。

错误码

除 E_RUNTIME 之外,这里列出的每一条,ot check 在你花任何钱之前就会告诉你。只有 E_RUNTIME 不行:它意味着任务已经在跑了,所以它是全额退款,而不是被提前拦下。

E_MANIFEST没有 outcometick.json,或者它格式不对,又或者 schema 版本我们不认识。
E_ENTRYentry 在指定文件里找不到对应的类,或者这个类没有继承 SDK 的基类。
E_HOOK_SIG声明了的 hook 参数个数不对,或者返回了 Order 和「什么都不返回」以外的类型。
E_IMPORTimport 了允许列表之外的东西,间接引入的也算。会把出问题的引用链打给你。
E_FORBIDDEN导入时发现了线程、子进程、eval、动态 import、反射或原生扩展。
E_NONDETERMINISM用了不带种子的随机,或者读了系统时间。请改用 ctx.random 和 ctx.now。
E_STATE实例状态不能序列化,这个市场日就没法在 worker 之间搬。
E_BUDGET冒烟跑时超了每事件预算。这次没有计费。
E_COVERAGE要的采集流超出了它实际被采集的时间窗。
E_LIMIT超了提交限额——文件数、源码总量,或者序列文件大小。
E_SCOPE要的平台、币种或日期范围我们提供不了。
E_RUNTIME任务已经开始跑,但没能跑完——沙箱崩了、喂给它的数据中途断了,或者回放提前结束。这次没有计费。

策略准备好了?贴进编辑器,或者从 CLI 提交。

跑一次回测 →