Advanced Topics
Advanced usage: JavaScript multi-threading, communication between live tradings, API rate limiting, options trading and on-chain trading with Web3.
JavaScript Multi-threading
JavaScript strategies can use the threading object to create threads that really run in parallel, and exchange data between them with messages, shared dictionaries, locks and similar objects. This page explains when to use threads and how to organize thread code; for the parameters and return values of each function see Threads in the syntax manual.
Pick the right tool first
| Need | Recommended | Languages |
|---|---|---|
| Send several API requests at once (for example tickers from several exchanges) and wait for the results | exchange.Go, with EventLoop to wait for completion events | All languages |
| Long-running background work: separate market data collection, risk checks, heavy computation | threading.Thread | JavaScript only |
| Serve HTTP, WebSocket or TCP from inside the strategy | threading.Serve | JavaScript only |
When you only need a few concurrent requests, exchange.Go() is simpler and involves no data passing between threads. The threading object on this page is for JavaScript strategies only; Python and Rust strategies use exchange.Go().
These functions can be called in the backtesting system, but the threads actually run one after another there; this only keeps the code runnable in backtests.
Threads run in isolated environments
The function passed to threading.Thread() runs in a separate JavaScript environment. This is the most important thing to keep in mind when writing thread code:
- A thread function cannot reference outer variables or closures, nor call other functions defined in the strategy. Pass the data it needs as arguments:
threading.Thread(func, arg1, arg2, ...). - Plain objects and arrays passed as arguments are deep-copied: changing them inside the thread does not affect other threads. When several threads need to see the same data, use a dictionary created by
threading.Dict(). - Functions can be passed as arguments too;
threading.Thread()also accepts function source strings, which can be used to load external libraries in the thread. - Platform API functions such as
exchange.GetTicker()andLog()can be called directly in a thread. - The return value of the thread function is retrieved with
join():t.join().ret.
Exchanging data between threads
| Method | Usage | Notes |
|---|---|---|
| Messages | t.postMessage(msg) sends to thread t; inside a thread, threading.currentThread().peekMessage(timeout) reads the messages it received; a child thread sends back to the main thread with threading.mainThread().postMessage(msg) | Each thread has its own inbox, read in order. peekMessage(-1) does not block and returns an empty value when there is no message |
| Shared dictionary | var d = threading.Dict(), pass it to threads as an argument, then each thread uses d.get(key) and d.set(key, value) | Good for holding the "latest state", such as the latest price or a running flag |
| Thread data | t.setData(key, value), t.getData(key) | Key-value pairs attached to a thread object; invalid after the thread ends (join(), terminate()) |
| Synchronization objects | threading.Lock(), threading.Event(), threading.Condition() | Passed to threads as arguments for mutual exclusion and waiting for notifications |
A message received by a thread also raises an event, so the thread object's eventLoop can wait for messages and other events in one place.
Thread lifecycle
t.join()waits for the thread to end and returns its result, with an optional timeout;t.terminate()ends a thread forcibly.- When a thread has ended and is no longer referenced, its resources are reclaimed automatically; there is no need to call
join()just to free them. An error is raised when more than 2000 threads are kept referenced and cannot be reclaimed. threading.pending()returns the number of running threads (main thread included).- All threads end when the live trading stops. Waits in
peekMessage(),join(), locks and events are interrupted by the stop.
Serving from inside the strategy
threading.Serve(address, handler, ...args) starts an HTTP (WebSocket included) or TCP service inside the strategy process. Each request or connection calls the handler in its own thread. It returns a Server object (addr() gives the actual listening address, close() shuts it down). Like thread functions, handlers run in isolated environments and receive what they need as arguments; a threading.Dict() is commonly used to share state with the main thread. For the address syntax and the methods of the ctx object see Serve.
The old global function __Serve() still works but only returns the listening address string; use threading.Serve() in new code.
Examples
Examples
-
Several threads compute in parallel, the main thread collects the results
Each thread fetches the K-lines of one symbol and computes a moving average; the result goes back to the main thread as the return value. Note that the symbol is passed as an argument and the thread function references no outer variables.
javascriptfunction 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) { // runs in the thread: only arguments and platform APIs are available 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, "close:", r.close, "MA20:", r.ma20) } } } -
A background thread collects prices, the main thread reads them and sends commands
The background thread writes the latest price into a shared dictionary and reports errors to the main thread through messages; the main thread tells it to exit with a message.
javascriptfunction main() { var shared = threading.Dict() var worker = threading.Thread(function(dict, symbol) { while (true) { // read commands from the main thread; -1 means do not block 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("failed to get ticker: " + GetLastError()) } Sleep(1000) } return "worker exited" }, shared, "BTC_USDT") for (var i = 0; i < 10; i++) { // wait at most 1 second for a message from the background thread var msg = threading.currentThread().peekMessage(1000) if (msg) { Log("background thread reports:", msg) } LogStatus("last:", shared.get("last"), "time:", _D(shared.get("time"))) } worker.postMessage("stop") Log(worker.join().ret) } -
A status endpoint with threading.Serve
The main thread writes the state into a shared dictionary; the HTTP handler gets the same dictionary as an argument and returns it as JSON.
javascriptfunction 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("listening on:", server.addr()) while (true) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { state.set("last", ticker.Last) state.set("updated", _D()) } Sleep(3000) } }
See Also
Communication Between Live Trading Strategies
Every live trading has a channel whose ID is the live trading ID. A live trading publishes data on its own channel with SetChannelData(), and other live tradings read it with GetChannelData(liveTradingId). The data is relayed by the platform server, so it can travel across dockers and servers.
A channel holds the latest state, not a message queue: each publish overwrites the previous data, and a subscriber always reads the current latest copy. If history is needed, the subscriber keeps it itself.
Typical uses:
- Master/follower: a master strategy analyzes the market and publishes signals; several follower strategies read them and trade on their own accounts.
- Status monitoring: each strategy publishes its running status; a monitoring live trading collects them for display or alerts.
- Data sharing: one live trading computes indicators and publishes the results; others use them directly instead of computing them again.
Key points
- The first read subscribes: the first
GetChannelData()call for a channel subscribes to it and returns an empty value (null/None). From then on the server pushes the channel's updates to this live trading, and later calls return the latest data. A subscriber should start reading at startup and handle empty values. - Subscription limit: a live trading can subscribe to at most 10 different channels (UUID channels below included). Beyond that, the call returns an empty value and logs the error
channel subscriber exceed limit. - **Data format**: in JavaScript and Python,
SetChannelData()accepts any JSON-serializable data and the subscriber reads the parsed object. Unchanged data is not sent again. For the data size limit seeSetChannelData. - Rust:
SetChannelData(string)only accepts a string, so build the JSON text yourself;GetChannelData()takes no channel argument and cannot choose the channel to read, so it cannot subscribe to other live tradings or UUID channels. Rust strategies are suited to publishing; write subscribers in JavaScript or Python. - **Cross-platform push**: an external system (a TradingView alert, your own program, etc.) can push data to a given live trading on a channel identified by a 32-character UUID through the extended API's
method=pub; the live trading reads it withGetChannelData(UUID). SeeSetChannelDataandGetChannelDatafor details. - Live trading feature: channels are meant for communication between live tradings; do not rely on them in backtests. The current live trading ID is available from
_G(). - Do not pass keys or other sensitive information through channels.
Basic usage
Examples
-
Publisher: publish a market summary
javascriptfunction main() { var robotId = _G() // current live trading ID, which is also this live trading's channel ID var updateId = 0 while (true) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { // publish the latest state, overwriting the previous data SetChannelData({ robotId: robotId, updateId: ++updateId, timestamp: Date.now(), symbol: "BTC_USDT", lastPrice: ticker.Last }) LogStatus("channel", robotId, "publish #", updateId, "last price:", ticker.Last) } Sleep(60000) // publish once a minute } }pythonimport time def main(): robotId = _G() # current live trading ID, which is also this live trading's channel ID updateId = 0 while True: ticker = exchange.GetTicker("BTC_USDT") if ticker: updateId += 1 # publish the latest state, overwriting the previous data SetChannelData({ "robotId": robotId, "updateId": updateId, "timestamp": int(time.time() * 1000), "symbol": "BTC_USDT", "lastPrice": ticker["Last"] }) LogStatus("channel", robotId, "publish #", updateId, "last price:", ticker["Last"]) Sleep(60000) # publish once a minuterustfn main() { let robotId = _G!(); // current live trading ID, which is also this live trading's channel ID let mut updateId = 0; loop { if let Ok(ticker) = exchange.GetTicker("BTC_USDT") { updateId += 1; // Rust's SetChannelData only accepts a string; build the JSON text yourself let state = format!( r#"{{"robotId": "{}", "updateId": {}, "timestamp": {}, "symbol": "BTC_USDT", "lastPrice": {}}}"#, robotId, updateId, Unix() * 1000, ticker.Last ); SetChannelData(&state); LogStatus!("channel", robotId, "publish #", updateId, "last price:", ticker.Last); } Sleep(60000); // publish once a minute } } -
Subscriber: read two channels
javascriptfunction main() { // live trading IDs to subscribe to (change as needed) var channels = ["632799", "632800"] while (true) { var msg = "" for (var i = 0; i < channels.length; i++) { // the first call subscribes and returns null; later calls return the latest data var state = GetChannelData(channels[i]) if (state) { msg += "channel " + channels[i] + ": #" + state.updateId + " " + _D(state.timestamp) + " last " + state.lastPrice + "\n" } else { msg += "channel " + channels[i] + ": waiting for data\n" } } LogStatus(msg) Sleep(5000) } }pythondef main(): # live trading IDs to subscribe to (change as needed) channels = ["632799", "632800"] while True: msg = "" for ch in channels: # the first call subscribes and returns None; later calls return the latest data state = GetChannelData(ch) if state: msg += "channel {}: #{} {} last {}\n".format(ch, state["updateId"], _D(state["timestamp"]), state["lastPrice"]) else: msg += "channel {}: waiting for data\n".format(ch) LogStatus(msg) Sleep(5000)rust// Rust's GetChannelData() takes no channel argument and cannot subscribe to other live tradings' channels -
Scenario: master/follower strategies
The master strategy computes a moving-average crossover signal and publishes it; the follower reads the signal and places an order when it changes.
Master strategy (publishes signals)
javascriptfunction 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("current signal:", signal, "price:", records[n - 1].Close) } Sleep(60000) } }pythonimport 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("current signal:", signal, "price:", records[-1]["Close"]) Sleep(60000)rustfn 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's SetChannelData only accepts a string; build the JSON text yourself let data = format!( r#"{{"timestamp": {}, "symbol": "BTC_USDT", "signal": "{}", "price": {}}}"#, Unix() * 1000, signal, price ); SetChannelData(&data); LogStatus!("current signal:", signal, "price:", price); } } Sleep(60000); } } -
Follower strategy (reads and executes signals)
javascriptfunction main() { var masterId = "632799" // live trading ID of the master strategy var lastSignal = null while (true) { var data = GetChannelData(masterId) if (!data) { LogStatus("waiting for the master strategy's signal...") } else { if (data.signal !== lastSignal) { Log("new signal:", data.signal, "signal price:", 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("current signal:", data.signal, "signal time:", _D(data.timestamp)) } Sleep(5000) } }pythondef main(): masterId = "632799" # live trading ID of the master strategy lastSignal = None while True: data = GetChannelData(masterId) if not data: LogStatus("waiting for the master strategy's signal...") else: if data["signal"] != lastSignal: Log("new signal:", data["signal"], "signal price:", 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("current signal:", data["signal"], "signal time:", _D(data["timestamp"])) Sleep(5000)rust// Rust's GetChannelData() takes no channel argument and cannot read the master strategy's channel -
Scenario: monitoring several strategies
Each strategy publishes its status as the publisher above does; the monitoring live trading reads every channel, shows them in a table and marks those without an update for over 2 minutes as abnormal.
javascriptfunction main() { var monitorList = ["632799", "632800", "632801"] // at most 10 while (true) { var table = {type: "table", title: "Strategy status", cols: ["Live trading ID", "Status", "Last update", "Symbol", "Last price"], rows: []} for (var i = 0; i < monitorList.length; i++) { var data = GetChannelData(monitorList[i]) if (data) { var status = Date.now() - data.timestamp < 120000 ? "running" : "abnormal" table.rows.push([monitorList[i], status, _D(data.timestamp), data.symbol || "-", data.lastPrice || "-"]) } else { table.rows.push([monitorList[i], "waiting for data", "-", "-", "-"]) } } LogStatus("`" + JSON.stringify(table) + "`") Sleep(10000) } }pythonimport json import time def main(): monitorList = ["632799", "632800", "632801"] # at most 10 while True: table = {"type": "table", "title": "Strategy status", "cols": ["Live trading ID", "Status", "Last update", "Symbol", "Last price"], "rows": []} for robotId in monitorList: data = GetChannelData(robotId) if data: status = "running" if time.time() * 1000 - data["timestamp"] < 120000 else "abnormal" table["rows"].append([robotId, status, _D(data["timestamp"]), data.get("symbol", "-"), data.get("lastPrice", "-")]) else: table["rows"].append([robotId, "waiting for data", "-", "-", "-"]) LogStatus("`" + json.dumps(table) + "`") Sleep(10000)rust// Rust's GetChannelData() takes no channel argument and cannot read other live tradings' channels
See Also
API Rate Limiting Control
Exchanges limit how often their API may be called. Going over the limit gets requests rejected at best and the account temporarily banned at worst. With exchange.IO("rate", ...) or exchange.IO("quota", ...) you can cap the call frequency of standard functions locally on the docker: a call that exceeds the limit is never sent.
javascript
exchange.IO("rate" | "quota", name, count, window[, "delay"])
Two modes
- rate (token bucket): the bucket capacity defaults to
count. It starts full and refills at a steady "count per window"; each call takes one token. Short bursts are allowed, while the long-run average never exceeds "count per window". Writingcountas"10/5"means 10 refills per window with a bucket capacity of 5, which limits bursts. - quota (fixed window): at most
countcalls per window; the counter resets when the next window starts. Windows are aligned to the Unix epoch:"1s"to whole seconds,"1m"to whole minutes,"1h"to whole hours,"1d"to UTC midnight (08:00 Beijing time). For example, counting that starts at 12:00:00.900 is already in a new window at 12:00:01.000.
Use quota with a window matching the exchange's counting period when you must guarantee "no more than N calls in any of the exchange's periods"; use rate when you only need to control the average frequency.
Parameters
| Parameter | Description |
|---|---|
| name | The function to limit, see the table below. Several names separated by commas (such as "GetTicker,GetDepth") share one rule and their calls are counted together. "*" is a fallback rule that applies only to functions without a rule of their own. |
| count | Calls allowed per window, must be greater than 0; in rate mode it can be written as "count/burst". Passing 0 or a negative number deletes the rule for that name. |
| window | A duration in the syntax of Go's time.ParseDuration: units ns, us (or µs), ms, s, m, h, decimals allowed ("1.5s"), units can be combined ("1h30m"); "Nd" means N days (decimals allowed, such as "0.5d", not combinable with other units). "@HHMM" or "@HHMMSS" (such as "@0800") counts per day and resets at that time of day (Beijing time); it works with both rate and quota. |
| action | When omitted, a call over the limit fails immediately; with "delay" the call blocks until a call is available and is then sent. Stopping the live trading interrupts the wait. |
Function names that can be limited
| Category | Names |
|---|---|
| Market data | GetTicker, GetTickers, GetDepth, GetTrades, GetRecords, GetMarkets, GetFundings |
| Account | GetAccount, GetAssets, GetPositions, SetMarginLevel |
| Trading | CreateOrder (Buy and Sell count here too), CancelOrder, ModifyOrder |
| Order queries | GetOrder, GetOrders, GetHistoryOrders |
| Conditional orders | CreateConditionOrder, ModifyConditionOrder, CancelConditionOrder, GetConditionOrder, GetConditionOrders, GetHistoryConditionOrders |
| Custom requests | IO/api: limits only exchange.IO("api", ...), other exchange.IO() commands are unaffected |
GetAccountandGetAssetsare the same underlying request; a rule under either name applies to both functions.- Calls made concurrently through
exchange.Go()are counted under the function actually called.
Scope of rules
- Rules are set per exchange object: a rule on
exchanges[0]does not affectexchanges[1]. - Rules last for the current run only. Set them again after the live trading restarts, usually at the beginning of
main(). - Setting the same name again replaces its rule; an empty name (
exchange.IO("rate", "")) clears all rules of that exchange object. - Each call is counted under one rule only: a function with its own rule is no longer counted under
"*", so"*"cannot be used as a "total quota for all calls" stacked on top of specific rules.
When the limit is exceeded
With the default action, a call over the limit sends no request and is treated as a failed call (JavaScript returns null, Python returns None, Rust returns Err). The error message looks like:
rate limit exceeded: GetTicker 10/1s
quota limit exceeded: GetTicker 10/1m
quota limit exceeded: GetRecords 2000/day (resets at 0800)
With the "delay" action the call blocks until a call is available, so the time recorded in the log is the time after the wait. With a rule that resets daily, "delay" may wait until the next day; use it with care.
Examples
Examples
-
Default action: calls over the limit fail
javascriptfunction main() { // GetTicker at most 5 calls per second on average (token bucket, capacity 5) exchange.IO("rate", "GetTicker", 5, "1s") for (var i = 0; i < 10; i++) { var ticker = exchange.GetTicker("BTC_USDT") if (ticker) { Log("call", i + 1, "succeeded:", ticker.Last) } else { // a call over the limit sends no request and returns null Log("call", i + 1, "rate limited:", GetLastError()) } } }pythondef main(): # GetTicker at most 5 calls per second on average (token bucket, capacity 5) exchange.IO("rate", "GetTicker", 5, "1s") for i in range(10): ticker = exchange.GetTicker("BTC_USDT") if ticker: Log("call", i + 1, "succeeded:", ticker["Last"]) else: # a call over the limit sends no request and returns None Log("call", i + 1, "rate limited:", GetLastError())rustfn main() { // GetTicker at most 5 calls per second on average (token bucket, capacity 5) let _ = exchange.IO(("rate", "GetTicker", 5, "1s")); for i in 0..10 { match exchange.GetTicker("BTC_USDT") { Ok(ticker) => Log!("call", i + 1, "succeeded:", ticker.Last), // a call over the limit sends no request and returns Err Err(e) => Log!("call", i + 1, "rate limited:", e), } } } -
Rules grouped after the exchange's own limits
Market data and trading each share one rule; trading calls wait instead of failing when over the limit; every other function without a rule of its own falls back to
"*".javascriptfunction main() { // market data: GetTicker and GetDepth together at most 20 calls per second on average exchange.IO("rate", "GetTicker,GetDepth", 20, "1s") // trading: placing orders (Buy/Sell included) and cancelling together 5 per second, wait when over the limit exchange.IO("rate", "CreateOrder,CancelOrder", 5, "1s", "delay") // fallback: other functions (such as GetAccount, GetPositions) together 60 per minute, window aligned to whole minutes exchange.IO("quota", "*", 60, "1m") while (true) { var ticker = exchange.GetTicker("BTC_USDT") var depth = exchange.GetDepth("BTC_USDT") if (ticker && depth) { Log("last:", ticker.Last, "best bid:", depth.Bids[0].Price) } Sleep(1000) } }pythondef main(): # market data: GetTicker and GetDepth together at most 20 calls per second on average exchange.IO("rate", "GetTicker,GetDepth", 20, "1s") # trading: placing orders (Buy/Sell included) and cancelling together 5 per second, wait when over the limit exchange.IO("rate", "CreateOrder,CancelOrder", 5, "1s", "delay") # fallback: other functions (such as GetAccount, GetPositions) together 60 per minute, window aligned to whole minutes exchange.IO("quota", "*", 60, "1m") while True: ticker = exchange.GetTicker("BTC_USDT") depth = exchange.GetDepth("BTC_USDT") if ticker and depth: Log("last:", ticker["Last"], "best bid:", depth["Bids"][0]["Price"]) Sleep(1000)rustfn main() { // market data: GetTicker and GetDepth together at most 20 calls per second on average let _ = exchange.IO(("rate", "GetTicker,GetDepth", 20, "1s")); // trading: placing orders (Buy/Sell included) and cancelling together 5 per second, wait when over the limit let _ = exchange.IO(("rate", "CreateOrder,CancelOrder", 5, "1s", "delay")); // fallback: other functions (such as GetAccount, GetPositions) together 60 per minute, window aligned to whole minutes let _ = exchange.IO(("quota", "*", 60, "1m")); loop { if let (Ok(ticker), Ok(depth)) = (exchange.GetTicker("BTC_USDT"), exchange.GetDepth("BTC_USDT")) { Log!("last:", ticker.Last, "best bid:", depth.Bids[0].Price); } Sleep(1000); } } -
Burst capacity, daily quota and deleting rules
javascriptfunction main() { // 10 calls per second on average, but at most 2 in a burst exchange.IO("rate", "GetDepth", "10/2", "1s") // resets every day at 08:00 Beijing time, at most 2000 calls per day exchange.IO("quota", "GetRecords", 2000, "@0800") // units can be combined: at most 100 calls every 1 hour 30 minutes exchange.IO("rate", "GetOrders", 100, "1h30m") // count 0: delete the rule for GetOrders exchange.IO("rate", "GetOrders", 0) // empty name: clear all rules of this exchange object exchange.IO("rate", "") }pythondef main(): # 10 calls per second on average, but at most 2 in a burst exchange.IO("rate", "GetDepth", "10/2", "1s") # resets every day at 08:00 Beijing time, at most 2000 calls per day exchange.IO("quota", "GetRecords", 2000, "@0800") # units can be combined: at most 100 calls every 1 hour 30 minutes exchange.IO("rate", "GetOrders", 100, "1h30m") # count 0: delete the rule for GetOrders exchange.IO("rate", "GetOrders", 0) # empty name: clear all rules of this exchange object exchange.IO("rate", "")rustfn main() { // 10 calls per second on average, but at most 2 in a burst let _ = exchange.IO(("rate", "GetDepth", "10/2", "1s")); // resets every day at 08:00 Beijing time, at most 2000 calls per day let _ = exchange.IO(("quota", "GetRecords", 2000, "@0800")); // units can be combined: at most 100 calls every 1 hour 30 minutes let _ = exchange.IO(("rate", "GetOrders", 100, "1h30m")); // count 0: delete the rule for GetOrders let _ = exchange.IO(("rate", "GetOrders", 0)); // empty name: clear all rules of this exchange object let _ = exchange.IO(("rate", "")); }
See Also
Options Trading
The FMZ Quant Trading Platform supports options trading on the cryptocurrency futures exchanges below. Options are used the same way as futures contracts: set the contract to an option code with exchange.SetContractType() (the option code is the exchange's native code, and the format differs between exchanges). After that, market data functions such as GetTicker() and GetDepth() and trading functions such as Buy(), Sell() (set the trade direction with exchange.SetDirection() before placing orders), CancelOrder() and GetPositions() all work on that option contract. You can also place orders with the full instrument code in the form pair.optionCode, for example BTC_USDT.BTC-260925-145000-C.
Option order books are usually thin: when there is no bid or ask, Buy and Sell in the Ticker are 0, and Last may be 0 for a contract that has never traded; see each exchange below. Whether exchange.GetMarkets() lists option contracts depends on the exchange; where it does not, get the option codes from the exchange's API or website.
Futures_Deribit
After setting an option contract you can get market data, place and cancel orders and query positions. Option code examples: BTC-13SEP24-60000-C, XRP_USDC-27SEP24-1-C; combination examples: BTC-CS-6SEP24-57000_57500, BTC-PCAL-20SEP24_13SEP24-55000. The result of exchange.GetMarkets() includes option contracts.
Reference strategy: Deribit options test strategy
Futures_OKX
Used the same way as Deribit. Set the trading pair to BTC_USD or similar; option codes look like BTC-USD-200626-4500-C. For an option that has never traded, Last in GetTicker() is the mark price. exchange.GetMarkets() does not list option contracts; the option contract list is available from OKX's /api/v5/public/instruments endpoint, for example BTC options:
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
Binance European options (USDT-settled) are supported. Set the trading pair to BTC_USDT or similar; option codes look like BTC-260925-145000-C (underlying-expiry YYMMDD-strike-C/P). Options trading must be enabled on the account. Limitations:
- Only limit orders are supported; market orders, conditional orders and order amendment (
exchange.ModifyOrder()) are not. - Leverage and margin mode settings such as
exchange.SetMarginLevel()are not supported. - Unified (portfolio margin) accounts do not support options.
exchange.GetMarkets()does not list option contracts.
Futures_Bybit
Options with two settlement currencies are supported:
- USDC-settled: set the trading pair to
ETH_USDCor similar; option codes look likeETH-25NOV22-1375-P. - USDT-settled: set the trading pair to
ETH_USDTor similar; the option code has an extra settlement-currency suffix compared with USDC-settled ones, likeETH-25JUN27-2800-C-USDT.
The result of exchange.GetMarkets() includes options of both settlement currencies. Bybit has no kline endpoint for options, so GetRecords() is built from trades.
Futures_Aevo
USDC options on Aevo are supported. Set the trading pair to ETH_USDC or similar; option codes look like ETH-30JUN23-1600-C. Last in GetTicker() is the mark price. Aevo has no kline endpoint, so GetRecords() is built from trades and is empty while the contract has no trades. The result of exchange.GetMarkets() includes option contracts.
Futures_GateIO
USDT options on Gate are supported. Set the trading pair to BTC_USDT or similar; option codes look like BTC_USDT-20211130-65000-C. The result of exchange.GetMarkets() includes option contracts. If options are not enabled on the account, order and position queries return the exchange's error.
Futures_Kraken
Options on Kraken Futures are supported. Set the trading pair to ETH_USD or similar; option codes look like OF_ETHUSD_261225_4000_C (OF_, underlying and quote currency, expiry YYMMDD, strike, C/P). The underlying and quote currency in the code must match the trading pair, and BTC is written as XBT in the code (e.g. OF_XBTUSD_...).
- Kraken has no option listing endpoint, so
exchange.GetMarkets()does not include option contracts; get the option codes from the Kraken website. LastinGetTicker()is the mark price, andBuy,Sell,HighandLoware 0; the raw data inInfocarries the implied volatility, the greeks and other fields.- Market data, klines, orders, positions and order history queries work; option orders go through the same order endpoint as futures contracts and have not been verified in live trading yet.
- Options have no funding rate, and
exchange.SetMarginLevel()is not supported.
See Also
Web3
To swap tokens on the decentralized exchanges Uniswap and PancakeSwap, use the Uniswap exchange object (see Uniswap and PancakeSwap below). To read on-chain data, call smart contracts and send custom transactions, use the Web3 exchange object, which supports Ethereum and other EVM-compatible chains as well as TRON.
Uniswap and PancakeSwap
The Uniswap exchange object connects to the V2 and V3 pools of Uniswap or PancakeSwap on one chain and maps on-chain swaps to spot trading functions: check prices with exchange.GetTicker() and place orders with exchange.CreateOrder(), with no ABIs to register and no contract calls to encode. Routing, quoting, token approval, price protection and sending transactions are all handled by the exchange object.
When to use the Uniswap exchange object and when to use Web3
- To swap tokens on Uniswap or PancakeSwap: use the Uniswap exchange object.
- To call other contracts or other DEX features (such as providing liquidity or managing V3 positions), to use other chains, or to build custom transactions: use the Web3 exchange object, see Advanced Topics → Web3 → Ethereum (EVM).
A strategy can add both kinds of exchange objects and use the same wallet with them.
Configure the exchange object
| Field | Description |
|---|---|
| DEX | Uniswap or PancakeSwap |
| Chain | Ethereum, Arbitrum, Base, BNB Chain. One exchange object is one DEX on one chain |
| Private Key | Wallet private key (hex string). The key can be deployed locally on the docker, see Getting Started → Key Security |
| Rpc Address | Node address of the chain; a public node is filled in when the chain is chosen (https://ethereum-rpc.publicnode.com for Ethereum, for example). Several nodes separated by commas back each other up |
| Rpc Api Key | Node authentication, may be left empty. Written as Name: value it is sent as a request header with that name; otherwise it is sent as Authorization: Basic <value> |
On the first call the exchange object checks that the node is on the configured chain and reports an error otherwise, so transactions never go to another chain. The wallet needs the chain's native coin (ETH or BNB) to pay gas.
Trading pairs
- Pairs are written as
base_quote, such asETH_USDCorUNI_USDT. - Token names are resolved in this order: built-in common tokens (native coin, wrapped native coin, USDC, USDT, etc.) → tokens registered with
exchange.IO("token", name, contractAddress)→ the official token lists. When the official list has several tokens with the same name on a chain, use the contract address instead. - Tokens not in the token table can be used directly by contract address as part of the pair, for example
0x1f9840a85d5af5bf1d1762f925bdaddc4201f984_USDC. - The native coin and its wrapped token are two different assets:
ETHandWETH,BNBandWBNBare different currencies. When trading the native coin, the router wraps and unwraps automatically. Converting between the two cannot be done with orders; useexchange.IO("wrap", amount)andexchange.IO("unwrap", amount), which call the wrapped token contract directly, 1:1, costing only gas. exchange.GetMarkets()lists only common pairs; pairs not listed can be traded as well.
What the standard functions do
| Function | Behavior |
|---|---|
exchange.GetTicker() | Best bid and ask are executable prices from actual quotes of a certain size (pool fees included); there are no 24-hour statistics on chain |
exchange.GetDepth() | Price levels derived from on-chain quotes of increasing size, not a real order book |
exchange.GetTrades() | Recent on-chain swaps in the pair's pools |
exchange.GetAccount(), exchange.GetAssets() | Balances of the native coin and of the tokens in the token table |
exchange.CreateOrder() | Swaps on chain immediately, see below |
exchange.GetOrder() | The order ID is the transaction hash and the status comes from the receipt: unfinished before it is mined, filled or failed after |
exchange.GetOrders() | Orders sent in the current run that are not mined yet |
exchange.CancelOrder() | Sends a replacement transaction with the same nonce, best effort, see below |
exchange.GetRecords(), exchange.GetTickers() and exchange.GetHistoryOrders() are not supported.
Placing orders
A DEX has no order book; every order is a swap executed on chain immediately. It either fills completely or reverts completely (losing only gas); it never fills partially and never rests waiting for a price.
- Limit orders: the limit price is the worst execution price. A quote is taken first; if the current price cannot reach the limit, an error is returned and no transaction is sent. Otherwise the minimum to receive / maximum to pay is written into the on-chain transaction, and if the price moves before the transaction is mined so that the limit can no longer be met, the whole swap reverts.
- Market orders: the minimum to receive / maximum to pay is the quote minus slippage. Slippage defaults to 0.5% and is changed with
exchange.IO("slippage", ratio). - Amount: for sells, the amount of base currency to sell; for limit buys, the amount of base currency to buy; for market buys, the amount of quote currency to spend.
- Settings for a single order can be appended after the side argument, for example
exchange.CreateOrder("ETH_USDC", 'sell;{"slippage":0.01,"route":"v3"}', -1, 0.1):slippageis the slippage for this order,routerestricts the route type (v2,v3,hopfor two hops,direct). - Before selling a token (ERC20), the router's allowance is checked; if it is not enough, an approval transaction is sent first and waited for. By default only the amount needed is approved;
exchange.IO("approve", "max")switches to unlimited approval and saves later approval transactions. - A transaction not mined before its deadline (120 seconds by default, change it with
exchange.IO("deadline", seconds)) reverts, so it cannot fill after the price has moved a lot.
Cancelling orders
exchange.CancelOrder() sends a zero-value transaction to yourself with the original order's nonce and a higher fee; if it is mined first, the original order becomes invalid. This is a best effort: the original order may be mined and filled before the replacement, and cancelling an order that is already mined returns an error. Check the final status with exchange.GetOrder() after cancelling.
Common exchange.IO() commands
| Command | Purpose |
|---|---|
exchange.IO("slippage", ratio) | Slippage for market orders, default 0.005 |
exchange.IO("deadline", seconds) | Transaction deadline, default 120 seconds |
exchange.IO("gasMultiplier", x) | Gas limit = node estimate × x, default 1.2 |
exchange.IO("approve", "exact" or "max") | Approval mode |
exchange.IO("token", name, contractAddress) | Register a token; without arguments, list the token table |
exchange.IO("route", symbol, side, amount) | Quote only: prices of the candidate routes and the best one, no order |
exchange.IO("simulate", symbol, side, amount[, price]) | Build the transaction as an order would and only simulate it on chain, no gas spent |
exchange.IO("transfer", toAddress, amount[, token]) | Send the native coin or a token; the amount can be "all" |
exchange.IO("receipt", txHash[, waitMs]) | Receipt of a transfer or other transaction, optionally waiting for it to be mined |
exchange.IO("wrap", amount), exchange.IO("unwrap", amount) | Convert between the native coin and the wrapped token 1:1 |
exchange.IO("contracts") | Contract addresses of this DEX on this chain |
exchange.IO("base", nodeAddress), exchange.IO("sendBase", nodeAddress) | Switch nodes; set a node used only for broadcasting transactions (private transaction channel) |
exchange.IO("address") | Wallet address |
For the parameters and return values of each command see the Uniswap category of the syntax manual.
Examples
Example: quote, simulate, then sell at market
Uses ETH_USDC on Ethereum. Note that CreateOrder sends a real transaction.
javascript
function main() {
var symbol = "ETH_USDC"
exchange.IO("slippage", 0.003) // 0.3% slippage for market orders
var t = exchange.GetTicker(symbol)
Log("bid:", t.Buy, "ask:", t.Sell)
// quote only: best route for selling 0.1 ETH
var r = exchange.IO("route", symbol, "sell", 0.1)
Log("best route:", r.best, "price:", r.price)
// simulate on chain first, no gas spent
if (!exchange.IO("simulate", symbol, "sell", 0.1)) {
Log("simulation failed:", GetLastError())
return
}
// sell 0.1 ETH at market; the order ID is the transaction hash
var id = exchange.CreateOrder(symbol, "sell", -1, 0.1)
if (!id) {
Log("order failed:", GetLastError())
return
}
while (true) {
var o = exchange.GetOrder(id)
if (o && o.Status != ORDER_STATE_PENDING) {
Log("status:", o.Status, "filled:", o.DealAmount, "average price:", o.AvgPrice)
break
}
Sleep(3000)
}
}See Also
Ethereum (EVM)
With ChainType set to ETH, the Web3 exchange object connects to nodes of Ethereum and every EVM-compatible chain (BSC, Base, Arbitrum, Optimism, Polygon and so on), and uses the exchange.IO() commands to query balances, call contracts and send transactions. This page walks through the commands in the order of a typical on-chain operation; for the full parameters of each command see the corresponding exchange.IO("command", ...) in the Web3 category of the syntax manual.
If you only want to swap tokens on Uniswap or PancakeSwap, use the Uniswap exchange object (see Advanced Topics → Web3 → Uniswap and PancakeSwap): it supports standard functions such as exchange.GetTicker() and exchange.CreateOrder() directly, with no contract calls to encode yourself.
1. Configure the exchange object
Add an exchange on the "Exchange" page (/m/add-platform), choose the protocol "Cryptocurrency" and the exchange Web3:
| Field | Description |
|---|---|
| ChainType | ETH: Ethereum and all EVM-compatible chains; TRON: TRON, see Advanced Topics → Web3 → TRON |
| Private Key | Wallet private key (hex string, the 0x prefix is optional). The key can be deployed locally on the docker, see Getting Started → Key Security |
| Rpc Address | Node address, by default https://ethereum-rpc.publicnode.com (a public Ethereum mainnet node). For other chains enter a node of that chain, for example BSC: https://bsc-dataseed.binance.org. http(s):// and ws(s):// are supported. Several nodes separated by commas back each other up |
| Rpc Api Key | Node authentication, may be left empty. Written as Name: value (such as x-api-key: xxx) it is sent as a request header with that name; otherwise it is sent as Authorization: Basic <value> |
With several nodes, requests start from the node that last succeeded and move on only when a node is unavailable (connection failure, timeout, rate limiting); errors such as a failed contract execution are returned directly. Nodes whose chain ID differs from the first node's are skipped, so transactions are never sent to another chain.
At runtime, exchange.IO("base", nodeAddress) switches nodes (several nodes can be passed as an array or a comma-separated string), exchange.IO("key", privateKey) switches the wallet private key, and exchange.IO("address") returns the current wallet address.
Among the standard functions only exchange.GetAccount() and exchange.GetAssets() are available; they return the wallet's native coin balance (the currency is recognized from the chain ID, BNB on BSC for example).
2. Query balances and read contracts
Read-only contract methods (view/pure) cost no gas and return decoded results directly:
javascript
exchange.IO("api", "eth", "eth_getBalance", wallet, "latest") // native coin balance, on-chain integer (hex string)
exchange.IO("api", tokenAddress, "balanceOf", wallet) // ERC20 balance, on-chain integer
exchange.IO("api", tokenAddress, "decimals") // token decimals
exchange.IO("api", "eth", method, ...args)calls the node's JSON-RPC methods directly, such aseth_gasPrice,eth_blockNumberandeth_getTransactionReceipt.exchange.IO("api", contractAddress, method, ...args)calls a contract method. The method can be a name, a full signature (such as"approve(address,uint256)", to tell overloads apart) or a selector (such as"0x095ea7b3").- On-chain amounts are integers.
exchange.IO("fromUnits", onChainInteger, decimals)converts to a readable amount andexchange.IO("toUnits", "1.5", decimals)converts back; a token contract address can be passed in place of the decimals. Both compute exactly on strings. - Use
exchange.IO("multicall", ...)to read many contracts in one request, andexchange.IO("logs", ...)to query event logs.
3. Register ABIs
Standard ERC20 methods (balanceOf, decimals, allowance, approve, transfer and others) are built in and need no registration. Before calling methods of other contracts, register the contract's ABI with exchange.IO("abi", contractAddress, abi).
Common contracts can use built-in templates by passing the template name as the third argument: "weth", "uniswapV3Pool", "uniswapV3Factory", "uniswapV3QuoterV2", "uniswapV3SwapRouter02", "uniswapV3PositionManager", "permit2" (PancakeSwap V3 uses the same templates, aliases such as "pancakeV3Pool" also work). Addresses of common contracts are available from exchange.IO("contracts").
javascript
exchange.IO("abi", poolAddress, "uniswapV3Pool")
var slot0 = exchange.IO("api", poolAddress, "slot0")
The ABI of other contracts can be obtained from a block explorer, for example Etherscan's V2 API (an Etherscan API key is required, chainid is the chain ID, take the result field of the response):
url
https://api.etherscan.io/v2/api?chainid=1&module=contract&action=getabi&address=0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45&apikey=YourApiKey
4. Send transactions
When a write method of a contract is called, the exchange object signs the transaction with the configured private key, broadcasts it and returns the transaction hash. Before sending you can replace "api" with "call" and rehearse the same call with exchange.IO("call", ...): it is simulated on the node without signing or spending gas; on failure it returns an empty value and GetLastError() holds the reason given by the contract.
Taking approve as an example:
javascript
var amount = exchange.IO("toUnits", "100", tokenAddress) // 100 tokens as an on-chain integer
var txHash = exchange.IO("api", tokenAddress, "approve", spender, amount)
When the method's stateMutability is payable, pass one extra argument before the method arguments: the amount of native coin to attach (on-chain integer). The last argument can be an options object:
| Option | Description |
|---|---|
| gasLimit | Gas limit. Estimated by the node (eth_estimateGas) when omitted. Do not use 21000 for contract calls; that is only enough for a plain transfer |
| gasPrice | Fixed gas price; when given, a legacy transaction is sent. When omitted, chains that support EIP-1559 get an EIP-1559 transaction: the tip is the larger of the node's suggestion and the tips actually paid in recent blocks, and the max fee is 2 × baseFee + tip |
| nonce | A specific nonce. Allocated automatically when omitted and kept in sync with the on-chain pending count, so consecutive sends never reuse a nonce |
| dryRun | When true, sign without broadcasting and return fields such as hash, raw (the signed transaction), nonce and gasLimit, for checking the transaction or sending it through another channel |
Native coin is sent with exchange.IO("api", "eth", "send", toAddress, amount), where the amount is an on-chain integer (wei). Its options also accept data (hex call data) to send a transaction {to, data, value} returned by an aggregator API as is; gas is then estimated as for a contract call. Note: a plain transfer without gasPrice bids a fixed 100 Gwei with a gas limit of 21000, usually too high on Ethereum mainnet; query eth_gasPrice first and pass it in.
To send transactions through a private channel (such as Flashbots Protect or MEV Blocker) and avoid front-running, set a node used only for broadcasting with exchange.IO("sendBase", nodeAddress).
5. Wait for the transaction to be mined
exchange.IO("waitReceipt", txHash, {timeout, confirmations}) waits until the transaction is mined with the required confirmations and returns the receipt: status is 1 for success and 0 for failure (with the reason in revertReason), and events holds the events decoded with the registered ABIs. It returns an empty value on timeout.
6. Nonce management, speeding up and cancelling
exchange.IO("nonce")shows the on-chain and local nonce counts;exchange.IO("nonce", "sync")resyncs from the chain (use it after the same wallet has sent transactions elsewhere).- When a transaction stays unmined for a long time,
exchange.IO("speedUp", txHash)resends it with the same nonce and a higher fee, andexchange.IO("cancelTx", txHash)replaces it with a zero-value transaction to yourself using the same nonce. Both only work before the original transaction is mined. - The local nonce record only exists inside the current live trading instance: instances that share one wallet cannot see each other's records and may still collide, so give each instance its own wallet.
Other commands
- Encoding and decoding:
exchange.IO("encode", ...)encodes contract call data or values by type (like Solidity'sabi.encode),exchange.IO("encodePacked", ...)does packed encoding (for example a Uniswap V3 swap path), andexchange.IO("decode", ...)decodes by type. - Signing:
exchange.IO("sign", ...)signs a 32-byte hash,exchange.IO("signTypedData", ...)signs EIP-712 typed data (such as ERC-20 Permit), andexchange.IO("signMessage", ...)signs a message with EIP-191. - Uniswap V3 math:
exchange.IO("uniswapV3", ...)converts between ticks, prices and sqrtPrice, and between liquidity and token amounts. - Hashing:
exchange.IO("hash", "keccak256", "raw", "hex", text)computes keccak256 and other digests, for example method selectors and EIP-712 digests; its parameters are the same as those of theEncode()function. - Complete examples: a swap through an aggregator (quote, build the transaction, rehearse it with
exchange.IO("call", ...), send it withdata) is in the examples ofexchange.IO("call", ...)in the syntax manual; an ERC-20 Permit signature verified by the contract is in the examples ofexchange.IO("sign", ...)andexchange.IO("signTypedData", ...).
Examples
Example: query balances, approve and wait for the transaction
Uses USDC on Ethereum mainnet. Note that this code sends a real transaction and spends gas.
javascript
function main() {
var usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC on Ethereum mainnet
var spender = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" // the contract to approve, Uniswap SwapRouter02 here
var wallet = exchange.IO("address")
// balances: standard ERC20 methods need no ABI registration
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)
// read a contract: current allowance
var allowance = exchange.IO("api", usdc, "allowance", wallet, spender)
Log("current allowance:", exchange.IO("fromUnits", allowance, usdc))
// approve 100 USDC: rehearse first, then send
var amount = exchange.IO("toUnits", "100", usdc)
if (!exchange.IO("call", usdc, "approve", spender, amount)) {
Log("rehearsal failed:", GetLastError())
return
}
var txHash = exchange.IO("api", usdc, "approve", spender, amount)
Log("tx hash:", txHash)
// wait at most 3 minutes for the transaction to be mined
var receipt = exchange.IO("waitReceipt", txHash, {timeout: 180000})
if (receipt && receipt.status == 1) {
Log("approved in block:", receipt.blockNumber)
} else if (receipt) {
Log("transaction failed:", receipt.revertReason)
} else {
// not mined yet: speedUp resends with a higher fee, cancelTx cancels it
Log("not mined before timeout:", GetLastError())
}
}See Also
TRON
With ChainType set to TRON, the Web3 exchange object connects to a TRON node. Usage is largely the same as on Ethereum (see Advanced Topics → Web3 → Ethereum (EVM)): the exchange.IO() commands for registering ABIs, calling contracts, encoding and decoding, signing and switching private keys are the same, addresses use the TRON format (starting with T), and TRX amounts are in sun (1 TRX = 1000000 sun). This page covers the configuration and the TRON-specific parts.
Configure the exchange object
| Field | Description |
|---|---|
| ChainType | Choose TRON |
| Private Key | Wallet private key (hex string). The key can be deployed locally on the docker, see Getting Started → Key Security |
| Rpc Address | HTTP address of a TRON full node, for example the official node https://api.trongrid.io (testnets: https://nile.trongrid.io, https://api.shasta.trongrid.io) |
| Rpc Api Key | TronGrid API key. Enter only the key itself; it is sent as the TRON-PRO-API-KEY request header. It works without a key, but TronGrid rate-limits keyless requests more strictly |
The docker accesses TRON through the full node's HTTP API (/wallet/...) and no longer uses gRPC. The old gRPC address grpc.trongrid.io:50051 that the form fills in by default for TRON is replaced automatically with https://api.trongrid.io (grpc.nile.trongrid.io:50051 and grpc.shasta.trongrid.io:50051 likewise become the HTTP addresses of the corresponding testnets); any other gRPC address is rejected, so enter the node's HTTP address instead.
At runtime, exchange.IO("base", nodeAddress) switches nodes, exchange.IO("key", privateKey) switches wallets, and exchange.IO("address") returns the current wallet address (starting with T). The standard functions exchange.GetAccount() and exchange.GetAssets() return the wallet's TRX balance. An account that has not been activated yet (no on-chain record) reads as 0 TRX.
Calling smart contracts
As on Ethereum, use exchange.IO("api", contractAddress, method, ...args): read-only methods return results directly, write methods sign and broadcast a transaction and return the transaction ID. Standard TRC20 methods are built in; for other contracts without a registered ABI, the ABI is read from the chain automatically, and only when that fails do you need to register it with exchange.IO("abi", contractAddress, abi). The last argument of a write method can be {gasLimit: amount} to set the fee limit (feeLimit, in sun).
javascript
// USDT (TRC20) contract
var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
Log(exchange.IO("api", usdt, "balanceOf", exchange.IO("address"))) // on-chain integer, USDT has 6 decimals
Encoding and decoding work as on Ethereum; address arguments can be written directly as T addresses:
javascript
exchange.IO("encode", "address", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")
// 000000000000000000000000a614f803b6fd780986a42c78ec9c7f77e6ded13c
exchange.IO("encodePacked", "address", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")
// a614f803b6fd780986a42c78ec9c7f77e6ded13c
exchange.IO("decode", "string", "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a5465746865722055534400000000000000000000000000000000000000000000")
// Tether USD
Calling TRON node methods
exchange.IO("api", "tron", method, ...args) calls TRON node methods; method names are case-insensitive. Methods that need a signature (transfers, triggering contracts, etc.) are signed and broadcast automatically. Common methods:
| Method | Arguments | Description |
|---|---|---|
send | to address, amount (sun) | Send TRX from the current wallet |
Transfer | from address, to address, amount (sun) | Send TRX; the from address must be the current wallet |
GetAccount | address | Account information |
GetAccountResource | address | The account's energy and bandwidth resources |
GetContractABI | contract address | The contract's on-chain ABI |
GetAssetIssueByName | name | TRC10 asset information |
GetNowBlock | none | Current block |
GetBlockByNum | block height | A given block |
GetTransactionByID | transaction ID | Transaction content |
GetTransactionInfoByID | transaction ID | Execution result of a transaction (fees, energy used, logs, etc.) |
GetChainParameters | none | Chain parameters |
TriggerConstantContract | caller address (may be empty), contract address, method, encoded arguments | Read-only contract call; the result is in constant_result (hex strings, decode them with exchange.IO("decode", ...)) |
TRC20ContractBalance | address, contract address | TRC20 balance (on-chain integer) |
TRC20GetName, TRC20GetSymbol, TRC20GetDecimals | contract address | Name, symbol and decimals of a TRC20 token |
TRC20Send, TRC20Approve | from address, to or spender address, contract address, amount, feeLimit | TRC20 transfer and approval |
TRC20Call | caller address (may be empty), contract address, call data, read-only flag, feeLimit | Call a contract with raw call data |
ParseTRC20NumericProperty, ParseTRC20StringProperty | hex data | Parse numbers and strings returned by TRC20 |
Node endpoints not in the table can be called with a path and a request body: exchange.IO("api", "tron", "/wallet/endpointName", {body}).
Differences from Ethereum
The following commands only work on Ethereum (EVM) and report an error on TRON: call, multicall, logs, waitReceipt, nonce, speedUp, cancelTx and contracts; sendBase and multiple fallback nodes also only apply to Ethereum. On TRON, simulate a contract call with the node method TriggerConstantContract, and query a transaction's execution result with GetTransactionInfoByID.
toUnits, fromUnits, uniswapV3, the encoding and decoding commands and the signing commands work on TRON as well, and a TRC20 contract address can be passed as the decimals:
javascript
var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
var raw = exchange.IO("api", usdt, "balanceOf", exchange.IO("address"))
Log(exchange.IO("fromUnits", raw, usdt)) // converted to a readable amount with the contract's decimals()
When the node rejects a contract call at validation time (for example because the contract does not exist), the error carries the node's reason, such as tron contract call rejected (CONTRACT_VALIDATE_ERROR): Smart contract is not exist.; a contract execution failure (revert) reports tron contract execution failed with the reason.
Signing
exchange.IO("hash", "sign", "hex", "hex", txHash) signs a 32-byte hash with the current private key and returns the 65-byte signature r‖s‖v (v is 0 or 1); other hash algorithms (such as "sha256") compute digests, the same as the Encode() function. When r, s and v (v being 27 or 28) are needed separately for contract verification, use exchange.IO("sign", ...), see the Web3 category of the syntax manual.
Examples
-
Examples
Query TRX and USDT balances and read token information
javascriptfunction main() { var usdt = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" var wallet = exchange.IO("address") // TRX balance (standard function, in TRX) Log("account:", exchange.GetAccount()) // USDT balance: an on-chain integer, scale it by the decimals var raw = exchange.IO("api", "tron", "TRC20ContractBalance", wallet, usdt) var decimals = exchange.IO("api", "tron", "TRC20GetDecimals", usdt) Log("USDT:", raw / Math.pow(10, decimals)) // read-only call of name() (selector 0x06fdde03) with TRC20Call, then parse the returned string var ret = exchange.IO("api", "tron", "TRC20Call", "", usdt, "0x06fdde03", true, 0) // constant_result holds hex strings, parse them directly Log("name:", exchange.IO("api", "tron", "ParseTRC20StringProperty", ret.constant_result[0])) } -
Read several contract methods at once with a Multicall contract
javascriptfunction 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)] ] // register the aggregate method of the Multicall contract 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])) }
See Also