Tide is the language your eTrader trading robots and chart indicators are written in. This page is the whole of it: every type, every event, every call, with a worked example for each. Read it top to bottom and you can write a bot.
bot "Golden Cross" { version "1.0" }
input int fast = 50
input int slow = 200
input lots volume = 0.10
fn onBar() {
guard positions.count() == 0 else { return }
if crossesOver(ema(close, fast), ema(close, slow)) {
trade.buy(volume,
sl: symbol.ask - 40 * symbol.pip,
tp: symbol.ask + 90 * symbol.pip)
}
}
Read it start to finish and you will know the language. Every section has a runnable example.
Tide is a programming language with one job: describing what a trading strategy does, and then doing it. You write a plain text file with a .tide extension, compile it, and get a small bundle that eTrader runs for you.
There are two kinds of thing you can build.
| You write | It compiles to | It does |
|---|---|---|
| A bot | .etb | Watches the market and places trades on your account, from eTrader's servers, with your computer switched off. |
| An indicator | .eti | Draws on the chart in the desktop terminal. Lines, bands, arrows, histograms, panels. |
Two smaller kinds exist for organising code: a library (.etl) holds functions you import into several bots, and a script runs once and exits, for one-off jobs like closing everything.
Most trading languages were designed when a strategy was a C program that happened to run on a chart. Reading a moving average takes a handle, a global variable, an array, a copy call and a release call. Placing one order takes a request structure, a send call, a return code and a result structure. Most of a robot ends up being paperwork rather than trading.
In Tide an indicator is an expression and an order is one call with named arguments:
let trend = ema(close, 50)
if close[0] > trend[0] {
trade.buy(0.10, sl: 1.0800, tp: 1.0950)
}
That is the whole idea. Everything below is detail.
If you have written anything in JavaScript, Python, Swift or C, you can read Tide today. If you have never programmed, this page is written to be read start to finish: every concept is introduced before it is used, and every section has a runnable example.
Here is a complete robot. Nothing is left out, nothing is simplified away. It buys when a fast moving average crosses above a slow one, and it never holds more than one position at a time.
bot "Golden Cross" {
version "1.0"
}
input int fast = 50
input int slow = 200
input lots volume = 0.10
fn onBar() {
guard positions.count() == 0 else { return }
if crossesOver(ema(close, fast), ema(close, slow)) {
trade.buy(volume,
sl: symbol.ask - 40 * symbol.pip,
tp: symbol.ask + 90 * symbol.pip)
}
}
Now the same file, one piece at a time.
bot "Golden Cross" {
version "1.0"
}
Every file starts with exactly one of these. The word bot says this compiles to a robot. The text is the name a trader sees in their bots list. The block holds metadata: version, author, description and a few others covered in how a file is laid out.
input int fast = 50
input lots volume = 0.10
An input is a setting the trader fills in when they install the bot. Each line becomes one field in a form, automatically. You never write form code. int gives a number field, lots gives a volume field that snaps to the instrument's step size, and the value after = is the default.
fn onBar() {
...
}
fn declares a function. A function named onBar is special: eTrader calls it once at the start of every new candle. There are other handlers for every tick, for a timer, and for trade events, all listed in events.
guard positions.count() == 0 else { return }
Read it out loud: carry on only if there are no open positions, otherwise leave. Guards say what must be true, instead of nesting the real work inside an if. The else block must exit, so a guard can never silently fall through.
if crossesOver(ema(close, fast), ema(close, slow)) {
ema(close, fast) is an exponential moving average of the closing price. It is not a number, it is a series: one value per candle, going back through history. crossesOver(a, b) is true on exactly the candle where a moves from below b to above it. Series are the one idea worth learning properly, and they get their own section next.
trade.buy(volume,
sl: symbol.ask - 40 * symbol.pip,
tp: symbol.ask + 90 * symbol.pip)
One call. The first argument is the volume. sl: and tp: are named, so you cannot get them the wrong way round. symbol.pip is the size of one pip on whatever instrument the bot is running on, so the same file works on EURUSD and on gold without a change.
crossesOver to crossesUnder and trade.buy to trade.sell. You now have the mirror strategy, and nothing else needs touching.A .tide file always reads top to bottom in the same order. Only the first line is required.
bot "Name" { ... } // 1. what this is, and its metadata
permissions { ... } // 2. what it is allowed to reach
import "risk.tidelib" as risk // 3. code from other files
input int length = 14 // 4. the trader's settings
let trend = ema(close, 200) // 5. file-level values and state
var lastEntry: time // these live for the life of the bot
fn onBar() { ... } // 6. event handlers and your own functions
Everything here is optional except the name in quotes.
| Key | Example | What it does |
|---|---|---|
version | "1.2.0" | Shown to the trader and stamped into the bundle. |
author | "Your Name" | Shown in the bots list. |
description | "Buys pullbacks in an uptrend." | One line under the name in the install screen. |
icon | "icon.png" | A small image packed into the bundle. |
website | "https://yoursite.com" | A support link on the install screen. |
timeframes | M15, H1 | The bot refuses to start on any other timeframe. |
symbols | "EURUSD", "GBPUSD" | The bot refuses to start on any other instrument. |
minBars | 500 | Waits until this much history is loaded before the first event. |
tags | "trend", "swing" | Search keywords. |
Indicators take three more: panel (overlay to draw on the price chart, separate for its own pane), scale (price, auto, percent or fixed(0, 100)) and precision.
// a line comment, to the end of the line
/* a block comment,
over several lines */
Names are case sensitive. Values and functions are written likeThis, types and structs LikeThis, and compile-time constants LIKE_THIS. Underscores are allowed in numbers for readability: 1_000_000 is one million.
Tide checks types when it compiles, not when it trades. A mistake in a unit is caught on your desk instead of on a live account.
| Type | What it holds | How you write one |
|---|---|---|
int | A whole number | 42 -7 1_000_000 |
num | A number with decimals | 3.14 1e-6 .5 |
bool | True or false | true false |
text | A string of characters | "EURUSD" "line\n" |
time | A moment in time | 2026.09.11 2026.09.11 14:30 now |
duration | A length of time | 30s 5m 4h 2d 1w |
price | A number that rounds to the instrument's digits | 1.08452 |
lots | A number that snaps to the instrument's volume step | 0.10 |
color | A colour | #3d7bf5 #3d7bf5aa red |
price and lots are separate types. Passing a volume where a price belongs is the single most common way to lose money in an automated strategy. In Tide it does not compile.let stop: price = 1.0850
let size: lots = 0.10
trade.buy(stop) // compile error: expected lots, found price
trade.buy(size, sl: stop) // fine
| Type | What it is | Example |
|---|---|---|
series<T> | One value per candle, going back in time | close, ema(close, 50) |
array<T> | An ordered list you can grow | [1, 2, 3] |
map<text, T> | Values looked up by name | {"a": 1, "b": 2} |
T? | A value that might be missing | Position? |
struct | Your own named fields | Setup(entry: ..., stop: ...) |
enum | A fixed set of choices | Bias.long |
Anything that can fail to produce a value has a type ending in ?. You cannot use it until you have dealt with the empty case, which is what stops a strategy reading a value that was never there.
let pos = positions.byTicket(12345) // Position?
if pos is none {
log.warn("That position is gone")
return
}
let profit = positions.byTicket(12345)?.profit ?? 0
// ^ only if present
// ^ otherwise use this
text(1.234) // "1.234"
text.parseNum("1.5") // 1.5
int(3.9) // 3 (truncates)
num(3) // 3.0
lots(0.137) // snaps to the instrument step, e.g. 0.13
price(1.084523) // rounds to the instrument digits, e.g. 1.08452
A series is a value that has one reading per candle. The closing price is a series. A moving average is a series. Whether this candle is green is a series. Almost everything you touch in a strategy is one.
You read a series with square brackets, and the index counts backwards in time. Always. There is no setting to reverse it.
close[0] // this candle's close, the one still forming
close[1] // the candle before it
close[10] // ten candles ago
Add, subtract, compare or combine two series and you get another series, computed candle by candle. You never write a loop for this.
let gap = close - open // series<num>: the body of each candle
let bullish = close > open // series<bool>: was each candle green
let body = abs(close - open)
let strong = body > atr(14) * 0.8
if strong[0] && bullish[0] {
log.info("Big green candle")
}
[0] at the moment you need one actual number, usually inside an if.The candles of whatever instrument and timeframe the bot is running on are always available by name, with no setup:
| Series | What it holds |
|---|---|
open high low close | The four prices of each candle |
time | The opening time of each candle |
volume | Tick volume of each candle |
spread | The spread recorded on each candle |
hl2 hlc3 ohlc4 | The usual average prices, ready made |
let daily = series.of("EURUSD", D1)
let dailyTrend = ema(daily.close, 50)
let gold = series.of("XAUUSD").close
if close[0] > dailyTrend[0] {
log.info("Above the daily trend")
}
Timeframe names are M1 M2 M3 M4 M5 M6 M10 M12 M15 M20 M30 H1 H2 H3 H4 H6 H8 H12 D1 W1 MN1.
highest(high, 20)[0] // highest high of the last 20 candles
lowest(low, 20)[0] // lowest low
highestBar(high, 20)[0] // how many candles back that high was
sum(volume, 10)[0] // volume over ten candles
avg(close, 20)[0] // same as sma(close, 20)[0]
// how far into the 20-candle range we are, 0 to 1
let position = (close - lowest(low, 20)) / (highest(high, 20) - lowest(low, 20))
A series is lazy and cached. Writing let e = ema(close, 200) at the top of a file costs nothing until something reads it, and reading it twice on the same candle only computes it once. Declare your indicators at file level and use them freely.
let fast = ema(close, 12)
let slow = ema(close, 26)
let rs = rsi(close, 14)
fn onBar() {
if fast[0] > slow[0] && rs[0] < 70 { ... } // no recomputation
}
A series and a single number are different things, and mixing them up is the mistake beginners make most. The compiler catches it.
let e = ema(close, 50)
if e > 1.08 { } // compile error: that compares a series to a number
if e[0] > 1.08 { } // correct: compares this candle's value
let x = 10 // cannot be changed after this line
var y = 10 // can be changed
const MAX = 100 // fixed at compile time, usable in input limits
let z: num = 10 // say the type when it is not obvious
var buf: array<num> = []
let is the default and reassigning one is a compile error. Reach for var only when a value genuinely has to change.
| Declared | Lives for | Use it for |
|---|---|---|
| Inside a function | That one call | Working values |
At file level with let | The life of the bot | Indicators and settings worked out once |
At file level with var | The life of the bot | State that must survive between candles |
In store | Forever, across restarts | State that must survive a redeploy or a machine move |
var tradesToday = 0
var lastEntry: time?
fn onBar() {
if clock.startOfDay(now) > clock.startOfDay(lastEntry ?? now) {
tradesToday = 0 // a new day, reset the counter
}
}
var starts again from its default when that happens. Anything that must genuinely outlive the process belongs in store.| Group | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / % ** | ** is power. % is remainder. |
| Comparison | == != < <= > >= | Work on numbers, text, time and duration. |
| Logical | && || ! | Short-circuit: the right side is skipped when the answer is already known. |
| Choice | cond ? a : b | The short form of an if. |
| Fallback | a ?? b | Use b when a is missing. |
| Range | 0..10 0..=10 | Up to ten, and up to and including ten. |
| Assign | = += -= *= /= %= | |
| Access | . [ ] ( ) | Field, index, call. |
| Pipe | |> | x |> f(y) is the same as f(x, y). |
let norm = (close - lowest(low, 20)) / (highest(high, 20) - lowest(low, 20))
// norm is a series, computed candle by candle. No loop was written.
int by another gives an int and throws away the remainder. Write num(a) / b when you want decimals.?? handles it in one character: let r = a / b ?? 0.Useful when a value passes through several steps and the nesting would read backwards.
let s = close[0] |> round(2) |> text() |> text.pad(8)
// the same as: text.pad(text(round(close[0], 2)), 8)
if spread > 20 {
log.warn("Spread too wide")
} else if spread > 10 {
log.info("Spread is workable")
} else {
trade.buy(0.1)
}
match bias {
.long => trade.buy(volume)
.short => trade.sell(volume)
.flat => {}
}
if and match are also expressions, so they can produce a value:
let size = if account.equity > 10_000 { 1.0 } else { 0.5 }
for i in 0..10 { ... } // 0 to 9
for i in 0..=10 { ... } // 0 to 10
for pos in positions.mine() { ... }
for key, value in myMap { ... }
while spread > 20 { ... } // still bounded by the step budget
break // leave the loop
continue // skip to the next turn
Four things every strategy needs, which in other languages you have to build by hand out of static variables and timestamps.
once {
log.info("This runs on the very first tick and never again")
}
atNewBar {
recount() // the first tick of each new candle
}
every(5m) {
checkNews() // throttled by the wall clock, not by candles
}
guard positions.count() < 3 else { return }
guard symbol.sessionOpen else { return }
A guard states the condition you need and exits when it fails, so the real work stays at the left margin instead of drifting right inside three levels of nesting. The else block has to leave the function, so a guard can never fall through by accident.
// nested, and getting harder to read with each rule
fn onBar() {
if positions.count() == 0 {
if symbol.sessionOpen {
if spread < 20 {
trade.buy(volume)
}
}
}
}
// the same rules, flat
fn onBar() {
guard positions.count() == 0 else { return }
guard symbol.sessionOpen else { return }
guard spread < 20 else { return }
trade.buy(volume)
}
fn pipsBetween(a: price, b: price) -> num {
return abs(a - b) / symbol.pip
}
fn quiet() { log.debug("no return type needed") }
fn half(x: num) -> num => x / 2 // one expression, no braces
fn openTrade(volume: lots, stopPips: int = 200, comment: text = "") {
trade.buy(volume, sl: symbol.ask - stopPips * symbol.pip, comment: comment)
}
openTrade(0.1)
openTrade(0.1, stopPips: 300)
openTrade(0.1, comment: "breakout")
Named arguments are what make the trading API readable. trade.buy(0.1, sl: 1.0800, tp: 1.0900) cannot be got the wrong way round, and it still reads correctly a year later.
Numbers, text, booleans, times and durations are passed by value: the function gets its own copy. Arrays, maps and structs are passed by reference: the function sees the same object you do, and changing it changes yours. There are no pointers and no address operator.
fn applyToAll(f: fn(Position) -> void) {
for pos in positions.mine() { f(pos) }
}
applyToAll((pos) => trade.breakEven(pos, offsetPips: 5))
In a library, pub marks what other files may import. Everything else stays private to the file.
pub fn positionSize(percent: num, stopPips: int) -> lots { ... }
A struct groups related values under one name. Build one by calling its name with named arguments.
struct Setup {
entry: price
stop: price
target: price
score: num
}
let s = Setup(
entry: symbol.ask,
stop: symbol.ask - 20 * symbol.pip,
target: symbol.ask + 60 * symbol.pip,
score: 0.8)
log.info("Risking " + text(pipsBetween(s.entry, s.stop)) + " pips")
An enum is a fixed set of names. Inside a match you can drop the enum name and write just the dot.
enum Bias { long, short, flat }
fn currentBias() -> Bias {
if close[0] > ema(close, 200)[0] { return Bias.long }
if close[0] < ema(close, 200)[0] { return Bias.short }
return Bias.flat
}
match currentBias() {
.long => trade.buy(volume)
.short => trade.sell(volume)
.flat => {}
}
match over an enum has to cover every case. Add a name to the enum later and the compiler tells you which matches now have a hole in them.try {
let r = http.get("https://licence.mysite.com/verify")
log.info("Licence server said " + text(r.status))
} catch e {
log.warn("Licence server unreachable: " + e.message)
}
A runtime error inside an event handler does not stop the bot. The runtime catches it, writes it to the bot's journal with the file and line, and carries on with the next event.
Three errors in a row on the same line is treated differently: the bot is stopped and the trader is notified. A robot that quietly does nothing while its owner believes it is working is worse than one that stops.
onInit can return a status. Failing there stops the bot before it can place a single order, which is where a bad licence key or a missing setting belongs.
fn onInit() -> status {
if account.currency != "USD" {
return status.failed("This bot only runs on a USD account")
}
return status.ok
}
if account.equity < 500 {
bot.stop("Equity below the floor, refusing to trade")
}
Every input line becomes one field in the form the trader fills in when installing the bot. You describe the setting; eTrader builds the control, validates it, remembers it and passes it in.
input <type> name [= default] [{ metadata }]
input group "Heading" [{ advanced: true, hint: "..." }]
input group "Signal"
input int fastLength = 12 { label: "Fast EMA", min: 2, max: 400 }
input int slowLength = 26 { label: "Slow EMA", min: 2, max: 400 }
input source priceSource = close
input group "Risk"
input lots volume = 0.10 { min: 0.01, max: 100, step: 0.01 }
input int stopPips = 200 { label: "Stop loss (pips)" }
| Type | The trader sees |
|---|---|
int num | A number field, with the minimum, maximum and step you set |
bool | A switch |
text | A text field |
lots | A volume field, snapped to the instrument's step size |
price | A price field, at the instrument's number of digits |
time | A date and time picker |
duration | A duration field |
color | A colour swatch |
symbol | An instrument picker, filled in live by the platform |
timeframe | A timeframe picker, M1 through MN1 |
source | A price source picker: close, open, high, low, hl2, hlc3, ohlc4 |
select | A dropdown, from an enum or from an options: list |
secret | A masked field. Encrypted at rest and never returned by any API |
model | An AI model picker, filled in live by the platform |
file | A small file upload, read back with resource.read() |
label, hint, min, max, step, options, group, advanced, required, dependsOn and unit.
input select style = "balanced" {
label: "Trading style",
options: [ "scalp": "Scalping", "balanced": "Balanced", "swing": "Swing" ]
}
input int maxSpread = 20 { label: "Max spread", unit: "points", min: 0, max: 500 }
input bool notifyByWebhook = false
input secret webhookUrl { label: "Webhook URL", dependsOn: notifyByWebhook }
dependsOn hides a field until the field it names is switched on, so the form stays short. advanced: true on a group folds it away behind a disclosure.
A secret input is masked in the form, encrypted where it is stored, decrypted only inside the worker running your bot, and never returned by any API, including to you. Use it for licence keys and for API keys.
input secret aiKey { label: "AI API key", hint: "Encrypted. Never leaves the runtime." }
input secret licenceKey { label: "Licence key", required: true }
var in onInit.A bot does nothing on its own. eTrader calls your handlers when something happens.
| Handler | Called |
|---|---|
fn onInit() -> status | Once at start. Return status.ok or status.failed(reason) |
fn onDeinit(reason: text) | Once at stop |
fn onTick() | On every price change |
fn onBar() | On the first tick of a new candle on the bot's timeframe |
fn onTimer() | On the interval set with timer.every(...) |
fn onTrade(e: TradeEvent) | On any change to orders, positions or deals |
fn onFilled(d: Deal) | When an order belonging to this bot fills |
fn onClosed(p: ClosedPosition) | When a position belonging to this bot closes |
fn onWebhook(m: Webhook) | On an inbound HTTP call to this bot instance |
onBar is the right default. It runs once per candle, so the same signal cannot fire twice, and a strategy built on candle closes behaves the same in a backtest as it does live. Use onTick only when you genuinely need every price change, such as managing a trailing stop.
fn onBar() {
if crossesOver(fast, slow) { trade.buy(volume) } // once per candle
}
fn onTick() {
for pos in positions.mine() {
trade.trail(pos, distancePips: 40, stepPips: 5) // needs every tick
}
}
fn onFilled(d: Deal) {
notify("Filled", d.symbol + " " + text(d.volume) + " at " + priceText(d.price))
}
fn onClosed(p: ClosedPosition) {
store.set("lastResult", p.profit > 0 ? "win" : "loss")
log.info("Closed for " + money(p.profit))
}
fn onInit() -> status {
timer.every(5m)
return status.ok
}
fn onTimer() {
log.info("Equity " + money(account.equity))
}
| Handler | Called |
|---|---|
fn onInit() -> status | Once |
fn onCalculate(b: Bars) -> int | When candles change. Return how many you computed |
fn onChartEvent(e: ChartEvent) | On a click, drag, key press or panel button |
fn onDeinit(reason: text) | On removal from the chart |
plot line on its own is a complete indicator. See writing an indicator.trade.buy(0.10)
trade.sell(0.10)
trade.buy(0.10,
sl: symbol.ask - 40 * symbol.pip,
tp: symbol.ask + 90 * symbol.pip,
comment: "breakout",
magic: 1234,
slippage: 3,
symbol: "EURUSD")
Only the volume is required. Every other argument is named and optional.
trade.buyLimit(1.0800, 0.10, sl: 1.0750, tp: 1.0900)
trade.sellLimit(1.0900, 0.10)
trade.buyStop(1.0950, 0.10)
trade.sellStop(1.0750, 0.10)
trade.buyStopLimit(1.0950, 1.0940, 0.10)
A take-profit ladder is native, because the eTrader engine supports one. Pass a list and the volume is split across the rungs.
trade.buy(0.30, sl: stop, tp: [ t1, t2, t3 ])
trade.close(pos)
trade.closePartial(pos, 0.05)
trade.closeAll(symbol: "EURUSD")
trade.closeAll(magic: 1234)
trade.closeProfitable()
trade.closeLosing()
trade.modify(pos, sl: newStop, tp: newTarget)
trade.modifyPending(ord, price: 1.0810, expiry: now + 4h)
trade.cancel(ord)
trade.cancelAll(symbol: "EURUSD")
trade.reverse(pos)
trade.breakEven(pos, offsetPips: 5)
trade.trail(pos, distancePips: 40, stepPips: 5)
Every trade call returns a result. Check it, because a broker can refuse an order for a dozen ordinary reasons.
let r = trade.buy(volume, sl: stop)
if !r.ok {
log.error("Order refused: " + r.message + " (" + text(r.retcode) + ")")
return
}
log.info("Ticket " + text(r.ticket) + " filled at " + priceText(r.price))
The result carries ok, ticket, price, volume, error, retcode and message. Every call is written to the bot's journal with the instance id, so a trader can always see which bot opened which position.
let margin = trade.calcMargin("buy", symbol.name, volume, symbol.ask)
guard margin < account.freeMargin * 0.5 else {
log.warn("Not enough free margin, skipping")
return
}
positions.all() // every position on the account
positions.mine() // only the ones this bot opened
positions.count() // how many, in total
positions.count("EURUSD") // how many on one instrument
positions.bySymbol("EURUSD")
positions.byTicket(12345) // Position?
positions.byMagic(1234)
positions.profit() // sum of open profit
positions.volume("EURUSD")
positions.longVolume() positions.shortVolume()
positions.oldest() positions.newest()
positions.mine() is the one to reach for. It returns only what this bot instance opened, so two bots on the same account never touch each other's trades.
Position {
ticket, symbol, type, volume,
openPrice, currentPrice, sl, tp,
profit, swap, commission,
openTime, magic, comment, botId
}
orders.all() orders.mine() orders.count()
orders.bySymbol("EURUSD") orders.byTicket(12345) orders.byMagic(1234)
let since = clock.startOfDay(now)
let closed = history.positions(since, now)
let today = history.profit(since, now)
log.info(text(closed.len) + " trades today for " + money(today))
for deal in history.deals(now - 7d, now) {
log.debug(deal.symbol + " " + money(deal.profit))
}
input lots volume = 0.10
input int trailPips = 40
input int stepPips = 5
fn onBar() {
guard positions.mine().len == 0 else { return }
guard crossesOver(ema(close, 12), ema(close, 26)) else { return }
trade.buy(volume, sl: symbol.ask - 60 * symbol.pip)
}
fn onTick() {
for pos in positions.mine() {
if pos.profit > 0 {
trade.trail(pos, distancePips: trailPips, stepPips: stepPips)
}
}
}
account.balance account.equity account.profit
account.margin account.freeMargin account.marginLevel
account.currency account.leverage account.isDemo
account.login account.name account.server
account.company account.credit account.hedging
account.marginCall account.stopOut account.tradeAllowed
symbol.name symbol.bid symbol.ask
symbol.last symbol.spread symbol.digits
symbol.point symbol.pip symbol.tickSize
symbol.tickValue symbol.contractSize symbol.stopsLevel
symbol.volumeMin symbol.volumeMax symbol.volumeStep
symbol.swapLong symbol.swapShort symbol.freezeLevel
symbol.tradeAllowed symbol.sessionOpen symbol.time
symbol.high symbol.low // today's range
symbol.point is the smallest price step. symbol.pip is what a trader means by a pip, which on a five-digit or three-digit quote is ten points. Use symbol.pip for stops and targets and the same file works on EURUSD, on USDJPY and on gold.symbol.normalize(1.084523) // 1.08452, at the instrument's digits
symbol.normalizeLots(0.137) // 0.13, snapped to the volume step
symbols.list() // every instrument you can trade
symbols.info("XAUUSD")
symbols.select("XAUUSD", true)
symbols.tick("XAUUSD")
symbols.book("EURUSD") // depth of market, where the broker provides it
guard symbol.sessionOpen else { return }
guard !clock.isWeekend(now) else { return }
guard clock.inSession(08:00, 17:00) else { return }
This is the calculation every serious strategy needs and the one most beginners get wrong. Risk a fixed percentage of equity, and let the stop distance decide the volume.
input num percentRisk = 1.0 { label: "Risk per trade (%)", min: 0.1, max: 5 }
input int stopPips = 200
fn positionSize(percent: num, stopPips: int) -> lots {
let riskMoney = account.equity * percent / 100
let perPip = symbol.tickValue * (symbol.pip / symbol.tickSize)
let raw = riskMoney / (stopPips * perPip)
return lots(clamp(raw, symbol.volumeMin, symbol.volumeMax))
}
fn onBar() {
guard positions.mine().len == 0 else { return }
guard crossesOver(ema(close, 12), ema(close, 26)) else { return }
let volume = positionSize(percentRisk, stopPips)
log.info("Risking " + money(account.equity * percentRisk / 100)
+ " over " + text(stopPips) + " pips, so " + lotsText(volume))
trade.buy(volume,
sl: symbol.ask - stopPips * symbol.pip,
tp: symbol.ask + stopPips * 2 * symbol.pip)
}
Reading it back: turn the percentage into money, work out what one pip is worth on this instrument at one lot, divide, then clamp to what the broker will accept. Returning lots(...) snaps the answer to the volume step, so the broker never rejects the size.
An indicator draws on a chart. The same language writes it, and simple ones need no logic at all.
indicator "Triple EMA" { panel: overlay }
input int a = 8
input int b = 21
input int c = 55
plot ema(close, a) { title: "Fast", color: #3d7bf5 }
plot ema(close, b) { title: "Medium", color: #e8a33d }
plot ema(close, c) { title: "Slow", color: #d0342c, width: 1.6 }
That is the whole file. There is no calculation loop, no buffer to size, no initialisation and no cleanup. Each plot takes a series and draws it.
indicator "RSI with bands" {
panel: separate
scale: fixed(0, 100)
precision: 1
}
input int length = 14
plot rsi(close, length) { title: "RSI", color: #3d7bf5, width: 1.4 }
hline(70) { color: #d0342c, style: dashed }
hline(50) { color: #86868b, style: dotted }
hline(30) { color: #1a7f37, style: dashed }
Write onCalculate and fill buffers by hand. Return how many candles you computed.
indicator "Custom band" { panel: overlay }
buffer upper
buffer lower
fn onCalculate(b: Bars) -> int {
let basis = sma(close, 20)
let width = atr(14) * 1.5
upper[0] = basis[0] + width[0]
lower[0] = basis[0] - width[0]
return b.count
}
custom("myind.eti").buffer(0). If nothing else needs to read it, a plain plot is simpler and faster.fn onChartEvent(e: ChartEvent) {
match e.kind {
.click => log.info("Clicked at " + priceText(e.price))
.key => if e.key == "r" { chart.redraw() }
.drag => {}
}
}
plot <series> { title:, color:, width:, style:, panel:, visible:, precision: }
line stepline area histogram columns dots cross arrows candles bars zigzag section fill none
Pass a series to color: and every candle is coloured on its own reading.
let r = rsi(close, 14)
plot r {
title: "RSI",
color: r[0] > 70 ? #d0342c : r[0] < 30 ? #1a7f37 : #3d7bf5
}
let bb = bands(close, 20, 2)
plot bb.upper { title: "Upper", color: #3d7bf5 }
plot bb.basis { title: "Basis", color: #86868b, style: dashed }
plot bb.lower { title: "Lower", color: #3d7bf5 }
fill(bb.upper, bb.lower, color: #3d7bf522)
hline(0) { color: #86868b, style: dotted }
vline(clock.startOfDay(now)) { color: #86868b }
Some indicators produce more than one line. They return a struct, and you read the parts by name.
let m = macd(close, 12, 26, 9)
plot m.line { title: "MACD", color: #3d7bf5 }
plot m.signal { title: "Signal", color: #e8a33d }
plot m.histogram { title: "Hist", style: histogram,
color: m.histogram[0] > 0 ? #1a7f37 : #d0342c }
Everything a trader can draw by hand, a script can draw too. Each call takes its anchors, then a block of options.
draw.trendline("t1", time[20], low[20], time[0], low[0],
color: #3d7bf5, width: 1.4, ray: true)
draw.rect("zone", time[30], 1.0850, time[0], 1.0880,
color: #3d7bf522, fill: true, back: true)
draw.arrowUp("entry", time[0], low[0] - 10 * symbol.pip, color: #1a7f37)
draw.label("hud", "Trend up", corner: topLeft, x: 12, y: 12,
color: #1a7f37, size: 11)
draw.text("note", time[5], high[5], "Failed breakout", color: #d0342c)
hline vline trendline ray channel regression stddevChannel pitchfork fibo fiboFan fiboArc fiboTimes fiboChannel expansion gannLine gannFan gannGrid cycles elliott3 elliott5 rect triangle ellipse arrow arrowUp arrowDown arrowCheck arrowStop arrowThumb arrowPrice text label button edit bitmap rectLabel, each under draw.
draw.move("t1", 1, time[0], close[0])
draw.set("t1", "color", #d0342c)
draw.get("t1")
draw.find("t1")
draw.count()
draw.delete("t1")
draw.deleteAll(prefix: "zone")
Building a heads-up display out of rectangles and labels, positioning each one by hand, is a day of work in most trading languages. Tide has a panel block.
panel "Risk" {
at: topRight, width: 220, theme: auto
row { label("Equity"); value(money(account.equity)) }
row { label("Open risk"); value(pct(openRisk),
color: openRisk > 2 ? red : green) }
row { label("Today"); value(money(history.profit(
clock.startOfDay(now), now))) }
separator()
button("Close all", onPress: () => trade.closeAll())
button("Break even", onPress: beAll, enabled: positions.count() > 0)
slider("Lots", bind: riskLots, min: 0.01, max: 5, step: 0.01)
toggle("Auto-trail", bind: autoTrail)
}
Panels follow the terminal's own theme in both light and dark, so a panel written once looks correct for every trader without a colour setting.
| Element | What it is |
|---|---|
row { ... } | A line, laid out left to right |
label(text) | Static text |
value(text, color:) | A value, right-aligned, recomputed live |
separator() | A dividing line |
button(text, onPress:, enabled:) | A button that calls your function |
slider(text, bind:, min:, max:, step:) | A slider bound to a var |
toggle(text, bind:) | A switch bound to a var |
at: takes topLeft, topRight, bottomLeft or bottomRight.
Forty-five indicators, each one an expression. No handles, no buffer copying, no release call. Every one returns a series, or a struct of series where it naturally has more than one line.
| Call | Returns |
|---|---|
ma(src, len, type) | series |
sma(src, len) | series |
ema(src, len) | series |
smma(src, len) | series |
lwma(src, len) | series |
dema(src, len) | series |
tema(src, len) | series |
ama(src, len, fast, slow) | series |
frama(src, len) | series |
vidya(src, cmo, ema) | series |
sar(step, max) | series |
ichimoku(t, k, s) | {tenkan, kijun, senkouA, senkouB, chikou} |
alligator(...) | {jaw, teeth, lips} |
envelopes(src, len, type, dev) | {upper, lower} |
| Call | Returns |
|---|---|
rsi(src, len) | series |
macd(src, fast, slow, signal) | {line, signal, histogram} |
osma(src, fast, slow, signal) | series |
stoch(k, d, slowing) | {k, d} |
cci(src, len) | series |
williamsR(len) | series |
momentum(src, len) | series |
roc(src, len) | series |
demarker(len) | series |
rvi(len) | {main, signal} |
trix(src, len) | series |
ao() ac() | series |
bearsPower(len) bullsPower(len) | series |
gator(...) | {upper, lower} |
adx(len) adxWilder(len) | {main, plusDi, minusDi} |
chaikin(fast, slow, type) | series |
force(len, type) | series |
| Call | Returns |
|---|---|
atr(len) | series |
trueRange() | series |
stddev(src, len) | series |
bands(src, len, dev) | {upper, basis, lower} |
volumes() | series |
obv() ad() | series |
mfi(len) bwmfi() | series |
fractals() | {up, down} |
let band = custom("myind.eti", 20, 2.0).buffer(0)
The small functions a strategy reaches for constantly, so you never write the loop yourself.
| Call | What it gives you |
|---|---|
crossesOver(a, b) | True on the candle where a moves above b |
crossesUnder(a, b) | True on the candle where a moves below b |
rising(s, n) falling(s, n) | True when the series rose or fell for n candles |
changed(s) | True when the value differs from the previous candle |
barsSince(cond) | How many candles since the condition was last true |
valueWhen(cond, s, n) | The value of s the nth time the condition was true |
highest(s, n) lowest(s, n) | Highest and lowest over n candles |
highestBar(s, n) lowestBar(s, n) | How many candles back that extreme was |
sum(s, n) avg(s, n) cum(s) | Running totals and averages |
stdev(s, n) correlation(a, b, n) | Dispersion and correlation |
percentRank(s, n) | Where this reading sits in the last n, 0 to 100 |
linreg(s, n) | Linear regression value |
pivotHigh(n, m) pivotLow(n, m) | Swing points with n candles either side |
// entered more than 20 candles ago and still going
if barsSince(crossesOver(fast, slow))[0] > 20 { ... }
// the close on the candle the last cross happened
let entryPrice = valueWhen(crossesOver(fast, slow), close, 0)
// today's range as a percentage of the 20-day average range
let ratio = (high[0] - low[0]) / avg(high - low, 20)[0]
abs sign min max clamp round(x, digits) floor ceil trunc sqrt cbrt pow(x, y) exp ln log10 log2 sin cos tan asin acos atan atan2 sinh cosh tanh hypot mod isNaN isFinite
rand() // 0 to 1
randInt(1, 6)
seed(42)
The generator is seeded per bot instance and the seed is recorded, so a backtest replays exactly the same sequence. A strategy that uses randomness is still reproducible.
sum(values) mean(values) median(values) stdev(values)
variance(values) percentile(values, 90)
normalize(x, lo, hi) lerp(a, b, t)
Constants: PI E INF EPSILON INT_MAX INT_MIN.
ln, not log, because log.info(...) writes to the journal. The compiler refuses to build if any two built-in names ever collide.text(x) turns anything into text. After that:
text.len text.upper text.lower
text.trim text.trimLeft text.trimRight
text.find(sub) text.contains(sub)
text.startsWith(sub) text.endsWith(sub)
text.replace(a, b) text.replaceAll(a, b)
text.split(sep) text.join(array, sep)
text.slice(a, b) text.charAt(i) text.code(i)
text.fromCode(n) text.repeat(n) text.reverse
text.pad(n, ch) text.padLeft(n, ch)
text.format(fmt, args...)
text.compare(a, b)
text.parseNum text.parseInt text.parseTime
text.parseJson text.toJson(value)
These are the helpers the eTrader apps themselves use, so your output matches the rest of the platform.
| Call | Gives |
|---|---|
money(1234.5) | "1,234.50 USD", in the account currency |
pips(43.2) | A pip count, formatted |
pct(1.75) | A percentage |
priceText(1.084523) | The price at the instrument's digits |
lotsText(0.1) | A volume at the instrument's step |
log.info("Equity " + money(account.equity)
+ ", open " + text(positions.count())
+ " for " + money(positions.profit()))
let r = http.json("https://api.example.com/signal")
let bias = r["bias"] // "long"
let body = text.toJson({ "symbol": symbol.name, "action": "buy" })
time is the series of candle opening times, so time[1] means the previous candle's time, which is what a trader expects. The clock therefore lives under clock.now // this moment
clock.local clock.gmt clock.server
clock.gmtOffset clock.dst
let t = now
t.year t.month t.day
t.hour t.minute t.second
t.dayOfWeek t.dayOfYear
t.format("yyyy-MM-dd HH:mm")
now + 4h
now - 30m
clock.startOfDay(now)
clock.startOfWeek(now)
clock.startOfMonth(now)
clock.isWeekend(now)
clock.inSession(08:00, 17:00)
clock.of(2026, 9, 11, 14, 30, 0)
clock.parse("2026-09-11", "yyyy-MM-dd")
input time from = 08:00
input time to = 17:00
fn onBar() {
guard !clock.isWeekend(now) else { return }
guard clock.inSession(from, to) else { return }
guard now - (lastEntry ?? now - 1d) > 4h else { return }
...
}
var levels: array<price> = []
levels.push(1.0850)
levels.push(1.0900)
levels.len levels.pop() levels.shift()
levels.unshift(x) levels.insert(i, x)
levels.remove(i) levels.clear() levels.resize(n)
levels.fill(x) levels.copy() levels.slice(a, b)
levels.reverse() levels.sort() levels.sortBy(fn)
levels.indexOf(x) levels.contains(x)
levels.min() levels.max() levels.minIndex()
levels.maxIndex() levels.sum()
levels.map(fn) levels.filter(fn) levels.reduce(fn, init)
levels.find(fn) levels.any(fn) levels.all(fn)
levels.join(", ") levels.bsearch(x)
let wins = history.positions(now - 30d, now)
.filter((t) => t.profit > 0)
let total = history.positions(now - 30d, now)
.map((t) => t.profit)
.reduce((a, b) => a + b, 0)
log.info(text(wins.len) + " winners, " + money(total) + " net")
var lastEntryBySymbol: map<text, time> = {}
lastEntryBySymbol.set("EURUSD", now)
if lastEntryBySymbol.has("EURUSD") {
let t = lastEntryBySymbol.get("EURUSD")
}
lastEntryBySymbol.len lastEntryBySymbol.delete("EURUSD")
lastEntryBySymbol.clear() lastEntryBySymbol.keys()
lastEntryBySymbol.values() lastEntryBySymbol.entries()
for sym, t in lastEntryBySymbol {
log.debug(sym + " last entered " + t.format("HH:mm"))
}
A var lives as long as the process. A bot in the cloud can be restarted after a deploy or moved to another machine, and a var starts again at its default when that happens. Anything that must genuinely survive belongs in store.
store.set("tradesToday", 3)
store.set("lastSignal", now)
let n = store.get("tradesToday") ?? 0
store.delete("lastSignal")
store.keys()
store is private to one bot instance and survives restarts, redeploys and machine moves. globals works the same way but is shared across the whole account, so two bots can coordinate.
globals.set("riskOff", true)
// in another bot
guard !(globals.get("riskOff") ?? false) else { return }
input file symbolList
fn onInit() -> status {
let contents = resource.read(symbolList)
...
}
store replaces it completely.| Call | Where it goes |
|---|---|
log.info(text) | The bot's journal, which the trader can read |
log.warn(text) log.error(text) | The journal, flagged |
log.debug(text) | The journal, only when debug logging is on |
alert(text) | A pop-up in the terminal |
notify(title, body) | A push notification on the trader's phone |
email(subject, body) | The trader's email |
webhook(url, payload) | An address you declared in permissions |
chart.comment(text) | The corner of the chart |
sound(name) | A sound in the terminal |
fn onFilled(d: Deal) {
notify("Golden Cross filled",
d.symbol + " " + lotsText(d.volume) + " at " + priceText(d.price))
log.info("Ticket " + text(d.ticket) + " on " + d.symbol)
}
A bot may reach the network, but only addresses it declared up front. The trader is shown that list before they install anything.
permissions {
network "licence.mysite.com", "api.anthropic.com"
maxOrdersPerMinute 4
}
let r = http.get("https://api.example.com/signal", timeout: 5s)
if r.ok {
let data = r.json()
log.info("Bias is " + data["bias"])
}
http.post("https://api.example.com/report",
json: { "equity": account.equity, "open": positions.count() })
let quick = http.json("https://api.example.com/signal") // get and parse
http.get http.post http.put http.patch http.delete http.json, each taking headers: and timeout:. A response carries ok, status, body, headers and json().
try. A service that is up today will be down one morning, and a bot that stops trading because a web request threw is a bot that failed for the wrong reason.ai.ask(model, prompt, system:, key:, maxTokens:, temperature:) -> text
ai.json(model, prompt, schema:, key:) -> any
ai.embed(model, text, key:) -> array<num>
ai.models() -> array<text>
input bool useAi = false { label: "Ask an AI before entering" }
input model aiModel = "claude-opus-5"
input secret aiKey { label: "AI API key" }
fn aiAgrees() -> bool {
let answer = ai.ask(aiModel, key: aiKey, maxTokens: 8,
system: "Answer with exactly YES or NO.",
prompt: "EURUSD just crossed its fast EMA above the slow EMA. RSI is "
+ text(round(rsi(close, 14)[0], 1))
+ ". Is this a reasonable long entry? YES or NO.")
return text.upper(text.trim(answer)).startsWith("YES")
}
fn onBar() {
guard crossesOver(fast, slow) else { return }
guard !useAi || aiAgrees() else { return }
trade.buy(volume)
}
The model comes from an input model, so the trader picks it from a live list. The key comes from an input secret and is decrypted only inside the worker running the bot. If the trader leaves the key blank, the platform's own endpoint is used against a per-account quota.
A compiled .etb is bytecode, not source. If you sell bots, you can also bind each copy to one account.
permissions { network "licence.mysite.com" }
input secret licenceKey { label: "Licence key", required: true }
fn onInit() -> status {
let lic = licence.check(licenceKey, at: "https://licence.mysite.com/verify")
if !lic.valid {
return status.failed("Licence rejected: " + lic.reason)
}
log.info("Licensed to " + lic.holder + ", expires " + text(lic.expires))
licence.offlineGrace(7d)
return status.ok
}
The result carries valid, reason, holder, expires, plan and meta.
licence.fingerprint gives you a stable id for the install.licence.offlineGrace(7d) keeps a paying customer trading through an outage on your own server.onInit is the right place. The bot never starts, so it cannot place a single order on an unlicensed copy.Write a function once and use it in every bot.
// risk.tidelib
library "Risk" { version "1.0" }
pub fn positionSize(percent: num, stopPips: int) -> lots {
let riskMoney = account.equity * percent / 100
let perPip = symbol.tickValue * (symbol.pip / symbol.tickSize)
return lots(riskMoney / (stopPips * perPip))
}
pub fn openRiskPercent() -> num {
var total = 0.0
for pos in positions.mine() {
total += abs(pos.openPrice - pos.sl) * pos.volume
}
return total / account.equity * 100
}
// in your bot
import "risk.tidelib" as risk
import { positionSize } from "sizing.tidelib"
let v = risk.positionSize(percent: 1.0, stopPips: 200)
guard risk.openRiskPercent() < 5 else { return }
Only pub declarations are visible outside the library. Everything else stays private to the file.
.etb you hand to somebody always contains everything it needs.Every bundle declares what it wants, and the trader sees that list in plain words before installing: this bot wants to contact licence.mysite.com and api.anthropic.com, and may place up to 4 orders a minute.
permissions {
network "licence.mysite.com", "api.anthropic.com"
maxOrdersPerMinute 4
}
| Limit | In the cloud | On a chart |
|---|---|---|
| Steps per event | 2,000,000 | 500,000 |
| Memory | 32 MB | 16 MB |
| History depth | 100,000 candles | 20,000 candles |
| Web requests per minute | 30 | 0, indicators cannot call out |
| Orders per minute | As declared, capped at 60 | Not applicable |
| Time per event | 5 s | 100 ms |
Going over a limit stops that one event and writes a line to the journal. It does not stop the bot unless it keeps happening.
Execution is deterministic. The same bytecode, the same inputs and the same candles produce the same trades every time. rand() is seeded per instance and the seed is recorded, so a backtest reproduces a live run exactly rather than approximately.
Every bot has its own journal. Everything it logs, every order it sent and every error it hit is there, stamped with the file and line it came from. A failure in the cloud three weeks from now still reports ema-cross.tide:42:11, because source positions survive every stage of compilation.
// not this
log.info("here")
// this
log.info("Signal on " + symbol.name
+ " fast=" + text(round(fast[0], 5))
+ " slow=" + text(round(slow[0], 5))
+ " spread=" + text(symbol.spread)
+ " equity=" + money(account.equity))
fn onBar() {
heavyCalculation()
log.debug("Used " + text(runtime.steps) + " steps in "
+ text(runtime.elapsed) + "ms")
}
if bot.isTesting {
log.debug("Backtest run, skipping the notification")
} else {
notify("Filled", symbol.name)
}
bot.id bot.name bot.version bot.magic
bot.isTesting bot.isOptimising
lastError clearError()
tester.stat("profitFactor")
You upload an .etb, fill in its inputs and switch it on. It keeps trading with your computer off. There is no VPS to rent and no terminal to leave running.
Bots run in a separate worker pool inside the eTrader backend, never in the process that carries the price feed and the trading engine. A runaway loop in one trader's bot cannot cost anybody else a tick. A worker that goes over its budget is killed and its bots are moved; the bot that caused it is journalled, and after three strikes it is paused and the trader is told.
An .eti draws on a chart, so it runs where the chart is: in the eTrader desktop terminal for macOS.
| Piece | What it is | Runs |
|---|---|---|
| eTrader Code | The application you write Tide in, with an editor, a strategy tester, an optimiser and a debugger | macOS |
An .etb bot | A compiled robot | In eTrader's cloud, with your app closed |
An .eti indicator | A compiled chart indicator | On the chart, in the desktop terminal for macOS and in the web terminal |
| The web terminal | The browser terminal at etraderweb.com/terminal | Takes both file types. Its twelve built-in indicators are still there, and your own .eti indicators sit alongside them |
Bots and indicators are managed from the Files menu, which holds both: Indicators for your .eti files and Bots for your .etb files and the bots configured from them. Drag a file in, review the permissions it asks for, set its inputs and switch it on. Uploading a newer version of something you already have offers to replace it, keeping your settings and your running instances.
An indicator you add appears in the chart's own Indicators list under My Indicators, beside the built-in ones, with the same switch, the same eye and the same settings sheet. Its Inputs tab is generated from the inputs you declared; its Style tab lets whoever is using it recolour, rethicken, restyle or hide any plot you drew, without touching your source.
bot indicator library script permissions input group plot buffer panel
let var const fn return if else while for in match once every atNewBar guard
try catch import as pub struct enum true false none now and or not
break continue
Type names are reserved too: int num bool text time duration price lots color series array map secret model symbol timeframe source select file status void.
The readable summary. The parser carries the normative version.
program = programDecl , { permissions | importDecl | inputDecl | declaration } ;
programDecl = ("bot"|"indicator"|"library"|"script") , string , [ metaBlock ] ;
metaBlock = "{" , { ident , metaValue } , "}" ;
permissions = "permissions" , "{" , { permEntry } , "}" ;
inputDecl = "input" , ( "group" , string , [ objectLit ]
| type , ident , [ "=" , expr ] , [ objectLit ] ) ;
declaration = varDecl | fnDecl | structDecl | enumDecl
| plotDecl | bufferDecl | panelDecl ;
varDecl = ("let"|"var"|"const") , ident , [ ":" , type ] , "=" , expr ;
fnDecl = [ "pub" ] , "fn" , ident , "(" , [ params ] , ")" ,
[ "->" , type ] , body ;
body = block | "=>" , expr ;
type = ident , [ "<" , type , { "," , type } , ">" ] , [ "?" ] ;
expr = assignment ;
assignment = ternary , [ assignOp , assignment ] ;
ternary = pipe , [ "?" , expr , ":" , expr ] ;
pipe = orExpr , { "|>" , call } ;
orExpr = andExpr , { "||" , andExpr } ;
andExpr = equality , { "&&" , equality } ;
equality = comparison , { ("=="|"!=") , comparison } ;
comparison = range , { ("<"|"<="|">"|">=") , range } ;
range = additive , [ (".."|"..=") , additive ] ;
additive = multiplicative , { ("+"|"-"|"??") , multiplicative } ;
multiplicative = power , { ("*"|"/"|"%") , power } ;
power = unary , [ "**" , power ] ;
unary = [ "!" | "-" ] , postfix ;
postfix = primary , { "." ident | "[" expr "]" | "(" [ args ] ")" } ;
primary = literal | ident | "(" expr ")" | arrayLit | mapLit
| structLit | ifExpr | matchExpr ;
args = arg , { "," , arg } ;
arg = [ ident , ":" ] , expr ;
source.tide
| lexer tokens, each carrying its position in the file
v
AST recursive descent, with Pratt parsing for expressions
| resolver scopes, imports, name binding
v
typed AST type checking, series lifting, input schema
| lowering desugars every, once, atNewBar, guard, match, pipes
v
IR constant folding, dead code removal
| codegen
v
bytecode a stack machine, around 130 opcodes, plus a line table
| package
v
.etb / .eti manifest, inputs, permissions, code, resources, signature
Every stage keeps the source position, which is why an error in the cloud weeks later still names the file, the line and the column.
Some things are missing on purpose. Each one has a reason and, where a strategy genuinely needs the capability, a replacement.
| Not available | Why | Use instead |
|---|---|---|
| Raw sockets | Not next to a live trading engine | http.* to declared hosts |
| File and folder access | A shared cloud runtime would leak between tenants | store and globals |
| Loading a native library | Arbitrary native code in the cloud | Nothing. This one stays closed |
| GPU and graphics APIs | There is no GPU in the runtime | Nothing |
| A SQL database | A foot-gun inside a trading strategy | store |
| Pointers, manual memory | The virtual machine manages memory | Nothing to manage |
| An economic calendar | No calendar feed is wired in yet | Planned |
| ONNX and Python bridges | Covered by a simpler route | ai.* |
Nothing in this table blocks a strategy. Where a real one needs the capability, Tide gets its own version of it rather than a port of somebody else's.
Tide is being built now. This page describes the language as designed and as it is being implemented, so parts of it will change before the first release. Nothing here is available to download today.
If you write trading robots and want to be told when Tide opens, write to office@sghk.org and say so.
eTrader is a trading-technology platform, licensed by SGHK Softwares Limited (Hong Kong). It is software: not a broker, not an exchange and not a financial adviser, and it never holds client money. Trading accounts and order execution are provided by the independent brokers you connect to. Trading leveraged products carries a high risk of losing money rapidly. A trading robot does not reduce that risk and can lose money faster than you would by hand. Nothing on this page is investment advice.