oakscriptjs 0.13.1 → 0.14.0

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.
Files changed (122) hide show
  1. package/AGENTS.md +53 -0
  2. package/README.md +1 -1
  3. package/bin/oakscript.mjs +5 -0
  4. package/dist/cjs/check/checks.d.ts +78 -0
  5. package/dist/cjs/check/checks.d.ts.map +1 -0
  6. package/dist/cjs/check/checks.js +109 -0
  7. package/dist/cjs/check/checks.js.map +7 -0
  8. package/dist/cjs/check/format.d.ts +26 -0
  9. package/dist/cjs/check/format.d.ts.map +1 -0
  10. package/dist/cjs/check/format.js +106 -0
  11. package/dist/cjs/check/format.js.map +7 -0
  12. package/dist/cjs/check/harness.d.ts +94 -0
  13. package/dist/cjs/check/harness.d.ts.map +1 -0
  14. package/dist/cjs/check/harness.js +302 -0
  15. package/dist/cjs/check/harness.js.map +7 -0
  16. package/dist/cjs/check/index.d.ts +253 -0
  17. package/dist/cjs/check/index.d.ts.map +1 -0
  18. package/dist/cjs/check/index.js +466 -0
  19. package/dist/cjs/check/index.js.map +7 -0
  20. package/dist/cjs/check/node.d.ts +53 -0
  21. package/dist/cjs/check/node.d.ts.map +1 -0
  22. package/dist/cjs/check/node.js +318 -0
  23. package/dist/cjs/check/node.js.map +7 -0
  24. package/dist/cjs/check/spec.d.ts +44 -0
  25. package/dist/cjs/check/spec.d.ts.map +1 -0
  26. package/dist/cjs/check/spec.js +199 -0
  27. package/dist/cjs/check/spec.js.map +7 -0
  28. package/dist/cjs/index.d.ts +1 -1
  29. package/dist/cjs/index.js +1 -1
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/runtime/series.d.ts +6 -0
  32. package/dist/cjs/runtime/series.d.ts.map +1 -1
  33. package/dist/cjs/runtime/series.js +15 -0
  34. package/dist/cjs/runtime/series.js.map +2 -2
  35. package/dist/cjs/script/constants.d.ts +200 -0
  36. package/dist/cjs/script/constants.d.ts.map +1 -0
  37. package/dist/cjs/script/constants.js +217 -0
  38. package/dist/cjs/script/constants.js.map +7 -0
  39. package/dist/cjs/script/drawing-args.d.ts +25 -0
  40. package/dist/cjs/script/drawing-args.d.ts.map +1 -0
  41. package/dist/cjs/script/drawing-args.js +203 -0
  42. package/dist/cjs/script/drawing-args.js.map +7 -0
  43. package/dist/cjs/script/globals.d.ts +127 -0
  44. package/dist/cjs/script/globals.d.ts.map +1 -0
  45. package/dist/cjs/script/globals.js +17 -0
  46. package/dist/cjs/script/globals.js.map +7 -0
  47. package/dist/cjs/script/index.d.ts +499 -166
  48. package/dist/cjs/script/index.d.ts.map +1 -1
  49. package/dist/cjs/script/index.js +236 -96
  50. package/dist/cjs/script/index.js.map +3 -3
  51. package/dist/esm/check/checks.d.ts +78 -0
  52. package/dist/esm/check/checks.d.ts.map +1 -0
  53. package/dist/esm/check/checks.js +95 -0
  54. package/dist/esm/check/checks.js.map +1 -0
  55. package/dist/esm/check/format.d.ts +26 -0
  56. package/dist/esm/check/format.d.ts.map +1 -0
  57. package/dist/esm/check/format.js +105 -0
  58. package/dist/esm/check/format.js.map +1 -0
  59. package/dist/esm/check/harness.d.ts +94 -0
  60. package/dist/esm/check/harness.d.ts.map +1 -0
  61. package/dist/esm/check/harness.js +317 -0
  62. package/dist/esm/check/harness.js.map +1 -0
  63. package/dist/esm/check/index.d.ts +253 -0
  64. package/dist/esm/check/index.d.ts.map +1 -0
  65. package/dist/esm/check/index.js +470 -0
  66. package/dist/esm/check/index.js.map +1 -0
  67. package/dist/esm/check/node.d.ts +53 -0
  68. package/dist/esm/check/node.d.ts.map +1 -0
  69. package/dist/esm/check/node.js +347 -0
  70. package/dist/esm/check/node.js.map +1 -0
  71. package/dist/esm/check/spec.d.ts +44 -0
  72. package/dist/esm/check/spec.d.ts.map +1 -0
  73. package/dist/esm/check/spec.js +198 -0
  74. package/dist/esm/check/spec.js.map +1 -0
  75. package/dist/esm/index.d.ts +1 -1
  76. package/dist/esm/index.js +1 -1
  77. package/dist/esm/runtime/series.d.ts +6 -0
  78. package/dist/esm/runtime/series.d.ts.map +1 -1
  79. package/dist/esm/runtime/series.js +14 -0
  80. package/dist/esm/runtime/series.js.map +1 -1
  81. package/dist/esm/script/constants.d.ts +200 -0
  82. package/dist/esm/script/constants.d.ts.map +1 -0
  83. package/dist/esm/script/constants.js +151 -0
  84. package/dist/esm/script/constants.js.map +1 -0
  85. package/dist/esm/script/drawing-args.d.ts +25 -0
  86. package/dist/esm/script/drawing-args.d.ts.map +1 -0
  87. package/dist/esm/script/drawing-args.js +171 -0
  88. package/dist/esm/script/drawing-args.js.map +1 -0
  89. package/dist/esm/script/globals.d.ts +127 -0
  90. package/dist/esm/script/globals.d.ts.map +1 -0
  91. package/dist/esm/script/globals.js +28 -0
  92. package/dist/esm/script/globals.js.map +1 -0
  93. package/dist/esm/script/index.d.ts +499 -166
  94. package/dist/esm/script/index.d.ts.map +1 -1
  95. package/dist/esm/script/index.js +256 -83
  96. package/dist/esm/script/index.js.map +1 -1
  97. package/package.json +33 -1
  98. package/skills/oakscript/SKILL.md +198 -0
  99. package/skills/oakscript/examples/ma-cross-strategy.js +57 -0
  100. package/skills/oakscript/examples/ma-cross.js +34 -0
  101. package/skills/oakscript/examples/macd-oscillator.js +29 -0
  102. package/skills/oakscript/examples/mtf-trend-filter.js +46 -0
  103. package/skills/oakscript/reference/check.md +150 -0
  104. package/skills/oakscript/reference/errors.md +92 -0
  105. package/skills/oakscript/reference/pine-differences.md +85 -0
  106. package/skills/oakscript/reference/plotting.md +199 -0
  107. package/skills/oakscript/reference/script-structure.md +185 -0
  108. package/skills/oakscript/reference/strategies.md +140 -0
  109. package/skills/oakscript/reference/timeframes-symbols.md +97 -0
  110. package/skills/oakscript/spec.schema.json +199 -0
  111. package/src/check/checks.ts +175 -0
  112. package/src/check/format.ts +117 -0
  113. package/src/check/harness.ts +372 -0
  114. package/src/check/index.ts +708 -0
  115. package/src/check/node.ts +368 -0
  116. package/src/check/spec.ts +200 -0
  117. package/src/index.ts +1 -1
  118. package/src/runtime/series.ts +19 -0
  119. package/src/script/constants.ts +169 -0
  120. package/src/script/drawing-args.ts +179 -0
  121. package/src/script/globals.ts +129 -0
  122. package/src/script/index.ts +457 -81
@@ -0,0 +1,85 @@
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 })` or `input.int(14, { title: "Length", minval: 1 })` |
14
+ | `src = input.source(close, "Source")` | `const src = input.source(close, "Source")` |
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` | the Series `bar_index`, `last_bar_index`; in a bar loop `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: shape.triangleup, location: location.belowbar, size: size.small })` |
30
+ | `label.new(bar_index, high, "A", style=label.style_label_down)` (in an `if`) | in a bar loop: `label.new(bar_index, high, "A", { style: label.style_label_down })` (or `c.i`, `c.high`) |
31
+ | `line.new(x1, y1, x2, y2, extend=extend.right)` | `line.new(x1, y1, x2, y2, { extend: extend.right })` |
32
+ | `xloc.bar_time`, `yloc.abovebar`, `line.style_dashed`, `format.volume`, `display.none`, `dayofweek.monday` | the same |
33
+ | `request.security(sym, "D", ta.ema(close, 20))` | `request.security(sym, "D", () => ta.ema(close, 20))` |
34
+ | `request.security(sym, "W", high[1], lookahead = barmerge.lookahead_on)` | `request.security(sym, "W", () => high.offset(1), { lookahead: barmerge.lookahead_on })` |
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` (`strategy.opentrades` is the count: a JavaScript number cannot also hold the functions) |
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
+ The Pine constants exist with their Pine names: `shape.*`, `location.*`, `size.*`, `xloc.*`, `yloc.*`,
42
+ `extend.*`, `display.*`, `format.*`, `font.family_*`, `scale.*`, `order.*`, `currency.*`, `adjustment.*`,
43
+ `label.style_*`, `line.style_*`, `dayofweek.*`, `color.*`, `plot.style_*`, `plot.linestyle_*`, `hline.style_*`,
44
+ `strategy.long / short / fixed / cash / percent_of_equity`, `strategy.commission.*`, `strategy.direction.*`,
45
+ `strategy.oca.*`, `barmerge.*`, `position.*`, `text.align_* / wrap_* / format_*`, `alert.freq_*`,
46
+ `session.regular / extended`, `math.pi / e / phi / rphi`. Their values are the strings the functions take
47
+ (`shape.triangleup` is `"triangleup"`).
48
+
49
+ ## Execution differences
50
+
51
+ - **Vectorized**: top-level code runs once on all bars. A Pine statement that runs per bar becomes either a
52
+ Series expression or a bar-loop statement. Top-level JS `if` / ternaries see plain values only (inputs,
53
+ constants), never per-bar values.
54
+ - **`ta.*` in conditional code**: in Pine a `ta.*` call inside an `if` (or the right side of `and` / `or`) keeps
55
+ a history of the bars where it ran. Computing it before the loop sees every bar, which can differ. For exact
56
+ ports use `callsite` (`import { callsite } from "oakscriptjs";`): `callsite.crossover()`, `barssince()`,
57
+ `highest()`, `lowest()`, `sma()`, `sum()`, `stdev()`, `linreg()`, `pivothigh()`... called with plain values in the
58
+ loop, or `callsite.whenCalled(calledSeries, fn, ...sources)`.
59
+ - **Recalculation**: no rollback per tick in indicators; the host runs the script again on new data. Strategies
60
+ can run inside the bar (`calc_on_order_fills`, bar magnifier ticks); `varip(initial)` keeps its value through
61
+ those runs.
62
+ - **History**: at most 5000 bars back (`max_bars_back` is fixed), as Pine's default.
63
+ - **Time unit**: `time`, `time_close`, `timestamp()` and `input.time` are in ms as in Pine; `c.time` is the bar's
64
+ own time (UNIX seconds unless the host uses ms).
65
+
66
+ ## API differences
67
+
68
+ - `color.from_gradient` takes numbers: build per-bar gradients in a bar loop.
69
+ - `plot()` has no `show_last`, `trackprice` or `join` option. `offset` shifts the plot as in Pine (values past the
70
+ first or last bar are not drawn).
71
+ - `fill()` accepts `show_last` / `fillgaps` / `display` and ignores them.
72
+ - `input.enum` does not exist (no `enum` declarations): use `input.string(..., { options })`.
73
+ - `request.*`: only `request.security` and `request.security_lower_tf`. No `request.financial`,
74
+ `request.dividends`, `request.earnings`, `request.splits`, `request.economic`, `request.seed`,
75
+ `request.currency_rate`, `request.footprint`. `ticker.*`: only `ticker.heikinashi` and `ticker.standard`.
76
+ - `math.random(min, max, seed)`: with a seed, Pine's sequence, but one generator per seed value (Pine has one
77
+ per call site): two calls with the same seed need `callsite.random(seed)`.
78
+ - `math.log`, `math.exp`, `math.pow`, `math.sin`, `math.cos`, `math.tan`, `math.log10` can differ from Pine in
79
+ the last digit. Pine rounds constant expressions (`1 / 255`) to 16 decimals: wrap them in `math.constant()`
80
+ when a script truncates them (`math.constant(1 / 255)`).
81
+ - Strategy results come from the host's engine; the deprecated `when` argument does not exist.
82
+ - No Pine libraries (`import user/lib/1`), no `type` / `method` / `enum` declarations: use JS objects, classes
83
+ and functions. No type keywords (`float`, `series int`...).
84
+ - `syminfo.*`, `timeframe.*`, `time()`, `session.*` need the host's chart context (an app gives it).
85
+ - Strings: JS strings; `+` concatenates; `str.tostring(x, "#.##")`, `str.format("{0} {1}", a, b)`.
@@ -0,0 +1,199 @@
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`: `shape.xcross` (default), `shape.cross`, `shape.triangleup`,
79
+ `shape.triangledown`, `shape.flag`, `shape.circle`, `shape.arrowup`, `shape.arrowdown`, `shape.labelup`,
80
+ `shape.labeldown`, `shape.square`, `shape.diamond`. `location`: `location.abovebar` (default), `location.belowbar`,
81
+ `location.top`, `location.bottom`, `location.absolute` (at the series value). `size`: `size.auto`, `size.tiny`,
82
+ `size.small`, `size.normal`, `size.large`, `size.huge`.
83
+ - `plotchar(cond, title, { char, location, color, size, offset })`: a character.
84
+ - `plotarrow(series, title, { colorup, colordown, minheight, maxheight })`: an up arrow for a positive value,
85
+ a down arrow for a negative one.
86
+
87
+ ```js
88
+ indicator("Markers", { overlay: true });
89
+ const ema = ta.ema(close, 20);
90
+ plotshape(ta.crossover(close, ema), "Buy", { style: shape.labelup, location: location.belowbar, color: color.green, text: "Buy", textcolor: color.white });
91
+ plotshape(ta.crossunder(close, ema), "Sell", { style: shape.labeldown, location: location.abovebar, color: color.red, text: "Sell", textcolor: color.white });
92
+ // a pivot is known 5 bars later: offset -5 puts the mark on the pivot bar; "absolute" places it at the value
93
+ plotshape(ta.pivothigh(5, 5), "Pivot high", { style: shape.diamond, location: location.absolute, offset: -5, color: color.orange, size: size.tiny });
94
+ plotchar(ta.pivotlow(5, 5), "Pivot low", { char: "^", location: location.belowbar, offset: -5, color: color.teal });
95
+ plotarrow(ta.change(close, 3), "3-bar change", { colorup: color.teal, colordown: color.red });
96
+ plot(ema, "EMA");
97
+ ```
98
+
99
+ ## Background and bar colours
100
+
101
+ ```js
102
+ indicator("Trend colours", { overlay: true });
103
+ const ema = ta.ema(close, 50);
104
+ plot(ema, "EMA", { color: color.gray });
105
+ barcolor(color.when(close.gt(ema), color.teal, color.red), { title: "Trend bars" });
106
+ bgcolor(color.when(ta.cross(close, ema), color.new(color.yellow, 80)), { title: "Cross" });
107
+ ```
108
+
109
+ `bgcolor(colors, { offset, title, force_overlay })`, `barcolor(colors, { title })`: a colour or a per-bar array.
110
+
111
+ ## Candles and bars
112
+
113
+ `plotcandle(o, h, l, c, title, { color, wickcolor, bordercolor })` and `plotbar(o, h, l, c, title, { color })`
114
+ draw OHLC from four Series (no candle on a bar with an na value).
115
+
116
+ ```js
117
+ indicator("Heikin Ashi", { overlay: false });
118
+ const haClose = ohlc4;
119
+ const highs = [];
120
+ const lows = [];
121
+ const haOpen = eachBar((c) => {
122
+ const o = c.i === 0 ? (c.open + c.close) / 2 : (c.prev() + c.get(haClose, 1)) / 2; // c.prev(): haOpen[1]
123
+ highs.push(Math.max(c.high, o, c.get(haClose)));
124
+ lows.push(Math.min(c.low, o, c.get(haClose)));
125
+ return o;
126
+ });
127
+ plotcandle(haOpen, seriesOf(highs), seriesOf(lows), haClose, "HA", {
128
+ color: color.when(haClose.gt(haOpen), color.teal, color.red),
129
+ wickcolor: color.gray,
130
+ });
131
+ ```
132
+
133
+ ## Drawings
134
+
135
+ Drawings are created in a bar loop, with the Pine arguments: the first ones by position, the others by name in an
136
+ object after them (Pine's named arguments), e.g. `label.new(c.i, c.high, "A", { style: label.style_label_down })`.
137
+ Every argument can be named (`line.new({ x1, y1, x2, y2, extend: extend.right })`); an unknown name throws with the
138
+ closest names. In the loop, a Series argument is its value on the current bar: `label.new(bar_index, high, "A")` is
139
+ `label.new(c.i, c.high, "A")`.
140
+
141
+ | Call | Arguments |
142
+ |---|---|
143
+ | `label.new` | `x, y, text, xloc (xloc.bar_index / xloc.bar_time), yloc (yloc.price / yloc.abovebar / yloc.belowbar), color, style, textcolor, size, textalign, tooltip, text_font_family, force_overlay, text_formatting` |
144
+ | `line.new` | `x1, y1, x2, y2, xloc, extend (extend.none / left / right / both), color, style (line.style_solid / dashed / dotted / arrow_left / arrow_right / arrow_both), width, force_overlay` |
145
+ | `box.new` | `left, top, right, bottom, border_color, border_width, border_style, extend, xloc, bgcolor, text, text_size, text_color, text_halign, text_valign, text_wrap, text_font_family, force_overlay, text_formatting` |
146
+ | `table.new` | `position, columns, rows, bgcolor, frame_color, frame_width, border_color, border_width` |
147
+ | `table.cell` | `table_id, column, row, text, width, height, text_color, text_halign, text_valign, text_size, bgcolor, tooltip, text_font_family, text_formatting` |
148
+ | `polyline.new` | `points (chart.point.from_index(i, price)...), curved, closed, xloc, line_color, fill_color, line_style, line_width` |
149
+ | `linefill.new` | `line1, line2, color` |
150
+
151
+ Label styles: `label.style_label_down`, `label.style_label_up`, `label.style_label_left`, `label.style_label_right`,
152
+ `label.style_label_center`, `label.style_none`, and `label.style_<shape>`. `x` is a bar index (`c.i`, or `c.i + 10`
153
+ for a future bar) or, with `xloc.bar_time`, a UNIX time in ms. The constants are the strings (`"label_down"`), which
154
+ also work.
155
+ Positions of tables: `position.top_right`, `position.bottom_left`...
156
+
157
+ ```js
158
+ indicator("Drawings", { overlay: true, max_labels_count: 20 });
159
+ const ph = ta.pivothigh(5, 5);
160
+ const hi = ta.highest(50);
161
+ const lo = ta.lowest(50);
162
+ eachBar((c) => {
163
+ const p = c.get(ph);
164
+ if (!Number.isNaN(p)) {
165
+ label.new(c.i - 5, p, str.tostring(p, "#.##"), { color: color.red, style: label.style_label_down, textcolor: color.white, size: size.small });
166
+ }
167
+ if (c.i === c.n - 1) {
168
+ line.new(c.i - 49, c.get(hi), c.i + 10, c.get(hi), { color: color.green, style: line.style_dashed, width: 2 });
169
+ box.new(c.i - 49, c.get(hi), c.i, c.get(lo), { border_color: color.gray, border_style: line.style_dotted, bgcolor: color.new(color.gray, 90), text: "50-bar range" });
170
+ const t = table.new(position.bottom_right, 1, 1, color.new(color.black, 30));
171
+ table.cell(t, 0, 0, "Range " + str.tostring(c.get(hi) - c.get(lo), "#.##"), 0, 0, color.white);
172
+ }
173
+ });
174
+ plot(close, "Close", { display: "none" });
175
+ ```
176
+
177
+ - Keep one object and update it (`line.set_xy1(l, x, y)`, `label.set_text(lb, t)`, `box.set_top(b, y)`...)
178
+ rather than creating one per bar when only the last one matters.
179
+ - Objects above `max_labels_count` / `max_lines_count` / `max_boxes_count` / `max_polylines_count` (default 50)
180
+ are deleted oldest first; `label.delete(lb)` deletes one. `const keep = varDrawing();` then
181
+ `keep.value = line.new(...)` is Pine's `var line keep = na` (that object is never deleted by the limit).
182
+ - `label.all`, `line.all`... list the live objects.
183
+
184
+ ## Alerts
185
+
186
+ - `alertcondition(cond, title, message)`: a condition the user can create an alert on.
187
+ - `alert(message, freq)` inside a bar loop: an alert call on that bar; `freq`: `alert.freq_once_per_bar`
188
+ (default), `alert.freq_once_per_bar_close`, `alert.freq_all`.
189
+
190
+ ```js
191
+ indicator("Alerts", { overlay: true });
192
+ const ma = ta.sma(close, 20);
193
+ const up = ta.crossover(close, ma);
194
+ plot(ma, "SMA");
195
+ alertcondition(up, "Cross up", "Close crossed above the SMA");
196
+ eachBar((c) => {
197
+ if (c.get(up) === 1) alert("Close " + str.tostring(c.close, "#.##") + " crossed above the SMA", alert.freq_once_per_bar_close);
198
+ });
199
+ ```
@@ -0,0 +1,185 @@
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, // true: price pane; false (default): 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" in its own pane.
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). They
54
+ have Pine's names (`minval`, not `min`): an unknown name throws with the closest names. The title can also be in the
55
+ options object (`input.int(14, { title: "Length", minval: 1 })`), or every argument named (`input.int({ defval: 14,
56
+ title: "Length" })`).
57
+
58
+ ```js
59
+ indicator("Inputs demo");
60
+ const showMa = input.bool(true, "Show MA", { group: "Average", inline: "ma" });
61
+ const maLength = input.int(20, "Length", { minval: 1, maxval: 500, group: "Average", inline: "ma", active: showMa });
62
+ const maColor = input.color(color.blue, "Colour", { group: "Average" });
63
+ const mode = input.string("Close", "Price", { options: ["Close", "HL2"], tooltip: "Price of the average" });
64
+ const src = mode === "HL2" ? hl2 : close; // inputs are plain values: JS operators work on them
65
+ plot(ta.sma(src, maLength), "MA", { color: maColor, display: showMa ? "all" : "none" });
66
+ ```
67
+
68
+ **Input ids.** The host sets inputs by id: the title in snake case (`"Fast length"` is `fast_length`, a repeated
69
+ title gets `_2`, `_3`). Keep titles stable: renaming one resets the user's value.
70
+
71
+ ## Series
72
+
73
+ `close`, `open`, `high`, `low`, `volume`, `hl2`, `hlc3`, `ohlc4`, `hlcc4`, the `ta.*` results and the inputs from
74
+ `input.source` are `Series`: one number per bar, NaN for na.
75
+
76
+ | Pine | Series method |
77
+ |---|---|
78
+ | `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)` |
79
+ | `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) |
80
+ | `a and b`, `a or b`, `not a` | `a.and(b)`, `a.or(b)`, `a.not()` |
81
+ | `cond ? x : y` | `cond.iff(x, y)` (na and 0 are false) |
82
+ | `a[n]` | `a.offset(n)` (n >= 0) |
83
+
84
+ `b` can be a Series or a number. Comparisons follow the Pine rules (1e-10 tolerance). `series.toArray()` gives
85
+ the values as a `number[]` (oldest first); `seriesOf(numbers)` turns an array with one value per bar back into a
86
+ Series.
87
+
88
+ ```js
89
+ indicator("Body ratio", { overlay: false });
90
+ const body = close.sub(open);
91
+ const range = high.sub(low);
92
+ const ratio = range.gt(0).iff(body.div(range), 0); // guard the division
93
+ plot(ratio, "Body / range", { style: plot.style_columns, color: color.when(ratio.gt(0), color.teal, color.red) });
94
+ hline(0);
95
+ ```
96
+
97
+ `ta.*` takes Series and lengths (a plain number; `ta.sma`, `ta.wma`, `ta.stdev`, `ta.variance`, `ta.highest`,
98
+ `ta.lowest` also accept a Series of lengths) and returns a Series, or an array of Series for the Pine tuples
99
+ (`ta.macd`, `ta.bb`, `ta.kc`, `ta.dmi`, `ta.supertrend`, `ta.ichimoku`, `ta.vwap(src, anchor, mult)`). The chart
100
+ functions read the bars themselves: `ta.atr(14)`, `ta.tr(true)`, `ta.sar()`, `ta.supertrend(3, 10)`,
101
+ `ta.dmi(14, 14)`, `ta.wpr(14)`, `ta.mfi(hlc3, 14)`, `ta.highest(20)` (high), `ta.pivothigh(5, 5)`. The volume
102
+ indicators are variables: `ta.obv`, `ta.accdist`, `ta.pvt`, `ta.nvi`, `ta.pvi`, `ta.iii`, `ta.wad`, `ta.wvad`.
103
+
104
+ ## Bar loops: eachBar
105
+
106
+ `eachBar(fn)` calls `fn(c)` once per bar, oldest first, and returns a Series of the values it returns
107
+ (`true` / `false` become 1 / 0, no return value becomes NaN).
108
+
109
+ | `c.` | Value |
110
+ |---|---|
111
+ | `i`, `n` | bar index (Pine `bar_index`), number of bars (`c.i === c.n - 1` is the last bar) |
112
+ | `time` | bar open time, in the unit of the bars (UNIX seconds by default) |
113
+ | `open`, `high`, `low`, `close`, `volume` | the bar's prices |
114
+ | `get(series, k = 0)` | `series[k]` as a number (NaN before the first bar) |
115
+ | `prev(k = 1)` | this loop's own return value k bars back (Pine self-reference) |
116
+
117
+ ```js
118
+ indicator("ATR trailing stop", { overlay: true });
119
+ const atr = ta.atr(14);
120
+ let stop = NaN; // var float stop = na
121
+ let dir = 1; // var int dir = 1
122
+ const dirs = [];
123
+ const trail = eachBar((c) => {
124
+ const offset = 3 * c.get(atr);
125
+ if (Number.isNaN(offset)) { dirs.push(NaN); return NaN; }
126
+ if (dir === 1) {
127
+ stop = Number.isNaN(stop) ? c.close - offset : Math.max(stop, c.close - offset);
128
+ if (c.close < stop) { dir = -1; stop = c.close + offset; }
129
+ } else {
130
+ stop = Math.min(stop, c.close + offset);
131
+ if (c.close > stop) { dir = 1; stop = c.close - offset; }
132
+ }
133
+ dirs.push(dir);
134
+ return stop;
135
+ });
136
+ const up = seriesOf(dirs).eq(1);
137
+ plot(trail, "Stop", { style: plot.style_linebr, color: color.when(up, color.teal, color.red) });
138
+ plotshape(up.and(up.offset(1).not()), "Flip up", { style: shape.circle, location: location.belowbar, color: color.teal, size: size.tiny });
139
+ ```
140
+
141
+ - A `let` before the loop is Pine `var`: it keeps its value from bar to bar. A `const` inside the callback is a
142
+ plain per-bar variable.
143
+ - Several outputs from one loop: push into arrays declared before the loop, then `seriesOf(array)`.
144
+ - Plain `Math.*` is fine on numbers; Pine's `math.*` is available too (`math.max`, `math.round`...).
145
+ - History: `c.get(x, k)` and `x.offset(k)` reach at most 5000 bars back (the built-in prices have no limit).
146
+ - `varip(initial)` (declare it at the top level, use `.value`) keeps its value through the intrabar runs of a
147
+ strategy; without them it is a plain variable.
148
+ - A `ta.*` call in an `if` block or in the right side of `&&` / `||` sees only some bars in Pine. To port that
149
+ exactly, use the call sites of `callsite` (`import { callsite } from "oakscriptjs";`), e.g.
150
+ `const since = callsite.barssince();` then `since(cond)` in the loop.
151
+
152
+ ## Builtins
153
+
154
+ | Name | Value |
155
+ |---|---|
156
+ | `time`, `time_close`, `time_tradingday`, `timenow` | Series of UNIX ms |
157
+ | `time(tf, session?, timezone?)`, `time_close(tf, ...)` | period open / close time, NaN outside the session |
158
+ | `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) |
159
+ | `timestamp("2024-01-01")`, `timestamp(2024, 1, 1, 9, 30)` | UNIX ms |
160
+ | `barstate.isfirst / islast / isconfirmed / isrealtime / isnew / ishistory / islastconfirmedhistory` | Series of 1 / 0 |
161
+ | `session.isfirstbar / islastbar / ismarket / ispremarket / ispostmarket` | Series of 1 / 0 |
162
+ | `timeframe.period`, `timeframe.isintraday`, `timeframe.isdaily`, `timeframe.multiplier`..., `timeframe.change("W")`, `timeframe.in_seconds()` | chart timeframe values |
163
+ | `syminfo.tickerid`, `syminfo.mintick`, `syminfo.timezone`, `syminfo.currency`, `syminfo.type`... | symbol values |
164
+ | `str.tostring(x, "#.##")`, `str.format("{0} / {1}", a, b)`, `str.*` | text |
165
+ | `math.*`, `color.*`, `compare.*` | as in Pine; `compare` gives the Pine comparison of two numbers |
166
+
167
+ The time, session, timeframe and symbol values come from the host's chart context. An app supplies them; in your
168
+ own `executeScript` call pass them in the 4th argument, or the script stops with "... is not known: pass it as
169
+ executeScript(body, bars, inputs, { ... })".
170
+
171
+ ```js
172
+ indicator("Session marks", { overlay: true });
173
+ const newDay = timeframe.change("D");
174
+ bgcolor(color.when(newDay, color.new(color.blue, 80)), { title: "New day" });
175
+ plotchar(barstate.islast, "Last", { char: "L", location: location.abovebar });
176
+ plot(close, "Close", { display: "none" });
177
+ ```
178
+
179
+ ## Drawings
180
+
181
+ `label.new`, `line.new`, `box.new`, `polyline.new`, `linefill.new` and `table.new` create objects; the host draws
182
+ the ones alive after the last bar. Create them in a bar loop with bar indexes (`c.i`) or bar times. Old objects
183
+ are deleted above `max_*_count` (default 50). To keep one object and move it, store it in a variable and update
184
+ it with the `set_*` functions, or hold it with `varDrawing()` (Pine `var line l = na`). See
185
+ [plotting.md](plotting.md#drawings).
@@ -0,0 +1,140 @@
1
+ # Strategies
2
+
3
+ A strategy is a script declared with `strategy()` whose orders go to an order engine. OakScriptJS does not fill
4
+ orders itself: the host supplies the engine (an app's backtester, or your own), which fills the orders, keeps the
5
+ trades and computes the results the host shows (net profit, drawdown, list of trades...).
6
+
7
+ ## Declaration and sizing
8
+
9
+ ```js
10
+ strategy("Breakout", {
11
+ overlay: true, // default false
12
+ initial_capital: 10000, // default 100000
13
+ default_qty_type: strategy.percent_of_equity, // or strategy.fixed (units), strategy.cash (amount)
14
+ default_qty_value: 25, // default 100 (% of equity)
15
+ pyramiding: 0, // entries allowed in the same direction beyond the first
16
+ commission_type: strategy.commission.percent, // or .cash_per_contract, .cash_per_order
17
+ commission_value: 0.1,
18
+ slippage: 2, // ticks added to market and stop fills
19
+ });
20
+ const length = input.int(20, "Channel length", { minval: 2 });
21
+ const upper = ta.highest(high, length).offset(1);
22
+ const lower = ta.lowest(low, length).offset(1);
23
+ plot(upper, "Upper", { color: color.green });
24
+ plot(lower, "Lower", { color: color.red });
25
+ strategy.eachBar((c) => {
26
+ if (strategy.position_size <= 0) strategy.entry("Long", strategy.long, { stop: c.get(upper) });
27
+ if (strategy.position_size >= 0) strategy.entry("Short", strategy.short, { stop: c.get(lower) });
28
+ });
29
+ ```
30
+
31
+ Other properties, with the Pine Script v6 names and defaults: `currency`, `margin_long` / `margin_short` (100),
32
+ `process_orders_on_close` (false), `close_entries_rule` (`"FIFO"`), `calc_on_order_fills`, `calc_on_every_tick`,
33
+ `use_bar_magnifier`, `fill_orders_on_standard_ohlc`, `backtest_fill_limits_assumption`, `risk_free_rate` (2).
34
+ A host can override them from its settings.
35
+
36
+ Order size: the `qty` option of an order (units) wins over `default_qty_type` / `default_qty_value`.
37
+
38
+ ## The bar loop
39
+
40
+ `strategy.eachBar((c) => ...)` is the strategy's `eachBar` (same `c`, returns a Series) and runs once per script.
41
+ On each bar:
42
+
43
+ 1. the engine fills the orders placed before (market orders at the bar's open; stop / limit orders when the
44
+ bar reaches their price);
45
+ 2. the callback runs at the bar's close: read the state, place, change or cancel orders;
46
+ 3. with `process_orders_on_close: true`, new market orders fill at that close; else at the next bar's open.
47
+
48
+ Order calls and per-bar `strategy.*` reads throw outside `strategy.eachBar()`. Compute every Series before it.
49
+
50
+ ## Orders
51
+
52
+ | Call | Use |
53
+ |---|---|
54
+ | `strategy.entry(id, strategy.long / strategy.short, { qty, limit, stop, oca_name, oca_type, comment, alert_message })` | open a position; an entry in the other direction reverses the position; market order without `limit` / `stop` |
55
+ | `strategy.order(id, direction, { qty, limit, stop, ... })` | a raw order: adds to or reduces the position, no reversal logic |
56
+ | `strategy.exit(id, { from_entry, qty, qty_percent, stop, limit, loss, profit, trail_price, trail_points, trail_offset, comment, ... })` | stop loss / take profit / trailing stop for an entry; `stop` / `limit` / `trail_price` are prices, `loss` / `profit` / `trail_points` / `trail_offset` are in ticks |
57
+ | `strategy.close(id, { qty, qty_percent, comment, immediately })` | close the trades of entry `id` at market |
58
+ | `strategy.close_all({ comment, immediately })` | close the whole position at market |
59
+ | `strategy.cancel(id)`, `strategy.cancel_all()` | cancel pending orders |
60
+
61
+ - A call with the id of a pending order changes that order (calling `strategy.exit` on every bar with new prices
62
+ moves the stop).
63
+ - `NaN` in an option means "not given" (it is removed), as `na` in Pine.
64
+ - `strategy.exit` placed while the entry is still pending waits for it (`from_entry`).
65
+
66
+ ```js
67
+ strategy("Trailing exit", { overlay: true, initial_capital: 10000, default_qty_type: strategy.fixed, default_qty_value: 10 });
68
+ const fast = ta.ema(close, 12);
69
+ const slow = ta.ema(close, 26);
70
+ const atr = ta.atr(14);
71
+ const goLong = ta.crossover(fast, slow);
72
+ const goFlat = ta.crossunder(fast, slow);
73
+ strategy.eachBar((c) => {
74
+ if (c.get(goLong) === 1) strategy.entry("Long", strategy.long, { comment: "EMA cross" });
75
+ if (c.get(goFlat) === 1) strategy.close("Long", { comment: "Cross down" });
76
+ if (strategy.position_size > 0) {
77
+ const trade = strategy.opentrade(0); // Pine strategy.opentrades.*(0)
78
+ const stop = trade.entry_price - 2 * c.get(atr);
79
+ strategy.exit("Stop", { from_entry: "Long", stop, comment: "ATR stop" });
80
+ }
81
+ });
82
+ plot(fast, "Fast");
83
+ plot(slow, "Slow");
84
+ ```
85
+
86
+ ## Reading the results
87
+
88
+ Inside `strategy.eachBar()`, from the engine, for the current bar:
89
+
90
+ | Variable | Meaning |
91
+ |---|---|
92
+ | `strategy.position_size` | signed quantity: > 0 long, < 0 short, 0 flat |
93
+ | `strategy.position_avg_price`, `strategy.position_entry_name` | average entry price (NaN when flat), entry id |
94
+ | `strategy.equity`, `strategy.initial_capital` | initial capital + net profit + open profit |
95
+ | `strategy.netprofit`, `strategy.openprofit`, `strategy.grossprofit`, `strategy.grossloss` (and `_percent`) | profits |
96
+ | `strategy.opentrades`, `strategy.closedtrades`, `strategy.wintrades`, `strategy.losstrades`, `strategy.eventrades` | trade counts |
97
+ | `strategy.max_drawdown`, `strategy.max_runup`, `strategy.avg_trade`... | statistics |
98
+ | `strategy.opentrade(i)`, `strategy.closedtrade(i)` | trade `i` (0 = oldest): `{ entry_id, entry_price, entry_bar_index, entry_time, size, profit, commission, exit_price, exit_bar_index, ... }`, undefined when there is none |
99
+ | `strategy.default_entry_qty(price)` | the quantity a default-size entry would get |
100
+
101
+ Pine's `strategy.closedtrades.profit(i)` is `strategy.closedtrade(i)?.profit`. `strategy.initial_capital` and
102
+ `strategy.account_currency` can be read anywhere.
103
+
104
+ ```js
105
+ strategy("Equity curve", { overlay: false, initial_capital: 5000, default_qty_type: strategy.cash, default_qty_value: 1000 });
106
+ strategy.risk.allow_entry_in(strategy.direction.long); // risk rules: top level, after strategy()
107
+ const rsi = ta.rsi(close, 14);
108
+ const equity = strategy.eachBar((c) => {
109
+ if (c.get(rsi) < 30) strategy.entry("Long", strategy.long);
110
+ if (c.get(rsi) > 70) strategy.close_all({ comment: "RSI > 70" });
111
+ const last = strategy.closedtrade(strategy.closedtrades - 1);
112
+ if (last && last.exit_bar_index === c.i && last.profit < 0) alert("Losing trade closed");
113
+ return strategy.equity; // the loop's Series: equity per bar
114
+ });
115
+ plot(equity, "Equity", { color: color.teal });
116
+ hline(5000, "Initial capital", { color: color.gray });
117
+ ```
118
+
119
+ Risk rules: `strategy.risk.allow_entry_in(dir)`, `max_drawdown(value, type)`, `max_intraday_loss(value, type)`,
120
+ `max_cons_loss_days(count)`, `max_intraday_filled_orders(count)`, `max_position_size(contracts)` (type:
121
+ `"percent_of_equity"` or `"cash"`). An engine that does not support a rule or a variable throws
122
+ "... is not provided by the strategy engine".
123
+
124
+ ## The host side
125
+
126
+ A host passes an engine factory to `executeScript`:
127
+
128
+ ```ts
129
+ import { executeScript, type StrategyEngine } from 'oakscriptjs/script';
130
+
131
+ const run = executeScript(body, bars, inputs, chart, {
132
+ strategyEngine: ({ properties, bars, chart }) => new MyEngine(properties, bars), // implements StrategyEngine
133
+ });
134
+ // run.strategyConfig: the resolved properties
135
+ ```
136
+
137
+ `StrategyEngine` methods: `processBar(i)`, `processClose(i)`, `entry`, `order`, `exit`, `close`, `close_all`,
138
+ `cancel`, `cancel_all`, `get(variable)`, `openTrade(i)`, `closedTrade(i)`, and optionally `risk(rule)`,
139
+ `defaultEntryQty(price)`. The option objects keep the Pine names. Without an engine, `strategy()` declares the
140
+ strategy and `strategy.eachBar()` throws "no strategy engine".