输入/搜索内容
入门
欢迎使用发明者量化交易平台
快速开始
密钥安全性
平台基础
账号与计费
实盘计费与充值
子账号
交易所
通用协议
本地凭据文件
交易所特殊说明
证券与期货
加密货币
托管者
策略库
实盘
编写策略
开发工具
回测系统
进阶专题
数据与研究
对外接口

进阶用法:JavaScript多线程、实盘之间通信、API限流、期权交易、Web3链上交易。

JavaScript策略可以用threading对象创建真正并行执行的线程,并用消息、共享字典、锁等对象在线程之间通信。本页说明怎么选用、怎么组织线程代码;各函数的参数和返回值见语法手册Threads。

先选对工具

需求推荐适用语言
同时发出几个API请求(如同时取多个交易所的行情),等结果回来exchange.Go,配合EventLoop等待完成事件所有语言
长时间在后台运行的任务:独立的行情采集、风控巡检、耗时计算threading.Thread仅JavaScript
在策略内提供HTTP、WebSocket或TCP服务threading.Serve仅JavaScript

只是并发几个请求时,exchange.Go()更简单,也不需要处理线程间的数据传递。本页的threading对象只适用于JavaScript策略,Python、Rust策略请使用exchange.Go()。

回测系统中可以调用这些函数,但线程实际是按顺序执行的,只用于保证代码在回测中能运行。

线程运行在隔离的环境中

传给threading.Thread()的函数在一个独立的JavaScript环境中执行,这是写线程代码时最需要注意的一点:

  • 线程函数不能引用外部的变量和闭包,也不能调用策略里自定义的其它函数。需要的数据通过threading.Thread(func, arg1, arg2, ...)的参数传入。
  • 普通对象、数组作为参数时是深拷贝:线程里修改它不影响其它线程。需要多个线程看到同一份数据时,使用threading.Dict()创建的字典。
  • 函数也可以作为参数传入;threading.Thread()还支持传入函数源码字符串,用于在线程中加载外部库。
  • 线程里可以直接调用平台的API函数,如exchange.GetTicker()、Log()。
  • 线程函数的返回值通过join()取回:t.join().ret。

线程之间怎样交换数据

方式用法说明
消息t.postMessage(msg)发给线程t;线程内用threading.currentThread().peekMessage(timeout)读取自己收到的消息;子线程用threading.mainThread().postMessage(msg)发回主线程每个线程有自己的收件箱,按顺序读取。peekMessage(-1)不阻塞,没有消息时返回空值
共享字典var d = threading.Dict(),作为参数传入线程后各线程d.get(key)、d.set(key, value)适合保存「最新状态」,例如最新行情、运行标志
线程数据t.setData(key, value)、t.getData(key)挂在某个线程对象上的键值,线程结束(join()、terminate())后失效
同步对象threading.Lock()、threading.Event()、threading.Condition()作为参数传入线程,用于互斥访问和等待通知

线程收到消息时也会产生事件,可以用线程对象的eventLoop统一等待消息和其它事件。

线程的生命周期

  • t.join()等待线程结束并取回返回值,可以设置超时;t.terminate()强制结束线程。
  • 线程结束且不再被引用时,资源会自动回收,不必为了释放资源调用join()。持续引用、无法回收的线程累计超过2000个时会报错。
  • threading.pending()返回正在运行的线程数(包括主线程)。
  • 实盘停止时所有线程一起结束。peekMessage()、join()、锁和事件的等待都会被停止打断。

在策略内提供服务

threading.Serve(地址, 处理函数, ...参数)在策略进程内启动HTTP(含WebSocket)或TCP服务,每个请求或连接在独立的线程中调用处理函数,返回Server对象(addr()取实际监听地址,close()关闭)。处理函数与线程函数一样运行在隔离环境中,需要的数据通过参数传入,常用threading.Dict()与主线程共享状态。地址写法、ctx对象的方法见Serve。

旧的全局函数__Serve()仍可使用,它只返回监听地址字符串,新代码请使用threading.Serve()。

示例

示例

  • 多个线程并行计算,主线程汇总结果

    每个线程拉取一个交易对的K线并计算均线,结果通过返回值交给主线程。注意交易对通过参数传入,线程函数里没有引用外部变量。

    javascript
    function main() { var symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT"] var threads = [] for (var i = 0; i < symbols.length; i++) { threads.push(threading.Thread(function(symbol, period) { // 在线程中运行:只能使用参数和平台API var records = exchange.GetRecords(symbol, period) if (!records || records.length < 20) { return null } var ma = TA.MA(records, 20) return {symbol: symbol, close: records[records.length - 1].Close, ma20: ma[ma.length - 1]} }, symbols[i], PERIOD_H1)) } for (var i = 0; i < threads.length; i++) { var r = threads[i].join().ret if (r) { Log(r.symbol, "收盘价:", r.close, "MA20:", r.ma20) } } }
  • 后台线程采集行情,主线程读取与下发指令

    后台线程把最新价写进共享字典,并把异常通过消息报告给主线程;主线程通过消息通知后台线程退出。

    javascript
    function main() { var shared = threading.Dict() var worker = threading.Thread(function(dict, symbol) { while (true) { // 读取主线程发来的指令,-1 表示不阻塞 var cmd = threading.currentThread().peekMessage(-1) if (cmd == "stop") { break } var ticker = exchange.GetTicker(symbol) if (ticker) { dict.set("last", ticker.Last) dict.set("time", ticker.Time) } else { threading.mainThread().postMessage("行情获取失败: " + GetLastError()) } Sleep(1000) } return "worker exited" }, shared, "BTC_USDT") for (var i = 0; i < 10; i++) { // 最多等 1 秒后台线程的消息 var msg = threading.currentThread().peekMessage(1000) if (msg) { Log("后台线程报告:", msg) } LogStatus("最新价:", shared.get("last"), "时间:", _D(shared.get("time"))) } worker.postMessage("stop") Log(worker.join().ret) }
  • 用threading.Serve提供状态查询接口

    主线程把状态写进共享字典,HTTP处理函数从参数取到同一个字典并返回JSON。

    javascript
    function main() { var state = threading.Dict() var server = threading.Serve("http://127.0.0.1:8088", function(ctx, st) { if (ctx.path() == "/status") { ctx.setHeader("Content-Type", "application/json") ctx.write(JSON.stringify({last: st.get("last"), updated: st.get("updated")})) } else { ctx.setStatus(404) } }, state) Log("服务地址:", server.addr()) while (true) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { state.set("last", ticker.Last) state.set("updated", _D()) } Sleep(3000) } }

参考

每个实盘都有一个频道,频道ID就是实盘ID。实盘用SetChannelData()在自己的频道上发布数据,其它实盘用GetChannelData(实盘ID)读取。数据经平台服务端转发,可以跨托管者、跨服务器传递。

频道保存的是最新状态,不是消息队列:每次发布都覆盖上一次的数据,订阅端每次读到的都是当前最新的一份。需要历史记录时由订阅端自己保存。

常见用途:

  • 主从协同:主策略分析行情并发布信号,多个从策略读取信号在各自账户上执行。
  • 状态监控:各策略发布运行状态,监控实盘汇总展示或告警。
  • 数据共享:一个实盘计算指标、发布结果,其它实盘直接使用,避免重复计算。

使用要点

  • 首次读取即订阅:对某个频道第一次调用GetChannelData()时完成订阅并返回空值(null/None),之后服务端把该频道的更新推送到本实盘,再调用就能读到最新数据。订阅端应在启动时就开始读取,并处理空值。
  • 订阅上限:每个实盘最多订阅10个不同的频道(包括下面的UUID频道)。超出时该次调用返回空值,并记录一条错误日志channel subscriber exceed limit。
  • **数据格式**:JavaScript、Python的SetChannelData()可以传入任何可以JSON序列化的数据,订阅端读到的是解析后的对象。数据不变时不会重复发送。数据大小限制见SetChannelData。
  • Rust:SetChannelData(string)只接受字符串,需要自己拼好JSON文本;GetChannelData()没有频道参数,不能指定要读取的频道,因此不能订阅其它实盘或UUID频道。Rust策略适合作为广播端,订阅端请使用JavaScript或Python。
  • **跨平台推送**:外部系统(如TradingView告警、自建程序)可以通过扩展API的method=pub向指定实盘推送一个以32位UUID标识的频道数据,实盘用GetChannelData(UUID)读取,具体方法见SetChannelData、GetChannelData。
  • 实盘功能:频道通信用于实盘之间,回测时不要依赖它。当前实盘ID可以用_G()获取。
  • 不要在频道中传递密钥等敏感信息。

基本用法

示例

  • 广播端:发布行情摘要

    javascript
    function main() { var robotId = _G() // 当前实盘ID,也就是本实盘的频道ID var updateId = 0 while (true) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { // 发布最新状态,覆盖上一次的数据 SetChannelData({ robotId: robotId, updateId: ++updateId, timestamp: Date.now(), symbol: "BTC_USDT", lastPrice: ticker.Last }) LogStatus("频道", robotId, "第", updateId, "次发布,最新价:", ticker.Last) } Sleep(60000) // 每分钟发布一次 } }
    python
    import time def main(): robotId = _G() # 当前实盘ID,也就是本实盘的频道ID updateId = 0 while True: ticker = exchange.GetTicker("BTC_USDT") if ticker: updateId += 1 # 发布最新状态,覆盖上一次的数据 SetChannelData({ "robotId": robotId, "updateId": updateId, "timestamp": int(time.time() * 1000), "symbol": "BTC_USDT", "lastPrice": ticker["Last"] }) LogStatus("频道", robotId, "第", updateId, "次发布,最新价:", ticker["Last"]) Sleep(60000) # 每分钟发布一次
    rust
    fn main() { let robotId = _G!(); // 当前实盘ID,也就是本实盘的频道ID let mut updateId = 0; loop { if let Ok(ticker) = exchange.GetTicker("BTC_USDT") { updateId += 1; // Rust 的 SetChannelData 只接受字符串,自己拼 JSON 文本 let state = format!( r#"{{"robotId": "{}", "updateId": {}, "timestamp": {}, "symbol": "BTC_USDT", "lastPrice": {}}}"#, robotId, updateId, Unix() * 1000, ticker.Last ); SetChannelData(&state); LogStatus!("频道", robotId, "第", updateId, "次发布,最新价:", ticker.Last); } Sleep(60000); // 每分钟发布一次 } }
  • 订阅端:读取两个频道

    javascript
    function main() { // 要订阅的实盘ID(按实际情况修改) var channels = ["632799", "632800"] while (true) { var msg = "" for (var i = 0; i < channels.length; i++) { // 第一次调用完成订阅并返回 null,之后返回最新数据 var state = GetChannelData(channels[i]) if (state) { msg += "频道 " + channels[i] + ":#" + state.updateId + " " + _D(state.timestamp) + " 最新价 " + state.lastPrice + "\n" } else { msg += "频道 " + channels[i] + ":等待数据\n" } } LogStatus(msg) Sleep(5000) } }
    python
    def main(): # 要订阅的实盘ID(按实际情况修改) channels = ["632799", "632800"] while True: msg = "" for ch in channels: # 第一次调用完成订阅并返回 None,之后返回最新数据 state = GetChannelData(ch) if state: msg += "频道 {}:#{} {} 最新价 {}\n".format(ch, state["updateId"], _D(state["timestamp"]), state["lastPrice"]) else: msg += "频道 {}:等待数据\n".format(ch) LogStatus(msg) Sleep(5000)
    rust
    // Rust 的 GetChannelData() 没有频道参数,不能订阅其它实盘的频道
  • 场景:主从策略协同

    主策略计算均线交叉信号并发布;从策略读取信号,在信号变化时下单。

    主策略(发布信号)

    javascript
    function main() { while (true) { var records = exchange.GetRecords("BTC_USDT") if (records && records.length >= 21) { var ma5 = TA.MA(records, 5) var ma20 = TA.MA(records, 20) var n = records.length var signal = "HOLD" if (ma5[n - 1] > ma20[n - 1] && ma5[n - 2] <= ma20[n - 2]) { signal = "BUY" } else if (ma5[n - 1] < ma20[n - 1] && ma5[n - 2] >= ma20[n - 2]) { signal = "SELL" } SetChannelData({ timestamp: Date.now(), symbol: "BTC_USDT", signal: signal, price: records[n - 1].Close }) LogStatus("当前信号:", signal, "价格:", records[n - 1].Close) } Sleep(60000) } }
    python
    import time def main(): while True: records = exchange.GetRecords("BTC_USDT") if records and len(records) >= 21: ma5 = TA.MA(records, 5) ma20 = TA.MA(records, 20) signal = "HOLD" if ma5[-1] > ma20[-1] and ma5[-2] <= ma20[-2]: signal = "BUY" elif ma5[-1] < ma20[-1] and ma5[-2] >= ma20[-2]: signal = "SELL" SetChannelData({ "timestamp": int(time.time() * 1000), "symbol": "BTC_USDT", "signal": signal, "price": records[-1]["Close"] }) LogStatus("当前信号:", signal, "价格:", records[-1]["Close"]) Sleep(60000)
    rust
    fn main() { loop { if let Ok(records) = exchange.GetRecords("BTC_USDT", None, None) { let n = records.len(); if n >= 21 { let ma5 = TA.MA(&records, 5); let ma20 = TA.MA(&records, 20); let mut signal = "HOLD"; if ma5[n - 1] > ma20[n - 1] && ma5[n - 2] <= ma20[n - 2] { signal = "BUY"; } else if ma5[n - 1] < ma20[n - 1] && ma5[n - 2] >= ma20[n - 2] { signal = "SELL"; } let price = records[n - 1].Close; // Rust 的 SetChannelData 只接受字符串,自己拼 JSON 文本 let data = format!( r#"{{"timestamp": {}, "symbol": "BTC_USDT", "signal": "{}", "price": {}}}"#, Unix() * 1000, signal, price ); SetChannelData(&data); LogStatus!("当前信号:", signal, "价格:", price); } } Sleep(60000); } }
  • 从策略(读取信号并执行)

    javascript
    function main() { var masterId = "632799" // 主策略的实盘ID var lastSignal = null while (true) { var data = GetChannelData(masterId) if (!data) { LogStatus("等待主策略信号...") } else { if (data.signal !== lastSignal) { Log("收到新信号:", data.signal, "信号价格:", data.price) var ticker = exchange.GetTicker(data.symbol) if (ticker && data.signal === "BUY") { exchange.CreateOrder(data.symbol, "buy", ticker.Last, 0.01) } else if (ticker && data.signal === "SELL") { exchange.CreateOrder(data.symbol, "sell", ticker.Last, 0.01) } lastSignal = data.signal } LogStatus("当前信号:", data.signal, "信号时间:", _D(data.timestamp)) } Sleep(5000) } }
    python
    def main(): masterId = "632799" # 主策略的实盘ID lastSignal = None while True: data = GetChannelData(masterId) if not data: LogStatus("等待主策略信号...") else: if data["signal"] != lastSignal: Log("收到新信号:", data["signal"], "信号价格:", data["price"]) ticker = exchange.GetTicker(data["symbol"]) if ticker and data["signal"] == "BUY": exchange.CreateOrder(data["symbol"], "buy", ticker["Last"], 0.01) elif ticker and data["signal"] == "SELL": exchange.CreateOrder(data["symbol"], "sell", ticker["Last"], 0.01) lastSignal = data["signal"] LogStatus("当前信号:", data["signal"], "信号时间:", _D(data["timestamp"])) Sleep(5000)
    rust
    // Rust 的 GetChannelData() 没有频道参数,不能读取主策略实盘的频道
  • 场景:多策略状态监控

    各策略按上面广播端的方式发布状态,监控实盘读取所有频道,用表格展示,超过2分钟没有更新的标记为异常。

    javascript
    function main() { var monitorList = ["632799", "632800", "632801"] // 最多10个 while (true) { var table = {type: "table", title: "策略运行状态", cols: ["实盘ID", "状态", "最后更新", "交易对", "最新价"], rows: []} for (var i = 0; i < monitorList.length; i++) { var data = GetChannelData(monitorList[i]) if (data) { var status = Date.now() - data.timestamp < 120000 ? "运行中" : "异常" table.rows.push([monitorList[i], status, _D(data.timestamp), data.symbol || "-", data.lastPrice || "-"]) } else { table.rows.push([monitorList[i], "等待数据", "-", "-", "-"]) } } LogStatus("`" + JSON.stringify(table) + "`") Sleep(10000) } }
    python
    import json import time def main(): monitorList = ["632799", "632800", "632801"] # 最多10个 while True: table = {"type": "table", "title": "策略运行状态", "cols": ["实盘ID", "状态", "最后更新", "交易对", "最新价"], "rows": []} for robotId in monitorList: data = GetChannelData(robotId) if data: status = "运行中" if time.time() * 1000 - data["timestamp"] < 120000 else "异常" table["rows"].append([robotId, status, _D(data["timestamp"]), data.get("symbol", "-"), data.get("lastPrice", "-")]) else: table["rows"].append([robotId, "等待数据", "-", "-", "-"]) LogStatus("`" + json.dumps(table) + "`") Sleep(10000)
    rust
    // Rust 的 GetChannelData() 没有频道参数,不能读取其它实盘的频道

参考

交易所对API调用频率有限制,超限轻则请求被拒,重则账号被临时封禁。用exchange.IO("rate", ...)或exchange.IO("quota", ...)可以在托管者本地给标准函数设置调用频率上限:超限的调用不会发出请求。

javascript
exchange.IO("rate" | "quota", 名字, 次数, 窗口[, "delay"])

两种模式

  • rate(令牌桶):桶容量默认等于次数,开始时是满的,之后按「次数/窗口」的速度匀速补充,每次调用消耗一个。允许短时间连续调用,长期平均不超过「次数/窗口」。次数写成"10/5"时表示每个窗口补充10次、桶容量为5,用来限制突发。
  • quota(固定窗口):每个窗口内最多调用次数次,进入下一个窗口时清零。窗口按时间纪元对齐:"1s"对齐整秒,"1m"对齐整分钟,"1h"对齐整点,"1d"对齐UTC零点(即北京时间08:00)。例如12:00:00.900开始计数,到12:00:01.000就进入了新窗口。

需要严格保证「任意一个交易所计数周期内不超过N次」时用quota并让窗口与交易所的计数周期一致;只需要控制平均频率时用rate。

参数

参数说明
名字要限制的函数名,见下表。多个名字用逗号分隔(如"GetTicker,GetDepth")时共用一条规则,调用次数合并计算。"*"是兜底规则,只对没有专属规则的函数生效。
次数每个窗口允许的调用次数,必须大于0;rate模式可以写成"次数/突发"。传0或负数表示删除该名字的规则。
窗口时长,写法同Go语言的time.ParseDuration:单位ns、us(或µs)、ms、s、m、h,可以带小数("1.5s"),可以组合("1h30m");另外支持"Nd"表示N天(可以带小数,如"0.5d",不能与其它单位组合)。写成"@HHMM"或"@HHMMSS"(如"@0800")表示按天计数、每天在该时刻(北京时间)清零,rate和quota都可以使用。
动作省略时超限的调用立即失败;写"delay"时阻塞等待,直到有可用次数再发出请求。等待期间停止实盘会打断等待。

可以限制的函数名

类别名字
行情GetTicker、GetTickers、GetDepth、GetTrades、GetRecords、GetMarkets、GetFundings
账户GetAccount、GetAssets、GetPositions、SetMarginLevel
交易CreateOrder(Buy、Sell也计入)、CancelOrder、ModifyOrder
订单查询GetOrder、GetOrders、GetHistoryOrders
条件单CreateConditionOrder、ModifyConditionOrder、CancelConditionOrder、GetConditionOrder、GetConditionOrders、GetHistoryConditionOrders
自定义请求IO/api:只限制exchange.IO("api", ...),不影响其它exchange.IO()指令
  • GetAccount和GetAssets是同一个底层请求,写其中任何一个名字的规则对两个函数都生效。
  • 通过exchange.Go()并发调用时,按实际调用的函数计数。

规则的作用范围

  • 规则按交易所对象分别设置:exchanges[0]上的规则不影响exchanges[1]。
  • 规则只在本次运行中有效,实盘重启后需要重新设置,通常写在main()开头。
  • 同一个名字再次设置时覆盖原规则;名字传空字符串(exchange.IO("rate", ""))清空该交易所对象上的全部规则。
  • 一次调用只按一条规则计数:有专属规则的函数不再计入"*",所以"*"不能当作「所有调用的总配额」叠加在专属规则之上。

超限时的表现

默认动作下,超限的调用不发请求,按调用失败处理(JavaScript返回null,Python返回None,Rust返回Err),错误信息形如:

rate limit exceeded: GetTicker 10/1s quota limit exceeded: GetTicker 10/1m quota limit exceeded: GetRecords 2000/day (resets at 0800)

使用"delay"动作时调用会阻塞到有可用次数,日志中记录的调用时间是等待结束之后的时间。对每天清零的规则使用"delay",最长可能等待到第二天,请谨慎使用。

示例

示例

  • 默认动作:超限时调用失败

    javascript
    function main() { // GetTicker 平均每秒最多 5 次(令牌桶,容量 5) exchange.IO("rate", "GetTicker", 5, "1s") for (var i = 0; i < 10; i++) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { Log("第", i + 1, "次成功:", ticker.Last) } else { // 超限的调用不发请求,返回 null Log("第", i + 1, "次被限流:", GetLastError()) } } }
    python
    def main(): # GetTicker 平均每秒最多 5 次(令牌桶,容量 5) exchange.IO("rate", "GetTicker", 5, "1s") for i in range(10): ticker = exchange.GetTicker("BTC_USDT") if ticker: Log("第", i + 1, "次成功:", ticker["Last"]) else: # 超限的调用不发请求,返回 None Log("第", i + 1, "次被限流:", GetLastError())
    rust
    fn main() { // GetTicker 平均每秒最多 5 次(令牌桶,容量 5) let _ = exchange.IO(("rate", "GetTicker", 5, "1s")); for i in 0..10 { match exchange.GetTicker("BTC_USDT") { Ok(ticker) => Log!("第", i + 1, "次成功:", ticker.Last), // 超限的调用不发请求,返回 Err Err(e) => Log!("第", i + 1, "次被限流:", e), } } }
  • 按交易所的限频规则分组设置

    行情和交易分别共用一条规则;交易类超限时等待而不是失败;其余没有专属规则的函数用"*"兜底。

    javascript
    function main() { // 行情:GetTicker、GetDepth 合计平均每秒 20 次 exchange.IO("rate", "GetTicker,GetDepth", 20, "1s") // 交易:下单(含 Buy/Sell)、撤单合计每秒 5 次,超限时等待 exchange.IO("rate", "CreateOrder,CancelOrder", 5, "1s", "delay") // 兜底:其它函数(如 GetAccount、GetPositions)合计每分钟 60 次,窗口对齐整分钟 exchange.IO("quota", "*", 60, "1m") while (true) { var ticker = exchange.GetTicker("BTC_USDT") var depth = exchange.GetDepth("BTC_USDT") if (ticker && depth) { Log("最新价:", ticker.Last, "买一:", depth.Bids[0].Price) } Sleep(1000) } }
    python
    def main(): # 行情:GetTicker、GetDepth 合计平均每秒 20 次 exchange.IO("rate", "GetTicker,GetDepth", 20, "1s") # 交易:下单(含 Buy/Sell)、撤单合计每秒 5 次,超限时等待 exchange.IO("rate", "CreateOrder,CancelOrder", 5, "1s", "delay") # 兜底:其它函数(如 GetAccount、GetPositions)合计每分钟 60 次,窗口对齐整分钟 exchange.IO("quota", "*", 60, "1m") while True: ticker = exchange.GetTicker("BTC_USDT") depth = exchange.GetDepth("BTC_USDT") if ticker and depth: Log("最新价:", ticker["Last"], "买一:", depth["Bids"][0]["Price"]) Sleep(1000)
    rust
    fn main() { // 行情:GetTicker、GetDepth 合计平均每秒 20 次 let _ = exchange.IO(("rate", "GetTicker,GetDepth", 20, "1s")); // 交易:下单(含 Buy/Sell)、撤单合计每秒 5 次,超限时等待 let _ = exchange.IO(("rate", "CreateOrder,CancelOrder", 5, "1s", "delay")); // 兜底:其它函数(如 GetAccount、GetPositions)合计每分钟 60 次,窗口对齐整分钟 let _ = exchange.IO(("quota", "*", 60, "1m")); loop { if let (Ok(ticker), Ok(depth)) = (exchange.GetTicker("BTC_USDT"), exchange.GetDepth("BTC_USDT")) { Log!("最新价:", ticker.Last, "买一:", depth.Bids[0].Price); } Sleep(1000); } }
  • 突发容量、每日配额与删除规则

    javascript
    function main() { // 平均每秒 10 次,但最多连续突发 2 次 exchange.IO("rate", "GetDepth", "10/2", "1s") // 每天北京时间 08:00 清零,每天最多 2000 次 exchange.IO("quota", "GetRecords", 2000, "@0800") // 窗口可以组合单位:每 1 小时 30 分钟最多 100 次 exchange.IO("rate", "GetOrders", 100, "1h30m") // 次数传 0:删除 GetOrders 的规则 exchange.IO("rate", "GetOrders", 0) // 名字传空字符串:清空本交易所对象上的全部规则 exchange.IO("rate", "") }
    python
    def main(): # 平均每秒 10 次,但最多连续突发 2 次 exchange.IO("rate", "GetDepth", "10/2", "1s") # 每天北京时间 08:00 清零,每天最多 2000 次 exchange.IO("quota", "GetRecords", 2000, "@0800") # 窗口可以组合单位:每 1 小时 30 分钟最多 100 次 exchange.IO("rate", "GetOrders", 100, "1h30m") # 次数传 0:删除 GetOrders 的规则 exchange.IO("rate", "GetOrders", 0) # 名字传空字符串:清空本交易所对象上的全部规则 exchange.IO("rate", "")
    rust
    fn main() { // 平均每秒 10 次,但最多连续突发 2 次 let _ = exchange.IO(("rate", "GetDepth", "10/2", "1s")); // 每天北京时间 08:00 清零,每天最多 2000 次 let _ = exchange.IO(("quota", "GetRecords", 2000, "@0800")); // 窗口可以组合单位:每 1 小时 30 分钟最多 100 次 let _ = exchange.IO(("rate", "GetOrders", 100, "1h30m")); // 次数传 0:删除 GetOrders 的规则 let _ = exchange.IO(("rate", "GetOrders", 0)); // 名字传空字符串:清空本交易所对象上的全部规则 let _ = exchange.IO(("rate", "")); }

参考

发明者量化交易平台支持在以下加密货币期货交易所交易期权。期权的用法与期货合约相同:用exchange.SetContractType()把合约设为期权代码(期权代码就是交易所的原生代码,各交易所写法不同),之后GetTicker()、GetDepth()等行情函数,Buy()、Sell()(下单前用exchange.SetDirection()设置交易方向)、CancelOrder()、GetPositions()等交易函数都作用于该期权合约。也可以用完整的交易品种代码直接下单,形如交易对.期权代码,例如BTC_USDT.BTC-260925-145000-C。

期权合约的盘口通常较薄:买一、卖一没有挂单时Ticker的Buy、Sell为0,从未成交的合约Last也可能为0,各交易所的处理见下文。exchange.GetMarkets()是否列出期权合约因交易所而异;不列出时,期权代码需要从交易所的接口或网页获取。

Futures_Deribit

设置期权合约后即可获取行情、下单、撤单、查询持仓。期权代码例子:BTC-13SEP24-60000-C、XRP_USDC-27SEP24-1-C,组合合约例子:BTC-CS-6SEP24-57000_57500、BTC-PCAL-20SEP24_13SEP24-55000。exchange.GetMarkets()的结果包含期权合约。

可供参考的策略代码:Deribit期权测试策略

Futures_OKX

用法与Deribit相同,交易对设置为BTC_USD等,期权代码形如BTC-USD-200626-4500-C。从未成交的期权合约,GetTicker()的Last取标记价格。exchange.GetMarkets()不列出期权合约,可以通过OKX的/api/v5/public/instruments接口查询期权合约列表,例如查询BTC期权:

javascript
function main() { Log(HttpQuery("https://www.okx.com/api/v5/public/instruments?instType=OPTION&uly=BTC-USD")) }
python
import json import urllib.request def main(): ret = json.loads(urllib.request.urlopen("https://www.okx.com/api/v5/public/instruments?instType=OPTION&uly=BTC-USD").read().decode('utf-8')) Log(ret)
rust
fn main() { let body: String = HttpQuery("https://www.okx.com/api/v5/public/instruments?instType=OPTION&uly=BTC-USD", None); Log!(body); }

Futures_Binance

支持币安欧式期权(USDT结算),交易对设置为BTC_USDT等,期权代码形如BTC-260925-145000-C(标的-到期日YYMMDD-行权价-C/P)。需要账户已开通期权交易。限制:

  • 只支持限价单,不支持市价单、条件单和改单(exchange.ModifyOrder())。
  • 不支持exchange.SetMarginLevel()等杠杆、保证金模式设置。
  • 统一账户(组合保证金)不支持期权。
  • exchange.GetMarkets()不列出期权合约。

Futures_Bybit

支持两种结算的期权:

  • USDC结算:交易对设置为ETH_USDC等,期权代码形如ETH-25NOV22-1375-P。
  • USDT结算:交易对设置为ETH_USDT等,期权代码比USDC结算的多一段结算币后缀,形如ETH-25JUN27-2800-C-USDT。

exchange.GetMarkets()的结果包含两种结算的期权合约。Bybit期权没有K线接口,GetRecords()由成交记录合成。

Futures_Aevo

支持Aevo交易所的USDC期权,交易对设置为ETH_USDC等,期权代码形如ETH-30JUN23-1600-C。GetTicker()的Last为标记价格。Aevo没有K线接口,GetRecords()由成交记录合成,合约没有成交时为空。exchange.GetMarkets()的结果包含期权合约。

Futures_GateIO

支持Gate交易所的USDT期权,交易对设置为BTC_USDT等,期权代码形如BTC_USDT-20211130-65000-C。exchange.GetMarkets()的结果包含期权合约。账户没有开通期权时,查询订单、持仓会返回交易所的错误。

Futures_Kraken

支持Kraken期货的期权,交易对设置为ETH_USD等,期权代码形如OF_ETHUSD_261225_4000_C(OF_、标的与计价币、到期日YYMMDD、行权价、C/P),代码中的标的与计价币必须与交易对一致,BTC在代码中写作XBT(如OF_XBTUSD_...)。

  • Kraken没有期权合约列表接口,exchange.GetMarkets()不包含期权合约,期权代码需要从Kraken网页获取。
  • GetTicker()的Last取标记价格,Buy、Sell、High、Low为0,原始数据Info中有隐含波动率、希腊值等字段。
  • 行情、K线、订单、持仓、历史订单的查询可用;期权下单与期货合约走同一个下单接口,尚未经过实盘验证。
  • 期权没有资金费率,不支持exchange.SetMarginLevel()。

参考

在去中心化交易所Uniswap、PancakeSwap上兑换代币,请使用Uniswap交易所对象,见Uniswap与PancakeSwap;查询链上数据、调用智能合约、发送自定义交易,请使用Web3交易所对象,它支持以太坊等EVM兼容链和波场。

Uniswap交易所对象在一条链上连接Uniswap或PancakeSwap的V2、V3资金池,把链上兑换映射为现货交易函数:用exchange.GetTicker()看价格、用exchange.CreateOrder()下单,不需要自己注册ABI、编码合约调用。选路、询价、代币授权、价格保护、发送交易都由交易所对象完成。

什么时候用Uniswap交易所对象,什么时候用Web3

  • 在Uniswap、PancakeSwap上兑换代币:用Uniswap交易所对象。
  • 调用其它合约、DEX的其它功能(如提供流动性、管理V3头寸)、其它链、自定义交易:用Web3交易所对象,见以太坊(EVM)。

一个策略可以同时添加两种交易所对象,使用同一个钱包。

配置交易所对象

字段说明
DEXUniswap或PancakeSwap
ChainEthereum、Arbitrum、Base、BNB Chain。一个交易所对象只对应一条链上的一个DEX
Private Key钱包私钥(十六进制字符串)。支持把私钥本地化部署在托管者上,参看密钥安全性
Rpc Address该链的节点地址,选择Chain时自动填入公共节点(如以太坊为https://ethereum-rpc.publicnode.com)。可以写多个节点,用逗号分隔,互为备用
Rpc Api Key节点鉴权,可以留空。写成名称: 值时作为该名称的请求头发送,否则作为Authorization: Basic <值>发送

第一次调用时会核对节点所在的链与Chain是否一致,不一致时报错,不会把交易发到别的链上。钱包里需要有该链的原生币(ETH或BNB)支付gas。

交易对

  • 交易对写作ETH_USDC、UNI_USDT这样的基础币_计价币。
  • 代币名按以下顺序解析:内置的常用代币(原生币、包装币、USDC、USDT等)→ 用exchange.IO("token", 名字, 合约地址)登记的代币 → 官方代币列表。官方列表中同一条链上有同名代币时,请改用合约地址。
  • 不在代币表中的代币可以直接用合约地址作为交易对的一部分,例如0x1f9840a85d5af5bf1d1762f925bdaddc4201f984_USDC。
  • 原生币与包装币是两种资产:ETH与WETH、BNB与WBNB分别是不同的币。交易原生币时路由合约会自动包装、解包。两者之间的转换不能下单,用exchange.IO("wrap", 数量)、exchange.IO("unwrap", 数量)直接调用包装币合约,1:1兑换,只花gas。
  • exchange.GetMarkets()只列出常用的交易对,没有列出的交易对同样可以交易。

标准函数的含义

函数行为
exchange.GetTicker()买一、卖一是按一定规模实际询价得到的可成交价格(已包含池子手续费);链上没有24小时统计
exchange.GetDepth()按逐档递增的规模在链上询价,推算出的价位,不是真实的挂单簿
exchange.GetTrades()该交易对资金池最近的链上兑换记录
exchange.GetAccount()、exchange.GetAssets()钱包中原生币和代币表中各代币的余额
exchange.CreateOrder()立即在链上兑换,见下文
exchange.GetOrder()订单ID就是交易哈希,状态来自交易回执:上链前为未完成,上链后为成交或失败
exchange.GetOrders()本次运行发出、还没有上链的订单
exchange.CancelOrder()用同一个nonce发送一笔替换交易,尽力撤销,见下文

不支持exchange.GetRecords()、exchange.GetTickers()、exchange.GetHistoryOrders()。

下单

DEX没有挂单簿,每笔订单都是一次立即执行的链上兑换:要么整笔成交,要么整笔回滚(只损失gas),不会部分成交、也不会挂在那里等价格。

  • 限价单:限价是最差成交价。下单时先询价,按当前价格达不到限价时直接报错,不发交易;达得到时把「最少得到/最多支付」写进链上交易,交易上链前价格变动导致达不到时整笔回滚。
  • 市价单:按询价结果扣除滑点得到最少得到/最多支付的数量,滑点默认0.5%,用exchange.IO("slippage", 比例)修改。
  • 数量:卖出时是卖出的基础币数量;限价买入时是要买到的基础币数量;市价买入时是要花费的计价币数量。
  • 单笔订单可以在方向参数后附加设置,例如exchange.CreateOrder("ETH_USDC", 'sell;{"slippage":0.01,"route":"v3"}', -1, 0.1):slippage为本单滑点,route限定路径类型(v2、v3、hop两跳、direct直连)。
  • 卖出代币(ERC20)前会检查路由合约的授权额度,不够时先发送授权交易并等待上链。默认只授权本次需要的数量,exchange.IO("approve", "max")改为无限授权,省去之后的授权交易。
  • 交易超过截止时间(默认120秒,exchange.IO("deadline", 秒数)修改)仍未上链时会回滚,避免在价格大幅变化后才成交。

撤单

exchange.CancelOrder()用原订单的nonce发送一笔转给自己的0金额交易,手续费更高,先上链则原订单失效。这只是尽力撤销:原订单可能在替换交易之前上链并成交;原订单已经上链时撤单直接报错。撤单后用exchange.GetOrder()确认最终状态。

常用的exchange.IO()指令

指令作用
exchange.IO("slippage", 比例)市价单滑点,默认0.005
exchange.IO("deadline", 秒数)交易截止时间,默认120秒
exchange.IO("gasMultiplier", 倍数)gas上限 = 节点估算值 × 倍数,默认1.2
exchange.IO("approve", "exact" 或 "max")授权模式
exchange.IO("token", 名字, 合约地址)登记代币;不传参数时列出代币表
exchange.IO("route", 交易对, 方向, 数量)只询价:各候选路径的报价和最优路径,不下单
exchange.IO("simulate", 交易对, 方向, 数量[, 价格])按下单逻辑构造交易,只在链上模拟执行,不花gas
exchange.IO("transfer", 收款地址, 数量[, 代币])转出原生币或代币,数量可以写"all"
exchange.IO("receipt", 交易哈希[, 等待毫秒])查询转账等交易的回执,可以等待上链
exchange.IO("wrap", 数量)、exchange.IO("unwrap", 数量)原生币与包装币1:1互换
exchange.IO("contracts")当前DEX在当前链上的合约地址
exchange.IO("base", 节点地址)、exchange.IO("sendBase", 节点地址)切换节点;设置只用于广播交易的节点(私有交易通道)
exchange.IO("address")钱包地址

各指令的参数与返回值见语法手册Uniswap分类。

示例

示例:询价、模拟,然后市价卖出

以以太坊上的ETH_USDC为例。注意CreateOrder会发出真实交易。

javascript
function main() { var symbol = "ETH_USDC" exchange.IO("slippage", 0.003) // 市价单滑点 0.3% var t = exchange.GetTicker(symbol) Log("买一:", t.Buy, "卖一:", t.Sell) // 只询价:卖出 0.1 ETH 的最优路径 var r = exchange.IO("route", symbol, "sell", 0.1) Log("最优路径:", r.best, "价格:", r.price) // 链上模拟一遍,不花 gas if (!exchange.IO("simulate", symbol, "sell", 0.1)) { Log("模拟失败:", GetLastError()) return } // 市价卖出 0.1 ETH,订单 ID 是交易哈希 var id = exchange.CreateOrder(symbol, "sell", -1, 0.1) if (!id) { Log("下单失败:", GetLastError()) return } while (true) { var o = exchange.GetOrder(id) if (o && o.Status != ORDER_STATE_PENDING) { Log("状态:", o.Status, "成交数量:", o.DealAmount, "成交均价:", o.AvgPrice) break } Sleep(3000) } }

参考

Web3交易所对象选择ChainType为ETH时,可以连接以太坊以及所有EVM兼容链(BSC、Base、Arbitrum、Optimism、Polygon等)的节点,用exchange.IO()的各个指令查询余额、调用合约、发送交易。本页按一笔链上操作的流程介绍常用指令,每个指令的完整参数见语法手册Web3分类中对应的exchange.IO("指令", ...)。

只是想在Uniswap、PancakeSwap上兑换代币时,请使用Uniswap交易所对象:它直接支持exchange.GetTicker()、exchange.CreateOrder()等标准函数,不需要自己编码合约调用。

1. 配置交易所对象

在「交易所」页面(/m/add-platform)添加交易所,协议选择「加密货币」,交易所选择Web3:

字段说明
ChainTypeETH:以太坊及所有EVM兼容链;TRON:波场,见波场(TRON)
Private Key钱包私钥(十六进制字符串,可以带0x前缀)。支持把私钥本地化部署在托管者上,参看密钥安全性
Rpc Address节点地址,默认https://ethereum-rpc.publicnode.com(以太坊主网公共节点)。连接其它链时填写该链的节点,例如BSC:https://bsc-dataseed.binance.org。支持http(s)://和ws(s)://。可以写多个节点,用逗号分隔,互为备用
Rpc Api Key节点鉴权,可以留空。写成名称: 值(如x-api-key: xxx)时作为该名称的请求头发送;否则作为Authorization: Basic <值>发送

多个节点时,从上次成功的节点开始依次尝试:只有节点不可用(连接失败、超时、限流)时才换下一个,合约执行失败之类的错误直接返回;链ID与第一个节点不同的节点会被跳过,避免把交易发到另一条链。

运行中可以用exchange.IO("base", 节点地址)切换节点(多个节点可以传数组或逗号分隔的字符串),用exchange.IO("key", 私钥)切换钱包私钥,用exchange.IO("address")获取当前钱包地址。

标准函数中只有exchange.GetAccount()、exchange.GetAssets()可用,返回钱包的原生币余额(币种按链ID识别,如BSC为BNB)。

2. 查询余额与读取合约

调用合约的只读方法(view/pure)不消耗gas,直接返回解码后的结果:

javascript
exchange.IO("api", "eth", "eth_getBalance", wallet, "latest") // 原生币余额,链上整数(十六进制字符串) exchange.IO("api", tokenAddress, "balanceOf", wallet) // ERC20余额,链上整数 exchange.IO("api", tokenAddress, "decimals") // 代币精度
  • exchange.IO("api", "eth", 方法, ...参数)直接调用节点的JSON-RPC方法,如eth_gasPrice、eth_blockNumber、eth_getTransactionReceipt。
  • exchange.IO("api", 合约地址, 方法, ...参数)调用合约方法。方法可以写方法名、完整签名(如"approve(address,uint256)",用于区分重载)或方法选择器(如"0x095ea7b3")。
  • 链上数量都是整数。用exchange.IO("fromUnits", 链上整数, 精度)换算成可读数量,用exchange.IO("toUnits", "1.5", 精度)换算回链上整数;精度也可以直接传代币合约地址。两者都按字符串精确计算。
  • 批量读取多个合约时用exchange.IO("multicall", ...)一次请求完成;查询事件日志用exchange.IO("logs", ...)。

3. 注册ABI

标准ERC20方法(balanceOf、decimals、allowance、approve、transfer等)已内置,不需要注册。调用其它合约的方法前,需要用exchange.IO("abi", 合约地址, ABI)注册该合约的ABI。

常用合约可以直接使用内置模板,第三个参数传模板名:"weth"、"uniswapV3Pool"、"uniswapV3Factory"、"uniswapV3QuoterV2"、"uniswapV3SwapRouter02"、"uniswapV3PositionManager"、"permit2"(PancakeSwap V3使用相同的模板,也可以写"pancakeV3Pool"等别名)。常用合约的地址可以用exchange.IO("contracts")查询。

javascript
exchange.IO("abi", poolAddress, "uniswapV3Pool") var slot0 = exchange.IO("api", poolAddress, "slot0")

其它合约的ABI可以从区块浏览器获取,例如Etherscan的V2接口(需要Etherscan的API Key,chainid为链ID,取返回结果中的result字段):

url
https://api.etherscan.io/v2/api?chainid=1&module=contract&action=getabi&address=0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45&apikey=YourApiKey

4. 发送交易

调用合约的写方法时,交易所对象用配置的私钥签名并广播交易,返回交易哈希。发送前可以把"api"换成"call",用exchange.IO("call", ...)预演同一笔调用:在节点上模拟执行,不签名、不消耗gas,执行失败时返回空值,GetLastError()中有合约给出的失败原因。

以授权(approve)为例:

javascript
var amount = exchange.IO("toUnits", "100", tokenAddress) // 100个代币换算成链上整数 var txHash = exchange.IO("api", tokenAddress, "approve", spender, amount)

方法的stateMutability为payable时,方法参数之前要多传一个参数:附带的原生币数量(链上整数)。最后一个参数可以传选项对象:

选项说明
gasLimitgas上限。不传时由节点估算(eth_estimateGas)。合约调用不要写21000,那只够普通转账
gasPrice固定gas价格,传入时发送传统(legacy)交易。不传时,支持EIP-1559的链发送EIP-1559交易:小费取节点建议值与最近区块实际小费的较大者,最高费用为2 × baseFee + 小费
nonce指定nonce。不传时自动分配,并与链上待处理计数同步,连续发送不会重复使用nonce
dryRun设为true时只签名不广播,返回hash、raw(签名后的交易)、nonce、gasLimit等字段,可用于检查交易或交给其它渠道发送

转出原生币使用exchange.IO("api", "eth", "send", 收款地址, 数量),数量是链上整数(wei)。它的选项还支持data(十六进制调用数据),用于原样发送聚合器等API返回的交易{to, data, value},此时gas按合约调用估算。注意:普通转账不传gasPrice时按固定的100 Gwei出价、gas上限为21000,在以太坊主网上通常偏高,建议先用eth_gasPrice查询后传入。

需要把交易发到私有交易通道(如Flashbots Protect、MEV Blocker)避免被抢跑时,用exchange.IO("sendBase", 节点地址)设置只用于广播交易的节点。

5. 等待交易上链

exchange.IO("waitReceipt", 交易哈希, {timeout, confirmations})等待交易上链并达到确认数,返回交易回执:status为1表示成功、0表示失败(revertReason为失败原因),events为按已注册ABI解码的事件。超时未上链时返回空值。

6. nonce管理、加速与取消

  • exchange.IO("nonce")查看链上与本地的nonce计数;exchange.IO("nonce", "sync")按链上重新同步(在别处用同一个钱包发过交易后使用)。
  • 交易长时间未上链时,用exchange.IO("speedUp", 交易哈希)以同一个nonce、更高的手续费重发;用exchange.IO("cancelTx", 交易哈希)发送一笔同nonce、转给自己的0金额交易顶替原交易。两者都在原交易上链前才有效。
  • 本地nonce记录只在当前实盘内有效:多个实盘共用一个钱包时彼此看不到对方的记录,仍可能冲突,建议每个实盘使用独立的钱包。

其它指令

  • 编码与解码:exchange.IO("encode", ...)编码合约调用数据或按类型编码(同Solidity的abi.encode),exchange.IO("encodePacked", ...)紧凑编码(如Uniswap V3的兑换路径),exchange.IO("decode", ...)按类型解码。
  • 签名:exchange.IO("sign", ...)对32字节哈希签名,exchange.IO("signTypedData", ...)对EIP-712结构化数据签名(如ERC-20 Permit),exchange.IO("signMessage", ...)对消息做EIP-191签名。
  • Uniswap V3数学:exchange.IO("uniswapV3", ...)在tick、价格、sqrtPrice之间换算,在流动性与代币数量之间换算。
  • 哈希:exchange.IO("hash", "keccak256", "raw", "hex", 文本)计算keccak256等摘要,可用于计算方法选择器、EIP-712摘要,参数与Encode()函数相同。
  • 完整范例:通过聚合器兑换(询价、生成交易、用exchange.IO("call", ...)预演、带data发送)见手册中exchange.IO("call", ...)的范例;ERC-20 Permit签名并由合约验签见exchange.IO("sign", ...)、exchange.IO("signTypedData", ...)的范例。

示例

示例:查余额、授权并等待上链

以以太坊主网的USDC为例。注意这段代码会发出真实交易、消耗gas。

javascript
function main() { var usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // 以太坊主网 USDC var spender = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" // 被授权的合约,这里以 Uniswap SwapRouter02 为例 var wallet = exchange.IO("address") // 查余额:标准 ERC20 方法不需要注册 ABI var eth = exchange.IO("fromUnits", exchange.IO("api", "eth", "eth_getBalance", wallet, "latest"), 18) var usdcBalance = exchange.IO("fromUnits", exchange.IO("api", usdc, "balanceOf", wallet), usdc) Log("ETH:", eth, "USDC:", usdcBalance) // 读合约:当前授权额度 var allowance = exchange.IO("api", usdc, "allowance", wallet, spender) Log("当前授权:", exchange.IO("fromUnits", allowance, usdc)) // 授权 100 USDC:先预演,再发送 var amount = exchange.IO("toUnits", "100", usdc) if (!exchange.IO("call", usdc, "approve", spender, amount)) { Log("预演失败:", GetLastError()) return } var txHash = exchange.IO("api", usdc, "approve", spender, amount) Log("交易哈希:", txHash) // 等待上链,最多 3 分钟 var receipt = exchange.IO("waitReceipt", txHash, {timeout: 180000}) if (receipt && receipt.status == 1) { Log("授权成功,区块:", receipt.blockNumber) } else if (receipt) { Log("交易失败:", receipt.revertReason) } else { // 未上链:可以用 speedUp 加价重发,或 cancelTx 取消 Log("超时未上链:", GetLastError()) } }

参考

Web3交易所对象选择ChainType为TRON时连接波场节点。用法与以太坊(EVM)基本一致:注册ABI、调用合约、编码解码、签名、切换私钥等exchange.IO()指令相同,地址使用波场格式(T开头),TRX数量的单位是sun(1 TRX = 1000000 sun)。本页说明配置和波场特有的部分。

配置交易所对象

字段说明
ChainType选择TRON
Private Key钱包私钥(十六进制字符串)。支持把私钥本地化部署在托管者上,参看密钥安全性
Rpc Address波场全节点的HTTP地址,例如官方节点https://api.trongrid.io(测试网:https://nile.trongrid.io、https://api.shasta.trongrid.io)
Rpc Api KeyTronGrid的API Key,只填Key本身,会作为TRON-PRO-API-KEY请求头发送。不填也能使用,但TronGrid对没有Key的请求限频更严格

托管者通过全节点的HTTP接口(/wallet/...)访问波场,不再使用gRPC。选择TRON时表单默认填入的旧gRPC地址grpc.trongrid.io:50051会自动换成https://api.trongrid.io(grpc.nile.trongrid.io:50051、grpc.shasta.trongrid.io:50051同样换成对应测试网的HTTP地址);其它gRPC地址会报错,请改填节点的HTTP地址。

运行中可以用exchange.IO("base", 节点地址)切换节点,用exchange.IO("key", 私钥)切换钱包,用exchange.IO("address")获取当前钱包地址(T开头)。标准函数exchange.GetAccount()、exchange.GetAssets()返回钱包的TRX余额。账户还未激活(链上没有记录)时余额为0。

调用智能合约

与以太坊相同,使用exchange.IO("api", 合约地址, 方法, ...参数):只读方法直接返回结果,写方法签名并广播交易,返回交易ID。TRC20标准方法已内置;其它合约没有注册ABI时,会自动从链上读取该合约的ABI,读取不到时再用exchange.IO("abi", 合约地址, ABI)手动注册。写方法的最后一个参数可以传{gasLimit: 数量}设置手续费上限(feeLimit,单位sun)。

javascript
// USDT(TRC20)合约 var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" Log(exchange.IO("api", usdt, "balanceOf", exchange.IO("address"))) // 链上整数,USDT精度为6

编码解码与以太坊一致,地址参数可以直接写T开头的地址:

javascript
exchange.IO("encode", "address", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") // 000000000000000000000000a614f803b6fd780986a42c78ec9c7f77e6ded13c exchange.IO("encodePacked", "address", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") // a614f803b6fd780986a42c78ec9c7f77e6ded13c exchange.IO("decode", "string", "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a5465746865722055534400000000000000000000000000000000000000000000") // Tether USD

调用波场节点的方法

exchange.IO("api", "tron", 方法, ...参数)调用波场节点的方法,方法名不区分大小写。需要签名的方法(转账、触发合约等)会自动签名并广播。常用方法:

方法参数说明
send收款地址, 数量(sun)从当前钱包转出TRX
Transfer付款地址, 收款地址, 数量(sun)转出TRX,付款地址必须是当前钱包
GetAccount地址账户信息
GetAccountResource地址账户的能量、带宽资源
GetContractABI合约地址合约在链上的ABI
GetAssetIssueByName名称TRC10资产信息
GetNowBlock无当前区块
GetBlockByNum区块高度指定区块
GetTransactionByID交易ID交易内容
GetTransactionInfoByID交易ID交易执行结果(手续费、能量消耗、日志等)
GetChainParameters无链参数
TriggerConstantContract调用者地址(可为空), 合约地址, 方法, 参数编码只读调用合约,结果在constant_result中(十六进制字符串,可以用exchange.IO("decode", ...)解码)
TRC20ContractBalance地址, 合约地址TRC20余额(链上整数)
TRC20GetName、TRC20GetSymbol、TRC20GetDecimals合约地址TRC20的名称、符号、精度
TRC20Send、TRC20Approve付款地址, 收款或被授权地址, 合约地址, 数量, feeLimitTRC20转账、授权
TRC20Call调用者地址(可为空), 合约地址, 调用数据, 是否只读, feeLimit用原始调用数据调用合约
ParseTRC20NumericProperty、ParseTRC20StringProperty十六进制数据解析TRC20返回的数值、字符串

表中没有的节点接口,可以直接传路径和请求体:exchange.IO("api", "tron", "/wallet/接口名", {请求体})。

与以太坊的差异

以下指令只支持以太坊(EVM),在波场上调用会报错:call、multicall、logs、waitReceipt、nonce、speedUp、cancelTx、contracts;sendBase和多个节点互为备用也只对以太坊有效。在波场上模拟执行合约调用可以用节点方法TriggerConstantContract,查询交易的执行结果用GetTransactionInfoByID。

toUnits、fromUnits、uniswapV3、编码解码和签名指令在波场上同样可用,精度参数可以直接传TRC20合约地址:

javascript
var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" var raw = exchange.IO("api", usdt, "balanceOf", exchange.IO("address")) Log(exchange.IO("fromUnits", raw, usdt)) // 按合约的 decimals() 换算为可读数量

合约调用在节点校验阶段被拒绝时(例如合约不存在),错误信息中是节点给出的原因,例如tron contract call rejected (CONTRACT_VALIDATE_ERROR): Smart contract is not exist.;合约执行失败(revert)时报tron contract execution failed及失败原因。

签名

exchange.IO("hash", "sign", "hex", "hex", 交易哈希)用当前私钥对32字节哈希签名,返回65字节签名r‖s‖v(v为0或1);hash的其它算法(如"sha256")用于计算摘要,功能与Encode()函数相同。需要分别取得r、s、v(v为27或28)用于合约校验时,使用exchange.IO("sign", ...),见语法手册Web3分类。

示例

  • 示例

    查询TRX与USDT余额,读取代币信息

    javascript
    function main() { var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" var wallet = exchange.IO("address") // TRX 余额(标准函数,单位 TRX) Log("账户:", exchange.GetAccount()) // USDT 余额:链上整数,按精度换算 var raw = exchange.IO("api", "tron", "TRC20ContractBalance", wallet, usdt) var decimals = exchange.IO("api", "tron", "TRC20GetDecimals", usdt) Log("USDT:", raw / Math.pow(10, decimals)) // 用 TRC20Call 只读调用 name()(选择器 0x06fdde03),再解析返回的字符串 var ret = exchange.IO("api", "tron", "TRC20Call", "", usdt, "0x06fdde03", true, 0) // constant_result 中是十六进制字符串,直接解析 Log("名称:", exchange.IO("api", "tron", "ParseTRC20StringProperty", ret.constant_result[0])) }
  • 用Multicall合约一次读取多个合约方法

    javascript
    function main() { var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" var multicall = "TGXuuKAb4bnrn137u39EKbYzKNXvdCes98" var wallet = exchange.IO("address") var calls = [ [usdt, exchange.IO("encode", usdt, "name")], [usdt, exchange.IO("encode", usdt, "decimals")], [usdt, exchange.IO("encode", usdt, "balanceOf", wallet)] ] // 注册 Multicall 合约的 aggregate 方法 exchange.IO("abi", multicall, `[{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct TronMulticall.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"view","type":"function"}]`) var ret = exchange.IO("api", multicall, "aggregate", calls) Log("name:", exchange.IO("decode", "string", ret.returnData[0])) Log("decimals:", exchange.IO("decode", "uint8", ret.returnData[1])) Log("balanceOf:", exchange.IO("decode", "uint256", ret.returnData[2])) }

参考