oakscriptjs 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +48 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +42 -12
- package/dist/cjs/script/index.js.map +2 -2
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +57 -12
- package/dist/esm/script/index.js.map +1 -1
- package/package.json +3 -1
- package/skills/oakscript/SKILL.md +162 -0
- package/skills/oakscript/examples/ma-cross-strategy.js +57 -0
- package/skills/oakscript/examples/ma-cross.js +34 -0
- package/skills/oakscript/examples/macd-oscillator.js +29 -0
- package/skills/oakscript/examples/mtf-trend-filter.js +46 -0
- package/skills/oakscript/reference/errors.md +68 -0
- package/skills/oakscript/reference/pine-differences.md +84 -0
- package/skills/oakscript/reference/plotting.md +193 -0
- package/skills/oakscript/reference/script-structure.md +182 -0
- package/skills/oakscript/reference/strategies.md +140 -0
- package/skills/oakscript/reference/timeframes-symbols.md +93 -0
- package/src/index.ts +1 -1
- package/src/script/index.ts +50 -13
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Moving-average cross on the price pane: two averages, a fill between them,
|
|
2
|
+
// markers on the crosses and two alert conditions.
|
|
3
|
+
indicator("MA Cross", { shorttitle: "MA X", overlay: true });
|
|
4
|
+
|
|
5
|
+
const src = input.source("close", "Source");
|
|
6
|
+
const fastLength = input.int(9, "Fast length", { minval: 1 });
|
|
7
|
+
const slowLength = input.int(21, "Slow length", { minval: 1 });
|
|
8
|
+
const maType = input.string("EMA", "MA type", { options: ["SMA", "EMA", "WMA"] });
|
|
9
|
+
|
|
10
|
+
// Inputs are plain values: a JS `if` on them is fine.
|
|
11
|
+
function ma(source, length) {
|
|
12
|
+
if (maType === "SMA") return ta.sma(source, length);
|
|
13
|
+
if (maType === "WMA") return ta.wma(source, length);
|
|
14
|
+
return ta.ema(source, length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fast = ma(src, fastLength);
|
|
18
|
+
const slow = ma(src, slowLength);
|
|
19
|
+
const bullish = fast.gt(slow); // Series of 1 / 0: fast > slow
|
|
20
|
+
|
|
21
|
+
const fastPlot = plot(fast, "Fast", { color: color.teal, linewidth: 2 });
|
|
22
|
+
const slowPlot = plot(slow, "Slow", { color: color.orange, linewidth: 2 });
|
|
23
|
+
fill(fastPlot, slowPlot, {
|
|
24
|
+
color: color.when(bullish, color.new(color.teal, 85), color.new(color.red, 85)),
|
|
25
|
+
title: "Trend fill",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const crossUp = ta.crossover(fast, slow);
|
|
29
|
+
const crossDown = ta.crossunder(fast, slow);
|
|
30
|
+
plotshape(crossUp, "Cross up", { style: "triangleup", location: "belowbar", color: color.teal, size: "small" });
|
|
31
|
+
plotshape(crossDown, "Cross down", { style: "triangledown", location: "abovebar", color: color.red, size: "small" });
|
|
32
|
+
|
|
33
|
+
alertcondition(crossUp, "Cross up", "Fast MA crossed above the slow MA");
|
|
34
|
+
alertcondition(crossDown, "Cross down", "Fast MA crossed below the slow MA");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// MACD in its own pane: a four-colour histogram, the MACD and signal lines,
|
|
2
|
+
// a zero level and a background on the signal crosses.
|
|
3
|
+
indicator("MACD Oscillator", { shorttitle: "MACD", overlay: false, precision: 4 });
|
|
4
|
+
|
|
5
|
+
const src = input.source("close", "Source");
|
|
6
|
+
const fastLength = input.int(12, "Fast length", { minval: 1 });
|
|
7
|
+
const slowLength = input.int(26, "Slow length", { minval: 1 });
|
|
8
|
+
const signalLength = input.int(9, "Signal smoothing", { minval: 1, maxval: 50 });
|
|
9
|
+
|
|
10
|
+
// A tuple comes back as an array: destructure it.
|
|
11
|
+
const [macdLine, signalLine, hist] = ta.macd(src, fastLength, slowLength, signalLength);
|
|
12
|
+
|
|
13
|
+
// Four colours: above / below zero, growing / falling. color.when() takes two colours,
|
|
14
|
+
// so build the per-bar array in a bar loop (one entry per bar, undefined = na).
|
|
15
|
+
const histColors = [];
|
|
16
|
+
eachBar((c) => {
|
|
17
|
+
const h = c.get(hist);
|
|
18
|
+
const previous = c.get(hist, 1);
|
|
19
|
+
if (Number.isNaN(h)) histColors.push(undefined);
|
|
20
|
+
else if (h >= 0) histColors.push(h > previous ? "#26A69A" : "#B2DFDB");
|
|
21
|
+
else histColors.push(h < previous ? "#FF5252" : "#FFCDD2");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
plot(hist, "Histogram", { style: plot.style_columns, color: histColors });
|
|
25
|
+
plot(macdLine, "MACD", { color: color.blue, linewidth: 2 });
|
|
26
|
+
plot(signalLine, "Signal", { color: color.orange });
|
|
27
|
+
hline(0, "Zero", { color: color.gray, linestyle: hline.style_dotted });
|
|
28
|
+
|
|
29
|
+
bgcolor(color.when(ta.cross(macdLine, signalLine), color.new(color.gray, 85)), { title: "Signal cross" });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Multi-timeframe filter: a higher-timeframe EMA decides the trend, and a cross of the
|
|
2
|
+
// chart EMA gives a signal only when it agrees with that trend, once per trend.
|
|
3
|
+
indicator("MTF Trend Filter", { shorttitle: "MTF", overlay: true });
|
|
4
|
+
|
|
5
|
+
const filterTf = input.timeframe("D", "Filter timeframe");
|
|
6
|
+
const filterLength = input.int(20, "Filter EMA length", { minval: 1 });
|
|
7
|
+
const signalLength = input.int(10, "Signal EMA length", { minval: 1 });
|
|
8
|
+
|
|
9
|
+
// The expression runs on the higher-timeframe bars: compute every series INSIDE it.
|
|
10
|
+
// Returning an array gives one series per element (a PineScript tuple).
|
|
11
|
+
const [htfEma, htfClose] = request.security("", filterTf, () => [ta.ema(close, filterLength), close]);
|
|
12
|
+
const upTrend = htfClose.gt(htfEma);
|
|
13
|
+
|
|
14
|
+
const signalEma = ta.ema(close, signalLength);
|
|
15
|
+
plot(htfEma, "Filter EMA", { color: color.gray, linewidth: 2, style: plot.style_stepline });
|
|
16
|
+
plot(signalEma, "Signal EMA", { color: color.when(upTrend, color.teal, color.red) });
|
|
17
|
+
|
|
18
|
+
const crossUp = ta.crossover(close, signalEma);
|
|
19
|
+
const crossDown = ta.crossunder(close, signalEma);
|
|
20
|
+
|
|
21
|
+
// Stateful logic (PineScript `var` + `:=`): a closure variable in a bar loop.
|
|
22
|
+
let lastSignal = 0; // var int lastSignal = 0
|
|
23
|
+
const signal = eachBar((c) => {
|
|
24
|
+
const up = c.get(upTrend) === 1;
|
|
25
|
+
if (c.get(crossUp) === 1 && up && lastSignal !== 1) {
|
|
26
|
+
lastSignal = 1;
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
if (c.get(crossDown) === 1 && !up && lastSignal !== -1) {
|
|
30
|
+
lastSignal = -1;
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
plotshape(signal.eq(1), "Long", { style: "labelup", location: "belowbar", color: color.teal, text: "L", textcolor: color.white });
|
|
37
|
+
plotshape(signal.eq(-1), "Short", { style: "labeldown", location: "abovebar", color: color.red, text: "S", textcolor: color.white });
|
|
38
|
+
bgcolor(color.when(upTrend, color.new(color.teal, 92), color.new(color.red, 92)), { title: "Trend" });
|
|
39
|
+
|
|
40
|
+
// One label on the last bar with the filter state.
|
|
41
|
+
eachBar((c) => {
|
|
42
|
+
if (c.i === c.n - 1) {
|
|
43
|
+
const text = (c.get(upTrend) === 1 ? "Uptrend " : "Downtrend ") + filterTf;
|
|
44
|
+
label.new(c.i, c.high, text, "bar_index", "price", color.gray, "label_down", color.white);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Errors and how to read them
|
|
2
|
+
|
|
3
|
+
A script fails in one of three ways: it does not compile, it stops with a runtime error, or it runs and gives a
|
|
4
|
+
wrong output. Fix them in that order, then check the values.
|
|
5
|
+
|
|
6
|
+
## Compile errors (syntax)
|
|
7
|
+
|
|
8
|
+
The source is loaded as JavaScript: a syntax error comes with a line and a column of your source.
|
|
9
|
+
|
|
10
|
+
| Message | Cause | Fix |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `Missing initializer in const declaration`, `Unexpected token ':'` | a TypeScript annotation (`const a: number = 5`) | remove the types: the script is JavaScript |
|
|
13
|
+
| `Unexpected identifier`, `Unexpected token '='` | Pine syntax: `x := 1`, `var x = 0`, `a = b ? c : d` on one line without `const`, `=>` functions, named arguments `f(a, length = 5)` | JS syntax: `x = 1`, `let x = 0`, `const a = ...`, `function f() {}`, options objects |
|
|
14
|
+
| `Unexpected token 'export'` / `'import'` at a wrong place | an import below other code, an export | imports first, no exports |
|
|
15
|
+
|
|
16
|
+
A host that compiles a script also runs it once on zero bars (a declaration pass: title, inputs, plots,
|
|
17
|
+
strategy properties). Errors in the declarations or in top-level code show up there, with the line.
|
|
18
|
+
|
|
19
|
+
## Runtime errors
|
|
20
|
+
|
|
21
|
+
The run stops at the first error; the message names the call. The ones you will meet:
|
|
22
|
+
|
|
23
|
+
| Message | Cause | Fix |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `TypeError: series.toArray is not a function` | a number or NaN passed where a Series is expected: `plot(close - open)`, `plot(5)`, `ta.sma(a * 2, 10)` | Series methods: `close.sub(open)`, `a.mul(2)`; a constant level: `hline(5)` |
|
|
26
|
+
| `TypeError: ta.xyz is not a function` / `xyz is not defined` | a name that does not exist (typo, Pine-only name such as `shape`, `location`, `size`, `xloc`, `bar_index`) | [pine-differences.md](pine-differences.md): strings for the constants, `c.i` for `bar_index` |
|
|
27
|
+
| `TypeError: Cannot read properties of undefined (reading 'toArray')` | a missing Series argument, e.g. `ta.vwma(close, 20)` | pass it: `ta.vwma(close, 20, volume)` |
|
|
28
|
+
| `Error on bar N: Invalid value of the 'length' argument (0.0) in the 'sma' function. It must be > 0.` | a length of 0, negative or NaN (often an input) | `minval: 1` on the input, or guard the value |
|
|
29
|
+
| `Error on bar N: The 'sma' function references too many historical candles (6000), the limit is 5000.` | a window longer than 5000 bars | a shorter length |
|
|
30
|
+
| `Error on bar N: The script attempts to reference historical data that is too far from the current bar (K bars back)...` | `x.offset(k)` / `c.get(x, k)` with k above 5000, or a history that keeps growing (`c.get(x, c.i)`) | bounded offsets |
|
|
31
|
+
| `c.get(): the offset cannot be negative` / `Series.offset(-1)...` | reading a future bar | offsets >= 0; for a left shift of a marker use `offset` of `plotshape` |
|
|
32
|
+
| `indicator(): the script already declared indicator(); a script has one declaration.` | two declarations | one `indicator()` or `strategy()` |
|
|
33
|
+
| `strategy.entry() must be called inside strategy.eachBar().` | an order at the top level | move it into `strategy.eachBar((c) => ...)` |
|
|
34
|
+
| `strategy.position_size must be read inside strategy.eachBar().` | a per-bar strategy value at the top level | read it in the loop; return it from the loop to plot it |
|
|
35
|
+
| `strategy.eachBar(): declare the script with strategy() first.` | `strategy.*` in an `indicator()` script | declare `strategy(...)` |
|
|
36
|
+
| `strategy.eachBar() runs once per script run.` | two strategy loops | one loop with all the logic |
|
|
37
|
+
| `strategy.X is not provided by the strategy engine.` | the host's engine lacks a variable or a risk rule | use another value, or ask the host |
|
|
38
|
+
| `request.security(): the expression returned a series computed on the chart bars...` | `request.security("", "D", () => myEma)` | compute inside: `() => ta.ema(close, 20)` |
|
|
39
|
+
| `request.security needs the bars of X (D): pass them with ChartContext.bars.` (`MissingSymbolDataError`) | another symbol whose bars the host has not loaded | an app loads them and runs again; in your own host give `bars()` |
|
|
40
|
+
| `Invalid symbol: X` | a symbol without bars | check the ticker id, or `ignore_invalid_symbol = true` |
|
|
41
|
+
| `Invalid timeframe "XYZ"` | a bad timeframe string | `"1"`, `"60"`, `"240"`, `"D"`, `"W"`, `"M"`... |
|
|
42
|
+
| `request.security_lower_tf: timeframe "D" is higher than the chart timeframe "60".` | `security_lower_tf` on a higher timeframe | `request.security` for higher timeframes |
|
|
43
|
+
| `The chart timeframe is not known: pass it as executeScript(body, bars, inputs, { timeframe }).` (also time zone, session, `syminfo.*`) | a direct `executeScript` call without the chart context | pass `{ timeframe, timezone, session, ... }`; an app gives them |
|
|
44
|
+
| `chart.point.now() needs a current bar: call it inside eachBar().` | a per-bar call at the top level | call it in a bar loop |
|
|
45
|
+
| `oakscriptjs/script functions can only run inside executeScript()` | a script API call outside a run (e.g. from a timer, or a Node module that calls `plot` at import) | only the host runs the body |
|
|
46
|
+
| `Script did not finish within Ns and was stopped.` (app) | an endless or very slow loop | bound the loops, compute Series outside the bar loops |
|
|
47
|
+
|
|
48
|
+
## Silent mistakes (no error, wrong output)
|
|
49
|
+
|
|
50
|
+
| Symptom | Cause | Fix |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| a branch taken on every bar, or never | `if (ta.crossover(a, b))`, `cond ? x : y`, `a > b` with Series at the top level: an object is always true, an object comparison always false | `cond.iff(x, y)`, `a.gt(b)`, or `c.get(cond) === 1` in a loop |
|
|
53
|
+
| one colour on every bar | `color: fast > slow ? color.green : color.red` | `color.when(fast.gt(slow), color.green, color.red)` |
|
|
54
|
+
| an empty plot | NaN on every bar: a `ta.*` Series returned from a bar loop, a division by zero, a NaN input | compute `ta.*` before the loop, guard divisions (`range.gt(0).iff(x.div(range), 0)`) |
|
|
55
|
+
| a plot that starts late | the warm-up of long lengths, or of a higher timeframe (20 daily bars on an hourly chart is 480 bars) | expected; shorter lengths or more bars |
|
|
56
|
+
| values that look like the future (backtests too good) | `barmerge.lookahead_on` on a series that is not offset | lookahead off, or `.offset(1)` inside the expression |
|
|
57
|
+
| a strategy without trades | conditions read as `c.get(cond)` on NaN-only Series, orders placed outside the conditions you expect, `pyramiding: 0` blocking a second entry in the same direction | plot the conditions (`plotshape`) and check them first |
|
|
58
|
+
| `na(series)` / `nz(series)` has no effect | they take numbers | in a loop on `c.get(x)`, or `x.iff(x, 0)` |
|
|
59
|
+
| an input never changes the output | the input value is not used, or its title changed (new id) | use the returned value; keep titles stable |
|
|
60
|
+
|
|
61
|
+
## Checking the output
|
|
62
|
+
|
|
63
|
+
After a clean run, read the values instead of trusting the code:
|
|
64
|
+
|
|
65
|
+
- the last values of each plot (`run.result.plots.plot0.slice(-5)` in a direct call; the app shows them in the
|
|
66
|
+
data window and the legend);
|
|
67
|
+
- the number of markers / labels (a signal on every bar, or none, is a bug);
|
|
68
|
+
- for a strategy, the trade count and a few trades (entry and exit bars and prices).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Pine Script v6 and OakScript
|
|
2
|
+
|
|
3
|
+
The names, arguments, defaults and results follow Pine Script v6 (the `ta.*`, `math.*`, `str.*`, `array.*`,
|
|
4
|
+
`matrix.*`, `map.*` and drawing functions are covered almost entirely). The differences come from JavaScript and
|
|
5
|
+
from the vectorized execution.
|
|
6
|
+
|
|
7
|
+
## Translation table
|
|
8
|
+
|
|
9
|
+
| Pine Script v6 | OakScript |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `//@version=6` | nothing |
|
|
12
|
+
| `indicator("T", overlay=true)` | `indicator("T", { overlay: true })` |
|
|
13
|
+
| `len = input.int(14, "Length", minval=1)` | `const len = input.int(14, "Length", { minval: 1 })` |
|
|
14
|
+
| `src = input.source(close, "Source")` | `const src = input.source("close", "Source")` (the default is a name) |
|
|
15
|
+
| `a + b`, `a - b`, `a * b`, `a / b` on series | `a.add(b)`, `a.sub(b)`, `a.mul(b)`, `a.div(b)` |
|
|
16
|
+
| `a > b`, `a == b`, `a and b`, `not a` on series | `a.gt(b)`, `a.eq(b)`, `a.and(b)`, `a.not()` |
|
|
17
|
+
| `cond ? x : y` on series | `cond.iff(x, y)` |
|
|
18
|
+
| `x[1]` | `x.offset(1)`; in a bar loop `c.get(x, 1)` |
|
|
19
|
+
| `na`, `na(x)`, `nz(x)` | `NaN`, `na(v)` / `nz(v)` on numbers; `x.iff(x, 0)` for `nz` on a Series |
|
|
20
|
+
| `var float s = na` + `s := ...` | `let s = NaN;` before `eachBar((c) => { s = ...; })` |
|
|
21
|
+
| `if close > open` (per bar) | in a bar loop: `if (c.close > c.open)` |
|
|
22
|
+
| `for i = 0 to n - 1` | `for (let i = 0; i < n; i++)` in a bar loop |
|
|
23
|
+
| `switch` / `if` returning a value | JS `switch` / `if` in a bar loop, or `iff` chains on Series |
|
|
24
|
+
| `bar_index`, `last_bar_index` | `c.i`, `c.n - 1` |
|
|
25
|
+
| `myFunc(x) => ...` | a JS `function`; on Series it composes Series methods |
|
|
26
|
+
| `[a, b] = f()` | `const [a, b] = f()` |
|
|
27
|
+
| `plot(x, "T", color=c, style=plot.style_columns)` | `plot(x, "T", { color: c, style: plot.style_columns })` |
|
|
28
|
+
| `plot(x, color = cond ? c1 : c2)` | `plot(x, "T", { color: color.when(cond, c1, c2) })` |
|
|
29
|
+
| `plotshape(cond, style=shape.triangleup, location=location.belowbar, size=size.small)` | `plotshape(cond, "T", { style: "triangleup", location: "belowbar", size: "small" })` |
|
|
30
|
+
| `label.new(bar_index, high, "A", style=label.style_label_down)` | `label.new(c.i, c.high, "A", undefined, undefined, undefined, "label_down")` (positional) |
|
|
31
|
+
| `line.new(x1, y1, x2, y2, extend=extend.right)` | `line.new(x1, y1, x2, y2, "bar_index", "right")` |
|
|
32
|
+
| `xloc.bar_time`, `yloc.abovebar`, `line.style_dashed`, `format.volume`, `display.none` | `"bar_time"`, `"abovebar"`, `"dashed"`, `"volume"`, `"none"` |
|
|
33
|
+
| `dayofweek.monday` | `2` (1 = Sunday ... 7 = Saturday) |
|
|
34
|
+
| `request.security(sym, "D", ta.ema(close, 20))` | `request.security(sym, "D", () => ta.ema(close, 20))` |
|
|
35
|
+
| `strategy.entry("L", strategy.long, when = cond)` | `strategy.eachBar((c) => { if (c.get(cond) === 1) strategy.entry("L", strategy.long); })` |
|
|
36
|
+
| `strategy.opentrades.entry_price(0)` | `strategy.opentrade(0)?.entry_price` |
|
|
37
|
+
| `strategy.closedtrades.profit(i)` | `strategy.closedtrade(i)?.profit` |
|
|
38
|
+
| `array.new_float(10, 0)`, `matrix.new<float>()`, `map.new<string, float>()` | `import { array, matrix, map } from "oakscriptjs";` then `array.new_float(10, 0)`, `matrix.new_matrix(...)`, `map.new_map()`; or plain JS arrays / `Map` |
|
|
39
|
+
| `log.info("x = " + str.tostring(x))` | not available: plot it with `display: "data_window"` or put it in a label |
|
|
40
|
+
|
|
41
|
+
Constants that **do** exist as namespaces: `color.*`, `plot.style_*`, `plot.linestyle_*`, `hline.style_*`,
|
|
42
|
+
`strategy.long / short / fixed / cash / percent_of_equity`, `strategy.commission.*`, `strategy.direction.*`,
|
|
43
|
+
`strategy.oca.*`, `barmerge.*`, `position.*`, `text.align_* / wrap_* / format_*`, `alert.freq_*`,
|
|
44
|
+
`session.regular / extended`, `math.pi / e / phi / rphi`.
|
|
45
|
+
|
|
46
|
+
## Execution differences
|
|
47
|
+
|
|
48
|
+
- **Vectorized**: top-level code runs once on all bars. A Pine statement that runs per bar becomes either a
|
|
49
|
+
Series expression or a bar-loop statement. Top-level JS `if` / ternaries see plain values only (inputs,
|
|
50
|
+
constants), never per-bar values.
|
|
51
|
+
- **`ta.*` in conditional code**: in Pine a `ta.*` call inside an `if` (or the right side of `and` / `or`) keeps
|
|
52
|
+
a history of the bars where it ran. Computing it before the loop sees every bar, which can differ. For exact
|
|
53
|
+
ports use `callsite` (`import { callsite } from "oakscriptjs";`): `callsite.crossover()`, `barssince()`,
|
|
54
|
+
`highest()`, `lowest()`, `sma()`, `sum()`, `stdev()`, `linreg()`, `pivothigh()`... called with plain values in the
|
|
55
|
+
loop, or `callsite.whenCalled(calledSeries, fn, ...sources)`.
|
|
56
|
+
- **Recalculation**: no rollback per tick in indicators; the host runs the script again on new data. Strategies
|
|
57
|
+
can run inside the bar (`calc_on_order_fills`, bar magnifier ticks); `varip(initial)` keeps its value through
|
|
58
|
+
those runs.
|
|
59
|
+
- **History**: at most 5000 bars back (`max_bars_back` is fixed), as Pine's default.
|
|
60
|
+
- **Time unit**: `time`, `time_close`, `timestamp()` and `input.time` are in ms as in Pine; `c.time` is the bar's
|
|
61
|
+
own time (UNIX seconds unless the host uses ms).
|
|
62
|
+
|
|
63
|
+
## API differences
|
|
64
|
+
|
|
65
|
+
- `ta.vwma(source, length, volume)` needs the volume Series (Pine reads the chart volume).
|
|
66
|
+
- `color.from_gradient` takes numbers: build per-bar gradients in a bar loop.
|
|
67
|
+
- `plot()` has no `offset`, `show_last`, `trackprice` or `join` option; shift a series right with
|
|
68
|
+
`x.offset(k)`. `plotshape`, `plotchar`, `plotarrow` and `bgcolor` accept `offset` (negative too).
|
|
69
|
+
- `fill()` accepts `show_last` / `fillgaps` / `display` and ignores them.
|
|
70
|
+
- `input.enum` and `input.price` do not exist (use `input.string(..., { options })` and `input.float`).
|
|
71
|
+
- `request.*`: only `request.security` and `request.security_lower_tf`. No `request.financial`,
|
|
72
|
+
`request.dividends`, `request.earnings`, `request.splits`, `request.economic`, `request.seed`,
|
|
73
|
+
`request.currency_rate`, `request.footprint`. `ticker.*`: only `ticker.heikinashi` and `ticker.standard`.
|
|
74
|
+
- `box.set_xloc()` is not implemented.
|
|
75
|
+
- `math.random(min, max, seed)`: with a seed, Pine's sequence, but one generator per seed value (Pine has one
|
|
76
|
+
per call site): two calls with the same seed need `callsite.random(seed)`.
|
|
77
|
+
- `math.log`, `math.exp`, `math.pow`, `math.sin`, `math.cos`, `math.tan`, `math.log10` can differ from Pine in
|
|
78
|
+
the last digit. Pine rounds constant expressions (`1 / 255`) to 16 decimals: wrap them in `math.constant()`
|
|
79
|
+
when a script truncates them (`math.constant(1 / 255)`).
|
|
80
|
+
- Strategy results come from the host's engine; the deprecated `when` argument does not exist.
|
|
81
|
+
- No Pine libraries (`import user/lib/1`), no `type` / `method` / `enum` declarations: use JS objects, classes
|
|
82
|
+
and functions. No type keywords (`float`, `series int`...).
|
|
83
|
+
- `syminfo.*`, `timeframe.*`, `time()`, `session.*` need the host's chart context (an app gives it).
|
|
84
|
+
- Strings: JS strings; `+` concatenates; `str.tostring(x, "#.##")`, `str.format("{0} {1}", a, b)`.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Plotting
|
|
2
|
+
|
|
3
|
+
Every output is declared and filled in one call. Titles name the outputs in the host's legend, settings and
|
|
4
|
+
alerts: give each one a title.
|
|
5
|
+
|
|
6
|
+
## plot
|
|
7
|
+
|
|
8
|
+
`plot(series, title, { color, linewidth, style, linestyle, histbase, display, force_overlay, editable })`
|
|
9
|
+
returns a handle for `fill()`.
|
|
10
|
+
|
|
11
|
+
| `style` | Draws |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `plot.style_line` (default), `plot.style_linebr` | a line; `linebr` leaves gaps on na bars |
|
|
14
|
+
| `plot.style_stepline`, `plot.style_steplinebr`, `plot.style_stepline_diamond` | steps |
|
|
15
|
+
| `plot.style_histogram` | bars from `histbase` (default 0) |
|
|
16
|
+
| `plot.style_columns` | columns from 0 |
|
|
17
|
+
| `plot.style_area`, `plot.style_areabr` | filled area |
|
|
18
|
+
| `plot.style_circles`, `plot.style_cross` | points |
|
|
19
|
+
|
|
20
|
+
`linestyle`: `plot.linestyle_solid` / `_dashed` / `_dotted`. `display`: `"all"`, `"none"` (hidden but listed),
|
|
21
|
+
`"data_window"`, `"status_line"`, `"pane"`. `force_overlay: true` draws on the price pane from a script in its
|
|
22
|
+
own pane. NaN values are not drawn.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
indicator("RSI with levels", { overlay: false });
|
|
26
|
+
const r = ta.rsi(close, 14);
|
|
27
|
+
plot(r, "RSI", { color: color.purple, linewidth: 2 });
|
|
28
|
+
plot(ta.sma(r, 14), "RSI MA", { color: color.orange, linestyle: plot.linestyle_dashed });
|
|
29
|
+
plot(r.sub(50), "Momentum", { style: plot.style_columns, color: color.when(r.gt(50), color.new(color.teal, 60), color.new(color.red, 60)) });
|
|
30
|
+
const upper = hline(70, "Overbought", { color: color.red });
|
|
31
|
+
const lower = hline(30, "Oversold", { color: color.green });
|
|
32
|
+
hline(50, "Middle", { color: color.gray, linestyle: hline.style_dotted });
|
|
33
|
+
fill(upper, lower, color.new(color.purple, 90), "Band");
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Colours
|
|
37
|
+
|
|
38
|
+
- Constants: `color.red`, `color.green`, `color.blue`, `color.orange`, `color.teal`, `color.purple`, `color.gray`,
|
|
39
|
+
`color.white`, `color.black`, `color.yellow`, `color.aqua`, `color.lime`, `color.maroon`, `color.navy`,
|
|
40
|
+
`color.olive`, `color.silver`, `color.fuchsia`; any CSS colour string (`"#2962FF"`, `"rgba(41,98,255,0.5)"`).
|
|
41
|
+
- `color.new(c, transp)` (0 opaque, 100 invisible), `color.rgb(r, g, b, transp?)`,
|
|
42
|
+
`color.from_gradient(value, bottom, top, bottomColor, topColor)` on numbers, `color.r/g/b/t(c)`.
|
|
43
|
+
- **Per-bar colours** are arrays with one colour per bar (`undefined` = na, not drawn):
|
|
44
|
+
`color.when(cond, colorTrue, colorFalse?)` for two colours; a bar loop for more, or for a gradient.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
indicator("RSI gradient", { overlay: false });
|
|
48
|
+
const r = ta.rsi(close, 14);
|
|
49
|
+
const colors = [];
|
|
50
|
+
eachBar((c) => {
|
|
51
|
+
colors.push(color.from_gradient(c.get(r), 30, 70, color.red, color.green)); // numbers, one bar at a time
|
|
52
|
+
});
|
|
53
|
+
plot(r, "RSI", { color: colors, linewidth: 2 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## hline and fill
|
|
57
|
+
|
|
58
|
+
- `hline(price, title, { color, linestyle, linewidth })`: a constant level (default dashed);
|
|
59
|
+
`linestyle: hline.style_solid / _dashed / _dotted`.
|
|
60
|
+
- `fill(handle1, handle2, color, title)` or `fill(h1, h2, { color, title })`: between two plots or two hlines.
|
|
61
|
+
The colour can be a per-bar array.
|
|
62
|
+
- Gradient fill: `fill(p1, p2, topValue, bottomValue, topColor, bottomColor, title)` (values: numbers or Series).
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
indicator("Bollinger fill", { overlay: true });
|
|
66
|
+
const [basis, upper, lower] = ta.bb(close, 20, 2);
|
|
67
|
+
plot(basis, "Basis", { color: color.orange });
|
|
68
|
+
const pu = plot(upper, "Upper", { color: color.blue });
|
|
69
|
+
const pl = plot(lower, "Lower", { color: color.blue });
|
|
70
|
+
fill(pu, pl, upper, lower, color.new(color.blue, 70), color.new(color.blue, 95), "Gradient");
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Note: `ta.bb` returns `[basis, upper, lower]` as in Pine.
|
|
74
|
+
|
|
75
|
+
## Shapes, characters, arrows
|
|
76
|
+
|
|
77
|
+
- `plotshape(cond, title, { style, location, color, text, textcolor, size, offset, tooltip })`: a shape on
|
|
78
|
+
each bar where `cond` is not 0 / na. `style`: `"xcross"` (default), `"cross"`, `"triangleup"`,
|
|
79
|
+
`"triangledown"`, `"flag"`, `"circle"`, `"arrowup"`, `"arrowdown"`, `"labelup"`, `"labeldown"`, `"square"`,
|
|
80
|
+
`"diamond"`. `location`: `"abovebar"` (default), `"belowbar"`, `"top"`, `"bottom"`, `"absolute"` (at the
|
|
81
|
+
series value). `size`: `"auto"`, `"tiny"`, `"small"`, `"normal"`, `"large"`, `"huge"`.
|
|
82
|
+
- `plotchar(cond, title, { char, location, color, size, offset })`: a character.
|
|
83
|
+
- `plotarrow(series, title, { colorup, colordown, minheight, maxheight })`: an up arrow for a positive value,
|
|
84
|
+
a down arrow for a negative one.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
indicator("Markers", { overlay: true });
|
|
88
|
+
const ema = ta.ema(close, 20);
|
|
89
|
+
plotshape(ta.crossover(close, ema), "Buy", { style: "labelup", location: "belowbar", color: color.green, text: "Buy", textcolor: color.white });
|
|
90
|
+
plotshape(ta.crossunder(close, ema), "Sell", { style: "labeldown", location: "abovebar", color: color.red, text: "Sell", textcolor: color.white });
|
|
91
|
+
// a pivot is known 5 bars later: offset -5 puts the mark on the pivot bar; "absolute" places it at the value
|
|
92
|
+
plotshape(ta.pivothigh(5, 5), "Pivot high", { style: "diamond", location: "absolute", offset: -5, color: color.orange, size: "tiny" });
|
|
93
|
+
plotchar(ta.pivotlow(5, 5), "Pivot low", { char: "^", location: "belowbar", offset: -5, color: color.teal });
|
|
94
|
+
plotarrow(ta.change(close, 3), "3-bar change", { colorup: color.teal, colordown: color.red });
|
|
95
|
+
plot(ema, "EMA");
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Background and bar colours
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
indicator("Trend colours", { overlay: true });
|
|
102
|
+
const ema = ta.ema(close, 50);
|
|
103
|
+
plot(ema, "EMA", { color: color.gray });
|
|
104
|
+
barcolor(color.when(close.gt(ema), color.teal, color.red), { title: "Trend bars" });
|
|
105
|
+
bgcolor(color.when(ta.cross(close, ema), color.new(color.yellow, 80)), { title: "Cross" });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`bgcolor(colors, { offset, title, force_overlay })`, `barcolor(colors, { title })`: a colour or a per-bar array.
|
|
109
|
+
|
|
110
|
+
## Candles and bars
|
|
111
|
+
|
|
112
|
+
`plotcandle(o, h, l, c, title, { color, wickcolor, bordercolor })` and `plotbar(o, h, l, c, title, { color })`
|
|
113
|
+
draw OHLC from four Series (no candle on a bar with an na value).
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
indicator("Heikin Ashi", { overlay: false });
|
|
117
|
+
const haClose = ohlc4;
|
|
118
|
+
const highs = [];
|
|
119
|
+
const lows = [];
|
|
120
|
+
const haOpen = eachBar((c) => {
|
|
121
|
+
const o = c.i === 0 ? (c.open + c.close) / 2 : (c.prev() + c.get(haClose, 1)) / 2; // c.prev(): haOpen[1]
|
|
122
|
+
highs.push(Math.max(c.high, o, c.get(haClose)));
|
|
123
|
+
lows.push(Math.min(c.low, o, c.get(haClose)));
|
|
124
|
+
return o;
|
|
125
|
+
});
|
|
126
|
+
plotcandle(haOpen, seriesOf(highs), seriesOf(lows), haClose, "HA", {
|
|
127
|
+
color: color.when(haClose.gt(haOpen), color.teal, color.red),
|
|
128
|
+
wickcolor: color.gray,
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Drawings
|
|
133
|
+
|
|
134
|
+
Drawings are created in a bar loop, with **positional** arguments in the Pine order and string constants. Skip
|
|
135
|
+
an argument with `undefined`.
|
|
136
|
+
|
|
137
|
+
| Call | Arguments |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `label.new` | `x, y, text, xloc ("bar_index" / "bar_time"), yloc ("price" / "abovebar" / "belowbar"), color, style, textcolor, size, textalign, tooltip` |
|
|
140
|
+
| `line.new` | `x1, y1, x2, y2, xloc, extend ("none" / "left" / "right" / "both"), color, style ("solid" / "dashed" / "dotted" / "arrow_left" / "arrow_right" / "arrow_both"), width` |
|
|
141
|
+
| `box.new` | `left, top, right, bottom, border_color, border_width, border_style, extend, xloc, bgcolor, text, text_size, text_color` |
|
|
142
|
+
| `table.new` | `position, columns, rows, bgcolor, frame_color, frame_width, border_color, border_width` |
|
|
143
|
+
| `table.cell` | `table, column, row, text, width, height, text_color, text_halign, text_valign, text_size, bgcolor, tooltip` |
|
|
144
|
+
| `polyline.new` | `points (chart.point.from_index(i, price)...), curved, closed, xloc, line_color, fill_color, line_style, line_width` |
|
|
145
|
+
| `linefill.new` | `line1, line2, color` |
|
|
146
|
+
|
|
147
|
+
Label styles: `"label_down"`, `"label_up"`, `"label_left"`, `"label_right"`, `"label_center"`, `"none"`, and the
|
|
148
|
+
shape names. `x` is a bar index (`c.i`, or `c.i + 10` for a future bar) or, with `"bar_time"`, a UNIX time in ms.
|
|
149
|
+
Positions of tables: `position.top_right`, `position.bottom_left`...
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
indicator("Drawings", { overlay: true, max_labels_count: 20 });
|
|
153
|
+
const ph = ta.pivothigh(5, 5);
|
|
154
|
+
const hi = ta.highest(50);
|
|
155
|
+
const lo = ta.lowest(50);
|
|
156
|
+
eachBar((c) => {
|
|
157
|
+
const p = c.get(ph);
|
|
158
|
+
if (!Number.isNaN(p)) {
|
|
159
|
+
label.new(c.i - 5, p, str.tostring(p, "#.##"), "bar_index", "price", color.red, "label_down", color.white, "small");
|
|
160
|
+
}
|
|
161
|
+
if (c.i === c.n - 1) {
|
|
162
|
+
line.new(c.i - 49, c.get(hi), c.i + 10, c.get(hi), "bar_index", "none", color.green, "dashed", 2);
|
|
163
|
+
box.new(c.i - 49, c.get(hi), c.i, c.get(lo), color.gray, 1, "dotted", "none", "bar_index", color.new(color.gray, 90), "50-bar range");
|
|
164
|
+
const t = table.new(position.bottom_right, 1, 1, color.new(color.black, 30));
|
|
165
|
+
table.cell(t, 0, 0, "Range " + str.tostring(c.get(hi) - c.get(lo), "#.##"), 0, 0, color.white);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
plot(close, "Close", { display: "none" });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- Keep one object and update it (`line.set_xy1(l, x, y)`, `label.set_text(lb, t)`, `box.set_top(b, y)`...)
|
|
172
|
+
rather than creating one per bar when only the last one matters.
|
|
173
|
+
- Objects above `max_labels_count` / `max_lines_count` / `max_boxes_count` / `max_polylines_count` (default 50)
|
|
174
|
+
are deleted oldest first; `label.delete(lb)` deletes one. `const keep = varDrawing();` then
|
|
175
|
+
`keep.value = line.new(...)` is Pine's `var line keep = na` (that object is never deleted by the limit).
|
|
176
|
+
- `label.all`, `line.all`... list the live objects.
|
|
177
|
+
|
|
178
|
+
## Alerts
|
|
179
|
+
|
|
180
|
+
- `alertcondition(cond, title, message)`: a condition the user can create an alert on.
|
|
181
|
+
- `alert(message, freq)` inside a bar loop: an alert call on that bar; `freq`: `alert.freq_once_per_bar`
|
|
182
|
+
(default), `alert.freq_once_per_bar_close`, `alert.freq_all`.
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
indicator("Alerts", { overlay: true });
|
|
186
|
+
const ma = ta.sma(close, 20);
|
|
187
|
+
const up = ta.crossover(close, ma);
|
|
188
|
+
plot(ma, "SMA");
|
|
189
|
+
alertcondition(up, "Cross up", "Close crossed above the SMA");
|
|
190
|
+
eachBar((c) => {
|
|
191
|
+
if (c.get(up) === 1) alert("Close " + str.tostring(c.close, "#.##") + " crossed above the SMA", alert.freq_once_per_bar_close);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Script structure
|
|
2
|
+
|
|
3
|
+
## Execution model
|
|
4
|
+
|
|
5
|
+
Pine Script runs a script once per bar. OakScript runs the script body **once per calculation, over all the bars
|
|
6
|
+
at once**:
|
|
7
|
+
|
|
8
|
+
- Top-level code computes whole Series (one value per bar) with `ta.*` and the Series methods.
|
|
9
|
+
- Logic that needs the bars one by one (state, `if` on values, loops) goes in `eachBar((c) => ...)` or, for a
|
|
10
|
+
strategy, `strategy.eachBar((c) => ...)`.
|
|
11
|
+
- The host runs the body again when bars arrive or an input changes; every run starts from scratch, so plain
|
|
12
|
+
`let` variables are reset on each run (as Pine resets `var` on a recalculation).
|
|
13
|
+
|
|
14
|
+
Order of a script: declaration, inputs, Series math, bar loops, outputs. Outputs can appear anywhere after the
|
|
15
|
+
values they plot.
|
|
16
|
+
|
|
17
|
+
## Declaration
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
indicator("Average True Range Bands", {
|
|
21
|
+
shorttitle: "ATR Bands", // legend title
|
|
22
|
+
overlay: true, // default true: price pane; false: own pane
|
|
23
|
+
precision: 2, // decimals shown
|
|
24
|
+
max_labels_count: 100, // also max_lines_count, max_boxes_count, max_polylines_count (default 50)
|
|
25
|
+
});
|
|
26
|
+
const atr = ta.atr(14);
|
|
27
|
+
plot(close.add(atr.mul(2)), "Upper");
|
|
28
|
+
plot(close.sub(atr.mul(2)), "Lower");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`strategy(title, options)` replaces `indicator()` for a strategy (default `overlay: false`); its properties are in
|
|
32
|
+
[strategies.md](strategies.md). A script without a declaration runs with the title "Untitled" and overlay on.
|
|
33
|
+
|
|
34
|
+
## Inputs
|
|
35
|
+
|
|
36
|
+
Each `input.*` call declares the input for the host's settings and returns its current value: the host's
|
|
37
|
+
override when there is one, else the default.
|
|
38
|
+
|
|
39
|
+
| Call | Returns |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `input.int(defval, title, { minval, maxval, step })` | integer (rounded) |
|
|
42
|
+
| `input.float(defval, title, { minval, maxval, step })` | number |
|
|
43
|
+
| `input.bool(defval, title)` | boolean |
|
|
44
|
+
| `input.string(defval, title, { options: [...] })` | string (a choice list with `options`) |
|
|
45
|
+
| `input.text_area(defval, title)` | multi-line string |
|
|
46
|
+
| `input.color(defval, title)` | colour string |
|
|
47
|
+
| `input.source("close", title)` | Series (`open high low close volume hl2 hlc3 ohlc4 hlcc4`, or another script's output when the host allows it) |
|
|
48
|
+
| `input.timeframe("D", title, { options })` | timeframe string (`""`: the chart timeframe) |
|
|
49
|
+
| `input.session("0930-1600", title)` | session string |
|
|
50
|
+
| `input.symbol("NASDAQ:AAPL", title)` | ticker id for `request.security` |
|
|
51
|
+
| `input.time(timestamp("2024-01-01"), title)` | UNIX time in ms |
|
|
52
|
+
|
|
53
|
+
Options of every kind: `group`, `inline`, `tooltip`, `confirm`, `display`, `active` (false greys the input out).
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
indicator("Inputs demo");
|
|
57
|
+
const showMa = input.bool(true, "Show MA", { group: "Average", inline: "ma" });
|
|
58
|
+
const maLength = input.int(20, "Length", { minval: 1, maxval: 500, group: "Average", inline: "ma", active: showMa });
|
|
59
|
+
const maColor = input.color(color.blue, "Colour", { group: "Average" });
|
|
60
|
+
const mode = input.string("Close", "Price", { options: ["Close", "HL2"], tooltip: "Price of the average" });
|
|
61
|
+
const src = mode === "HL2" ? hl2 : close; // inputs are plain values: JS operators work on them
|
|
62
|
+
plot(ta.sma(src, maLength), "MA", { color: maColor, display: showMa ? "all" : "none" });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Input ids.** The host sets inputs by id: the title in snake case (`"Fast length"` is `fast_length`, a repeated
|
|
66
|
+
title gets `_2`, `_3`). Keep titles stable: renaming one resets the user's value.
|
|
67
|
+
|
|
68
|
+
## Series
|
|
69
|
+
|
|
70
|
+
`close`, `open`, `high`, `low`, `volume`, `hl2`, `hlc3`, `ohlc4`, `hlcc4`, the `ta.*` results and the inputs from
|
|
71
|
+
`input.source` are `Series`: one number per bar, NaN for na.
|
|
72
|
+
|
|
73
|
+
| Pine | Series method |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `a + b`, `a - b`, `a * b`, `a / b`, `a % b`, `-a`, `math.pow(a, n)` | `a.add(b)`, `a.sub(b)`, `a.mul(b)`, `a.div(b)`, `a.mod(b)`, `a.neg()`, `a.pow(n)` |
|
|
76
|
+
| `a > b`, `a >= b`, `a < b`, `a <= b`, `a == b`, `a != b` | `a.gt(b)`, `a.gte(b)`, `a.lt(b)`, `a.lte(b)`, `a.eq(b)`, `a.neq(b)` (1 / 0, false with na) |
|
|
77
|
+
| `a and b`, `a or b`, `not a` | `a.and(b)`, `a.or(b)`, `a.not()` |
|
|
78
|
+
| `cond ? x : y` | `cond.iff(x, y)` (na and 0 are false) |
|
|
79
|
+
| `a[n]` | `a.offset(n)` (n >= 0) |
|
|
80
|
+
|
|
81
|
+
`b` can be a Series or a number. Comparisons follow the Pine rules (1e-10 tolerance). `series.toArray()` gives
|
|
82
|
+
the values as a `number[]` (oldest first); `seriesOf(numbers)` turns an array with one value per bar back into a
|
|
83
|
+
Series.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
indicator("Body ratio", { overlay: false });
|
|
87
|
+
const body = close.sub(open);
|
|
88
|
+
const range = high.sub(low);
|
|
89
|
+
const ratio = range.gt(0).iff(body.div(range), 0); // guard the division
|
|
90
|
+
plot(ratio, "Body / range", { style: plot.style_columns, color: color.when(ratio.gt(0), color.teal, color.red) });
|
|
91
|
+
hline(0);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`ta.*` takes Series and lengths (a plain number; `ta.sma`, `ta.wma`, `ta.stdev`, `ta.variance`, `ta.highest`,
|
|
95
|
+
`ta.lowest` also accept a Series of lengths) and returns a Series, or an array of Series for the Pine tuples
|
|
96
|
+
(`ta.macd`, `ta.bb`, `ta.kc`, `ta.dmi`, `ta.supertrend`, `ta.ichimoku`, `ta.vwap(src, anchor, mult)`). The chart
|
|
97
|
+
functions read the bars themselves: `ta.atr(14)`, `ta.tr(true)`, `ta.sar()`, `ta.supertrend(3, 10)`,
|
|
98
|
+
`ta.dmi(14, 14)`, `ta.wpr(14)`, `ta.mfi(hlc3, 14)`, `ta.highest(20)` (high), `ta.pivothigh(5, 5)`. The volume
|
|
99
|
+
indicators are variables: `ta.obv`, `ta.accdist`, `ta.pvt`, `ta.nvi`, `ta.pvi`, `ta.iii`, `ta.wad`, `ta.wvad`.
|
|
100
|
+
|
|
101
|
+
## Bar loops: eachBar
|
|
102
|
+
|
|
103
|
+
`eachBar(fn)` calls `fn(c)` once per bar, oldest first, and returns a Series of the values it returns
|
|
104
|
+
(`true` / `false` become 1 / 0, no return value becomes NaN).
|
|
105
|
+
|
|
106
|
+
| `c.` | Value |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `i`, `n` | bar index (Pine `bar_index`), number of bars (`c.i === c.n - 1` is the last bar) |
|
|
109
|
+
| `time` | bar open time, in the unit of the bars (UNIX seconds by default) |
|
|
110
|
+
| `open`, `high`, `low`, `close`, `volume` | the bar's prices |
|
|
111
|
+
| `get(series, k = 0)` | `series[k]` as a number (NaN before the first bar) |
|
|
112
|
+
| `prev(k = 1)` | this loop's own return value k bars back (Pine self-reference) |
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
indicator("ATR trailing stop", { overlay: true });
|
|
116
|
+
const atr = ta.atr(14);
|
|
117
|
+
let stop = NaN; // var float stop = na
|
|
118
|
+
let dir = 1; // var int dir = 1
|
|
119
|
+
const dirs = [];
|
|
120
|
+
const trail = eachBar((c) => {
|
|
121
|
+
const offset = 3 * c.get(atr);
|
|
122
|
+
if (Number.isNaN(offset)) { dirs.push(NaN); return NaN; }
|
|
123
|
+
if (dir === 1) {
|
|
124
|
+
stop = Number.isNaN(stop) ? c.close - offset : Math.max(stop, c.close - offset);
|
|
125
|
+
if (c.close < stop) { dir = -1; stop = c.close + offset; }
|
|
126
|
+
} else {
|
|
127
|
+
stop = Math.min(stop, c.close + offset);
|
|
128
|
+
if (c.close > stop) { dir = 1; stop = c.close - offset; }
|
|
129
|
+
}
|
|
130
|
+
dirs.push(dir);
|
|
131
|
+
return stop;
|
|
132
|
+
});
|
|
133
|
+
const up = seriesOf(dirs).eq(1);
|
|
134
|
+
plot(trail, "Stop", { style: plot.style_linebr, color: color.when(up, color.teal, color.red) });
|
|
135
|
+
plotshape(up.and(up.offset(1).not()), "Flip up", { style: "circle", location: "belowbar", color: color.teal, size: "tiny" });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- A `let` before the loop is Pine `var`: it keeps its value from bar to bar. A `const` inside the callback is a
|
|
139
|
+
plain per-bar variable.
|
|
140
|
+
- Several outputs from one loop: push into arrays declared before the loop, then `seriesOf(array)`.
|
|
141
|
+
- Plain `Math.*` is fine on numbers; Pine's `math.*` is available too (`math.max`, `math.round`...).
|
|
142
|
+
- History: `c.get(x, k)` and `x.offset(k)` reach at most 5000 bars back (the built-in prices have no limit).
|
|
143
|
+
- `varip(initial)` (declare it at the top level, use `.value`) keeps its value through the intrabar runs of a
|
|
144
|
+
strategy; without them it is a plain variable.
|
|
145
|
+
- A `ta.*` call in an `if` block or in the right side of `&&` / `||` sees only some bars in Pine. To port that
|
|
146
|
+
exactly, use the call sites of `callsite` (`import { callsite } from "oakscriptjs";`), e.g.
|
|
147
|
+
`const since = callsite.barssince();` then `since(cond)` in the loop.
|
|
148
|
+
|
|
149
|
+
## Builtins
|
|
150
|
+
|
|
151
|
+
| Name | Value |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `time`, `time_close`, `time_tradingday`, `timenow` | Series of UNIX ms |
|
|
154
|
+
| `time(tf, session?, timezone?)`, `time_close(tf, ...)` | period open / close time, NaN outside the session |
|
|
155
|
+
| `year(t)`, `month(t)`, `dayofmonth(t)`, `dayofweek(t)`, `hour(t)`, `minute(t)`, `second(t)`, `weekofyear(t)` | calendar fields of a time (number or Series) in the exchange time zone (or the given one) |
|
|
156
|
+
| `timestamp("2024-01-01")`, `timestamp(2024, 1, 1, 9, 30)` | UNIX ms |
|
|
157
|
+
| `barstate.isfirst / islast / isconfirmed / isrealtime / isnew / ishistory / islastconfirmedhistory` | Series of 1 / 0 |
|
|
158
|
+
| `session.isfirstbar / islastbar / ismarket / ispremarket / ispostmarket` | Series of 1 / 0 |
|
|
159
|
+
| `timeframe.period`, `timeframe.isintraday`, `timeframe.isdaily`, `timeframe.multiplier`..., `timeframe.change("W")`, `timeframe.in_seconds()` | chart timeframe values |
|
|
160
|
+
| `syminfo.tickerid`, `syminfo.mintick`, `syminfo.timezone`, `syminfo.currency`, `syminfo.type`... | symbol values |
|
|
161
|
+
| `str.tostring(x, "#.##")`, `str.format("{0} / {1}", a, b)`, `str.*` | text |
|
|
162
|
+
| `math.*`, `color.*`, `compare.*` | as in Pine; `compare` gives the Pine comparison of two numbers |
|
|
163
|
+
|
|
164
|
+
The time, session, timeframe and symbol values come from the host's chart context. An app supplies them; in your
|
|
165
|
+
own `executeScript` call pass them in the 4th argument, or the script stops with "... is not known: pass it as
|
|
166
|
+
executeScript(body, bars, inputs, { ... })".
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
indicator("Session marks", { overlay: true });
|
|
170
|
+
const newDay = timeframe.change("D");
|
|
171
|
+
bgcolor(color.when(newDay, color.new(color.blue, 80)), { title: "New day" });
|
|
172
|
+
plotchar(barstate.islast, "Last", { char: "L", location: "abovebar" });
|
|
173
|
+
plot(close, "Close", { display: "none" });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Drawings
|
|
177
|
+
|
|
178
|
+
`label.new`, `line.new`, `box.new`, `polyline.new`, `linefill.new` and `table.new` create objects; the host draws
|
|
179
|
+
the ones alive after the last bar. Create them in a bar loop with bar indexes (`c.i`) or bar times. Old objects
|
|
180
|
+
are deleted above `max_*_count` (default 50). To keep one object and move it, store it in a variable and update
|
|
181
|
+
it with the `set_*` functions, or hold it with `varDrawing()` (Pine `var line l = na`). See
|
|
182
|
+
[plotting.md](plotting.md#drawings).
|