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,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Checks a script source: runs it like a host (a declaration run on zero bars, then a run on bars) and
|
|
3
|
+
* returns a report an agent or a host can act on: what the script declares (inputs, outputs, strategy), what its outputs
|
|
4
|
+
* contain (values, warm-up, ranges, signal counts, trades), warnings for the mistakes that give no error (an empty plot,
|
|
5
|
+
* a signal on every bar or never, an input that changes nothing...), and the result of a spec (the expected inputs,
|
|
6
|
+
* outputs and signals) and of a reference (expected values per bar). The `oakscript check` command prints it.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { checkScript } from 'oakscriptjs/check';
|
|
10
|
+
* const report = checkScript(source, { spec: { overlay: true, outputs: ['Fast', 'Slow'] } });
|
|
11
|
+
* report.ok; report.errors; report.warnings; report.spec?.items;
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @module check
|
|
15
|
+
*/
|
|
16
|
+
import type { Bar } from '../types/index.js';
|
|
17
|
+
import type { ScriptRunResult } from '../script/index.js';
|
|
18
|
+
import type { InputConfig } from '../runtime/types.js';
|
|
19
|
+
import {
|
|
20
|
+
compileScript,
|
|
21
|
+
inferTimeframe,
|
|
22
|
+
runScript,
|
|
23
|
+
syntheticBars,
|
|
24
|
+
timeframeSeconds,
|
|
25
|
+
type ScriptRun,
|
|
26
|
+
type TestChartOptions,
|
|
27
|
+
} from './harness.js';
|
|
28
|
+
import { runChecks, signalBars, type CheckContext, type CheckItem } from './checks.js';
|
|
29
|
+
import { validateSpec } from './spec.js';
|
|
30
|
+
|
|
31
|
+
export { validateSpec, specJsonSchema, SPEC_FIELDS, SPEC_FILE_FIELDS } from './spec.js';
|
|
32
|
+
export { runChecks, signalBars, type CheckContext, type CheckItem, type CheckPlot, type CheckSignal, type SignalBars } from './checks.js';
|
|
33
|
+
export { formatReport, assertCheck } from './format.js';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
compileScript,
|
|
37
|
+
inferTimeframe,
|
|
38
|
+
rewriteImports,
|
|
39
|
+
runScript,
|
|
40
|
+
scriptArguments,
|
|
41
|
+
syntheticBars,
|
|
42
|
+
testChart,
|
|
43
|
+
timeframeSeconds,
|
|
44
|
+
LIBRARY_PARAMETER,
|
|
45
|
+
SCRIPT_GLOBALS,
|
|
46
|
+
SCRIPT_PREFIX,
|
|
47
|
+
SCRIPT_SUFFIX,
|
|
48
|
+
TestEngine,
|
|
49
|
+
wrapSource,
|
|
50
|
+
type RunScriptOptions,
|
|
51
|
+
type ScriptRun,
|
|
52
|
+
type TestChartOptions,
|
|
53
|
+
} from './harness.js';
|
|
54
|
+
|
|
55
|
+
// ── Spec, reference and report types ─────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** Expected count of a signal (a shape, character, arrow, alert condition, background or bar colour) over the bars. */
|
|
58
|
+
export interface SignalSpec {
|
|
59
|
+
/** At least this many bars with the signal. */
|
|
60
|
+
min?: number;
|
|
61
|
+
/** At most this many bars with the signal. */
|
|
62
|
+
max?: number;
|
|
63
|
+
/** At least this share of the bars (0 to 1). */
|
|
64
|
+
minShare?: number;
|
|
65
|
+
/** At most this share of the bars (0 to 1). */
|
|
66
|
+
maxShare?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Expected values of a plot. */
|
|
70
|
+
export interface PlotSpec {
|
|
71
|
+
/** Every value is at least this. */
|
|
72
|
+
min?: number;
|
|
73
|
+
/** Every value is at most this. */
|
|
74
|
+
max?: number;
|
|
75
|
+
/** The first value comes at this bar index or before (the warm-up). */
|
|
76
|
+
warmup?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Expected input: the keys are compared when given. */
|
|
80
|
+
export interface InputSpec {
|
|
81
|
+
type?: InputConfig['type'];
|
|
82
|
+
defval?: unknown;
|
|
83
|
+
title?: string;
|
|
84
|
+
min?: number;
|
|
85
|
+
max?: number;
|
|
86
|
+
options?: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What the user asked for, written before the script: each given entry is checked. Inputs, plots and signals are
|
|
91
|
+
* found by id or title (case insensitive).
|
|
92
|
+
*/
|
|
93
|
+
export interface CheckSpec {
|
|
94
|
+
kind?: 'indicator' | 'strategy';
|
|
95
|
+
title?: string;
|
|
96
|
+
overlay?: boolean;
|
|
97
|
+
/** Inputs that must exist, with the expected type, default... */
|
|
98
|
+
inputs?: Record<string, InputSpec>;
|
|
99
|
+
/** Outputs that must exist (a title, or a title and a kind: plot, shape, char, arrow, candle, bar, hline, bgcolor, barcolor, alertcondition). */
|
|
100
|
+
outputs?: Array<string | { title: string; kind?: string }>;
|
|
101
|
+
/** Value checks per plot. */
|
|
102
|
+
plots?: Record<string, PlotSpec>;
|
|
103
|
+
/** Count checks per signal. */
|
|
104
|
+
signals?: Record<string, SignalSpec>;
|
|
105
|
+
/** Number of closed trades of a strategy. */
|
|
106
|
+
trades?: { min?: number; max?: number };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Expected values per bar, e.g. the outputs of the Pine script a port follows, on the same bars. */
|
|
110
|
+
export interface CheckReference {
|
|
111
|
+
/** Bar times (UNIX seconds). */
|
|
112
|
+
times: number[];
|
|
113
|
+
/** Expected values per plot id or title, aligned with `times` (NaN or null: na). */
|
|
114
|
+
columns: Record<string, Array<number | null>>;
|
|
115
|
+
/** Equal when |a - b| <= tolerance * max(1, |a|, |b|) (default 1e-10), or both na. */
|
|
116
|
+
tolerance?: number;
|
|
117
|
+
/** A column without a plot of that id or title is listed as ignored instead of failing (a chart export holds other columns). */
|
|
118
|
+
ignoreUnknown?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A run on edge-case bars (`scenarios: true`): a short history, daily bars, flat prices, no volume, a gap. */
|
|
122
|
+
export interface CheckScenario {
|
|
123
|
+
name: string;
|
|
124
|
+
description: string;
|
|
125
|
+
timeframe: string;
|
|
126
|
+
bars: () => Bar[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CheckOptions extends TestChartOptions {
|
|
130
|
+
/** Bars of the run (default: 1,500 deterministic synthetic bars of the timeframe). */
|
|
131
|
+
bars?: Bar[];
|
|
132
|
+
/** Input values by id (default: the defaults). */
|
|
133
|
+
inputs?: Record<string, unknown>;
|
|
134
|
+
spec?: CheckSpec;
|
|
135
|
+
reference?: CheckReference;
|
|
136
|
+
/** Compiles the source (default: `new Function`; the CLI compiles with the file name for error lines). */
|
|
137
|
+
compile?: (source: string) => () => void;
|
|
138
|
+
/** Wraps each run (the CLI stops a run that takes too long). */
|
|
139
|
+
run?: <T>(fn: () => T) => T;
|
|
140
|
+
/** File name of the source, to read the error lines from the stack (`<file>:line:column`). */
|
|
141
|
+
filename?: string;
|
|
142
|
+
/** Re-run with each input changed to find inputs that change no output (default true). */
|
|
143
|
+
inputSweep?: boolean;
|
|
144
|
+
/** Behaviour checks: called with the outputs of the run per bar (see {@link CheckContext}); their results join the spec items. */
|
|
145
|
+
checks?: (t: CheckContext) => void;
|
|
146
|
+
/** Also run on the edge-case bars of {@link SCENARIOS} (or the given ones): their errors fail the check. */
|
|
147
|
+
scenarios?: boolean | CheckScenario[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type CheckStage = 'syntax' | 'types' | 'declaration' | 'run' | 'timeout' | 'spec' | 'reference' | 'scenario';
|
|
151
|
+
|
|
152
|
+
export interface CheckError {
|
|
153
|
+
stage: CheckStage;
|
|
154
|
+
message: string;
|
|
155
|
+
line?: number;
|
|
156
|
+
column?: number;
|
|
157
|
+
/** TypeScript error code (stage "types"). */
|
|
158
|
+
code?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface CheckWarning {
|
|
162
|
+
code:
|
|
163
|
+
| 'empty-plot'
|
|
164
|
+
| 'constant-plot'
|
|
165
|
+
| 'late-start'
|
|
166
|
+
| 'signal-never'
|
|
167
|
+
| 'signal-everywhere'
|
|
168
|
+
| 'one-colour'
|
|
169
|
+
| 'unused-input'
|
|
170
|
+
| 'input-error'
|
|
171
|
+
| 'no-trades'
|
|
172
|
+
| 'no-output';
|
|
173
|
+
message: string;
|
|
174
|
+
/** Output or input title (or id). */
|
|
175
|
+
target?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PlotSummary {
|
|
179
|
+
id: string;
|
|
180
|
+
title: string;
|
|
181
|
+
/** Bars with a value (not na). */
|
|
182
|
+
values: number;
|
|
183
|
+
/** Bar index of the first value (-1: none). */
|
|
184
|
+
firstBar: number;
|
|
185
|
+
min: number | null;
|
|
186
|
+
max: number | null;
|
|
187
|
+
/** Values of the last 5 bars (null: na). */
|
|
188
|
+
last: Array<number | null>;
|
|
189
|
+
/** Distinct per-bar colours (0: the plot colour is the same on every bar). */
|
|
190
|
+
colours: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface SignalSummary {
|
|
194
|
+
kind: 'shape' | 'char' | 'arrow' | 'alertcondition' | 'bgcolor' | 'barcolor';
|
|
195
|
+
id: string;
|
|
196
|
+
title: string;
|
|
197
|
+
/** Bars with the signal. */
|
|
198
|
+
count: number;
|
|
199
|
+
/** count / number of bars. */
|
|
200
|
+
share: number;
|
|
201
|
+
firstBar: number;
|
|
202
|
+
lastBar: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface TradeSummary {
|
|
206
|
+
entry: string;
|
|
207
|
+
direction: 'long' | 'short';
|
|
208
|
+
entryBar: number;
|
|
209
|
+
entryPrice: number;
|
|
210
|
+
exitBar: number | null;
|
|
211
|
+
exitPrice: number | null;
|
|
212
|
+
profit: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface CheckReport {
|
|
216
|
+
/** No error, no failed spec or reference check. Warnings do not change it. */
|
|
217
|
+
ok: boolean;
|
|
218
|
+
bars: { count: number; first: number | null; last: number | null; timeframe: string; synthetic: boolean };
|
|
219
|
+
errors: CheckError[];
|
|
220
|
+
warnings: CheckWarning[];
|
|
221
|
+
declaration?: {
|
|
222
|
+
kind: 'indicator' | 'strategy';
|
|
223
|
+
title: string;
|
|
224
|
+
shortTitle?: string;
|
|
225
|
+
overlay: boolean;
|
|
226
|
+
inputs: Array<Pick<InputConfig, 'id' | 'type' | 'defval' | 'title' | 'min' | 'max' | 'options'>>;
|
|
227
|
+
outputs: Array<{ kind: string; id: string; title: string }>;
|
|
228
|
+
strategy?: Record<string, unknown>;
|
|
229
|
+
};
|
|
230
|
+
plots: PlotSummary[];
|
|
231
|
+
signals: SignalSummary[];
|
|
232
|
+
drawings: { labels: number; lines: number; boxes: number; tables: number; polylines: number; linefills: number };
|
|
233
|
+
/** alert() calls of the script (all bars). */
|
|
234
|
+
alertCalls: number;
|
|
235
|
+
strategy?: { closedTrades: number; openTrade: boolean; netProfit: number; wins: number; first: TradeSummary[] };
|
|
236
|
+
/** Spec items and behaviour checks. */
|
|
237
|
+
spec?: { passed: number; failed: number; items: CheckItem[] };
|
|
238
|
+
reference?: Array<{
|
|
239
|
+
column: string;
|
|
240
|
+
output?: string;
|
|
241
|
+
/** No plot of this id or title, and `ignoreUnknown` is set. */
|
|
242
|
+
ignored?: boolean;
|
|
243
|
+
equal: number;
|
|
244
|
+
total: number;
|
|
245
|
+
firstMismatch?: { bar: number; time: number; expected: number | null; actual: number | null };
|
|
246
|
+
}>;
|
|
247
|
+
/** Runs on edge-case bars: their errors (also in `errors`, stage "scenario") and the plots with no value the main run does not have. */
|
|
248
|
+
scenarios?: Array<{ name: string; description: string; bars: number; errors: CheckError[]; warnings: CheckWarning[] }>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
const lower = (s: string | undefined): string => (s ?? '').trim().toLowerCase();
|
|
254
|
+
|
|
255
|
+
/** Line and column of a script error, from `error.scriptLine` or the `<filename>:line:column` frames of its stack. */
|
|
256
|
+
export function errorLocation(error: unknown, filename?: string): { line?: number; column?: number } {
|
|
257
|
+
const e = error as { scriptLine?: number; scriptColumn?: number; stack?: string } | undefined;
|
|
258
|
+
if (e && typeof e.scriptLine === 'number') return { line: e.scriptLine, column: e.scriptColumn };
|
|
259
|
+
if (!filename || typeof e?.stack !== 'string') return {};
|
|
260
|
+
const name = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
261
|
+
const m = new RegExp(`${name}:(\\d+)(?::(\\d+))?`).exec(e.stack);
|
|
262
|
+
return m ? { line: Number(m[1]), column: m[2] ? Number(m[2]) : undefined } : {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function messageOf(error: unknown): string {
|
|
266
|
+
// an error of another realm (the time limit of the CLI) is not `instanceof Error`
|
|
267
|
+
const e = error as { name?: unknown; message?: unknown } | null;
|
|
268
|
+
if (e && typeof e === 'object' && typeof e.message === 'string') {
|
|
269
|
+
return `${!e.name || e.name === 'Error' ? '' : `${String(e.name)}: `}${e.message}`;
|
|
270
|
+
}
|
|
271
|
+
return String(error);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isTimeout(error: unknown): boolean {
|
|
275
|
+
return /Script execution timed out/i.test(messageOf(error));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** An input value different from the default, valid for the input (undefined: not tried). */
|
|
279
|
+
function alternative(input: InputConfig): unknown {
|
|
280
|
+
const inRange = (v: number) => (input.min === undefined || v >= input.min) && (input.max === undefined || v <= input.max);
|
|
281
|
+
switch (input.type) {
|
|
282
|
+
case 'int': {
|
|
283
|
+
const d = Number(input.defval);
|
|
284
|
+
return [d + 1, d - 1, d * 2, d + 10].find((v) => Number.isFinite(v) && v !== d && inRange(v));
|
|
285
|
+
}
|
|
286
|
+
case 'float': {
|
|
287
|
+
const d = Number(input.defval);
|
|
288
|
+
return [d * 1.5, d + 1, d / 2, d - 1].find((v) => Number.isFinite(v) && v !== d && inRange(v));
|
|
289
|
+
}
|
|
290
|
+
case 'bool':
|
|
291
|
+
return !input.defval;
|
|
292
|
+
case 'string':
|
|
293
|
+
return input.options?.find((o) => o !== input.defval);
|
|
294
|
+
case 'source':
|
|
295
|
+
return input.defval === 'open' ? 'close' : 'open';
|
|
296
|
+
case 'color':
|
|
297
|
+
return input.defval === '#123456' ? '#654321' : '#123456';
|
|
298
|
+
default:
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Everything a run draws or signals, to compare two runs. */
|
|
304
|
+
function fingerprint(run: ScriptRunResult, engine: ScriptRun['engine']): string {
|
|
305
|
+
return JSON.stringify([run.result, engine ? [engine.closed, engine.openTradeNow] : null]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function findByName<T extends { id: string; title?: string }>(items: T[], name: string): T | undefined {
|
|
309
|
+
const n = lower(name);
|
|
310
|
+
return items.find((x) => lower(x.id) === n) ?? items.find((x) => lower(x.title) === n);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Report parts ─────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
function summarizePlots(run: ScriptRunResult, bars: Bar[]): PlotSummary[] {
|
|
316
|
+
const index = new Map(bars.map((b, i) => [b.time, i]));
|
|
317
|
+
return run.plotConfig.map((p) => {
|
|
318
|
+
const points = (run.result.plots[p.id] ?? []) as Array<{ time: number; value: number; color?: string }>;
|
|
319
|
+
const byTime = new Map(points.map((pt) => [pt.time, pt.value]));
|
|
320
|
+
let min = Infinity;
|
|
321
|
+
let max = -Infinity;
|
|
322
|
+
const colours = new Set<string>();
|
|
323
|
+
for (const pt of points) {
|
|
324
|
+
if (pt.value < min) min = pt.value;
|
|
325
|
+
if (pt.value > max) max = pt.value;
|
|
326
|
+
if (pt.color !== undefined) colours.add(String(pt.color));
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
id: p.id,
|
|
330
|
+
title: p.title ?? p.id,
|
|
331
|
+
values: points.length,
|
|
332
|
+
firstBar: points.length ? (index.get(points[0]!.time) ?? -1) : -1,
|
|
333
|
+
min: points.length ? min : null,
|
|
334
|
+
max: points.length ? max : null,
|
|
335
|
+
last: bars.slice(-5).map((b) => byTime.get(b.time) ?? null),
|
|
336
|
+
colours: colours.size,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function summarizeSignals(run: ScriptRunResult, bars: Bar[]): SignalSummary[] {
|
|
342
|
+
const n = Math.max(1, bars.length);
|
|
343
|
+
return signalBars(run, bars).map((x) => ({
|
|
344
|
+
kind: x.kind,
|
|
345
|
+
id: x.id,
|
|
346
|
+
title: x.title,
|
|
347
|
+
count: x.bars.length,
|
|
348
|
+
share: x.bars.length / n,
|
|
349
|
+
firstBar: x.bars[0] ?? -1,
|
|
350
|
+
lastBar: x.bars[x.bars.length - 1] ?? -1,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function summarizeStrategy(engine: NonNullable<ScriptRun['engine']>): NonNullable<CheckReport['strategy']> {
|
|
355
|
+
const trades = engine.closed;
|
|
356
|
+
const first = trades.slice(0, 3).map(
|
|
357
|
+
(t): TradeSummary => ({
|
|
358
|
+
entry: t.entry_id,
|
|
359
|
+
direction: t.size >= 0 ? 'long' : 'short',
|
|
360
|
+
entryBar: t.entry_bar_index,
|
|
361
|
+
entryPrice: t.entry_price,
|
|
362
|
+
exitBar: t.exit_bar_index ?? null,
|
|
363
|
+
exitPrice: t.exit_price ?? null,
|
|
364
|
+
profit: t.profit,
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
return {
|
|
368
|
+
closedTrades: trades.length,
|
|
369
|
+
openTrade: engine.openTradeNow !== null,
|
|
370
|
+
netProfit: trades.reduce((s, t) => s + t.profit, 0),
|
|
371
|
+
wins: trades.filter((t) => t.profit > 0).length,
|
|
372
|
+
first,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function warningsOf(report: CheckReport, bars: Bar[]): CheckWarning[] {
|
|
377
|
+
const w: CheckWarning[] = [];
|
|
378
|
+
const n = bars.length;
|
|
379
|
+
const outputs = report.declaration?.outputs ?? [];
|
|
380
|
+
const drawings = Object.values(report.drawings).reduce((a, b) => a + b, 0);
|
|
381
|
+
if (!outputs.length && !drawings && !report.strategy && !report.alertCalls) {
|
|
382
|
+
w.push({ code: 'no-output', message: 'The script declares no output (plot, shape, colour, drawing, alert): it shows nothing.' });
|
|
383
|
+
}
|
|
384
|
+
for (const p of report.plots) {
|
|
385
|
+
if (p.values === 0) {
|
|
386
|
+
w.push({
|
|
387
|
+
code: 'empty-plot',
|
|
388
|
+
target: p.title,
|
|
389
|
+
message: `Plot "${p.title}" has no value on any bar (na everywhere): a ta.* Series returned from a bar loop, a division by zero, a NaN input or a length longer than the bars?`,
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (p.values > 1 && p.min === p.max) {
|
|
394
|
+
w.push({ code: 'constant-plot', target: p.title, message: `Plot "${p.title}" has the same value (${p.min}) on every bar: check that it is meant (a level is an hline()).` });
|
|
395
|
+
}
|
|
396
|
+
if (n >= 20 && p.firstBar > n / 2) {
|
|
397
|
+
w.push({ code: 'late-start', target: p.title, message: `Plot "${p.title}" starts on bar ${p.firstBar} of ${n}: a long warm-up (long lengths, or a higher timeframe).` });
|
|
398
|
+
}
|
|
399
|
+
if (p.colours === 1 && p.values >= 20) {
|
|
400
|
+
w.push({ code: 'one-colour', target: p.title, message: `Plot "${p.title}" has per-bar colours but the same colour on every bar: is the colour condition ever false?` });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (n >= 20) {
|
|
404
|
+
for (const s of report.signals) {
|
|
405
|
+
if (s.count === 0) {
|
|
406
|
+
w.push({ code: 'signal-never', target: s.title, message: `${s.kind} "${s.title}" is never true on the ${n} bars: check the condition (or test on more bars).` });
|
|
407
|
+
} else if (s.share > 0.5 && s.kind !== 'bgcolor' && s.kind !== 'barcolor') {
|
|
408
|
+
w.push({
|
|
409
|
+
code: 'signal-everywhere',
|
|
410
|
+
target: s.title,
|
|
411
|
+
message: `${s.kind} "${s.title}" is true on ${Math.round(s.share * 100)} % of the bars: a Series used as a JavaScript boolean (always true), or a condition that is not a cross?`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (report.strategy && report.strategy.closedTrades === 0 && !report.strategy.openTrade) {
|
|
417
|
+
w.push({ code: 'no-trades', message: `The strategy makes no trade on the ${n} bars: check the entry conditions (plot them with plotshape) and where the orders are placed.` });
|
|
418
|
+
}
|
|
419
|
+
return w;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function checkSpec(spec: CheckSpec, report: CheckReport, run: ScriptRunResult | undefined): NonNullable<CheckReport['spec']> {
|
|
423
|
+
const items: NonNullable<CheckReport['spec']>['items'] = [];
|
|
424
|
+
const item = (check: string, ok: boolean, expected?: unknown, actual?: unknown) => items.push({ check, ok, expected, actual });
|
|
425
|
+
const d = report.declaration;
|
|
426
|
+
if (spec.kind !== undefined) item('kind', d?.kind === spec.kind, spec.kind, d?.kind);
|
|
427
|
+
if (spec.title !== undefined) item('title', d?.title === spec.title, spec.title, d?.title);
|
|
428
|
+
if (spec.overlay !== undefined) item('overlay', d?.overlay === spec.overlay, spec.overlay, d?.overlay);
|
|
429
|
+
const inputs = run?.inputConfig ?? [];
|
|
430
|
+
for (const [name, want] of Object.entries(spec.inputs ?? {})) {
|
|
431
|
+
const got = findByName(inputs, name);
|
|
432
|
+
if (!got) {
|
|
433
|
+
item(`input "${name}" exists`, false, name, inputs.map((i) => i.id));
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
for (const key of ['type', 'defval', 'title', 'min', 'max', 'options'] as const) {
|
|
437
|
+
if (want[key] === undefined) continue;
|
|
438
|
+
item(`input "${name}" ${key}`, JSON.stringify(got[key]) === JSON.stringify(want[key]), want[key], got[key]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const all = [
|
|
442
|
+
...(d?.outputs ?? []),
|
|
443
|
+
...(run?.hlineConfig ?? []).map((h) => ({ kind: 'hline', id: h.id, title: h.title ?? h.id })),
|
|
444
|
+
...(run?.barColorConfig ?? []).map((b) => ({ kind: b.kind, id: b.id, title: b.title ?? b.id })),
|
|
445
|
+
...(run?.alertConfig ?? []).map((a) => ({ kind: 'alertcondition', id: a.id, title: a.title ?? a.id })),
|
|
446
|
+
];
|
|
447
|
+
for (const o of spec.outputs ?? []) {
|
|
448
|
+
const { title, kind } = typeof o === 'string' ? { title: o, kind: undefined } : o;
|
|
449
|
+
const got = all.filter((x) => lower(x.title) === lower(title) || lower(x.id) === lower(title));
|
|
450
|
+
const match = kind ? got.find((x) => x.kind === kind) : got[0];
|
|
451
|
+
item(`output "${title}"${kind ? ` (${kind})` : ''} exists`, !!match, kind ? `${kind} "${title}"` : title, got.length ? got.map((x) => `${x.kind} "${x.title}"`) : all.map((x) => `${x.kind} "${x.title}"`));
|
|
452
|
+
}
|
|
453
|
+
for (const [name, want] of Object.entries(spec.plots ?? {})) {
|
|
454
|
+
const p = findByName(report.plots, name);
|
|
455
|
+
if (!p) {
|
|
456
|
+
item(`plot "${name}" exists`, false, name, report.plots.map((x) => x.title));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (want.min !== undefined) item(`plot "${name}" min`, p.min !== null && p.min >= want.min, `>= ${want.min}`, p.min);
|
|
460
|
+
if (want.max !== undefined) item(`plot "${name}" max`, p.max !== null && p.max <= want.max, `<= ${want.max}`, p.max);
|
|
461
|
+
if (want.warmup !== undefined) item(`plot "${name}" first value`, p.firstBar >= 0 && p.firstBar <= want.warmup, `bar <= ${want.warmup}`, p.firstBar);
|
|
462
|
+
}
|
|
463
|
+
for (const [name, want] of Object.entries(spec.signals ?? {})) {
|
|
464
|
+
const s = findByName(report.signals, name);
|
|
465
|
+
if (!s) {
|
|
466
|
+
item(`signal "${name}" exists`, false, name, report.signals.map((x) => x.title));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (want.min !== undefined) item(`signal "${name}" count`, s.count >= want.min, `>= ${want.min}`, s.count);
|
|
470
|
+
if (want.max !== undefined) item(`signal "${name}" count`, s.count <= want.max, `<= ${want.max}`, s.count);
|
|
471
|
+
if (want.minShare !== undefined) item(`signal "${name}" share`, s.share >= want.minShare, `>= ${want.minShare}`, Number(s.share.toFixed(4)));
|
|
472
|
+
if (want.maxShare !== undefined) item(`signal "${name}" share`, s.share <= want.maxShare, `<= ${want.maxShare}`, Number(s.share.toFixed(4)));
|
|
473
|
+
}
|
|
474
|
+
if (spec.trades) {
|
|
475
|
+
const t = report.strategy?.closedTrades;
|
|
476
|
+
if (t === undefined) item('trades', false, 'a strategy', d?.kind);
|
|
477
|
+
else {
|
|
478
|
+
if (spec.trades.min !== undefined) item('closed trades', t >= spec.trades.min, `>= ${spec.trades.min}`, t);
|
|
479
|
+
if (spec.trades.max !== undefined) item('closed trades', t <= spec.trades.max, `<= ${spec.trades.max}`, t);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const failed = items.filter((i) => !i.ok).length;
|
|
483
|
+
return { passed: items.length - failed, failed, items };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function checkReference(ref: CheckReference, run: ScriptRunResult, bars: Bar[]): NonNullable<CheckReport['reference']> {
|
|
487
|
+
const tol = ref.tolerance ?? 1e-10;
|
|
488
|
+
const index = new Map(bars.map((b, i) => [b.time, i]));
|
|
489
|
+
const na = (v: number | null | undefined) => v === null || v === undefined || Number.isNaN(v);
|
|
490
|
+
return Object.entries(ref.columns).map(([column, expected]) => {
|
|
491
|
+
const plot = findByName(
|
|
492
|
+
run.plotConfig.map((p) => ({ id: p.id, title: p.title ?? p.id })),
|
|
493
|
+
column
|
|
494
|
+
);
|
|
495
|
+
if (!plot) return ref.ignoreUnknown ? { column, ignored: true, equal: 0, total: 0 } : { column, equal: 0, total: expected.length };
|
|
496
|
+
const values = new Map(((run.result.plots[plot.id] ?? []) as Array<{ time: number; value: number }>).map((p) => [p.time, p.value]));
|
|
497
|
+
let equal = 0;
|
|
498
|
+
let total = 0;
|
|
499
|
+
let firstMismatch: { bar: number; time: number; expected: number | null; actual: number | null } | undefined;
|
|
500
|
+
ref.times.forEach((time, k) => {
|
|
501
|
+
const bar = index.get(time);
|
|
502
|
+
if (bar === undefined) return;
|
|
503
|
+
total++;
|
|
504
|
+
const e = expected[k];
|
|
505
|
+
const a = values.get(time);
|
|
506
|
+
const same = na(e) && na(a) ? true : na(e) || na(a) ? false : Math.abs(a! - e!) <= tol * Math.max(1, Math.abs(a!), Math.abs(e!));
|
|
507
|
+
if (same) equal++;
|
|
508
|
+
else firstMismatch ??= { bar, time, expected: na(e) ? null : e!, actual: na(a) ? null : a! };
|
|
509
|
+
});
|
|
510
|
+
return { column, output: plot.title, equal, total, ...(firstMismatch ? { firstMismatch } : {}) };
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── checkScript ──────────────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
/** Default number of synthetic bars of a check. */
|
|
517
|
+
export const CHECK_BARS = 1500;
|
|
518
|
+
|
|
519
|
+
const HOUR = 3600;
|
|
520
|
+
|
|
521
|
+
/** The edge-case bars of `scenarios: true`. */
|
|
522
|
+
export const SCENARIOS: readonly CheckScenario[] = [
|
|
523
|
+
{ name: 'short', description: '30 hourly bars (a short history)', timeframe: '60', bars: () => syntheticBars(30) },
|
|
524
|
+
{ name: 'daily', description: '800 daily bars', timeframe: 'D', bars: () => syntheticBars(800, 86400) },
|
|
525
|
+
{ name: 'minute', description: '1,000 one-minute bars', timeframe: '1', bars: () => syntheticBars(1000, 60) },
|
|
526
|
+
{
|
|
527
|
+
name: 'flat',
|
|
528
|
+
description: '200 bars with the same price and no volume (zero ranges)',
|
|
529
|
+
timeframe: '60',
|
|
530
|
+
bars: () => syntheticBars(200).map((b) => ({ ...b, open: 100, high: 100, low: 100, close: 100, volume: 0 })),
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'no-volume',
|
|
534
|
+
description: '500 bars without volume (na)',
|
|
535
|
+
timeframe: '60',
|
|
536
|
+
bars: () => syntheticBars(500).map((b) => ({ ...b, volume: NaN })),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: 'gap',
|
|
540
|
+
description: '500 hourly bars with a 3-day gap in the middle',
|
|
541
|
+
timeframe: '60',
|
|
542
|
+
bars: () => syntheticBars(500).map((b, i) => (i >= 250 ? { ...b, time: b.time + 72 * HOUR } : b)),
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Checks a script source (the body of a script, as a host runs it): compiles it, runs it on zero bars (the
|
|
548
|
+
* declarations) and on the bars, and reports its declarations, outputs, warnings and the spec / reference results.
|
|
549
|
+
* Never throws for a script error: the errors are in the report.
|
|
550
|
+
*/
|
|
551
|
+
export function checkScript(source: string, options: CheckOptions = {}): CheckReport {
|
|
552
|
+
const timeframe = options.timeframe ?? (options.bars ? inferTimeframe(options.bars) : '60');
|
|
553
|
+
const step = timeframeSeconds(timeframe) || 3600;
|
|
554
|
+
const bars = options.bars ?? syntheticBars(CHECK_BARS, step);
|
|
555
|
+
const inputs = options.inputs ?? {};
|
|
556
|
+
const wrap = options.run ?? (<T>(fn: () => T) => fn());
|
|
557
|
+
const runOptions = { ...options, timeframe };
|
|
558
|
+
const report: CheckReport = {
|
|
559
|
+
ok: false,
|
|
560
|
+
bars: { count: bars.length, first: bars[0]?.time ?? null, last: bars[bars.length - 1]?.time ?? null, timeframe, synthetic: !options.bars },
|
|
561
|
+
errors: [],
|
|
562
|
+
warnings: [],
|
|
563
|
+
plots: [],
|
|
564
|
+
signals: [],
|
|
565
|
+
drawings: { labels: 0, lines: 0, boxes: 0, tables: 0, polylines: 0, linefills: 0 },
|
|
566
|
+
alertCalls: 0,
|
|
567
|
+
};
|
|
568
|
+
const fail = (stage: CheckStage, error: unknown) =>
|
|
569
|
+
report.errors.push({ stage: isTimeout(error) ? 'timeout' : stage, message: messageOf(error), ...errorLocation(error, options.filename) });
|
|
570
|
+
// a misspelt key would be skipped and the check would pass without checking it
|
|
571
|
+
if (options.spec) for (const message of validateSpec(options.spec)) report.errors.push({ stage: 'spec', message });
|
|
572
|
+
|
|
573
|
+
// compile once to report syntax errors apart from the run errors
|
|
574
|
+
try {
|
|
575
|
+
(options.compile ?? compileScript)(source);
|
|
576
|
+
} catch (e) {
|
|
577
|
+
fail('syntax', e);
|
|
578
|
+
return finish();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let main: ScriptRun | undefined;
|
|
582
|
+
try {
|
|
583
|
+
const decl = wrap(() => runScript(source, [], inputs, runOptions)).run;
|
|
584
|
+
report.declaration = {
|
|
585
|
+
kind: decl.strategyConfig ? 'strategy' : 'indicator',
|
|
586
|
+
title: decl.metadata.title,
|
|
587
|
+
...(decl.metadata.shortTitle ? { shortTitle: decl.metadata.shortTitle } : {}),
|
|
588
|
+
overlay: decl.metadata.overlay,
|
|
589
|
+
inputs: decl.inputConfig.map(({ id, type, defval, title, min, max, options: opts }) => ({
|
|
590
|
+
id,
|
|
591
|
+
type,
|
|
592
|
+
defval,
|
|
593
|
+
...(title !== undefined ? { title } : {}),
|
|
594
|
+
...(min !== undefined ? { min } : {}),
|
|
595
|
+
...(max !== undefined ? { max } : {}),
|
|
596
|
+
...(opts !== undefined ? { options: opts } : {}),
|
|
597
|
+
})),
|
|
598
|
+
outputs: decl.outputs.map((o) => ({ kind: o.kind, id: o.id, title: o.title ?? o.id })),
|
|
599
|
+
...(decl.strategyConfig ? { strategy: { ...decl.strategyConfig } } : {}),
|
|
600
|
+
};
|
|
601
|
+
} catch (e) {
|
|
602
|
+
fail('declaration', e);
|
|
603
|
+
return finish();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
main = wrap(() => runScript(source, bars, inputs, runOptions));
|
|
608
|
+
} catch (e) {
|
|
609
|
+
fail('run', e);
|
|
610
|
+
return finish();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const run = main.run;
|
|
614
|
+
const r = run.result;
|
|
615
|
+
report.plots = summarizePlots(run, bars);
|
|
616
|
+
report.signals = summarizeSignals(run, bars);
|
|
617
|
+
report.drawings = {
|
|
618
|
+
labels: r.labels?.length ?? 0,
|
|
619
|
+
lines: r.lines?.length ?? 0,
|
|
620
|
+
boxes: r.boxes?.length ?? 0,
|
|
621
|
+
tables: r.tables?.length ?? 0,
|
|
622
|
+
polylines: r.polylines?.length ?? 0,
|
|
623
|
+
linefills: r.linefills?.length ?? 0,
|
|
624
|
+
};
|
|
625
|
+
report.alertCalls = (r as { alertCalls?: unknown[] }).alertCalls?.length ?? 0;
|
|
626
|
+
if (main.engine) report.strategy = summarizeStrategy(main.engine);
|
|
627
|
+
report.warnings = warningsOf(report, bars);
|
|
628
|
+
|
|
629
|
+
if (options.inputSweep !== false) {
|
|
630
|
+
const base = fingerprint(run, main.engine);
|
|
631
|
+
for (const input of run.inputConfig) {
|
|
632
|
+
const value = alternative(input);
|
|
633
|
+
if (value === undefined) continue;
|
|
634
|
+
try {
|
|
635
|
+
const other = wrap(() => runScript(source, bars, { ...inputs, [input.id]: value }, runOptions));
|
|
636
|
+
if (fingerprint(other.run, other.engine) === base) {
|
|
637
|
+
report.warnings.push({
|
|
638
|
+
code: 'unused-input',
|
|
639
|
+
target: input.title ?? input.id,
|
|
640
|
+
message: `Input "${input.title ?? input.id}" changes no output (tried ${JSON.stringify(value)} instead of ${JSON.stringify(input.defval)}): is its value used?`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
} catch (e) {
|
|
644
|
+
report.warnings.push({
|
|
645
|
+
code: 'input-error',
|
|
646
|
+
target: input.title ?? input.id,
|
|
647
|
+
message: `Input "${input.title ?? input.id}" = ${JSON.stringify(value)} stops the script: ${messageOf(e)}`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (options.spec) report.spec = checkSpec(options.spec, report, run);
|
|
654
|
+
if (options.checks) {
|
|
655
|
+
const trades = main.engine?.closed ?? [];
|
|
656
|
+
const items = wrapChecks(() => runChecks(options.checks!, run, bars, inputs, trades));
|
|
657
|
+
const spec = (report.spec ??= { passed: 0, failed: 0, items: [] });
|
|
658
|
+
spec.items.push(...items);
|
|
659
|
+
spec.failed = spec.items.filter((i) => !i.ok).length;
|
|
660
|
+
spec.passed = spec.items.length - spec.failed;
|
|
661
|
+
}
|
|
662
|
+
if (options.reference) report.reference = checkReference(options.reference, run, bars);
|
|
663
|
+
if (options.scenarios) runScenarios(options.scenarios === true ? SCENARIOS : options.scenarios);
|
|
664
|
+
return finish();
|
|
665
|
+
|
|
666
|
+
function wrapChecks(fn: () => CheckItem[]): CheckItem[] {
|
|
667
|
+
try {
|
|
668
|
+
return wrap(fn);
|
|
669
|
+
} catch (e) {
|
|
670
|
+
return [{ check: 'checks (stopped)', ok: false, actual: messageOf(e) }];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function runScenarios(list: readonly CheckScenario[]): void {
|
|
675
|
+
const known = new Set(report.warnings.map((w) => `${w.code}:${w.target ?? ''}`));
|
|
676
|
+
report.scenarios = list.map((sc) => {
|
|
677
|
+
const b = sc.bars();
|
|
678
|
+
const r = checkScript(source, {
|
|
679
|
+
...options,
|
|
680
|
+
bars: b,
|
|
681
|
+
timeframe: sc.timeframe,
|
|
682
|
+
spec: undefined,
|
|
683
|
+
reference: undefined,
|
|
684
|
+
checks: undefined,
|
|
685
|
+
scenarios: false,
|
|
686
|
+
inputSweep: false,
|
|
687
|
+
});
|
|
688
|
+
for (const e of r.errors) report.errors.push({ ...e, stage: 'scenario', message: `[${sc.name}: ${sc.description}] ${e.message}` });
|
|
689
|
+
return {
|
|
690
|
+
name: sc.name,
|
|
691
|
+
description: sc.description,
|
|
692
|
+
bars: b.length,
|
|
693
|
+
errors: r.errors,
|
|
694
|
+
// only the na that leaks through (a division by a zero range...): other warnings follow from the bars
|
|
695
|
+
warnings: r.warnings.filter((w) => w.code === 'empty-plot' && !known.has(`${w.code}:${w.target ?? ''}`)),
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function finish(): CheckReport {
|
|
701
|
+
if (options.spec && !report.spec) report.spec = checkSpec(options.spec, report, main?.run);
|
|
702
|
+
report.ok =
|
|
703
|
+
report.errors.length === 0 &&
|
|
704
|
+
(report.spec?.failed ?? 0) === 0 &&
|
|
705
|
+
(report.reference ?? []).every((c) => c.ignored || (c.equal === c.total && c.output !== undefined));
|
|
706
|
+
return report;
|
|
707
|
+
}
|
|
708
|
+
}
|