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.
- package/AGENTS.md +53 -0
- package/README.md +1 -1
- package/bin/oakscript.mjs +5 -0
- package/dist/cjs/check/checks.d.ts +78 -0
- package/dist/cjs/check/checks.d.ts.map +1 -0
- package/dist/cjs/check/checks.js +109 -0
- package/dist/cjs/check/checks.js.map +7 -0
- package/dist/cjs/check/format.d.ts +26 -0
- package/dist/cjs/check/format.d.ts.map +1 -0
- package/dist/cjs/check/format.js +106 -0
- package/dist/cjs/check/format.js.map +7 -0
- package/dist/cjs/check/harness.d.ts +94 -0
- package/dist/cjs/check/harness.d.ts.map +1 -0
- package/dist/cjs/check/harness.js +302 -0
- package/dist/cjs/check/harness.js.map +7 -0
- package/dist/cjs/check/index.d.ts +253 -0
- package/dist/cjs/check/index.d.ts.map +1 -0
- package/dist/cjs/check/index.js +466 -0
- package/dist/cjs/check/index.js.map +7 -0
- package/dist/cjs/check/node.d.ts +53 -0
- package/dist/cjs/check/node.d.ts.map +1 -0
- package/dist/cjs/check/node.js +318 -0
- package/dist/cjs/check/node.js.map +7 -0
- package/dist/cjs/check/spec.d.ts +44 -0
- package/dist/cjs/check/spec.d.ts.map +1 -0
- package/dist/cjs/check/spec.js +199 -0
- package/dist/cjs/check/spec.js.map +7 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/runtime/series.d.ts +6 -0
- package/dist/cjs/runtime/series.d.ts.map +1 -1
- package/dist/cjs/runtime/series.js +15 -0
- package/dist/cjs/runtime/series.js.map +2 -2
- package/dist/cjs/script/constants.d.ts +200 -0
- package/dist/cjs/script/constants.d.ts.map +1 -0
- package/dist/cjs/script/constants.js +217 -0
- package/dist/cjs/script/constants.js.map +7 -0
- package/dist/cjs/script/drawing-args.d.ts +25 -0
- package/dist/cjs/script/drawing-args.d.ts.map +1 -0
- package/dist/cjs/script/drawing-args.js +203 -0
- package/dist/cjs/script/drawing-args.js.map +7 -0
- package/dist/cjs/script/globals.d.ts +127 -0
- package/dist/cjs/script/globals.d.ts.map +1 -0
- package/dist/cjs/script/globals.js +17 -0
- package/dist/cjs/script/globals.js.map +7 -0
- package/dist/cjs/script/index.d.ts +499 -166
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +236 -96
- package/dist/cjs/script/index.js.map +3 -3
- package/dist/esm/check/checks.d.ts +78 -0
- package/dist/esm/check/checks.d.ts.map +1 -0
- package/dist/esm/check/checks.js +95 -0
- package/dist/esm/check/checks.js.map +1 -0
- package/dist/esm/check/format.d.ts +26 -0
- package/dist/esm/check/format.d.ts.map +1 -0
- package/dist/esm/check/format.js +105 -0
- package/dist/esm/check/format.js.map +1 -0
- package/dist/esm/check/harness.d.ts +94 -0
- package/dist/esm/check/harness.d.ts.map +1 -0
- package/dist/esm/check/harness.js +317 -0
- package/dist/esm/check/harness.js.map +1 -0
- package/dist/esm/check/index.d.ts +253 -0
- package/dist/esm/check/index.d.ts.map +1 -0
- package/dist/esm/check/index.js +470 -0
- package/dist/esm/check/index.js.map +1 -0
- package/dist/esm/check/node.d.ts +53 -0
- package/dist/esm/check/node.d.ts.map +1 -0
- package/dist/esm/check/node.js +347 -0
- package/dist/esm/check/node.js.map +1 -0
- package/dist/esm/check/spec.d.ts +44 -0
- package/dist/esm/check/spec.d.ts.map +1 -0
- package/dist/esm/check/spec.js +198 -0
- package/dist/esm/check/spec.js.map +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/runtime/series.d.ts +6 -0
- package/dist/esm/runtime/series.d.ts.map +1 -1
- package/dist/esm/runtime/series.js +14 -0
- package/dist/esm/runtime/series.js.map +1 -1
- package/dist/esm/script/constants.d.ts +200 -0
- package/dist/esm/script/constants.d.ts.map +1 -0
- package/dist/esm/script/constants.js +151 -0
- package/dist/esm/script/constants.js.map +1 -0
- package/dist/esm/script/drawing-args.d.ts +25 -0
- package/dist/esm/script/drawing-args.d.ts.map +1 -0
- package/dist/esm/script/drawing-args.js +171 -0
- package/dist/esm/script/drawing-args.js.map +1 -0
- package/dist/esm/script/globals.d.ts +127 -0
- package/dist/esm/script/globals.d.ts.map +1 -0
- package/dist/esm/script/globals.js +28 -0
- package/dist/esm/script/globals.js.map +1 -0
- package/dist/esm/script/index.d.ts +499 -166
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +256 -83
- package/dist/esm/script/index.js.map +1 -1
- package/package.json +33 -1
- package/skills/oakscript/SKILL.md +198 -0
- package/skills/oakscript/examples/ma-cross-strategy.js +57 -0
- package/skills/oakscript/examples/ma-cross.js +34 -0
- package/skills/oakscript/examples/macd-oscillator.js +29 -0
- package/skills/oakscript/examples/mtf-trend-filter.js +46 -0
- package/skills/oakscript/reference/check.md +150 -0
- package/skills/oakscript/reference/errors.md +92 -0
- package/skills/oakscript/reference/pine-differences.md +85 -0
- package/skills/oakscript/reference/plotting.md +199 -0
- package/skills/oakscript/reference/script-structure.md +185 -0
- package/skills/oakscript/reference/strategies.md +140 -0
- package/skills/oakscript/reference/timeframes-symbols.md +97 -0
- package/skills/oakscript/spec.schema.json +199 -0
- package/src/check/checks.ts +175 -0
- package/src/check/format.ts +117 -0
- package/src/check/harness.ts +372 -0
- package/src/check/index.ts +708 -0
- package/src/check/node.ts +368 -0
- package/src/check/spec.ts +200 -0
- package/src/index.ts +1 -1
- package/src/runtime/series.ts +19 -0
- package/src/script/constants.ts +169 -0
- package/src/script/drawing-args.ts +179 -0
- package/src/script/globals.ts +129 -0
- package/src/script/index.ts +457 -81
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Other timeframes and symbols
|
|
2
|
+
|
|
3
|
+
## request.security
|
|
4
|
+
|
|
5
|
+
`request.security(symbol, timeframe, expression, gaps?, lookahead?, ignore_invalid_symbol?)` runs `expression` on
|
|
6
|
+
the bars of `symbol` on `timeframe` and returns its values aligned on the chart bars (a Series).
|
|
7
|
+
|
|
8
|
+
- `symbol`: `""` or `syminfo.tickerid` for the chart symbol, a ticker id (`"NASDAQ:AAPL"`, from
|
|
9
|
+
`input.symbol`) for another one, `ticker.heikinashi(syminfo.tickerid)` for Heikin Ashi bars.
|
|
10
|
+
- `timeframe`: `"1"`, `"5"`, `"60"`, `"240"`, `"D"`, `"W"`, `"M"`, `"2D"`...; `""` is the chart timeframe.
|
|
11
|
+
- `expression`: a **function** that computes the series. Everything it needs on that timeframe is computed
|
|
12
|
+
inside it: `() => ta.ema(close, 20)`. A Series computed outside runs on the chart bars, and the call throws
|
|
13
|
+
"the expression returned a series computed on the chart bars".
|
|
14
|
+
- Return an array for several values (a Pine tuple): `const [h, l] = request.security("", "D", () => [high, low]);`
|
|
15
|
+
- Inputs and plain numbers from outside can be used inside (`() => ta.ema(close, length)`).
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
indicator("Daily levels", { overlay: true });
|
|
19
|
+
const length = input.int(20, "Daily EMA length", { minval: 1 });
|
|
20
|
+
const dailyEma = request.security("", "D", () => ta.ema(close, length));
|
|
21
|
+
const [prevHigh, prevLow] = request.security("", "D", () => [high.offset(1), low.offset(1)]);
|
|
22
|
+
plot(dailyEma, "Daily EMA", { color: color.orange, style: plot.style_stepline });
|
|
23
|
+
plot(prevHigh, "Previous day high", { color: color.green, style: plot.style_linebr });
|
|
24
|
+
plot(prevLow, "Previous day low", { color: color.red, style: plot.style_linebr });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Lookahead and gaps
|
|
28
|
+
|
|
29
|
+
- `barmerge.lookahead_off` (default): a higher-timeframe value appears on the chart bar that completes its
|
|
30
|
+
period; before that, the bars show the previous period's value. This is the setting without future leak.
|
|
31
|
+
- `barmerge.lookahead_on`: the value of the period appears from its first chart bar. With a series that is not
|
|
32
|
+
offset (`close`, `ta.ema(close, 20)`) this reads the future on history bars: use it only with
|
|
33
|
+
`high.offset(1)`-style expressions (the previous period) or for display.
|
|
34
|
+
- `barmerge.gaps_on`: the value only on the bar where it appears, NaN elsewhere (use `plot.style_linebr`).
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
indicator("Weekly open", { overlay: true });
|
|
38
|
+
const weekOpen = request.security("", "W", () => open, { lookahead: barmerge.lookahead_on });
|
|
39
|
+
plot(weekOpen, "Week open", { color: color.purple, style: plot.style_stepline });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The arguments after the expression are Pine's, by position (`gaps, lookahead, ignore_invalid_symbol`) or by name in an
|
|
43
|
+
object (`{ gaps, lookahead, ignore_invalid_symbol }`); an unknown name throws with the closest names. `currency` and
|
|
44
|
+
`calc_bars_count` are not supported (they throw).
|
|
45
|
+
|
|
46
|
+
### Other symbols
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
indicator("Relative strength", { overlay: false });
|
|
50
|
+
const other = input.symbol("NASDAQ:AAPL", "Compare with");
|
|
51
|
+
const otherClose = request.security(other, "", () => close);
|
|
52
|
+
const ratio = close.div(otherClose);
|
|
53
|
+
plot(ratio, "Ratio", { color: color.blue });
|
|
54
|
+
plot(ta.sma(ratio, 50), "Ratio SMA", { color: color.orange });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The library fetches nothing: the host gives the bars of the other symbol. An app loads them when the script asks
|
|
58
|
+
(the first run reports what it needs, the app loads it and runs again); in your own `executeScript` call, pass
|
|
59
|
+
`bars(symbol, timeframe)` and `symbolInfo(symbol)` in the chart context and catch `MissingSymbolDataError`
|
|
60
|
+
(`error.requests` lists the `{ symbol, timeframe }` to load). A symbol with no bars stops the script with
|
|
61
|
+
"Invalid symbol: X" (or gives NaN with `ignore_invalid_symbol = true`).
|
|
62
|
+
|
|
63
|
+
The values are merged by time: with lookahead off a chart bar shows the last bar of the other symbol that closed
|
|
64
|
+
at or before its own close.
|
|
65
|
+
|
|
66
|
+
### Lower timeframes
|
|
67
|
+
|
|
68
|
+
`request.security` on a timeframe below the chart's reads the lower-timeframe bars (given by the host as for
|
|
69
|
+
another symbol) and shows, per chart bar, the value on the last lower bar inside it.
|
|
70
|
+
`request.security_lower_tf(symbol, timeframe, expression)` returns, for each chart bar, an **array of numbers**:
|
|
71
|
+
the values on all the lower bars inside that bar (a plain `number[][]`, not a Series). Read it in a bar loop:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
indicator("Intrabar up volume", { overlay: false });
|
|
75
|
+
const vols = request.security_lower_tf("", "15", () => volume.mul(close.gt(open).iff(1, 0)));
|
|
76
|
+
const upVolume = eachBar((c) => vols[c.i].reduce((sum, v) => sum + v, 0));
|
|
77
|
+
plot(upVolume, "Up volume", { style: plot.style_columns, color: color.teal });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Timeframe values
|
|
81
|
+
|
|
82
|
+
- `timeframe.period` (`"60"`, `"1D"`...), `timeframe.multiplier`, `timeframe.isintraday`, `timeframe.isdaily`,
|
|
83
|
+
`timeframe.isweekly`, `timeframe.ismonthly`, `timeframe.isdwm`, `timeframe.isminutes`, `timeframe.isseconds`.
|
|
84
|
+
- `timeframe.change("D")`: 1 on the first chart bar of each new period.
|
|
85
|
+
- `timeframe.in_seconds(tf?)`, `timeframe.from_seconds(s)`.
|
|
86
|
+
- `input.timeframe("D", "Timeframe")` lets the user choose; `""` is the chart timeframe.
|
|
87
|
+
- `time("D")`, `time("240", "0930-1600")`: open time of the period of each bar, NaN outside the session.
|
|
88
|
+
|
|
89
|
+
A timeframe below the chart's with `request.security_lower_tf` and a higher one with `request.security` are the
|
|
90
|
+
normal cases; `request.security_lower_tf` on a higher timeframe throws.
|
|
91
|
+
|
|
92
|
+
## What the host must give
|
|
93
|
+
|
|
94
|
+
Session-aware values (`request.security` on another timeframe, `time()`, `timeframe.change`, `session.*`,
|
|
95
|
+
`ta.vwap` on an intraday chart) need the chart timeframe, the exchange time zone and the session. An app gives
|
|
96
|
+
them; a direct `executeScript` call passes `{ timeframe, timezone, session }` in the chart context
|
|
97
|
+
(`session: "24x7"` for a symbol that always trades, `"0930-1600"` for regular stock hours).
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "oakscript check spec",
|
|
4
|
+
"description": "What the user asked for, checked by `oakscript check --spec`: every given entry is checked, an unknown key is an error.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"kind": {
|
|
9
|
+
"enum": [
|
|
10
|
+
"indicator",
|
|
11
|
+
"strategy"
|
|
12
|
+
],
|
|
13
|
+
"description": "indicator() or strategy()"
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "the title of the declaration"
|
|
18
|
+
},
|
|
19
|
+
"overlay": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "true: price pane, false: own pane"
|
|
22
|
+
},
|
|
23
|
+
"inputs": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "inputs that must exist, by id or title",
|
|
26
|
+
"additionalProperties": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"description": "the expected input",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"type": {
|
|
32
|
+
"enum": [
|
|
33
|
+
"int",
|
|
34
|
+
"float",
|
|
35
|
+
"bool",
|
|
36
|
+
"string",
|
|
37
|
+
"text_area",
|
|
38
|
+
"source",
|
|
39
|
+
"color",
|
|
40
|
+
"timeframe",
|
|
41
|
+
"session",
|
|
42
|
+
"time",
|
|
43
|
+
"symbol"
|
|
44
|
+
],
|
|
45
|
+
"description": "input type"
|
|
46
|
+
},
|
|
47
|
+
"defval": {
|
|
48
|
+
"description": "default value"
|
|
49
|
+
},
|
|
50
|
+
"title": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "title"
|
|
53
|
+
},
|
|
54
|
+
"min": {
|
|
55
|
+
"type": "number",
|
|
56
|
+
"description": "minval"
|
|
57
|
+
},
|
|
58
|
+
"max": {
|
|
59
|
+
"type": "number",
|
|
60
|
+
"description": "maxval"
|
|
61
|
+
},
|
|
62
|
+
"options": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"description": "the choices of a string input"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"outputs": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"description": "outputs that must exist: a title, or { title, kind }",
|
|
75
|
+
"items": {
|
|
76
|
+
"oneOf": [
|
|
77
|
+
{
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"required": [
|
|
84
|
+
"title"
|
|
85
|
+
],
|
|
86
|
+
"properties": {
|
|
87
|
+
"title": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
},
|
|
90
|
+
"kind": {
|
|
91
|
+
"enum": [
|
|
92
|
+
"plot",
|
|
93
|
+
"shape",
|
|
94
|
+
"char",
|
|
95
|
+
"arrow",
|
|
96
|
+
"candle",
|
|
97
|
+
"bar",
|
|
98
|
+
"hline",
|
|
99
|
+
"bgcolor",
|
|
100
|
+
"barcolor",
|
|
101
|
+
"alertcondition"
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"plots": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"description": "value checks per plot, by id or title",
|
|
112
|
+
"additionalProperties": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"description": "the expected values",
|
|
115
|
+
"additionalProperties": false,
|
|
116
|
+
"properties": {
|
|
117
|
+
"min": {
|
|
118
|
+
"type": "number",
|
|
119
|
+
"description": "every value is at least this"
|
|
120
|
+
},
|
|
121
|
+
"max": {
|
|
122
|
+
"type": "number",
|
|
123
|
+
"description": "every value is at most this"
|
|
124
|
+
},
|
|
125
|
+
"warmup": {
|
|
126
|
+
"type": "number",
|
|
127
|
+
"description": "the first value comes at this bar index or before"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"signals": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"description": "count checks per signal (shape, char, arrow, alertcondition, bgcolor, barcolor), by id or title",
|
|
135
|
+
"additionalProperties": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"description": "the expected count",
|
|
138
|
+
"additionalProperties": false,
|
|
139
|
+
"properties": {
|
|
140
|
+
"min": {
|
|
141
|
+
"type": "number",
|
|
142
|
+
"description": "at least this many bars"
|
|
143
|
+
},
|
|
144
|
+
"max": {
|
|
145
|
+
"type": "number",
|
|
146
|
+
"description": "at most this many bars"
|
|
147
|
+
},
|
|
148
|
+
"minShare": {
|
|
149
|
+
"type": "number",
|
|
150
|
+
"description": "at least this share of the bars (0 to 1)"
|
|
151
|
+
},
|
|
152
|
+
"maxShare": {
|
|
153
|
+
"type": "number",
|
|
154
|
+
"description": "at most this share of the bars (0 to 1)"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"trades": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"description": "closed trades of a strategy",
|
|
162
|
+
"additionalProperties": false,
|
|
163
|
+
"properties": {
|
|
164
|
+
"min": {
|
|
165
|
+
"type": "number",
|
|
166
|
+
"description": "at least"
|
|
167
|
+
},
|
|
168
|
+
"max": {
|
|
169
|
+
"type": "number",
|
|
170
|
+
"description": "at most"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"$schema": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "the JSON Schema of the spec (oakscriptjs/skills/oakscript/spec.schema.json)"
|
|
177
|
+
},
|
|
178
|
+
"reference": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "CSV of expected values per bar (a time column, one column per plot)"
|
|
181
|
+
},
|
|
182
|
+
"export": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "chart export CSV: the bars (time, open, high, low, close, volume) and the expected plot values"
|
|
185
|
+
},
|
|
186
|
+
"tolerance": {
|
|
187
|
+
"type": "number",
|
|
188
|
+
"description": "relative tolerance of the reference values (default 1e-10)"
|
|
189
|
+
},
|
|
190
|
+
"checks": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "JavaScript file of behaviour checks (the body of a function of `t`)"
|
|
193
|
+
},
|
|
194
|
+
"scenarios": {
|
|
195
|
+
"type": "boolean",
|
|
196
|
+
"description": "also run on the built-in edge-case bars"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Behaviour checks of a script: the checks a spec cannot write as counts and ranges ("Buy only when the
|
|
3
|
+
* RSI is below 30", "the signal line stays under the MACD", "the value on bar 100 is X"). A check function gets `t`, the
|
|
4
|
+
* outputs of the run per bar, and records each check with `t.expect` / `t.everyBar`:
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* const rsi = t.plot("RSI");
|
|
8
|
+
* t.everyBar("RSI between 0 and 100", (i) => rsi.at(i) === null || (rsi.at(i) >= 0 && rsi.at(i) <= 100));
|
|
9
|
+
* t.expect("Buy only when the RSI is below 30", t.signal("Buy").bars.every((i) => rsi.at(i) < 30));
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @module check/checks
|
|
13
|
+
*/
|
|
14
|
+
import type { Bar } from '../types/index.js';
|
|
15
|
+
import type { ScriptRunResult, StrategyTrade } from '../script/index.js';
|
|
16
|
+
|
|
17
|
+
/** The bars of each signal output (shape, char, arrow, alert condition, background or bar colour). */
|
|
18
|
+
export interface SignalBars {
|
|
19
|
+
kind: 'shape' | 'char' | 'arrow' | 'alertcondition' | 'bgcolor' | 'barcolor';
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
/** Bar indexes with the signal, in order. */
|
|
23
|
+
bars: number[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const visible = (c: string | undefined): boolean =>
|
|
27
|
+
!!c && c !== 'transparent' && !/^#[0-9a-f]{6}00$/i.test(c) && !/,\s*0\s*\)$/.test(c);
|
|
28
|
+
|
|
29
|
+
/** The bars of each signal output of a run. */
|
|
30
|
+
export function signalBars(run: ScriptRunResult, bars: readonly Bar[]): SignalBars[] {
|
|
31
|
+
const index = new Map(bars.map((b, i) => [b.time, i]));
|
|
32
|
+
const of = (times: number[]) =>
|
|
33
|
+
[...new Set(times.map((t) => index.get(t)).filter((i): i is number => i !== undefined))].sort((a, b) => a - b);
|
|
34
|
+
const r = run.result;
|
|
35
|
+
const out: SignalBars[] = [];
|
|
36
|
+
for (const s of run.shapeConfig) {
|
|
37
|
+
out.push({ kind: s.kind === 'char' ? 'char' : 'shape', id: s.id, title: s.title ?? s.id, bars: of((r.markers ?? []).filter((m) => m.id === s.id).map((m) => m.time)) });
|
|
38
|
+
}
|
|
39
|
+
for (const a of run.arrowConfig) {
|
|
40
|
+
const times = (r.arrows ?? []).filter((x) => x.id === a.id && x.value !== 0 && !Number.isNaN(x.value)).map((x) => x.time as number);
|
|
41
|
+
out.push({ kind: 'arrow', id: a.id, title: a.title ?? a.id, bars: of(times) });
|
|
42
|
+
}
|
|
43
|
+
for (const b of run.barColorConfig) {
|
|
44
|
+
const list = b.kind === 'bgcolor' ? (r.bgColors ?? []) : (r.barColors ?? []);
|
|
45
|
+
const times = list.filter((x) => (x.id === undefined || x.id === b.id) && visible(x.color)).map((x) => x.time);
|
|
46
|
+
out.push({ kind: b.kind, id: b.id, title: b.title ?? b.id, bars: of(times) });
|
|
47
|
+
}
|
|
48
|
+
for (const a of r.alerts ?? []) out.push({ kind: 'alertcondition', id: a.id, title: a.title ?? a.id, bars: of(a.times) });
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A plot of the run, per bar. */
|
|
53
|
+
export interface CheckPlot {
|
|
54
|
+
id: string;
|
|
55
|
+
title: string;
|
|
56
|
+
/** Value per bar (null: na). */
|
|
57
|
+
values: Array<number | null>;
|
|
58
|
+
/** Colour per bar when the plot has per-bar colours (undefined otherwise). */
|
|
59
|
+
colors?: Array<string | null>;
|
|
60
|
+
/** The value on bar `i` (null: na or no such bar). */
|
|
61
|
+
at(i: number): number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A signal output of the run. */
|
|
65
|
+
export interface CheckSignal {
|
|
66
|
+
kind: SignalBars['kind'];
|
|
67
|
+
id: string;
|
|
68
|
+
title: string;
|
|
69
|
+
/** Bar indexes with the signal. */
|
|
70
|
+
bars: number[];
|
|
71
|
+
/** The signal is on bar `i`. */
|
|
72
|
+
on(i: number): boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What a check function gets. */
|
|
76
|
+
export interface CheckContext {
|
|
77
|
+
bars: readonly Bar[];
|
|
78
|
+
/** Number of bars. */
|
|
79
|
+
n: number;
|
|
80
|
+
/** A plot by id or title; throws when there is none. */
|
|
81
|
+
plot(name: string): CheckPlot;
|
|
82
|
+
/** A signal (shape, char, arrow, alert condition, background or bar colour) by id or title; throws when there is none. */
|
|
83
|
+
signal(name: string): CheckSignal;
|
|
84
|
+
/** Closed trades of a strategy (empty for an indicator). */
|
|
85
|
+
trades: readonly StrategyTrade[];
|
|
86
|
+
/** The whole result of the run (labels, lines, boxes, tables, markers... as a host gets them). */
|
|
87
|
+
result: ScriptRunResult['result'];
|
|
88
|
+
/** The value of an input in the run, by id or title. */
|
|
89
|
+
input(name: string): unknown;
|
|
90
|
+
/** Records a check: `ok` false fails it; `detail` is shown when it fails. */
|
|
91
|
+
expect(check: string, ok: boolean, detail?: unknown): void;
|
|
92
|
+
/** Records a check true on every bar from `from` (default 0) to `to` (default the last bar); fails on the first bar where `predicate(i)` is false. */
|
|
93
|
+
everyBar(check: string, predicate: (i: number) => boolean, options?: { from?: number; to?: number }): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CheckItem {
|
|
97
|
+
check: string;
|
|
98
|
+
ok: boolean;
|
|
99
|
+
expected?: unknown;
|
|
100
|
+
actual?: unknown;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lower = (s: string | undefined): string => (s ?? '').trim().toLowerCase();
|
|
104
|
+
|
|
105
|
+
/** Runs a check function on a run; returns its items (an exception is a failed item). */
|
|
106
|
+
export function runChecks(
|
|
107
|
+
fn: (t: CheckContext) => void,
|
|
108
|
+
run: ScriptRunResult,
|
|
109
|
+
bars: readonly Bar[],
|
|
110
|
+
inputs: Record<string, unknown>,
|
|
111
|
+
trades: readonly StrategyTrade[],
|
|
112
|
+
label = 'checks'
|
|
113
|
+
): CheckItem[] {
|
|
114
|
+
const items: CheckItem[] = [];
|
|
115
|
+
const index = new Map(bars.map((b, i) => [b.time, i]));
|
|
116
|
+
const signals = signalBars(run, bars);
|
|
117
|
+
const plots = new Map<string, CheckPlot>();
|
|
118
|
+
const find = <T extends { id: string; title?: string }>(list: T[], name: string): T | undefined =>
|
|
119
|
+
list.find((x) => lower(x.id) === lower(name)) ?? list.find((x) => lower(x.title) === lower(name));
|
|
120
|
+
const t: CheckContext = {
|
|
121
|
+
bars,
|
|
122
|
+
n: bars.length,
|
|
123
|
+
trades,
|
|
124
|
+
result: run.result,
|
|
125
|
+
plot(name) {
|
|
126
|
+
const p = find(run.plotConfig, name);
|
|
127
|
+
if (!p) throw new Error(`t.plot("${name}"): no plot with this id or title (plots: ${run.plotConfig.map((x) => `"${x.title ?? x.id}"`).join(', ') || 'none'})`);
|
|
128
|
+
const cached = plots.get(p.id);
|
|
129
|
+
if (cached) return cached;
|
|
130
|
+
const values: Array<number | null> = new Array(bars.length).fill(null);
|
|
131
|
+
let colors: Array<string | null> | undefined;
|
|
132
|
+
for (const pt of (run.result.plots[p.id] ?? []) as Array<{ time: number; value: number; color?: string }>) {
|
|
133
|
+
const i = index.get(pt.time);
|
|
134
|
+
if (i === undefined) continue;
|
|
135
|
+
values[i] = pt.value;
|
|
136
|
+
if (pt.color !== undefined) (colors ??= new Array(bars.length).fill(null))[i] = pt.color;
|
|
137
|
+
}
|
|
138
|
+
const plot: CheckPlot = { id: p.id, title: p.title ?? p.id, values, ...(colors ? { colors } : {}), at: (i) => values[i] ?? null };
|
|
139
|
+
plots.set(p.id, plot);
|
|
140
|
+
return plot;
|
|
141
|
+
},
|
|
142
|
+
signal(name) {
|
|
143
|
+
const s = find(signals, name);
|
|
144
|
+
if (!s) throw new Error(`t.signal("${name}"): no signal with this id or title (signals: ${signals.map((x) => `"${x.title}"`).join(', ') || 'none'})`);
|
|
145
|
+
const on = new Set(s.bars);
|
|
146
|
+
return { kind: s.kind, id: s.id, title: s.title, bars: s.bars, on: (i) => on.has(i) };
|
|
147
|
+
},
|
|
148
|
+
input(name) {
|
|
149
|
+
const i = find(run.inputConfig, name);
|
|
150
|
+
if (!i) throw new Error(`t.input("${name}"): no input with this id or title`);
|
|
151
|
+
return i.id in inputs ? inputs[i.id] : i.defval;
|
|
152
|
+
},
|
|
153
|
+
expect(check, ok, detail) {
|
|
154
|
+
items.push({ check, ok: !!ok, ...(ok || detail === undefined ? {} : { actual: detail }) });
|
|
155
|
+
},
|
|
156
|
+
everyBar(check, predicate, options = {}) {
|
|
157
|
+
const from = Math.max(0, options.from ?? 0);
|
|
158
|
+
const to = Math.min(bars.length - 1, options.to ?? bars.length - 1);
|
|
159
|
+
for (let i = from; i <= to; i++) {
|
|
160
|
+
if (!predicate(i)) {
|
|
161
|
+
items.push({ check, ok: false, expected: 'true on every bar', actual: `false on bar ${i}` });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
items.push({ check, ok: true });
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
fn(t);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
const m = e && typeof e === 'object' && 'message' in e ? String((e as Error).message) : String(e);
|
|
172
|
+
items.push({ check: `${label} (stopped)`, ok: false, actual: m });
|
|
173
|
+
}
|
|
174
|
+
return items;
|
|
175
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The check report as text (the output of `oakscript check`) and {@link assertCheck}, which throws it
|
|
3
|
+
* from a test of any test runner.
|
|
4
|
+
*
|
|
5
|
+
* @module check/format
|
|
6
|
+
*/
|
|
7
|
+
import type { CheckReport } from './index.js';
|
|
8
|
+
|
|
9
|
+
const fmt = (v: number | null | undefined): string =>
|
|
10
|
+
v === null || v === undefined ? 'na' : Number.isInteger(v) ? String(v) : Number(v.toPrecision(6)).toString();
|
|
11
|
+
|
|
12
|
+
const shown = (v: unknown): string => (v === undefined ? 'nothing' : JSON.stringify(v));
|
|
13
|
+
|
|
14
|
+
/** The report as text, for a terminal or an agent. `file` and `typesSkipped` come from the command. */
|
|
15
|
+
export function formatReport(r: CheckReport & { file?: string; typesSkipped?: string }): string {
|
|
16
|
+
const file = r.file ?? 'script';
|
|
17
|
+
const out: string[] = [];
|
|
18
|
+
out.push(`oakscript check ${file}: ${r.bars.count} ${r.bars.synthetic ? 'synthetic ' : ''}bars, timeframe ${r.bars.timeframe}`);
|
|
19
|
+
const d = r.declaration;
|
|
20
|
+
if (d) {
|
|
21
|
+
out.push(`${d.kind} "${d.title}"${d.overlay ? ' (overlay)' : ' (own pane)'}`);
|
|
22
|
+
if (d.inputs.length) {
|
|
23
|
+
out.push('inputs:');
|
|
24
|
+
for (const i of d.inputs) {
|
|
25
|
+
const extra = [i.options ? `options ${i.options.join(' | ')}` : '', i.min !== undefined ? `min ${i.min}` : '', i.max !== undefined ? `max ${i.max}` : '']
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(', ');
|
|
28
|
+
out.push(` ${i.id} (${i.type}) = ${JSON.stringify(i.defval)}${i.title ? ` "${i.title}"` : ''}${extra ? ` ${extra}` : ''}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (r.plots.length || r.signals.length) out.push('outputs:');
|
|
33
|
+
for (const p of r.plots) {
|
|
34
|
+
out.push(
|
|
35
|
+
p.values
|
|
36
|
+
? ` plot "${p.title}": ${p.values} values from bar ${p.firstBar}, ${fmt(p.min)} .. ${fmt(p.max)}, last ${p.last.map(fmt).join(' ')}${p.colours ? `, ${p.colours} colours` : ''}`
|
|
37
|
+
: ` plot "${p.title}": no value`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
for (const s of r.signals) {
|
|
41
|
+
out.push(` ${s.kind.padEnd(6)} "${s.title}": ${s.count} bars (${(s.share * 100).toFixed(1)} %)${s.count ? `, first ${s.firstBar}, last ${s.lastBar}` : ''}`);
|
|
42
|
+
}
|
|
43
|
+
const dr = Object.entries(r.drawings).filter(([, n]) => n > 0);
|
|
44
|
+
if (dr.length) out.push(`drawings: ${dr.map(([k, n]) => `${n} ${k}`).join(', ')}`);
|
|
45
|
+
if (r.alertCalls) out.push(`alert() calls: ${r.alertCalls}`);
|
|
46
|
+
if (r.strategy) {
|
|
47
|
+
const s = r.strategy;
|
|
48
|
+
out.push(`strategy: ${s.closedTrades} closed trades${s.openTrade ? ' + 1 open' : ''}, ${s.wins} wins, net profit ${fmt(s.netProfit)}`);
|
|
49
|
+
for (const t of s.first) {
|
|
50
|
+
out.push(` ${t.direction} "${t.entry}" bar ${t.entryBar} @ ${fmt(t.entryPrice)} -> bar ${t.exitBar ?? '-'} @ ${fmt(t.exitPrice)}: ${fmt(t.profit)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (r.errors.length) {
|
|
54
|
+
out.push('errors:');
|
|
55
|
+
for (const e of r.errors) {
|
|
56
|
+
const at = e.line ? `${file}:${e.line}${e.column ? `:${e.column}` : ''} ` : '';
|
|
57
|
+
out.push(` ${at}[${e.stage}${e.code ? ` ${e.code}` : ''}] ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (r.typesSkipped) out.push(`types: not checked (${r.typesSkipped})`);
|
|
61
|
+
if (r.warnings.length) {
|
|
62
|
+
out.push('warnings:');
|
|
63
|
+
for (const w of r.warnings) out.push(` [${w.code}] ${w.message}`);
|
|
64
|
+
}
|
|
65
|
+
if (r.spec) {
|
|
66
|
+
out.push(`spec and checks: ${r.spec.passed} passed, ${r.spec.failed} failed`);
|
|
67
|
+
for (const i of r.spec.items.filter((x) => !x.ok)) {
|
|
68
|
+
out.push(` FAIL ${i.check}${i.expected !== undefined ? `: expected ${shown(i.expected)}, got ${shown(i.actual)}` : i.actual !== undefined ? `: ${typeof i.actual === 'string' ? i.actual : shown(i.actual)}` : ''}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const c of r.reference ?? []) {
|
|
72
|
+
const m = c.firstMismatch;
|
|
73
|
+
out.push(
|
|
74
|
+
c.ignored
|
|
75
|
+
? `reference "${c.column}": ignored (no plot with this id or title)`
|
|
76
|
+
: c.output === undefined
|
|
77
|
+
? `reference "${c.column}": FAIL no plot with this id or title`
|
|
78
|
+
: `reference "${c.column}": ${c.equal} / ${c.total} bars equal${m ? `, first difference on bar ${m.bar}: expected ${fmt(m.expected)}, got ${fmt(m.actual)}` : ''}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (r.scenarios) {
|
|
82
|
+
out.push('scenarios:');
|
|
83
|
+
for (const s of r.scenarios) {
|
|
84
|
+
const notes = [...s.errors.map((e) => `ERROR ${e.message}`), ...s.warnings.map((w) => `[${w.code}] ${w.message}`)];
|
|
85
|
+
out.push(` ${s.name} (${s.description}): ${notes.length ? '' : 'ok'}`);
|
|
86
|
+
for (const n of notes) out.push(` ${n}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const failed =
|
|
90
|
+
(r.spec?.failed ?? 0) + (r.reference ?? []).filter((c) => !c.ignored && (c.equal !== c.total || c.output === undefined)).length;
|
|
91
|
+
out.push(
|
|
92
|
+
r.ok
|
|
93
|
+
? r.warnings.length
|
|
94
|
+
? `result: OK with ${r.warnings.length} warning(s)`
|
|
95
|
+
: 'result: OK'
|
|
96
|
+
: `result: FAIL (${r.errors.length} error(s), ${failed} failed check(s), ${r.warnings.length} warning(s))`
|
|
97
|
+
);
|
|
98
|
+
return out.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Throws the report as text when the check failed (an error, a failed spec item, check or reference column), or with
|
|
103
|
+
* `{ warnings: false }` when it has warnings. For the tests of a project:
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* it('my indicator follows its spec', () => {
|
|
107
|
+
* assertCheck(checkScript(source, { spec, checks: (t) => { ... } }));
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function assertCheck(report: CheckReport, options: { warnings?: boolean } = {}): void {
|
|
112
|
+
if (!report.ok || (options.warnings === false && report.warnings.length > 0)) {
|
|
113
|
+
const error = new Error(formatReport(report));
|
|
114
|
+
error.name = 'CheckError';
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|