Custom node
If a node is missing, build it yourself. A custom node has inputs on the left of the card, settings you change on the board and outputs on the right, each with a formula. You don’t write code: the formula is a calculation such as ema(price, 20) or rsi(price, 14) < 30.
Custom nodes compute exactly like the official ones: the same in tests and live, and never with prices from the future. They sit in the node panel under Custom nodes, connect like any other node and can be shared as a file.
Custom nodes come with Pro. On Free you can still build them and test them in the replay; a chain with a custom node can only be activated with Pro, see Plans.
In six steps
Section titled “In six steps”-
Open the node panel on the right and click New node under Custom nodes.
-
Pick an example such as Distance to EMA, or Start empty. An example is the fastest way: it is complete and easy to adapt.
-
Under Name and look, give the node a name, a short description for the panel and an icon.
-
Add the inputs. Each has a name shown on the card and a type: Price series (such as the price of a candle), Number (such as an RSI or the volume) or Signal (true or false). The colored field next to it, such as
price, is the name in formulas. -
Add settings if something should be adjustable on the board, such as the number of candles with default 20, from 2 to 500.
-
Add the outputs and write a formula for each. Names and functions are inserted with one click. On the right you see how the card looks on the board and a trial run over 500 hours of BTC/USDT. Save and add to board places the node on the board, and from then on it is under Custom nodes.
Errors appear in red right at the field, and the spot in the formula is marked, for example emaa(price, 20) with the hint “Perhaps you mean ema(…)”. You can save once everything is valid. Yellow notes, such as an input no formula reads, don’t block anything.
Writing formulas
Section titled “Writing formulas”A formula computes on every candle. price - ema(price, 20) gives the distance of the price to its EMA 20 on every candle. What you need:
| How to write it | Meaning |
|---|---|
+ - * / % ^ |
calculate; ×, ÷ and − work too |
> < >= <= == != |
compare; the result is a signal |
and, or, not |
combine signals |
condition ? a : b |
a or b per candle, such as close > open ? high : low |
price[1] |
the value one candle earlier, price[4] four candles earlier |
1.5, 0.02 |
decimals with a point |
Good to know:
- Signal output: the formula must give true or false, so it usually contains a comparison, such as
rsi(price, 14) < 30. - Lengths are fixed values: a number, a setting or a calculation from them, such as
ema(price, length * 2). 1 to 1,000 candles are allowed. A setting used as a length therefore needs a minimum and a maximum. - Later outputs read earlier ones: in RSI zones the first output computes
value = rsi(price, length), the next two only comparevalue < lowandvalue > high. - Empty values at the start: an EMA 200 only has a value after 200 candles. Before that the result is empty and every signal false. The replay loads these candles beforehand, so you won’t notice.
nz(value, 0)puts in a number where needed.
All functions
Section titled “All functions”Write functions in lowercase, EMA works too. The editor shows the same list under Functions at the formula field.
Averages
| Function | What it does | Example |
|---|---|---|
sma(value, candles) | Simple average: every candle counts the same. | sma(price, 50) |
ema(value, candles) | Exponential average: recent candles count more. | ema(price, 200) |
wma(value, candles) | Weighted average: the weight drops evenly. | wma(price, 20) |
rma(value, candles) | Wilder’s smoothed average, as used in the RSI and the ATR. | rma(price, 14) |
hma(value, candles) | Hull average: fast and still smooth. | hma(price, 21) |
Indicators
| Function | What it does | Example |
|---|---|---|
rsi(value, candles) | Relative strength from 0 to 100. | rsi(price, 14) < 30 |
stdev(value, candles) | Standard deviation: how much the value swings. | stdev(price, 20) |
highest(value, candles) | Highest value of the last candles, including the current one. | price > highest(price, 20)[1] |
lowest(value, candles) | Lowest value of the last candles, including the current one. | lowest(price, 20) |
sum(value, candles) | Sum of the last candles. | sum(volume, 24) |
change(value, candles = 1) | Difference from the value that many candles ago. | change(price, 4) |
roc(value, candles = 1) | Change in percent compared with the value that many candles ago. | roc(price, 24) < -5 |
prev(value, candles = 1) | The value that many candles ago. Short form: value[1]. | price > prev(price) |
atr(high, low, close, candles) | Average range of a candle, Average True Range. | atr(high, low, close, 14) |
tr(high, low, close) | Range of a candle including the gap to the previous close, True Range. | tr(high, low, close) |
stoch(value, high, low, candles) | Where the value sits within the range of the last candles, from 0 (lowest low) to 100 (highest high); empty when high and low are equal. | sma(stoch(close, high, low, 14), 3) < 20 |
cci(value, candles) | Distance from the average relative to the mean deviation; above 100 unusually strong, below −100 unusually weak. | cci((high + low + close) / 3, 20) > 100 |
dev(value, candles) | Mean deviation of the last values from their average. | dev(price, 20) |
wpr(high, low, close, candles) | Williams %R from −100 (close at the lowest low) to 0 (at the highest high) of the last candles; empty when high and low are equal. | wpr(high, low, close, 14) < -80 |
mfi(value, volume, candles) | Money Flow Index from 0 to 100: like the RSI, but weighted by volume; below 20 counts as oversold. | mfi((high + low + close) / 3, volume, 14) < 20 |
adx(high, low, close, candles, smoothing = candles) | Trend strength from 0 to 100, whatever the direction; above 25 counts as a strong trend. | adx(high, low, close, 14) > 25 |
diplus(high, low, close, candles) | Strength of the upward movement (+DI) from 0 to 100. | diplus(high, low, close, 14) > diminus(high, low, close, 14) |
diminus(high, low, close, candles) | Strength of the downward movement (−DI) from 0 to 100. | crossover(diminus(high, low, close, 14), diplus(high, low, close, 14)) |
linreg(value, candles, back = 0) | Linear regression: the value of the best-fit line through the last candles, at the current one or that many candles before. | linreg(price, 20) > linreg(price, 20)[1] |
highestbars(value, candles) | How many candles ago the highest value of the last candles was, 0 on the current one; with equal values the most recent counts. | 100 * (14 - highestbars(high, 15)) / 14 |
lowestbars(value, candles) | How many candles ago the lowest value of the last candles was, 0 on the current one; with equal values the most recent counts. | lowestbars(low, 20) == 0 |
Math
| Function | What it does | Example |
|---|---|---|
abs(value) | Absolute value: makes negative numbers positive. | abs(change(price)) |
min(a, b, …) | The smallest of the values. | min(open, close) |
max(a, b, …) | The largest of the values. | max(open, close) |
avg(a, b, …) | Average of the values on the same candle. | avg(high, low, close) |
sqrt(value) | Square root; empty for negative values. | sqrt(abs(change(price))) |
pow(value, exponent) | Power, like value ^ exponent. | pow(change(price), 2) |
log(value) | Natural logarithm; empty for 0 and negative values. | log(price / prev(price)) |
exp(value) | e to the power of the value. | exp(1) |
round(value, digits = 0) | Rounds to that many decimal places. | round(rsi(price, 14), 1) |
floor(value) | Rounds down to a whole number. | floor(price / 1000) * 1000 |
ceil(value) | Rounds up to a whole number. | ceil(price / 1000) * 1000 |
sign(value) | 1 for positive, -1 for negative values, otherwise 0. | sign(change(price)) |
clamp(value, low, high) | Limits the value to a range. | clamp(rsi(price, 14), 20, 80) |
nz(value, fallback = 0) | Uses the fallback while the value is still empty, e.g. during warm-up. | nz(volume) |
Signals
| Function | What it does | Example |
|---|---|---|
crossover(a, b) | True on the candle where a rises from below to above b. | crossover(ema(price, 50), ema(price, 200)) |
crossunder(a, b) | True on the candle where a falls from above to below b. | crossunder(price, sma(price, 200)) |
cross(a, b) | True on the candle where a crosses line b in either direction. | cross(price, 100000) |
rising(value, candles = 1) | True when the value is higher than on each of the candles before. | rising(volume, 3) |
falling(value, candles = 1) | True when the value is lower than on each of the candles before. | falling(price, 3) |
holds(signal, candles) | True when the signal has been true for that many candles in a row. | holds(price < ema(price, 200), 3) |
count(signal, candles) | How often the signal was true in the last candles. | count(close > open, 5) >= 4 |
barssince(signal) | Candles since the signal was last true; empty while it has never been true. | barssince(crossover(price, ema(price, 50))) <= 3 |
if(condition, then, else) | Picks one of two values per candle, like condition ? then : else. | if(close > open, high, low) |
Examples
Section titled “Examples”These examples are available in the editor under New node. You can also copy the JSON and paste it under Import.
Distance to EMA
Section titled “Distance to EMA”How far the price is above or below its average, in percent.
- Inputs:Price (price series, price)
- Settings:Candles length = 50
- Outputs:
- Distance (number):
(price - ema(price, length)) / ema(price, length) * 100
- Distance (number):
As JSON to import
{ "format": 1, "id": "custom.abstand-zum-ema", "version": "1.0.0", "label": { "de": "Abstand zum EMA", "en": "Distance to EMA" }, "description": { "de": "Abstand des Kurses zum EMA in %", "en": "Distance of the price to the EMA in %" }, "help": { "de": "Rechnet aus, wie viel Prozent der Kurs über (positiv) oder unter (negativ) seinem exponentiellen Durchschnitt liegt. Beispiel: Mit 50 Kerzen und „unter -8“ meldet sich ein Alarm, wenn der Kurs ungewöhnlich weit unter den Trend gefallen ist.", "en": "Calculates how many percent the price is above (positive) or below (negative) its exponential average. Example: with 50 candles and “below -8” an alert fires when the price has fallen unusually far below the trend." }, "icon": "ruler", "sentence": { "de": "EMA {length}", "en": "EMA {length}" }, "inputs": [ { "id": "price", "type": "price", "label": { "de": "Kurs", "en": "Price" } } ], "settings": [ { "id": "length", "label": { "de": "Kerzen", "en": "Candles" }, "help": { "de": "Über wie viele Kerzen der EMA rechnet, etwa 50 oder 200.", "en": "How many candles the EMA covers, such as 50 or 200." }, "default": 50, "min": 2, "max": 500, "integer": true } ], "outputs": [ { "id": "out", "type": "number", "unit": "percent", "label": { "de": "Abstand", "en": "Distance" }, "formula": "(price - ema(price, length)) / ema(price, length) * 100" } ]}RSI zones
Section titled “RSI zones”Two signals from one node: oversold and overbought, with your own limits.
- Inputs:Price (price series, price)
- Settings:Candles length = 14, Low low = 30, High high = 70
- Outputs:
- RSI (number):
rsi(price, length) - oversold (signal):
value < low - overbought (signal):
value > high
- RSI (number):
As JSON to import
{ "format": 1, "id": "custom.rsi-zonen", "version": "1.0.0", "label": { "de": "RSI-Zonen", "en": "RSI zones" }, "description": { "de": "Überverkauft und überkauft als Signal", "en": "Oversold and overbought as signals" }, "icon": "gauge", "sentence": { "de": "unter {low}, über {high}", "en": "below {low}, above {high}" }, "inputs": [ { "id": "price", "type": "price", "label": { "de": "Kurs", "en": "Price" } } ], "settings": [ { "id": "length", "label": { "de": "Kerzen", "en": "Candles" }, "default": 14, "min": 2, "max": 100, "integer": true }, { "id": "low", "label": { "de": "Unten", "en": "Low" }, "default": 30, "min": 0, "max": 100 }, { "id": "high", "label": { "de": "Oben", "en": "High" }, "default": 70, "min": 0, "max": 100 } ], "outputs": [ { "id": "value", "type": "number", "label": { "de": "RSI", "en": "RSI" }, "formula": "rsi(price, length)" }, { "id": "oversold", "type": "signal", "label": { "de": "überverkauft", "en": "oversold" }, "formula": "value < low" }, { "id": "overbought", "type": "signal", "label": { "de": "überkauft", "en": "overbought" }, "formula": "value > high" } ]}Golden cross
Section titled “Golden cross”The fast average crosses above the slow one, classically 50 over 200.
- Inputs:Price (price series, price)
- Settings:Fast fast = 50, Slow slow = 200
- Outputs:
- Cross (signal):
crossover(ema(price, fast), ema(price, slow))
- Cross (signal):
As JSON to import
{ "format": 1, "id": "custom.golden-cross", "version": "1.0.0", "label": { "de": "Golden Cross", "en": "Golden cross" }, "description": { "de": "Schneller EMA kreuzt langsamen nach oben", "en": "Fast EMA crosses above the slow one" }, "icon": "trending-up", "sentence": { "de": "EMA {fast} über {slow}", "en": "EMA {fast} over {slow}" }, "inputs": [ { "id": "price", "type": "price", "label": { "de": "Kurs", "en": "Price" } } ], "settings": [ { "id": "fast", "label": { "de": "Schnell", "en": "Fast" }, "default": 50, "min": 2, "max": 500, "integer": true }, { "id": "slow", "label": { "de": "Langsam", "en": "Slow" }, "default": 200, "min": 2, "max": 500, "integer": true } ], "outputs": [ { "id": "out", "type": "signal", "label": { "de": "Kreuz", "en": "Cross" }, "formula": "crossover(ema(price, fast), ema(price, slow))" } ]}Volume spike
Section titled “Volume spike”A candle with a multiple of the usual volume, as with news.
- Inputs:Volume (number, volume)
- Settings:Candles length = 20, Factor factor = 2.5
- Outputs:
- Spike (signal):
volume > sma(volume, length) * factor
- Spike (signal):
As JSON to import
{ "format": 1, "id": "custom.volumen-spitze", "version": "1.0.0", "label": { "de": "Volumen-Spitze", "en": "Volume spike" }, "description": { "de": "Volumen über dem Vielfachen des Schnitts", "en": "Volume above a multiple of its average" }, "icon": "chart-column", "sentence": { "de": "{factor} × Ø {length}", "en": "{factor} × avg {length}" }, "inputs": [ { "id": "volume", "type": "number", "label": { "de": "Volumen", "en": "Volume" } } ], "settings": [ { "id": "length", "label": { "de": "Kerzen", "en": "Candles" }, "default": 20, "min": 2, "max": 500, "integer": true }, { "id": "factor", "label": { "de": "Faktor", "en": "Factor" }, "default": 2.5, "min": 1, "max": 20, "step": 0.5 } ], "outputs": [ { "id": "out", "type": "signal", "label": { "de": "Spitze", "en": "Spike" }, "formula": "volume > sma(volume, length) * factor" } ]}Candle body
Section titled “Candle body”How large a candle’s body is, in percent; large bodies show strong moves.
- Inputs:Open (price series, open), Close (price series, close)
- Settings:none
- Outputs:
- Body (number):
(close - open) / open * 100 - green (signal):
close > open
- Body (number):
As JSON to import
{ "format": 1, "id": "custom.kerzenkoerper", "version": "1.0.0", "label": { "de": "Kerzenkörper", "en": "Candle body" }, "description": { "de": "Größe des Kerzenkörpers in %", "en": "Size of the candle body in %" }, "icon": "chart-candlestick", "inputs": [ { "id": "open", "type": "price", "label": { "de": "Eröffnung", "en": "Open" } }, { "id": "close", "type": "price", "label": { "de": "Schluss", "en": "Close" } } ], "outputs": [ { "id": "body", "type": "number", "unit": "percent", "label": { "de": "Körper", "en": "Body" }, "formula": "(close - open) / open * 100" }, { "id": "green", "type": "signal", "label": { "de": "grün", "en": "green" }, "formula": "close > open" } ]}Green streak
Section titled “Green streak”Several green candles in a row, a sign of steady buying pressure.
- Inputs:Open (price series, open), Close (price series, close)
- Settings:Candles length = 3
- Outputs:
- Streak (signal):
holds(close > open, length)
- Streak (signal):
As JSON to import
{ "format": 1, "id": "custom.gruene-serie", "version": "1.0.0", "label": { "de": "Grüne Serie", "en": "Green streak" }, "description": { "de": "Mehrere grüne Kerzen in Folge", "en": "Several green candles in a row" }, "icon": "repeat", "sentence": { "de": "{length} Kerzen in Folge", "en": "{length} candles in a row" }, "inputs": [ { "id": "open", "type": "price", "label": { "de": "Eröffnung", "en": "Open" } }, { "id": "close", "type": "price", "label": { "de": "Schluss", "en": "Close" } } ], "settings": [ { "id": "length", "label": { "de": "Kerzen", "en": "Candles" }, "default": 3, "min": 2, "max": 20, "integer": true } ], "outputs": [ { "id": "out", "type": "signal", "label": { "de": "Serie", "en": "Streak" }, "formula": "holds(close > open, length)" } ]}Saving a selection as a node
Section titled “Saving a selection as a node”If you already built a chain, you can turn part of it into a node without writing a formula:
-
Select the nodes on the board: click them one after another while holding ⌘ (Mac) or Ctrl, or hold Shift and drag a frame around them.
-
On the right under Properties, click Save as node.
-
The dialog shows the new node with formulas already written. What leads into the selection becomes an input, what leads out an output. Give it a name and click Save and replace.
The selection is replaced by the new node, connected as before. It computes exactly like the nodes did; the BTC Dip Buyer, for example, becomes rsi(price, 14) < 35 and price > ema(price, 200). Sources and actions stay on the board. Nodes that remember something across candles (Cooldown), depend on their whole history (Supertrend, OBV) or work with the time of day and calendar days (Time window, Pivot points, VWAP, High/Low with previous day or week) cannot be translated; the editor tells you which one it is.
Editing and deleting
Section titled “Editing and deleting”- Edit: the pencil next to the node in the node panel, or click the node on the board and choose Edit node under Properties.
- Save raises the version, for example from 1.0.0 to 1.1.0. The open chain computes with the new version right away. Other chains keep their version until you place the node there again.
- Delete in the dialog removes the node from Custom nodes. Chains that use it keep their copy and keep computing.
Sharing and importing
Section titled “Sharing and importing”- Export in the dialog saves the node as a file, such as
abstand-zum-ema.bellchain-node.json. You can pass the file on, for example in a chat. - Import in the editor’s top bar accepts chains and custom nodes. A node lands under Custom nodes.
- A chain carries its custom nodes. Whoever imports your chain gets the nodes too, without the files.
- Under JSON in the dialog you see the node as text, as it is in the file. What you change there shows up under Design right away, and the other way round.
With an account, your custom nodes and favorites are stored in your account and available on every device. Without an account this browser keeps them; after you sign in they move to your account.
Custom nodes are safe because they contain no code. Bellchain checks every formula before it computes: only known functions, fixed lengths, no loops, no access to the clock or the network. The format in detail is described under Node specification.
Limits
Section titled “Limits”| What | Limit |
|---|---|
| Inputs, settings, outputs | 8 each |
| Formula | 1,000 characters |
| Length in candles | 1 to 1,000 |
| Custom nodes per chain | 20 |
| Custom nodes in an account | 100 |
| Favorites | 50 |
Version 2026.10.21.23 · Changelog