oakscriptjs 0.13.2 → 0.14.1
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 +6 -1
- 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 +504 -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 +504 -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 +31 -1
- package/skills/oakscript/SKILL.md +62 -26
- package/skills/oakscript/examples/ma-cross.js +2 -2
- package/skills/oakscript/examples/mtf-trend-filter.js +3 -3
- package/skills/oakscript/reference/check.md +150 -0
- package/skills/oakscript/reference/errors.md +31 -7
- package/skills/oakscript/reference/pine-differences.md +17 -16
- package/skills/oakscript/reference/plotting.md +25 -19
- package/skills/oakscript/reference/script-structure.md +34 -17
- package/skills/oakscript/reference/timeframes-symbols.md +5 -1
- 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 +462 -81
|
@@ -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
|
+
}
|