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,198 @@
1
+ ---
2
+ name: oakscript
3
+ description: Write, port, review or fix OakScript scripts - indicators and strategies written in JavaScript with the oakscriptjs/script API, a Pine Script v6-style API. Use when asked to create or debug an indicator, oscillator, overlay, signal, alert or backtest strategy for OakScriptJS or for an app that runs OakScript scripts (such as the OpenTrader script editor), or to port a Pine Script v6 script to JavaScript.
4
+ ---
5
+
6
+ # Writing OakScript scripts
7
+
8
+ An OakScript script is **plain JavaScript** that reads like Pine Script v6. The host (a chart app, a worker,
9
+ a test) runs the whole script body on all the bars at once, and runs it again when the bars or the inputs change.
10
+
11
+ ```js
12
+ indicator("My SMA", { overlay: true }); // one declaration: indicator() or strategy()
13
+ const length = input.int(20, "Length", { minval: 1 }); // an input: declared AND read here
14
+ const sma = ta.sma(close, length); // Series math, all bars at once
15
+ plot(sma, "SMA", { color: color.blue, linewidth: 2 }); // an output
16
+ plotshape(ta.crossover(close, sma), "Cross", { style: shape.triangleup, location: location.belowbar });
17
+ ```
18
+
19
+ ## The script format
20
+
21
+ - The file is the script body: top-level statements, no wrapper function, no `export`.
22
+ - Every export of `oakscriptjs/script` is a global: `indicator`, `strategy`, `input`, `plot`, `ta`, `color`,
23
+ `close`, `request`, `eachBar`, `label`, `table`... No import is needed.
24
+ - Write JavaScript, not TypeScript: the app editor runs the source without a compile step, so type annotations
25
+ are syntax errors.
26
+ - Other modules: `import { array, matrix, map, callsite } from "oakscriptjs";` at the top of the script (these
27
+ are not globals). Use JS arrays and `Map` when you can.
28
+ - Outside an app, a host passes the body as a function: see [Running a script](#running-a-script).
29
+ - A source that exports `calculate(bars, inputs, chart)` is an older format (built-in indicator sources use it).
30
+ Write new scripts in the format above; a strategy must use it.
31
+
32
+ ## Workflow
33
+
34
+ 0. **Write the spec first** when you can run a command: the user's request as a small JSON file (title, pane,
35
+ inputs and defaults, outputs, what the signals look like, `"scenarios": true`) and, for the behaviour it cannot
36
+ count ("Buy only when the RSI is below 30"), a checks file; `oakscript check` checks both (step 6). An unknown spec
37
+ key is an error. See [check](reference/check.md#the-spec).
38
+ 1. **Declare** with `indicator(title, { overlay })` (overlay `true`: on the price pane; `false`, the default: own pane) or
39
+ `strategy(title, { ...properties })`. Exactly one declaration, at the top.
40
+ 2. **Inputs**: `input.int / float / bool / string / color / source / timeframe / session / symbol / time`.
41
+ They return plain values (a Series for `input.source`). The input id is the title in snake case
42
+ ("Fast length" is `fast_length`): hosts pass overrides by id.
43
+ 3. **Compute with Series** before any loop: `ta.*`, the price builtins and the Series methods
44
+ (`.add .sub .mul .div .gt .lt .eq .and .or .not .iff .offset`).
45
+ 4. **Per-bar state** (Pine `var`, `:=`, `if` on series values, loops): `eachBar((c) => ...)`; for a strategy,
46
+ `strategy.eachBar((c) => ...)` holds the orders.
47
+ 5. **Outputs**: `plot`, `hline`, `fill`, `plotshape`, `plotchar`, `plotarrow`, `bgcolor`, `barcolor`,
48
+ `plotcandle`, drawings (`label.new`, `line.new`, `box.new`, `table.new`), `alertcondition`, `alert`.
49
+ 6. **Check it**: `npx oakscript check script.js --spec spec.json` type-checks the script, runs it on 1,500 bars
50
+ and reports errors with their line, the inputs and outputs, the values and signal counts, the failed spec items and
51
+ warnings (an empty plot, a signal on every bar or never, an input that changes nothing). Fix until the exit code is
52
+ 0, or 2 with warnings you have read. For a port, `--export chart-export.csv` compares the values bar by bar. See
53
+ [check](reference/check.md).
54
+ 7. **Without a command**, check by hand: compile errors, runtime errors, then the plot values on the last bars
55
+ (an empty plot means NaN everywhere). See [errors](reference/errors.md).
56
+ 8. Start from the closest [example](#examples) rather than from scratch.
57
+
58
+ ## Rules
59
+
60
+ 1. **No operators on Series.** JavaScript has no operator overloading: `close - open`, `rsi > 50` and
61
+ `` `${rsi}` `` throw a TypeError, and `if (ta.crossover(a, b))` is always true (an object, no error). Write `close.sub(open)`, `a.gt(b)`, `cond.iff(x, y)`,
62
+ `x.offset(1)` (Pine `x[1]`), `a.and(b)`, `a.not()`. Conditions are Series of 1 / 0.
63
+ 2. **Plain numbers inside the bar loops.** In `eachBar` / `strategy.eachBar`, `c.close`, `c.get(series)`,
64
+ `c.get(series, 1)` are numbers: JS operators, `if`, `for` work. A condition reads `c.get(cond) === 1`.
65
+ 3. **Compute `ta.*` outside the loops.** A `ta.*` call inside a bar callback throws: compute the Series before
66
+ the loop and read it with `c.get()`.
67
+ 4. **Pine constants exist** as in Pine: `shape.*`, `location.*`, `size.*`, `xloc.*`, `yloc.*`, `extend.*`,
68
+ `display.*`, `format.*`, `font.*`, `scale.*`, `order.*`, `currency.*`, `label.style_*`, `line.style_*`,
69
+ `dayofweek.*`, `color.*`, `plot.style_*`, `plot.linestyle_*`, `hline.style_*`, `strategy.*`, `barmerge.*`,
70
+ `position.*`, `text.*`, `alert.freq_*`, `session.*`, `barstate.*`. Their values are strings
71
+ (`shape.triangleup` is `"triangleup"`), which also work.
72
+ 5. **Arguments**: Pine's named arguments are an object after the leading arguments, with Pine's names:
73
+ `plot(series, title, { color, linewidth, style })`, `input.int(14, { title: "Length", minval: 1 })`,
74
+ `request.security(sym, "W", () => open, { lookahead: barmerge.lookahead_on })`, `strategy.exit(id, { from_entry, stop })`,
75
+ `label.new(c.i, c.high, "A", { style: label.style_label_down, textcolor: color.white })`. Drawing functions
76
+ also take all their arguments by name (`line.new({ x1, y1, x2, y2, extend: extend.right })`); an unknown name
77
+ throws with the closest names. In a bar loop a Series argument of a drawing is its value on the current bar
78
+ (`label.new(bar_index, high, "A")`).
79
+ 6. **na is NaN.** There is no `na` constant: use `NaN` (`undefined` for a colour). `na(x)` and `nz(x, y)` take
80
+ numbers (use them in a bar loop); on a Series use `x.iff(x, 0)` for `nz(x)`, `fixnan(x)`, or a bar loop.
81
+ Plots skip NaN bars. JS comparisons with NaN are false; for the Pine rules (1e-10 tolerance) use
82
+ `compare.gt(a, b)`, `compare.eq(a, b)`...
83
+ 7. **Per-bar colours** are arrays with one colour per bar: `color.when(cond, colorTrue, colorFalse?)`
84
+ (omitted: na), or an array you fill in a bar loop. `color.new(c, transp)` sets the transparency (0-100).
85
+ 8. **Strategies**: order calls (`strategy.entry/exit/close/...`) and per-bar reads (`strategy.position_size`,
86
+ `strategy.equity`, `strategy.netprofit`...) only inside `strategy.eachBar()`, which runs once per script.
87
+ 9. **Other timeframes**: compute the series **inside** the expression:
88
+ `request.security("", "D", () => ta.ema(close, 20))`, never `() => myEma`.
89
+ 10. **No future bars**: offsets are 0 or positive; history reaches at most 5000 bars back.
90
+ 11. **Self-contained and fast**: no network, timers or DOM; the same bars give the same output. Hosts stop a
91
+ run that takes too long (seconds), so avoid nested loops over all bars.
92
+
93
+ ## Pine Script v6 at a glance
94
+
95
+ | Pine Script v6 | OakScript |
96
+ |---|---|
97
+ | `//@version=6` | nothing |
98
+ | `a + b`, `a > b`, `c ? x : y` (series) | `a.add(b)`, `a.gt(b)`, `c.iff(x, y)` |
99
+ | `close[1]` | `close.offset(1)`; in a loop `c.get(close, 1)` |
100
+ | `var x = 0` / `x := ...` | `let x = 0` before `eachBar`, assigned in the callback |
101
+ | `bar_index`, `last_bar_index` | the Series `bar_index`, `last_bar_index`; in a bar loop `c.i`, `c.n - 1` |
102
+ | `plot(x, "T", color=c, linewidth=2)` | `plot(x, "T", { color: c, linewidth: 2 })` |
103
+ | `[a, b, c] = ta.macd(...)` | `const [a, b, c] = ta.macd(...)` |
104
+ | `if cond` + `strategy.entry(...)` | `strategy.eachBar((c) => { if (c.get(cond) === 1) strategy.entry(...) })` |
105
+
106
+ Full table, the deviations and the unsupported parts: [reference/pine-differences.md](reference/pine-differences.md).
107
+
108
+ ## Common mistakes
109
+
110
+ - `if (series)`, `series ? a : b` at the top level: always true, with no error; use Series methods (rule 1).
111
+ - `close - open`, `ma > 50`: a TypeError; use Series methods (rule 1).
112
+ - `ta.sma(...)` inside `eachBar`: an error; compute it before the loop (rule 3).
113
+ - `ta.stoch(close, high, low, 14)` takes all four arguments, as in Pine.
114
+ - A per-bar colour from a JS ternary on a Series (`cond ? green : red`): one colour; use `color.when()`.
115
+ - `strategy.position_size` at the top level: read it inside `strategy.eachBar()`.
116
+ - Two `indicator()` calls, or `indicator()` and `strategy()` in one script: one declaration only.
117
+ - Pine named arguments with `=` (`label.new(x, y, text = "A", style = ...)`): an object, `{ style: ... }`.
118
+ - `strategy.opentrades.entry_price(0)`: `strategy.opentrades` is the count (a number); use
119
+ `strategy.opentrade(0)?.entry_price` (same for `strategy.closedtrades.*` with `strategy.closedtrade(i)`).
120
+
121
+ ## Reading errors
122
+
123
+ - **Syntax errors** come with the line and column of your source (the app editor maps them to your lines).
124
+ - **Runtime errors** stop the run: the message names the call and the fix, e.g. `strategy.entry() must be
125
+ called inside strategy.eachBar().`, `ta.smaa does not exist. Did you mean ta.sma?`, `JavaScript operators
126
+ (+ - * / ...) do not work on a Series: use its methods...` A host that runs a declaration pass first (zero bars) reports declaration
127
+ errors when the script is saved.
128
+ - **No error, wrong output**: an empty plot (NaN everywhere: a NaN input to the math, a division by zero),
129
+ a branch taken on every bar (a Series used as a JS boolean). Check the plot values on the last bars.
130
+
131
+ The messages and their fixes: [reference/errors.md](reference/errors.md).
132
+
133
+ ## Examples
134
+
135
+ Complete scripts, run by the package's CI on synthetic bars:
136
+
137
+ - [examples/ma-cross.js](examples/ma-cross.js): overlay, inputs (incl. a choice list), two MAs, a per-bar
138
+ coloured fill, cross markers, alert conditions.
139
+ - [examples/macd-oscillator.js](examples/macd-oscillator.js): own pane, tuple from `ta.macd`, four-colour
140
+ histogram built in a bar loop, zero line, background.
141
+ - [examples/mtf-trend-filter.js](examples/mtf-trend-filter.js): `request.security` on a higher timeframe
142
+ (tuple), `var`-style state in `eachBar`, signal labels, a label on the last bar.
143
+ - [examples/ma-cross-strategy.js](examples/ma-cross-strategy.js): `strategy()` with sizing and commission,
144
+ long / short entries, stop loss and take profit with `strategy.exit`, a results table.
145
+
146
+ ## Reference
147
+
148
+ - [reference/script-structure.md](reference/script-structure.md): declarations, inputs, Series, `eachBar`,
149
+ builtins (`time`, `barstate`, `timeframe`, `syminfo`), drawings' lifetime.
150
+ - [reference/plotting.md](reference/plotting.md): lines, histograms, shapes, characters, arrows, fills,
151
+ colours, background and bar colours, candles, labels, lines, boxes, tables, alerts.
152
+ - [reference/timeframes-symbols.md](reference/timeframes-symbols.md): `request.security` (higher and lower
153
+ timeframes, other symbols, tuples, lookahead), `request.security_lower_tf`, `timeframe.*`.
154
+ - [reference/strategies.md](reference/strategies.md): properties, sizing, orders, exits, reading the results,
155
+ risk rules, the host's order engine.
156
+ - [reference/pine-differences.md](reference/pine-differences.md): Pine Script v6 to OakScript, what is not
157
+ supported.
158
+ - [reference/errors.md](reference/errors.md): compiler and runtime errors, silent mistakes.
159
+ - [reference/check.md](reference/check.md): `oakscript check`, its warnings, the spec and reference files.
160
+
161
+ ## Type-checking
162
+
163
+ `oakscript check` runs this type check itself. On its own: `oakscriptjs/script/globals` types the script globals; a
164
+ `tsconfig.json` next to the scripts, then `npx tsc -p .` (one error per mistake, with its line; no output means no
165
+ type error):
166
+
167
+ ```json
168
+ {
169
+ "compilerOptions": {
170
+ "allowJs": true, "checkJs": true, "noEmit": true, "strict": true, "noImplicitAny": false,
171
+ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "moduleDetection": "force",
172
+ "lib": ["ES2022"], "types": ["oakscriptjs/script/globals"]
173
+ },
174
+ "include": ["*.js"]
175
+ }
176
+ ```
177
+
178
+ It needs `oakscriptjs` and `typescript` installed in the project. The checker does not see a Series used as a
179
+ JavaScript boolean (`if (cond)`, `cond ? a : b` with a Series `cond`): check those by hand (rule 1).
180
+
181
+ ## Running a script
182
+
183
+ In an app (OpenTrader's script editor) save the script: the app compiles it, then runs it on the chart bars.
184
+ In your own code, wrap the body in a function and pass it to `executeScript` (imports needed there):
185
+
186
+ ```ts
187
+ import { executeScript, indicator, input, plot, ta, close } from 'oakscriptjs/script';
188
+
189
+ const run = executeScript(() => {
190
+ indicator('My SMA', { overlay: true });
191
+ plot(ta.sma(close, input.int(20, 'Length')), 'SMA');
192
+ }, bars, { length: 50 }, { timeframe: '60', timezone: 'Etc/UTC', session: '24x7' });
193
+ // run.result.plots.plot0 -> [{ time, value }, ...]; run.inputConfig, run.plotConfig, run.strategyConfig...
194
+ ```
195
+
196
+ Bars are `{ time, open, high, low, close, volume }`, oldest first, `time` in UNIX seconds. The chart context
197
+ (4th argument) gives the timeframe, time zone and session that `request.security`, `time()` and
198
+ `timeframe.*` need. A strategy also needs an order engine: see [strategies](reference/strategies.md#the-host-side).
@@ -0,0 +1,57 @@
1
+ // Moving-average cross strategy: long and short entries on the crosses, a stop loss
2
+ // and a take profit from the entry price, and a results table on the last bar.
3
+ strategy("MA Cross Strategy", {
4
+ shorttitle: "MA X Strat",
5
+ overlay: true,
6
+ initial_capital: 10000,
7
+ default_qty_type: strategy.percent_of_equity,
8
+ default_qty_value: 50,
9
+ commission_type: strategy.commission.percent,
10
+ commission_value: 0.05,
11
+ });
12
+
13
+ const fastLength = input.int(10, "Fast length", { minval: 1 });
14
+ const slowLength = input.int(30, "Slow length", { minval: 1 });
15
+ const stopPercent = input.float(2, "Stop loss %", { minval: 0.1, step: 0.1 });
16
+ const takePercent = input.float(4, "Take profit %", { minval: 0.1, step: 0.1 });
17
+
18
+ // Series math happens before the bar loop, on all bars at once.
19
+ const fast = ta.sma(close, fastLength);
20
+ const slow = ta.sma(close, slowLength);
21
+ const longSignal = ta.crossover(fast, slow);
22
+ const shortSignal = ta.crossunder(fast, slow);
23
+
24
+ plot(fast, "Fast", { color: color.teal });
25
+ plot(slow, "Slow", { color: color.orange });
26
+
27
+ // Orders and strategy.* reads only work inside strategy.eachBar(): the callback runs at the
28
+ // close of each bar; market orders fill at the open of the next bar.
29
+ strategy.eachBar((c) => {
30
+ if (c.get(longSignal) === 1) strategy.entry("Long", strategy.long);
31
+ if (c.get(shortSignal) === 1) strategy.entry("Short", strategy.short);
32
+
33
+ const size = strategy.position_size;
34
+ const entryPrice = strategy.position_avg_price;
35
+ if (size > 0) {
36
+ strategy.exit("Long exit", {
37
+ from_entry: "Long",
38
+ stop: entryPrice * (1 - stopPercent / 100),
39
+ limit: entryPrice * (1 + takePercent / 100),
40
+ });
41
+ } else if (size < 0) {
42
+ strategy.exit("Short exit", {
43
+ from_entry: "Short",
44
+ stop: entryPrice * (1 + stopPercent / 100),
45
+ limit: entryPrice * (1 - takePercent / 100),
46
+ });
47
+ }
48
+
49
+ // Results so far, read from the order engine.
50
+ if (c.i === c.n - 1) {
51
+ const t = table.new(position.top_right, 2, 2, color.new(color.black, 20));
52
+ table.cell(t, 0, 0, "Net profit", 0, 0, color.white);
53
+ table.cell(t, 1, 0, str.tostring(strategy.netprofit, "#.##"), 0, 0, color.white);
54
+ table.cell(t, 0, 1, "Closed trades", 0, 0, color.white);
55
+ table.cell(t, 1, 1, str.tostring(strategy.closedtrades), 0, 0, color.white);
56
+ }
57
+ });
@@ -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: shape.triangleup, location: location.belowbar, color: color.teal, size: size.small });
31
+ plotshape(crossDown, "Cross down", { style: shape.triangledown, location: location.abovebar, color: color.red, size: 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: shape.labelup, location: location.belowbar, color: color.teal, text: "L", textcolor: color.white });
37
+ plotshape(signal.eq(-1), "Short", { style: shape.labeldown, location: 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, { color: color.gray, style: label.style_label_down, textcolor: color.white });
45
+ }
46
+ });
@@ -0,0 +1,150 @@
1
+ # Checking a script: `oakscript check`
2
+
3
+ `oakscript check` runs a script the way an app runs it (the declarations on zero bars, then a run on bars) and
4
+ prints what it declares and what it outputs, the errors with their line, warnings for the mistakes that give no error,
5
+ and the result of a spec: the inputs, outputs and signals the user asked for.
6
+
7
+ ```sh
8
+ npx oakscript check my-indicator.js # synthetic bars (1,500, hourly)
9
+ npx oakscript check my-indicator.js --spec spec.json # + the user's spec (and its checks file)
10
+ npx oakscript check my-indicator.js --scenarios # + edge-case bars
11
+ npx oakscript check my-port.js --export chart-export.csv # + bars and expected values of a chart export
12
+ ```
13
+
14
+ Exit code: `0` clean, `2` warnings only, `1` an error or a failed spec / reference check. Run it after each change
15
+ until it gives `0` (or `2` with warnings you have read and explained).
16
+
17
+ ## Workflow
18
+
19
+ 1. Write the spec from the user's request **before** the script (below): title, pane, inputs with their defaults,
20
+ outputs, what the signals should look like; and a checks file for the behaviour the spec cannot count ("Buy only
21
+ when the RSI is below 30"). Add `"scenarios": true`.
22
+ 2. Write the script.
23
+ 3. `npx oakscript check script.js --spec spec.json`; fix the errors, then the failed spec items and checks, then
24
+ read the warnings.
25
+ 4. For a port of a Pine script with known values, add `--export` (or `--bars` and `--reference`).
26
+
27
+ ## What it checks
28
+
29
+ | Stage | Reports |
30
+ |---|---|
31
+ | types | TypeScript errors of the script with the script globals (`TS2362` operator on a Series, `TS2551` unknown name...), when `typescript` is installed in the project; `--no-types` skips it |
32
+ | syntax | the JavaScript syntax error and its line |
33
+ | declaration | errors of the top-level code (the declaration run of an app), e.g. `close - open` |
34
+ | run | errors on the bars (inside `eachBar`), with the line |
35
+ | timeout | a run longer than `--timeout` (default 10 s) |
36
+ | spec | an unknown key or a wrong value in the spec (`"signal"` for `"signals"`): a misspelt spec would check nothing |
37
+ | scenario | an error on the edge-case bars of `--scenarios` |
38
+
39
+ Then, on a clean run: the inputs (id, type, default, options), each plot (number of values, first bar, min / max,
40
+ last 5 values, colours), each signal (shapes, characters, arrows, alert conditions, background and bar colours: bars,
41
+ share, first and last bar), drawings, `alert()` calls, and for a strategy the trades (count, wins, net profit, the
42
+ first trades) on a small built-in order engine (next-bar-open fills, stop / limit exits).
43
+
44
+ ## Warnings
45
+
46
+ | Code | Meaning |
47
+ |---|---|
48
+ | `empty-plot` | a plot with no value (na on every bar) |
49
+ | `constant-plot` | the same value on every bar (a level is an `hline()`) |
50
+ | `late-start` | the first value after half of the bars |
51
+ | `signal-never` | a signal that is never true |
52
+ | `signal-everywhere` | a signal on more than half of the bars: often a Series used as a JavaScript boolean |
53
+ | `one-colour` | per-bar colours that are the same on every bar |
54
+ | `unused-input` | changing the input changes no output (the check runs again with each input changed) |
55
+ | `input-error` | another valid value of the input stops the script |
56
+ | `no-trades` | a strategy without any trade |
57
+ | `no-output` | nothing is drawn |
58
+
59
+ A warning is not always a mistake (a rare signal on 1,500 bars): read it and keep it when it is what the user wants.
60
+
61
+ ## The spec
62
+
63
+ A JSON file; every entry is optional and only the given ones are checked. Inputs, plots and signals are found by id
64
+ or title (case insensitive). An unknown key or a wrong value is an error (with the closest key), so a misspelt entry
65
+ does not pass unchecked. Its JSON Schema is `node_modules/oakscriptjs/skills/oakscript/spec.schema.json`: point
66
+ `$schema` at it to get the keys in an editor.
67
+
68
+ ```json
69
+ {
70
+ "$schema": "./node_modules/oakscriptjs/skills/oakscript/spec.schema.json",
71
+ "kind": "indicator",
72
+ "title": "MACD Histogram",
73
+ "overlay": false,
74
+ "inputs": {
75
+ "fast_length": { "type": "int", "defval": 12 },
76
+ "Source": { "type": "source", "defval": "close" },
77
+ "MA type": { "options": ["SMA", "EMA"] }
78
+ },
79
+ "outputs": ["MACD", "Signal", { "title": "Zero", "kind": "hline" }],
80
+ "plots": { "MACD": { "warmup": 40 }, "RSI": { "min": 0, "max": 100 } },
81
+ "signals": { "Buy": { "min": 1, "maxShare": 0.1 } },
82
+ "trades": { "min": 1 },
83
+ "checks": "checks.js",
84
+ "scenarios": true,
85
+ "reference": "expected.csv",
86
+ "tolerance": 1e-10
87
+ }
88
+ ```
89
+
90
+ - `outputs`: titles that must exist; `kind` is one of `plot`, `shape`, `char`, `arrow`, `candle`, `bar`, `hline`,
91
+ `bgcolor`, `barcolor`, `alertcondition`.
92
+ - `plots`: `min` / `max` bound every value, `warmup` is the latest bar of the first value.
93
+ - `signals`: `min` / `max` count bars, `minShare` / `maxShare` the share of the bars (0 to 1).
94
+ - `checks`, `scenarios`, `reference`, `export`, `tolerance`: as `--checks`, `--scenarios`, `--reference`, `--export`,
95
+ `--tolerance` (paths relative to the spec file).
96
+
97
+ ## Behaviour checks
98
+
99
+ What a spec cannot write as counts and ranges goes in a checks file: plain JavaScript, the body of a function of `t`
100
+ (no import or export). `t.plot(name)` gives a plot per bar (`values`, `at(i)`, `colors`), `t.signal(name)` the bars of
101
+ a signal (`bars`, `on(i)`), `t.trades` the closed trades of a strategy, `t.input(name)` the value of an input, `t.bars`
102
+ and `t.n` the bars. Record each check with `t.expect(name, ok, detail?)` or `t.everyBar(name, (i) => ..., { from, to })`
103
+ (it fails on the first bar where the function is false).
104
+
105
+ ```js
106
+ const rsi = t.plot("RSI");
107
+ t.everyBar("RSI between 0 and 100", (i) => rsi.at(i) === null || (rsi.at(i) >= 0 && rsi.at(i) <= 100));
108
+ t.expect("Buy only when the RSI is below 30", t.signal("Buy").bars.every((i) => rsi.at(i) < 30));
109
+ t.expect("Buy right after the RSI was at or above 30", t.signal("Buy").bars.every((i) => rsi.at(i - 1) >= 30));
110
+ ```
111
+
112
+ The checks run on the main run; their results come after the spec items. An error in the file (an unknown plot, a
113
+ typo) fails the checks with its line.
114
+
115
+ ## Scenarios
116
+
117
+ `--scenarios` (or `"scenarios": true`) runs the script again on edge-case bars: 30 bars (a short history), 800 daily
118
+ bars, 1,000 one-minute bars, 200 flat bars (zero ranges, no volume), 500 bars without volume, and a 3-day gap. An
119
+ error there fails the check; a plot with no value that the main run does not have is listed per scenario (on 30
120
+ bars it is often expected, a division by a zero range is not).
121
+
122
+ ## Bars and reference files
123
+
124
+ - `--bars`: CSV with the columns `time` (UNIX seconds or milliseconds, or a date), `open`, `high`, `low`, `close`,
125
+ optional `volume`; or a JSON array of `{ time, open, high, low, close, volume }`. The timeframe comes from the bar
126
+ spacing unless `--timeframe` gives it. Other symbols of `request.security` get synthetic bars.
127
+ - `--reference`: CSV with a `time` column and one column per plot (id or title); empty or `na` is na. A value is
128
+ equal when `|a - b| <= tolerance * max(1, |a|, |b|)` (default `1e-10`) or both are na. Use the bars the reference
129
+ was computed on. The bar columns (`open`, `high`, `low`, `close`, `volume`) are skipped.
130
+ - `--export`: one CSV with both: the chart data exported with the original script on the chart (a `time` column,
131
+ the bars, then one column per plot title, as charting apps export them). The bars are the bars of the run, the
132
+ other columns the expected values; a column with no plot of that title (another script on the chart) is ignored.
133
+
134
+ ## Other options
135
+
136
+ `--inputs '{"length": 50}'` (input values by id), `--count 3000` (synthetic bars), `--timeframe D`, `--no-sweep`
137
+ (no reruns for unused inputs), `--json` (the whole report as JSON).
138
+
139
+ ## From code
140
+
141
+ ```ts
142
+ import { assertCheck, checkScript } from 'oakscriptjs/check';
143
+ const report = checkScript(source, { spec, bars, scenarios: true, checks: (t) => { /* as a checks file */ } });
144
+ // report.ok, errors, warnings, plots, signals, spec, reference, scenarios
145
+
146
+ // in a test of any runner: throws the report as text when the check fails ({ warnings: false }: also on warnings)
147
+ assertCheck(checkScript(source, { spec }));
148
+ ```
149
+
150
+ `checkScript` runs anywhere (no Node API); the command adds the file lines, the time limit and the type check.
@@ -0,0 +1,92 @@
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. `npx oakscript check script.js` ([check](check.md))
5
+ reports all three, with the lines, and warns about the silent mistakes of the last table.
6
+
7
+ ## Type-check errors
8
+
9
+ | Error | Cause | Fix |
10
+ |---|---|---|
11
+ | `TS2362` / `TS2363` `The left-hand (right-hand) side of an arithmetic operation must be of type...` | `close - open`, `rsi * 2` | Series methods: `close.sub(open)`, `rsi.mul(2)` |
12
+ | `TS2365` `Operator '>' cannot be applied to types 'Series' and 'number'` | `rsi > 50`, `c.close > ta.sma(close, 5)` in a loop | `rsi.gt(50)`; in a loop `c.close > c.get(sma)` with `sma` computed before |
13
+ | `TS2339` / `TS2551` `Property 'smaa' does not exist on type...` | a misspelt or Pine-only name | the name it suggests, [pine-differences.md](pine-differences.md) |
14
+ | `TS2322` `Type '"triangel"' is not assignable to type 'ShapeStyle'` | a bad constant string | one of the strings it lists |
15
+ | `TS2561` / `TS2353` `Object literal may only specify known properties` | a misspelt option (`colour`) | the option it suggests |
16
+ | `TS2554` `Expected 4 arguments, but got 3` | a missing argument (`ta.stoch(close, high, low)`) | pass it: `ta.stoch(close, high, low, 14)` |
17
+ | `TS2339` `Property 'entry_price' does not exist on type 'number'` | `strategy.opentrades.entry_price(0)` (`strategy.opentrades` is the count) | `strategy.opentrade(0)?.entry_price`, `strategy.closedtrade(i)?.profit` |
18
+ | `TS2769` `No overload matches this call` on a drawing | a misspelt named argument (`{ textcolour: ... }`) | the Pine name (`textcolor`) |
19
+ | `TS8010` `Type annotations can only be used in TypeScript files` | TypeScript syntax | remove the types |
20
+
21
+ ## Compile errors (syntax)
22
+
23
+ The source is loaded as JavaScript: a syntax error comes with a line and a column of your source.
24
+
25
+ | Message | Cause | Fix |
26
+ |---|---|---|
27
+ | `Missing initializer in const declaration`, `Unexpected token ':'` | a TypeScript annotation (`const a: number = 5`) | remove the types: the script is JavaScript |
28
+ | `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 |
29
+ | `Unexpected token 'export'` / `'import'` at a wrong place | an import below other code, an export | imports first, no exports |
30
+
31
+ A host that compiles a script also runs it once on zero bars (a declaration pass: title, inputs, plots,
32
+ strategy properties). Errors in the declarations or in top-level code show up there, with the line.
33
+
34
+ ## Runtime errors
35
+
36
+ The run stops at the first error; the message names the call. The ones you will meet:
37
+
38
+ | Message | Cause | Fix |
39
+ |---|---|---|
40
+ | `TypeError: JavaScript operators (+ - * / % ** < > <= >= ==) do not work on a Series: use its methods...` | an operator on a Series: `close - open`, `rsi > 50`, `fast > slow ? a : b` | the method it lists: `close.sub(open)`, `rsi.gt(50)`, `fast.gt(slow).iff(a, b)`; a colour: `color.when(fast.gt(slow), a, b)` |
41
+ | `TypeError: A Series is not a text...` | a Series in a text: `` `RSI ${rsi}` ``, `"RSI " + rsi` | in a loop: `str.tostring(c.get(rsi))` |
42
+ | `ta.sma() was called inside eachBar(): a ta.* call computes the whole Series at once...` | a `ta.*` function in a bar callback | compute it before the loop, read it with `c.get(x)`; a Pine call in an `if` block: `callsite.*` |
43
+ | `TypeError: ta.smaa does not exist. Did you mean ta.sma?` (also `math.`, `str.`, `color.`, `input.`, `request.`) | a misspelt or Pine-only member | a suggested name, [pine-differences.md](pine-differences.md) |
44
+ | `TypeError: series.toArray is not a function` | a number or NaN passed where a Series is expected: `plot(5)`, `ta.sma(c.close, 10)` | a Series; a constant level: `hline(5)` |
45
+ | `xyz is not defined` | a global that does not exist (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` |
46
+ | `TypeError: Cannot read properties of undefined (reading 'toArray')` | a missing Series argument, e.g. `ta.stoch(close, high, low)` | pass it: `ta.stoch(close, high, low, 14)` |
47
+ | `TypeError: label.new(): unknown argument "textcolour". Did you mean textcolor?` | a misspelt named argument of a drawing | the name it suggests |
48
+ | `TypeError: input.int(): unknown argument "min". Did you mean minval?` | an input option that is not Pine's (`min`, `max`, `choices`) | Pine's name: `minval`, `maxval`, `options` |
49
+ | `TypeError: request.security(): unknown argument "lookahed"...` | a misspelt named argument of `request.security` | `gaps`, `lookahead`, `ignore_invalid_symbol` |
50
+ | `TypeError: label.new(): argument 1 is a Series, a drawing takes one value: call label.new() inside eachBar()...` | a drawing created at the top level with a Series (`label.new(bar_index, high)`) | create it in a bar loop |
51
+ | `TypeError: strategy.opentrades.entry_price is not a function` | `strategy.opentrades` is the count | `strategy.opentrade(0)?.entry_price` |
52
+ | `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 |
53
+ | `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 |
54
+ | `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 |
55
+ | `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` |
56
+ | `indicator(): the script already declared indicator(); a script has one declaration.` | two declarations | one `indicator()` or `strategy()` |
57
+ | `strategy.entry() must be called inside strategy.eachBar().` | an order at the top level | move it into `strategy.eachBar((c) => ...)` |
58
+ | `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 |
59
+ | `strategy.eachBar(): declare the script with strategy() first.` | `strategy.*` in an `indicator()` script | declare `strategy(...)` |
60
+ | `strategy.eachBar() runs once per script run.` | two strategy loops | one loop with all the logic |
61
+ | `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 |
62
+ | `request.security(): the expression returned a series computed on the chart bars...` | `request.security("", "D", () => myEma)` | compute inside: `() => ta.ema(close, 20)` |
63
+ | `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()` |
64
+ | `Invalid symbol: X` | a symbol without bars | check the ticker id, or `ignore_invalid_symbol = true` |
65
+ | `Invalid timeframe "XYZ"` | a bad timeframe string | `"1"`, `"60"`, `"240"`, `"D"`, `"W"`, `"M"`... |
66
+ | `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 |
67
+ | `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 |
68
+ | `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 |
69
+ | `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 |
70
+ | `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 |
71
+
72
+ ## Silent mistakes (no error, wrong output)
73
+
74
+ | Symptom | Cause | Fix |
75
+ |---|---|---|
76
+ | a branch taken on every bar | `if (ta.crossover(a, b))`, `cond ? x : y`, `!cond`, `cond && x` with a Series `cond`: an object is always true | `cond.iff(x, y)`, `cond.not()`, `cond.and(x)`, or `c.get(cond) === 1` in a loop |
77
+ | one colour on every bar | `color: cond ? color.green : color.red` with a Series `cond` | `color.when(cond, color.green, color.red)` |
78
+ | an empty plot | NaN on every bar: a division by zero, a NaN input | guard divisions (`range.gt(0).iff(x.div(range), 0)`) |
79
+ | 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 |
80
+ | 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 |
81
+ | 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 |
82
+ | `na(series)` / `nz(series)` has no effect | they take numbers | in a loop on `c.get(x)`, or `x.iff(x, 0)` |
83
+ | 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 |
84
+
85
+ ## Checking the output
86
+
87
+ After a clean run, read the values instead of trusting the code:
88
+
89
+ - the last values of each plot (`run.result.plots.plot0.slice(-5)` in a direct call; the app shows them in the
90
+ data window and the legend);
91
+ - the number of markers / labels (a signal on every bar, or none, is a bug);
92
+ - for a strategy, the trade count and a few trades (entry and exit bars and prices).