openalgo-script 0.5.0 → 0.6.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/CHANGELOG.md +121 -0
- package/README.md +53 -22
- package/dist/adapters/charts/run.d.ts.map +1 -1
- package/dist/adapters/charts/run.js +6 -0
- package/dist/adapters/charts/run.js.map +1 -1
- package/dist/adapters/charts/tables.d.ts +6 -19
- package/dist/adapters/charts/tables.d.ts.map +1 -1
- package/dist/adapters/charts/tables.js.map +1 -1
- package/dist/adapters/charts/undrawable.d.ts +4 -0
- package/dist/adapters/charts/undrawable.d.ts.map +1 -0
- package/dist/adapters/charts/undrawable.js +47 -0
- package/dist/adapters/charts/undrawable.js.map +1 -0
- package/dist/core/backtest/drive.d.ts +36 -1
- package/dist/core/backtest/drive.d.ts.map +1 -1
- package/dist/core/backtest/drive.js +13 -107
- package/dist/core/backtest/drive.js.map +1 -1
- package/dist/core/backtest/index.d.ts +2 -0
- package/dist/core/backtest/index.d.ts.map +1 -1
- package/dist/core/backtest/index.js.map +1 -1
- package/dist/core/backtest/walk.d.ts +39 -0
- package/dist/core/backtest/walk.d.ts.map +1 -0
- package/dist/core/backtest/walk.js +125 -0
- package/dist/core/backtest/walk.js.map +1 -0
- package/dist/core/catalogue/catalogue.generated.d.ts +178 -1
- package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -1
- package/dist/core/catalogue/catalogue.generated.js +18 -1
- package/dist/core/catalogue/catalogue.generated.js.map +1 -1
- package/dist/core/catalogue/types.d.ts +8 -2
- package/dist/core/catalogue/types.d.ts.map +1 -1
- package/dist/core/catalogue/values.generated.d.ts +51 -0
- package/dist/core/catalogue/values.generated.d.ts.map +1 -1
- package/dist/core/check/assigned.d.ts +19 -0
- package/dist/core/check/assigned.d.ts.map +1 -0
- package/dist/core/check/assigned.js +14 -0
- package/dist/core/check/assigned.js.map +1 -0
- package/dist/core/check/call-sites.d.ts.map +1 -1
- package/dist/core/check/call-sites.js +9 -4
- package/dist/core/check/call-sites.js.map +1 -1
- package/dist/core/check/calls.d.ts.map +1 -1
- package/dist/core/check/calls.js +12 -4
- package/dist/core/check/calls.js.map +1 -1
- package/dist/core/check/check.d.ts.map +1 -1
- package/dist/core/check/check.js +15 -2
- package/dist/core/check/check.js.map +1 -1
- package/dist/core/check/checker.d.ts +26 -0
- package/dist/core/check/checker.d.ts.map +1 -1
- package/dist/core/check/checker.js +38 -2
- package/dist/core/check/checker.js.map +1 -1
- package/dist/core/check/constant.d.ts +16 -0
- package/dist/core/check/constant.d.ts.map +1 -1
- package/dist/core/check/constant.js +52 -0
- package/dist/core/check/constant.js.map +1 -1
- package/dist/core/check/deleted.d.ts +3 -0
- package/dist/core/check/deleted.d.ts.map +1 -0
- package/dist/core/check/deleted.js +109 -0
- package/dist/core/check/deleted.js.map +1 -0
- package/dist/core/check/expressions.js +1 -1
- package/dist/core/check/expressions.js.map +1 -1
- package/dist/core/check/library-output.d.ts.map +1 -1
- package/dist/core/check/library-output.js +1 -0
- package/dist/core/check/library-output.js.map +1 -1
- package/dist/core/check/library-prose.generated.js +10 -10
- package/dist/core/check/library-prose.generated.js.map +1 -1
- package/dist/core/check/library.d.ts +7 -0
- package/dist/core/check/library.d.ts.map +1 -1
- package/dist/core/check/library.js +1 -0
- package/dist/core/check/library.js.map +1 -1
- package/dist/core/check/surface.d.ts +9 -0
- package/dist/core/check/surface.d.ts.map +1 -1
- package/dist/core/check/surface.js +13 -0
- package/dist/core/check/surface.js.map +1 -1
- package/dist/core/engine/bar-source.d.ts +65 -0
- package/dist/core/engine/bar-source.d.ts.map +1 -0
- package/dist/core/engine/bar-source.js +58 -0
- package/dist/core/engine/bar-source.js.map +1 -0
- package/dist/core/engine/engine.d.ts +9 -11
- package/dist/core/engine/engine.d.ts.map +1 -1
- package/dist/core/engine/engine.js +24 -24
- package/dist/core/engine/engine.js.map +1 -1
- package/dist/core/engine/guard.d.ts.map +1 -1
- package/dist/core/engine/guard.js +10 -0
- package/dist/core/engine/guard.js.map +1 -1
- package/dist/core/engine/host.d.ts +4 -2
- package/dist/core/engine/host.d.ts.map +1 -1
- package/dist/core/engine/host.js.map +1 -1
- package/dist/core/engine/index.d.ts +1 -0
- package/dist/core/engine/index.d.ts.map +1 -1
- package/dist/core/engine/index.js.map +1 -1
- package/dist/core/engine/library/binding.d.ts +2 -0
- package/dist/core/engine/library/binding.d.ts.map +1 -1
- package/dist/core/engine/library/binding.js.map +1 -1
- package/dist/core/engine/library/index.d.ts +8 -0
- package/dist/core/engine/library/index.d.ts.map +1 -1
- package/dist/core/engine/library/index.js +10 -0
- package/dist/core/engine/library/index.js.map +1 -1
- package/dist/core/engine/library/objects.d.ts.map +1 -1
- package/dist/core/engine/library/objects.js +1 -1
- package/dist/core/engine/library/objects.js.map +1 -1
- package/dist/core/engine/request-body.d.ts +3 -1
- package/dist/core/engine/request-body.d.ts.map +1 -1
- package/dist/core/engine/request-body.js +3 -1
- package/dist/core/engine/request-body.js.map +1 -1
- package/dist/core/engine/request-plan.d.ts +3 -3
- package/dist/core/engine/request-plan.js +3 -3
- package/dist/core/engine/requests.d.ts +2 -2
- package/dist/core/engine/requests.d.ts.map +1 -1
- package/dist/core/engine/requests.js +5 -4
- package/dist/core/engine/requests.js.map +1 -1
- package/dist/core/engine/series.d.ts +18 -6
- package/dist/core/engine/series.d.ts.map +1 -1
- package/dist/core/engine/series.js +24 -5
- package/dist/core/engine/series.js.map +1 -1
- package/dist/core/engine/session/record.d.ts +4 -4
- package/dist/core/engine/session/record.js +4 -4
- package/dist/core/importer/calls.d.ts +29 -0
- package/dist/core/importer/calls.d.ts.map +1 -0
- package/dist/core/importer/calls.js +267 -0
- package/dist/core/importer/calls.js.map +1 -0
- package/dist/core/importer/context.d.ts +118 -0
- package/dist/core/importer/context.d.ts.map +1 -0
- package/dist/core/importer/context.js +120 -0
- package/dist/core/importer/context.js.map +1 -0
- package/dist/core/importer/expressions.d.ts +44 -0
- package/dist/core/importer/expressions.d.ts.map +1 -0
- package/dist/core/importer/expressions.js +223 -0
- package/dist/core/importer/expressions.js.map +1 -0
- package/dist/core/importer/importer.d.ts +13 -0
- package/dist/core/importer/importer.d.ts.map +1 -0
- package/dist/core/importer/importer.js +220 -0
- package/dist/core/importer/importer.js.map +1 -0
- package/dist/core/importer/index.d.ts +20 -0
- package/dist/core/importer/index.d.ts.map +1 -0
- package/dist/core/importer/index.js +18 -0
- package/dist/core/importer/index.js.map +1 -0
- package/dist/core/importer/lexer.d.ts +68 -0
- package/dist/core/importer/lexer.d.ts.map +1 -0
- package/dist/core/importer/lexer.js +207 -0
- package/dist/core/importer/lexer.js.map +1 -0
- package/dist/core/importer/names.d.ts +41 -0
- package/dist/core/importer/names.d.ts.map +1 -0
- package/dist/core/importer/names.js +78 -0
- package/dist/core/importer/names.js.map +1 -0
- package/dist/core/importer/orders.d.ts +8 -0
- package/dist/core/importer/orders.d.ts.map +1 -0
- package/dist/core/importer/orders.js +332 -0
- package/dist/core/importer/orders.js.map +1 -0
- package/dist/core/importer/outputs.d.ts +19 -0
- package/dist/core/importer/outputs.d.ts.map +1 -0
- package/dist/core/importer/outputs.js +266 -0
- package/dist/core/importer/outputs.js.map +1 -0
- package/dist/core/importer/parser.d.ts +5 -0
- package/dist/core/importer/parser.d.ts.map +1 -0
- package/dist/core/importer/parser.js +439 -0
- package/dist/core/importer/parser.js.map +1 -0
- package/dist/core/importer/presence.d.ts +32 -0
- package/dist/core/importer/presence.d.ts.map +1 -0
- package/dist/core/importer/presence.js +146 -0
- package/dist/core/importer/presence.js.map +1 -0
- package/dist/core/importer/settle.d.ts +23 -0
- package/dist/core/importer/settle.d.ts.map +1 -0
- package/dist/core/importer/settle.js +135 -0
- package/dist/core/importer/settle.js.map +1 -0
- package/dist/core/importer/statements.d.ts +4 -0
- package/dist/core/importer/statements.d.ts.map +1 -0
- package/dist/core/importer/statements.js +339 -0
- package/dist/core/importer/statements.js.map +1 -0
- package/dist/core/importer/syntax.d.ts +182 -0
- package/dist/core/importer/syntax.d.ts.map +1 -0
- package/dist/core/importer/syntax.js +14 -0
- package/dist/core/importer/syntax.js.map +1 -0
- package/dist/core/importer/table.d.ts +52 -0
- package/dist/core/importer/table.d.ts.map +1 -0
- package/dist/core/importer/table.js +156 -0
- package/dist/core/importer/table.js.map +1 -0
- package/dist/core/importer/values.d.ts +27 -0
- package/dist/core/importer/values.d.ts.map +1 -0
- package/dist/core/importer/values.js +236 -0
- package/dist/core/importer/values.js.map +1 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/stdlib/calendar/civil.d.ts.map +1 -1
- package/dist/core/stdlib/calendar/civil.js +5 -2
- package/dist/core/stdlib/calendar/civil.js.map +1 -1
- package/dist/core/version/version.generated.d.ts +1 -1
- package/dist/core/version/version.generated.js +1 -1
- package/package.json +5 -2
- package/spec/errors.json +380 -20
- package/src/adapters/charts/run.ts +6 -0
- package/src/adapters/charts/tables.ts +6 -19
- package/src/adapters/charts/undrawable.ts +51 -0
- package/src/core/backtest/drive.ts +66 -154
- package/src/core/backtest/index.ts +2 -0
- package/src/core/backtest/walk.ts +182 -0
- package/src/core/catalogue/catalogue.generated.ts +18 -1
- package/src/core/catalogue/types.ts +8 -2
- package/src/core/catalogue/values.generated.ts +16 -0
- package/src/core/check/assigned.ts +31 -0
- package/src/core/check/call-sites.ts +11 -4
- package/src/core/check/calls.ts +13 -4
- package/src/core/check/check.ts +14 -2
- package/src/core/check/checker.ts +39 -2
- package/src/core/check/constant.ts +57 -0
- package/src/core/check/deleted.ts +135 -0
- package/src/core/check/expressions.ts +1 -1
- package/src/core/check/library-output.ts +1 -0
- package/src/core/check/library-prose.generated.ts +10 -10
- package/src/core/check/library.ts +8 -0
- package/src/core/check/surface.ts +16 -0
- package/src/core/engine/bar-source.ts +113 -0
- package/src/core/engine/engine.ts +26 -25
- package/src/core/engine/guard.ts +12 -1
- package/src/core/engine/host.ts +4 -2
- package/src/core/engine/index.ts +1 -0
- package/src/core/engine/library/binding.ts +2 -0
- package/src/core/engine/library/index.ts +11 -0
- package/src/core/engine/library/objects.ts +1 -6
- package/src/core/engine/request-body.ts +5 -7
- package/src/core/engine/request-plan.ts +3 -3
- package/src/core/engine/requests.ts +11 -8
- package/src/core/engine/series.ts +30 -6
- package/src/core/engine/session/record.ts +4 -4
- package/src/core/importer/calls.ts +274 -0
- package/src/core/importer/context.ts +197 -0
- package/src/core/importer/expressions.ts +247 -0
- package/src/core/importer/importer.ts +226 -0
- package/src/core/importer/index.ts +19 -0
- package/src/core/importer/lexer.ts +285 -0
- package/src/core/importer/names.ts +104 -0
- package/src/core/importer/orders.ts +307 -0
- package/src/core/importer/outputs.ts +245 -0
- package/src/core/importer/parser.ts +436 -0
- package/src/core/importer/presence.ts +164 -0
- package/src/core/importer/settle.ts +141 -0
- package/src/core/importer/statements.ts +331 -0
- package/src/core/importer/syntax.ts +144 -0
- package/src/core/importer/table.ts +191 -0
- package/src/core/importer/values.ts +241 -0
- package/src/core/index.ts +13 -0
- package/src/core/stdlib/calendar/civil.ts +5 -2
- package/src/core/version/version.generated.ts +1 -1
|
@@ -34,42 +34,51 @@
|
|
|
34
34
|
* loop and the same record.
|
|
35
35
|
*/
|
|
36
36
|
import { reportOf, scheduleFromDeclaration } from '../accounting/index.js';
|
|
37
|
-
import type { ChargeSchedule, Contract
|
|
37
|
+
import type { ChargeSchedule, Contract } from '../accounting/index.js';
|
|
38
38
|
import type { Diagnostic } from '../diagnostics/index.js';
|
|
39
39
|
import type { CompiledProgram } from '../emit/index.js';
|
|
40
40
|
import { load } from '../engine/index.js';
|
|
41
41
|
import type {
|
|
42
|
-
|
|
43
|
-
Engine,
|
|
42
|
+
Drawing,
|
|
44
43
|
EngineHost,
|
|
44
|
+
Grid,
|
|
45
45
|
Instrument,
|
|
46
46
|
LedgerRow,
|
|
47
47
|
OrderIntent,
|
|
48
|
+
RequestProvider,
|
|
48
49
|
RoutedEffect,
|
|
50
|
+
Value,
|
|
49
51
|
} from '../engine/index.js';
|
|
50
52
|
import { declarationOf } from './declaration.js';
|
|
51
53
|
import type { RunDeclaration } from './declaration.js';
|
|
52
54
|
import { Delivery, framedAs, ordinalsOf } from './deliver.js';
|
|
53
|
-
import type {
|
|
55
|
+
import type { Destination } from './deliver.js';
|
|
54
56
|
import { marksFor, windowFor } from './range.js';
|
|
55
|
-
import
|
|
56
|
-
import {
|
|
57
|
-
import type {
|
|
58
|
-
RecordedBar,
|
|
59
|
-
RecordedDiagnostic,
|
|
60
|
-
RecordedFrame,
|
|
61
|
-
RecordedOrder,
|
|
62
|
-
RunRecord,
|
|
63
|
-
} from './record.js';
|
|
57
|
+
import { orderIn, recordOf } from './record.js';
|
|
58
|
+
import type { RecordedBar, RecordedFrame, RecordedOrder, RunRecord } from './record.js';
|
|
64
59
|
import { Simulator } from './simulate.js';
|
|
65
60
|
import { checkSettings } from './settings.js';
|
|
61
|
+
import { walk } from './walk.js';
|
|
62
|
+
import type { LogLine } from './walk.js';
|
|
66
63
|
import type { BacktestSettings } from './settings.js';
|
|
67
64
|
|
|
68
65
|
/** What a run came to, or why it could not be carried out at all. */
|
|
69
66
|
export type BacktestResult =
|
|
70
|
-
| {
|
|
67
|
+
| {
|
|
68
|
+
readonly ok: true;
|
|
69
|
+
readonly record: RunRecord;
|
|
70
|
+
readonly rows?: readonly (readonly Value[])[];
|
|
71
|
+
readonly log?: readonly LogLine[];
|
|
72
|
+
readonly surface?: Surface;
|
|
73
|
+
}
|
|
71
74
|
| { readonly ok: false; readonly diagnostic: Diagnostic };
|
|
72
75
|
|
|
76
|
+
/** The drawing objects and the grids the last bar left, `conformance.md` section 4. */
|
|
77
|
+
export interface Surface {
|
|
78
|
+
readonly drawings: readonly Drawing[];
|
|
79
|
+
readonly tables: readonly Grid[];
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
/**
|
|
74
83
|
* The facts of `host-interface.md` 4.1 that the contract does not hold.
|
|
75
84
|
*
|
|
@@ -117,8 +126,35 @@ export interface DriveOptions {
|
|
|
117
126
|
* written into a case that could never make its own expected output.
|
|
118
127
|
*/
|
|
119
128
|
readonly sourceText?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Whether to hand back every channel's value on every bar that ran, beside
|
|
131
|
+
* the record rather than inside it.
|
|
132
|
+
*
|
|
133
|
+
* A conformance case that asserts `values` compares one value per bar per
|
|
134
|
+
* plot, and the record holds none of them: it is the reproducible run, and a
|
|
135
|
+
* curve per plot is bytes it would carry for every run to serve the few cases
|
|
136
|
+
* that ask. So the rows are asked for, and a run that does not ask pays
|
|
137
|
+
* nothing. A bar that failed has no row, as in the second engine.
|
|
138
|
+
*/
|
|
139
|
+
readonly rows?: boolean;
|
|
140
|
+
/** Whether to hand back every line `print` wrote, beside the record, for the same reason. */
|
|
141
|
+
readonly log?: boolean;
|
|
142
|
+
/** Whether to hand back the drawing objects and grids the last bar left, for the same reason. */
|
|
143
|
+
readonly surface?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Bars for another instrument, `host-interface.md` 5, for a run whose host
|
|
146
|
+
* serves them.
|
|
147
|
+
*
|
|
148
|
+
* Left out, the run holds the chart's own bars and nothing else: a read of
|
|
149
|
+
* the chart's instrument at a coarser interval is folded from them, and a
|
|
150
|
+
* program that reads another instrument declares a capability this host does
|
|
151
|
+
* not have and is refused at load by name. Given, the host serves duty 3 and
|
|
152
|
+
* the engine asks it once per read, before bar 0.
|
|
153
|
+
*/
|
|
154
|
+
readonly requestBars?: RequestProvider;
|
|
120
155
|
}
|
|
121
156
|
|
|
157
|
+
|
|
122
158
|
/**
|
|
123
159
|
* One run against a simulated destination, from a compiled program to a record.
|
|
124
160
|
*
|
|
@@ -206,7 +242,12 @@ function drive(
|
|
|
206
242
|
const instrument = instrumentFor(settings.contract, options.instrument ?? {});
|
|
207
243
|
const loaded = load(program, {
|
|
208
244
|
settings: settings.inputs,
|
|
209
|
-
host: hostFor(
|
|
245
|
+
host: hostFor(
|
|
246
|
+
instrument,
|
|
247
|
+
settings.now,
|
|
248
|
+
(effect, bar) => sending?.route(effect, bar),
|
|
249
|
+
options.requestBars,
|
|
250
|
+
),
|
|
210
251
|
});
|
|
211
252
|
if (!loaded.ok) return { ok: false, diagnostic: loaded.diagnostic };
|
|
212
253
|
|
|
@@ -219,12 +260,15 @@ function drive(
|
|
|
219
260
|
const destination = choose(declared, schedule);
|
|
220
261
|
sending = destination;
|
|
221
262
|
|
|
222
|
-
const run = walk(engine, destination, bars, framed.covered);
|
|
263
|
+
const run = walk(engine, destination, bars, framed.covered, options);
|
|
223
264
|
const ordinals = ordinalsOf(destination.intents);
|
|
224
265
|
const marks = marksFor(bars, framed.covered);
|
|
225
266
|
|
|
226
267
|
return {
|
|
227
268
|
ok: true,
|
|
269
|
+
...(options.rows === true ? { rows: run.rows } : {}),
|
|
270
|
+
...(options.log === true ? { log: run.log } : {}),
|
|
271
|
+
...(options.surface === true ? { surface: { drawings: engine.drawings(), tables: engine.tables() } } : {}),
|
|
228
272
|
record: recordOf({
|
|
229
273
|
program: engine.program,
|
|
230
274
|
...(options.sourceText === undefined ? {} : { sourceText: options.sourceText }),
|
|
@@ -254,140 +298,6 @@ function simulated(venue: Simulator): Destination {
|
|
|
254
298
|
};
|
|
255
299
|
}
|
|
256
300
|
|
|
257
|
-
/** What one walk of the bars produced. */
|
|
258
|
-
interface Walked {
|
|
259
|
-
readonly fills: readonly RecordedFill[];
|
|
260
|
-
readonly frames: readonly Delivered[];
|
|
261
|
-
readonly diagnostics: readonly RecordedDiagnostic[];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* The bars, in order, with the venue answering between them.
|
|
266
|
-
*
|
|
267
|
-
* The one ordering rule: deliver, then append, then collect what the append
|
|
268
|
-
* reported, then ask the venue what this bar did. A driver that asked the venue
|
|
269
|
-
* before appending would price a fill against a bar the strategy had not seen.
|
|
270
|
-
*/
|
|
271
|
-
function walk(
|
|
272
|
-
engine: Engine,
|
|
273
|
-
destination: Destination,
|
|
274
|
-
bars: readonly RecordedBar[],
|
|
275
|
-
covered: ReportWindow,
|
|
276
|
-
): Walked {
|
|
277
|
-
const fills: RecordedFill[] = [];
|
|
278
|
-
const frames: Delivered[] = [];
|
|
279
|
-
const diagnostics: RecordedDiagnostic[] = [];
|
|
280
|
-
const intents = new Map<number, OrderIntent>();
|
|
281
|
-
const refs = new Map<number, string>();
|
|
282
|
-
const sizes = new Map<number, number>();
|
|
283
|
-
|
|
284
|
-
let pending: readonly Delivered[] = [];
|
|
285
|
-
let seq = 0;
|
|
286
|
-
|
|
287
|
-
for (let index = 0; index < covered.total; index += 1) {
|
|
288
|
-
const bar = bars[index];
|
|
289
|
-
if (bar === undefined) continue;
|
|
290
|
-
|
|
291
|
-
for (const one of pending) {
|
|
292
|
-
const ref = one.frame.orderRef;
|
|
293
|
-
if (typeof ref === 'string') refs.set(one.frame.intentId, ref);
|
|
294
|
-
engine.deliver(one.frame);
|
|
295
|
-
frames.push(one);
|
|
296
|
-
}
|
|
297
|
-
pending = [];
|
|
298
|
-
|
|
299
|
-
const result = engine.append(
|
|
300
|
-
{ time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close,
|
|
301
|
-
volume: bar.volume, oi: bar.oi },
|
|
302
|
-
{ isConfirmed: true, isRealtime: false },
|
|
303
|
-
covered.total,
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
for (const intent of intentsIn(result)) intents.set(intent.intentId, intent);
|
|
307
|
-
seq = settle(result, index, bar, { intents, refs, sizes }, fills, seq);
|
|
308
|
-
|
|
309
|
-
if (result.diagnostic !== undefined) {
|
|
310
|
-
diagnostics.push(diagnosticIn(result.diagnostic, index));
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
pending = destination.answers(index);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return { fills, frames, diagnostics };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** What the run is holding while it folds one bar's frames into fills. */
|
|
321
|
-
interface Books {
|
|
322
|
-
readonly intents: ReadonlyMap<number, OrderIntent>;
|
|
323
|
-
readonly refs: ReadonlyMap<number, string>;
|
|
324
|
-
readonly sizes: Map<number, number>;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* The fills one bar's fold settled.
|
|
329
|
-
*
|
|
330
|
-
* **The position sizes either side of step 6 are kept here and not read back
|
|
331
|
-
* from the engine**, because the engine reports a leg's net and a fill settles
|
|
332
|
-
* against one position reference: the two are different numbers whenever a leg
|
|
333
|
-
* holds more than one position, which is every flip. They are folded in the
|
|
334
|
-
* order the frames were folded in, from the same deltas, so they are the same
|
|
335
|
-
* arithmetic the position book did rather than a second reading of its result.
|
|
336
|
-
* When `FrameOutcome` carries them, these two lines come from the engine and
|
|
337
|
-
* this fold keeps only the sequence number.
|
|
338
|
-
*
|
|
339
|
-
* A refused frame settles nothing and is not a fill. Today the fold reports the
|
|
340
|
-
* refusal as a word rather than a code, so nothing here can write it into the
|
|
341
|
-
* record as a diagnostic; that is OS7018 and OS7019, and both are deferred.
|
|
342
|
-
*/
|
|
343
|
-
function settle(
|
|
344
|
-
result: BarResult,
|
|
345
|
-
index: number,
|
|
346
|
-
bar: RecordedBar,
|
|
347
|
-
books: Books,
|
|
348
|
-
fills: RecordedFill[],
|
|
349
|
-
from: number,
|
|
350
|
-
): number {
|
|
351
|
-
let seq = from;
|
|
352
|
-
for (const outcome of result.frames) {
|
|
353
|
-
if (outcome.refused !== undefined) continue;
|
|
354
|
-
if (outcome.delta <= 0 || outcome.price === null) continue;
|
|
355
|
-
const intent = books.intents.get(outcome.intentId);
|
|
356
|
-
if (intent === undefined || intent.side === null) continue;
|
|
357
|
-
|
|
358
|
-
const before = books.sizes.get(intent.positionRef) ?? 0;
|
|
359
|
-
const after = before + (intent.side === 'sell' ? -outcome.delta : outcome.delta);
|
|
360
|
-
books.sizes.set(intent.positionRef, after);
|
|
361
|
-
seq += 1;
|
|
362
|
-
|
|
363
|
-
fills.push({
|
|
364
|
-
seq,
|
|
365
|
-
intentId: outcome.intentId,
|
|
366
|
-
orderRef: books.refs.get(outcome.intentId) ?? '',
|
|
367
|
-
tag: intent.tag,
|
|
368
|
-
positionRef: intent.positionRef,
|
|
369
|
-
side: intent.side,
|
|
370
|
-
units: outcome.delta,
|
|
371
|
-
price: outcome.price,
|
|
372
|
-
// The bar the fold happened at, which is the bar the position exists
|
|
373
|
-
// from. Where the fill itself traded is the bar before it or this bar's
|
|
374
|
-
// own open, and neither is a bar the strategy could have acted on it in.
|
|
375
|
-
barIndex: index,
|
|
376
|
-
barTime: bar.time,
|
|
377
|
-
refSizeBefore: before,
|
|
378
|
-
refSizeAfter: after,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
return seq;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** Every intent one bar handed over, in the order the bar applied them. */
|
|
385
|
-
function intentsIn(result: BarResult): readonly OrderIntent[] {
|
|
386
|
-
const out: OrderIntent[] = [];
|
|
387
|
-
for (const effect of result.effects) for (const intent of effect.intents) out.push(intent);
|
|
388
|
-
return out;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
301
|
/** The ledger at the end, copied out of the engine's own array. */
|
|
392
302
|
function ordersOf(
|
|
393
303
|
rows: readonly LedgerRow[],
|
|
@@ -450,19 +360,21 @@ function instrumentFor(contract: Contract, facts: InstrumentFacts): Instrument {
|
|
|
450
360
|
/**
|
|
451
361
|
* The host a backtest is: an instrument record, a clock and a destination.
|
|
452
362
|
*
|
|
453
|
-
* Duty 3 is
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* with nothing in it.
|
|
363
|
+
* Duty 3 is served only when the caller supplied bars for it. A backtest with
|
|
364
|
+
* none holds the chart's own bars and nothing else, so a program that reads
|
|
365
|
+
* another instrument declares a capability this host does not have and is
|
|
366
|
+
* refused at load, by name, rather than drawing a line with nothing in it.
|
|
457
367
|
*/
|
|
458
368
|
function hostFor(
|
|
459
369
|
instrument: Instrument,
|
|
460
370
|
now: number | null,
|
|
461
371
|
route: (effect: RoutedEffect, bar: number) => void,
|
|
372
|
+
requestBars: RequestProvider | undefined,
|
|
462
373
|
): EngineHost {
|
|
463
374
|
return {
|
|
464
375
|
instrument,
|
|
465
376
|
...(now === null ? {} : { now }),
|
|
466
377
|
route,
|
|
378
|
+
...(requestBars === undefined ? {} : { requestBars }),
|
|
467
379
|
};
|
|
468
380
|
}
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
*/
|
|
37
37
|
export { backtest, backtestSupplied } from './drive.js';
|
|
38
38
|
export type { BacktestResult, DriveOptions, InstrumentFacts } from './drive.js';
|
|
39
|
+
export type { LogLine } from './walk.js';
|
|
40
|
+
export type { Surface } from './drive.js';
|
|
39
41
|
export { declarationOf } from './declaration.js';
|
|
40
42
|
export type { RunDeclaration } from './declaration.js';
|
|
41
43
|
export { marksFor, windowFor } from './range.js';
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One walk of a backtest's bars, with the venue answering between them.
|
|
3
|
+
*
|
|
4
|
+
* Split from `drive.ts`, which sets a run up and writes its record, because the
|
|
5
|
+
* walk is the one part of a backtest that runs once per bar and it is read on its
|
|
6
|
+
* own: the order in which a frame is delivered, a bar appended and its fills
|
|
7
|
+
* folded is the whole of what makes a backtest reproducible.
|
|
8
|
+
*/
|
|
9
|
+
import type { RecordedFill } from '../accounting/index.js';
|
|
10
|
+
import type { BarResult, Engine, HostBar, OrderIntent, Value } from '../engine/index.js';
|
|
11
|
+
import type { Delivered, Destination } from './deliver.js';
|
|
12
|
+
import type { ReportWindow } from './range.js';
|
|
13
|
+
import { diagnosticIn } from './record.js';
|
|
14
|
+
import type { RecordedBar, RecordedDiagnostic } from './record.js';
|
|
15
|
+
|
|
16
|
+
/** One line of the script's log, `conformance.md` section 4. */
|
|
17
|
+
export interface LogLine {
|
|
18
|
+
readonly barIndex: number;
|
|
19
|
+
readonly time: number | null;
|
|
20
|
+
readonly value: Value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What one walk of the bars produced. */
|
|
24
|
+
export interface Walked {
|
|
25
|
+
readonly fills: readonly RecordedFill[];
|
|
26
|
+
readonly frames: readonly Delivered[];
|
|
27
|
+
readonly diagnostics: readonly RecordedDiagnostic[];
|
|
28
|
+
readonly rows: readonly (readonly Value[])[];
|
|
29
|
+
readonly log: readonly LogLine[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The bars, in order, with the venue answering between them.
|
|
34
|
+
*
|
|
35
|
+
* The one ordering rule: deliver, then append, then collect what the append
|
|
36
|
+
* reported, then ask the venue what this bar did. A driver that asked the venue
|
|
37
|
+
* before appending would price a fill against a bar the strategy had not seen.
|
|
38
|
+
*/
|
|
39
|
+
export function walk(
|
|
40
|
+
engine: Engine,
|
|
41
|
+
destination: Destination,
|
|
42
|
+
bars: readonly RecordedBar[],
|
|
43
|
+
covered: ReportWindow,
|
|
44
|
+
keep: { readonly rows?: boolean; readonly log?: boolean },
|
|
45
|
+
): Walked {
|
|
46
|
+
const fills: RecordedFill[] = [];
|
|
47
|
+
const rows: (readonly Value[])[] = [];
|
|
48
|
+
const log: LogLine[] = [];
|
|
49
|
+
const frames: Delivered[] = [];
|
|
50
|
+
const diagnostics: RecordedDiagnostic[] = [];
|
|
51
|
+
const intents = new Map<number, OrderIntent>();
|
|
52
|
+
const refs = new Map<number, string>();
|
|
53
|
+
const sizes = new Map<number, number>();
|
|
54
|
+
|
|
55
|
+
let pending: readonly Delivered[] = [];
|
|
56
|
+
let seq = 0;
|
|
57
|
+
|
|
58
|
+
// The bars are a settled history, so the fold is handed all of them before
|
|
59
|
+
// bar 0 as `Engine.run` hands them. A `"lookahead"` read is the one reading
|
|
60
|
+
// that can tell (`compiled-program.md` 2.16.2): over history it reads the
|
|
61
|
+
// bucket a bar is inside in full, and a driver that fed the fold one bar at
|
|
62
|
+
// a time would answer the bucket so far, which is the live chart's reading.
|
|
63
|
+
const handed = bars.slice(0, covered.total).map(hostBarOf);
|
|
64
|
+
engine.history(handed);
|
|
65
|
+
|
|
66
|
+
for (let index = 0; index < covered.total; index += 1) {
|
|
67
|
+
const bar = bars[index];
|
|
68
|
+
const held = handed[index];
|
|
69
|
+
if (bar === undefined || held === undefined) continue;
|
|
70
|
+
|
|
71
|
+
for (const one of pending) {
|
|
72
|
+
const ref = one.frame.orderRef;
|
|
73
|
+
if (typeof ref === 'string') refs.set(one.frame.intentId, ref);
|
|
74
|
+
engine.deliver(one.frame);
|
|
75
|
+
frames.push(one);
|
|
76
|
+
}
|
|
77
|
+
pending = [];
|
|
78
|
+
|
|
79
|
+
const result = engine.append(
|
|
80
|
+
held,
|
|
81
|
+
{ isConfirmed: true, isRealtime: false },
|
|
82
|
+
covered.total,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const intent of intentsIn(result)) intents.set(intent.intentId, intent);
|
|
86
|
+
seq = settle(result, index, bar, { intents, refs, sizes }, fills, seq);
|
|
87
|
+
|
|
88
|
+
if (result.diagnostic !== undefined) {
|
|
89
|
+
diagnostics.push(diagnosticIn(result.diagnostic, index));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (keep.rows === true) rows.push([...result.columns]);
|
|
93
|
+
if (keep.log === true) {
|
|
94
|
+
for (const one of result.effects) {
|
|
95
|
+
if (one.effect === 'log') log.push({ barIndex: index, time: bar.time, value: one.args[0] ?? null });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pending = destination.answers(index);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { fills, frames, diagnostics, rows, log };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A recorded bar as the engine is handed one. */
|
|
106
|
+
function hostBarOf(bar: RecordedBar): HostBar {
|
|
107
|
+
return {
|
|
108
|
+
time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close,
|
|
109
|
+
volume: bar.volume, oi: bar.oi,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What the run is holding while it folds one bar's frames into fills. */
|
|
114
|
+
interface Books {
|
|
115
|
+
readonly intents: ReadonlyMap<number, OrderIntent>;
|
|
116
|
+
readonly refs: ReadonlyMap<number, string>;
|
|
117
|
+
readonly sizes: Map<number, number>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The fills one bar's fold settled.
|
|
122
|
+
*
|
|
123
|
+
* **The position sizes either side of step 6 are kept here and not read back
|
|
124
|
+
* from the engine**, because the engine reports a leg's net and a fill settles
|
|
125
|
+
* against one position reference: the two are different numbers whenever a leg
|
|
126
|
+
* holds more than one position, which is every flip. They are folded in the
|
|
127
|
+
* order the frames were folded in, from the same deltas, so they are the same
|
|
128
|
+
* arithmetic the position book did rather than a second reading of its result.
|
|
129
|
+
* When `FrameOutcome` carries them, these two lines come from the engine and
|
|
130
|
+
* this fold keeps only the sequence number.
|
|
131
|
+
*
|
|
132
|
+
* A refused frame settles nothing and is not a fill. Today the fold reports the
|
|
133
|
+
* refusal as a word rather than a code, so nothing here can write it into the
|
|
134
|
+
* record as a diagnostic; that is OS7018 and OS7019, and both are deferred.
|
|
135
|
+
*/
|
|
136
|
+
function settle(
|
|
137
|
+
result: BarResult,
|
|
138
|
+
index: number,
|
|
139
|
+
bar: RecordedBar,
|
|
140
|
+
books: Books,
|
|
141
|
+
fills: RecordedFill[],
|
|
142
|
+
from: number,
|
|
143
|
+
): number {
|
|
144
|
+
let seq = from;
|
|
145
|
+
for (const outcome of result.frames) {
|
|
146
|
+
if (outcome.refused !== undefined) continue;
|
|
147
|
+
if (outcome.delta <= 0 || outcome.price === null) continue;
|
|
148
|
+
const intent = books.intents.get(outcome.intentId);
|
|
149
|
+
if (intent === undefined || intent.side === null) continue;
|
|
150
|
+
|
|
151
|
+
const before = books.sizes.get(intent.positionRef) ?? 0;
|
|
152
|
+
const after = before + (intent.side === 'sell' ? -outcome.delta : outcome.delta);
|
|
153
|
+
books.sizes.set(intent.positionRef, after);
|
|
154
|
+
seq += 1;
|
|
155
|
+
|
|
156
|
+
fills.push({
|
|
157
|
+
seq,
|
|
158
|
+
intentId: outcome.intentId,
|
|
159
|
+
orderRef: books.refs.get(outcome.intentId) ?? '',
|
|
160
|
+
tag: intent.tag,
|
|
161
|
+
positionRef: intent.positionRef,
|
|
162
|
+
side: intent.side,
|
|
163
|
+
units: outcome.delta,
|
|
164
|
+
price: outcome.price,
|
|
165
|
+
// The bar the fold happened at, which is the bar the position exists
|
|
166
|
+
// from. Where the fill itself traded is the bar before it or this bar's
|
|
167
|
+
// own open, and neither is a bar the strategy could have acted on it in.
|
|
168
|
+
barIndex: index,
|
|
169
|
+
barTime: bar.time,
|
|
170
|
+
refSizeBefore: before,
|
|
171
|
+
refSizeAfter: after,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return seq;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Every intent one bar handed over, in the order the bar applied them. */
|
|
178
|
+
function intentsIn(result: BarResult): readonly OrderIntent[] {
|
|
179
|
+
const out: OrderIntent[] = [];
|
|
180
|
+
for (const effect of result.effects) for (const intent of effect.intents) out.push(intent);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
@@ -19,6 +19,7 @@ export const STAGE_LABELS = {
|
|
|
19
19
|
check: "checker",
|
|
20
20
|
runtime: "engine",
|
|
21
21
|
host: "host",
|
|
22
|
+
import: "importer",
|
|
22
23
|
} as const satisfies Readonly<Record<Stage, string>>;
|
|
23
24
|
|
|
24
25
|
export const ENTRIES = {
|
|
@@ -95,6 +96,8 @@ export const ENTRIES = {
|
|
|
95
96
|
OS3022: { code: "OS3022", title: "Two inputs share a settings key", severity: "error", stage: "check", since: 1, autofix: false, message: "This input is keyed by its title {name}, which is already the name of the input at line {line}.", fix: "Give this one a title of its own, or rename the input at line {line}.", placeholders: ["name", "line"] },
|
|
96
97
|
OS3023: { code: "OS3023", title: "An order names a leg this file does not declare", severity: "error", stage: "check", since: 1, autofix: false, message: "{name} was given a {argument}, and this file declares none.", fix: "Leave {argument} out. A file that declares no leg has exactly one, and every order acts on it.", placeholders: ["name", "argument"] },
|
|
97
98
|
OS3024: { code: "OS3024", title: "An input written in place has an empty title", severity: "error", stage: "check", since: 1, autofix: false, message: "An input() assigned to no name is named by its title, and this one's title is empty.", fix: "Give the title something to say: input(2, \"Precision\").", placeholders: [] },
|
|
99
|
+
OS3025: { code: "OS3025", title: "A setting is part of a larger expression here", severity: "error", stage: "check", since: 1, autofix: false, message: "{option} is fixed before the first bar and holds an input() only as the whole of its value, so it cannot hold an expression over one.", fix: "Declare the setting as the value itself, with an input() written as the whole of {option}, or write the value out as a literal.", placeholders: ["option"] },
|
|
100
|
+
OS3026: { code: "OS3026", title: "This option cannot be a setting", severity: "error", stage: "check", since: 1, autofix: false, message: "{option} is written into the compiled program as a plain value, so a setting cannot choose it in this format version.", fix: "Write the option out as a string literal and drop the input(): style = \"step\", for instance, is one of the values OS3008 names.", placeholders: ["option"] },
|
|
98
101
|
OS4001: { code: "OS4001", title: "History index is not usable", severity: "error", stage: "runtime", since: 1, autofix: false, message: "[{index}] is not a whole number of bars at or above zero.", fix: "Wrap the index in floor() or round(), and clamp a computed index with max(0, n).", placeholders: ["index"] },
|
|
99
102
|
OS4002: { code: "OS4002", title: "History index is deeper than the retained depth", severity: "error", stage: "runtime", since: 1, autofix: false, message: "[{index}] reaches past the retained depth of {depth} bars.", fix: "Raise the depth in one place: limits(history = {suggested}).", placeholders: ["index", "depth", "suggested"] },
|
|
100
103
|
OS4003: { code: "OS4003", title: "A whole number was required here", severity: "error", stage: "runtime", since: 1, autofix: false, message: "{name}'s {argument} was {found} on this bar; a whole number was required.", fix: "Round the value before passing it: floor() towards zero, round() to nearest.", placeholders: ["name", "argument", "found"] },
|
|
@@ -129,7 +132,7 @@ export const ENTRIES = {
|
|
|
129
132
|
OS6009: { code: "OS6009", title: "The request failed", severity: "error", stage: "host", since: 1, autofix: false, message: "The host could not fetch {symbol} at {timeframe}: {reason}.", fix: "Act on {reason} in the host: it is a connection, permission or quota problem. Where the value can be derived from the chart's own bars, derive it and drop the request.", placeholders: ["symbol", "timeframe", "reason"] },
|
|
130
133
|
OS6010: { code: "OS6010", title: "The engine was given no bars", severity: "error", stage: "host", since: 1, autofix: false, message: "There are no bars for {symbol} at {timeframe}, so the script cannot run.", fix: "Choose an instrument and interval that have history, or widen the chart's date range until bars exist.", placeholders: ["symbol", "timeframe"] },
|
|
131
134
|
OS6011: { code: "OS6011", title: "The bars are not in order", severity: "error", stage: "host", since: 1, autofix: false, message: "Bar {index} is dated {time}, which is not after bar {previous}.", fix: "Reload the history; if the same bar repeats, the feed is sending duplicates and the host has to sort and deduplicate the bars before the engine runs.", placeholders: ["index", "time", "previous"] },
|
|
132
|
-
OS6012: { code: "OS6012", title: "An instrument fact is not known", severity: "error", stage: "host", since: 1, autofix: false, message: "The host did not supply {fact} for {symbol}.", fix: "Supply {fact} in the host's instrument record, or
|
|
135
|
+
OS6012: { code: "OS6012", title: "An instrument fact is not known", severity: "error", stage: "host", since: 1, autofix: false, message: "The host did not supply {fact} for {symbol}.", fix: "Supply {fact} in the host's instrument record for {symbol}, or leave the session out of the record, which makes every session read absent rather than wrong.", placeholders: ["fact", "symbol"] },
|
|
133
136
|
OS6013: { code: "OS6013", title: "The request changed after the first bar", severity: "error", stage: "runtime", since: 1, autofix: false, message: "This request asked for {first} on bar 0 and for {found} on bar {bar}.", fix: "Compute the symbol and the timeframe from literals or inputs, not from bar data.", placeholders: ["first", "found", "bar"] },
|
|
134
137
|
OS6014: { code: "OS6014", title: "The feed does not offer this timeframe", severity: "error", stage: "host", since: 1, autofix: false, message: "The host has no {timeframe} data for {symbol}; it offers {available}.", fix: "Request one of {available}, or derive the interval you want from a lower one the host does serve.", placeholders: ["timeframe", "symbol", "available"] },
|
|
135
138
|
OS6015: { code: "OS6015", title: "The requested timeframe does not fold into the chart's", severity: "error", stage: "host", since: 1, autofix: false, message: "{requested} is not a whole multiple of {chart}.", fix: "Request a multiple of {chart}, for example {suggestion}.", placeholders: ["requested", "chart", "suggestion"] },
|
|
@@ -141,6 +144,8 @@ export const ENTRIES = {
|
|
|
141
144
|
OS6021: { code: "OS6021", title: "A run setting cannot be applied as stated", severity: "error", stage: "host", since: 1, autofix: false, message: "{setting} cannot be applied: {problem}.", fix: "State the setting so it can be carried out: declare a charge line after every line it is levied on, supply the tick size a slippage in ticks is measured in, write down the reason a tolerance needs a bound, or bring a bound past the cap back inside it. Nothing has been computed at the point this is refused, so correcting the setting and running again costs one run.", placeholders: ["setting", "problem"] },
|
|
142
145
|
OS6022: { code: "OS6022", title: "The bars are not the bars the record was made from", severity: "error", stage: "host", since: 1, autofix: false, message: "The bars supplied hash to {found}, and the record was made from {expected}.", fix: "Replay against the bars the record names. Where the revision is the point, make a second record over the revised bars and compare the two runs, rather than overwriting one run with the other under one name.", placeholders: ["found", "expected"] },
|
|
143
146
|
OS6023: { code: "OS6023", title: "Two cost models are stated at once", severity: "error", stage: "host", since: 1, autofix: false, message: "A charge schedule was supplied, and the declaration states a commission of {commission} in {commissionType}.", fix: "Supply the schedule and leave the declaration's commission at its default of zero, or state the commission in the declaration and supply no schedule. A schedule is the one of the two that can carry a floor, a cap, a charge levied on a charge, and a cost that falls on one side of the trade only.", placeholders: ["commission", "commissionType"] },
|
|
147
|
+
OS6024: { code: "OS6024", title: "The host cannot draw something this study declares", severity: "error", stage: "host", since: 1, autofix: false, message: "This host cannot draw {what}: {limit}.", fix: "Declare what this host draws, or run the study on a host that draws {what}.", placeholders: ["what", "limit"] },
|
|
148
|
+
OS6025: { code: "OS6025", title: "A bar has no time", severity: "error", stage: "host", since: 1, autofix: false, message: "Bar {index} was handed over with no time.", fix: "State the bar's open instant in whole milliseconds since the Unix epoch, UTC, and leave out a bar the host cannot date rather than handing it over without a time.", placeholders: ["index"] },
|
|
144
149
|
OS7001: { code: "OS7001", title: "Only a strategy can do that", severity: "error", stage: "check", since: 1, autofix: false, message: "{name} is available only in a file declared with strategy().", fix: "Change study(...) on line {line} to strategy(...), or replace {name} with signal(\"...\") to mark the bar without trading.", placeholders: ["name", "line"] },
|
|
145
150
|
OS7002: { code: "OS7002", title: "An order argument is absent", severity: "error", stage: "runtime", since: 1, autofix: false, message: "{name}'s {argument} is absent on this bar.", fix: "Guard the call with isNone({argument}), or supply a fallback with orElse() where one is genuinely correct.", placeholders: ["name", "argument"] },
|
|
146
151
|
OS7003: { code: "OS7003", title: "An order function inside a request expression", severity: "error", stage: "check", since: 1, autofix: false, message: "{name} inside a request expression would place an order from another instrument's bars.", fix: "Read the value with the request, and place the order at the top level from the result.", placeholders: ["name"] },
|
|
@@ -179,6 +184,18 @@ export const ENTRIES = {
|
|
|
179
184
|
OS8017: { code: "OS8017", title: "The condition is constant", severity: "warning", stage: "check", since: 1, autofix: false, message: "This condition is {value} on every bar.", fix: "Restore the test that was meant, or delete the branch that never runs.", placeholders: ["value"] },
|
|
180
185
|
OS8018: { code: "OS8018", title: "An input is never used", severity: "warning", stage: "check", since: 1, autofix: false, message: "The input {title} is declared at line {line} and never read.", fix: "Use the name the input assigns, or delete the input() and its dialog row.", placeholders: ["title", "line"] },
|
|
181
186
|
OS8019: { code: "OS8019", title: "A deleted object is still held", severity: "warning", stage: "check", since: 1, autofix: false, message: "{name} still holds the {kind} deleted at line {line}.", fix: "Assign none to the name on the same path as the delete, and where the object came out of an array, remove the element as well as deleting the object.", placeholders: ["name", "kind", "line"] },
|
|
187
|
+
OS9001: { code: "OS9001", title: "Version not read by the importer", severity: "error", stage: "import", since: 1, autofix: false, message: "The importer reads scripts written for version 5 or 6 of the source dialect, and this one {found}.", fix: "Bring the script up to version 5 or 6 in the source dialect, or translate it by hand, and import it again.", placeholders: ["found"] },
|
|
188
|
+
OS9002: { code: "OS9002", title: "Construct with no equivalent", severity: "error", stage: "import", since: 1, autofix: false, message: "The importer cannot translate {construct}, so the statement holding it was kept as a comment.", fix: "Translate that statement by hand from the comment that holds it: the rest of the script was translated around it.", placeholders: ["construct"] },
|
|
189
|
+
OS9003: { code: "OS9003", title: "Built-in with no mapping", severity: "error", stage: "import", since: 1, autofix: false, message: "{name} is not one of the built-ins the importer maps, so the statement holding it was kept as a comment.", fix: "Find the OpenScript library call that does the same job, check how it treats its first bars and absent values, and write the statement by hand.", placeholders: ["name"] },
|
|
190
|
+
OS9004: { code: "OS9004", title: "Declaration the importer cannot place", severity: "error", stage: "import", since: 1, autofix: false, message: "The importer writes the declaration from one indicator or strategy call at the top level, and this script {found}.", fix: "Give the source script exactly one indicator or strategy declaration at the top level, and import it again.", placeholders: ["found"] },
|
|
191
|
+
OS9005: { code: "OS9005", title: "Reads a statement that was not translated", severity: "error", stage: "import", since: 1, autofix: false, message: "{name} is declared by the statement at line {line}, which was not translated, so this statement that reads it was kept as a comment too.", fix: "Translate the statement at line {line} by hand first, then this one, which was kept as a comment only because it reads {name}.", placeholders: ["name", "line"] },
|
|
192
|
+
OS9006: { code: "OS9006", title: "Argument with no equivalent", severity: "error", stage: "import", since: 1, autofix: false, message: "{argument} changes what {call} computes or trades, and OpenScript has no equivalent the importer can write.", fix: "Translate the call by hand, deciding what OpenScript should do in place of {argument}, or remove {argument} from the source script if the script does not depend on it.", placeholders: ["argument", "call"] },
|
|
193
|
+
OS9007: { code: "OS9007", title: "Warmup and absent bars follow OpenScript", severity: "warning", stage: "import", since: 1, autofix: false, message: "{call} became {target} here and wherever else the script calls it, and {target} follows OpenScript's own warmup, arithmetic and handling of absent values, which the original is not guaranteed to share.", fix: "Compare the first bars of the translation with the original, and where the script depends on them, write the value as an explicit recurrence held in var, seeded the way the original seeds it.", placeholders: ["call", "target"] },
|
|
194
|
+
OS9008: { code: "OS9008", title: "Whole-number division in version 5", severity: "warning", stage: "import", since: 1, autofix: false, message: "Both sides of this division are whole-number constants, which version 5 of the source dialect divides without a fractional part, and OpenScript keeps the fraction.", fix: "If the script relied on the whole-number result, wrap the translated division in trunc(); otherwise leave it as it is.", placeholders: [] },
|
|
195
|
+
OS9009: { code: "OS9009", title: "Presentation argument left out", severity: "warning", stage: "import", since: 1, autofix: false, message: "{argument} changes how {call} is displayed or what it may allocate, not what it computes, and OpenScript has no equivalent, so the translation leaves it out.", fix: "Nothing needs to change unless the display mattered; where it did, restyle the translated call with the arguments OpenScript does have, such as its colour, width and style.", placeholders: ["argument", "call"] },
|
|
196
|
+
OS9010: { code: "OS9010", title: "Order placed under OpenScript's order model", severity: "warning", stage: "import", since: 1, autofix: false, message: "{call} was translated into OpenScript's order calls here and wherever else the script uses it, and OpenScript decides when an order fills, how a reversal and a repeated entry are handled and when an exit level starts to apply by its own order model.", fix: "Backtest the translation beside the original and compare the two trade lists before relying on it, and adjust the order calls by hand where they part.", placeholders: ["call"] },
|
|
197
|
+
OS9011: { code: "OS9011", title: "Equality over two absent values", severity: "warning", stage: "import", since: 1, autofix: false, message: "Both sides of {operator} can be absent on the same bar, and OpenScript counts two absent values as equal, which the original is not guaranteed to do.", fix: "If the original relied on two absent values never comparing equal, guard the translated comparison with isNone() on either side.", placeholders: ["operator"] },
|
|
198
|
+
OS9012: { code: "OS9012", title: "The translation does not compile", severity: "error", stage: "import", since: 1, autofix: false, message: "The OpenScript compiler refused the translation of this statement with {code}, so the statement was kept as a comment.", fix: "Look up {code} in this catalogue and translate the statement by hand in the form that code's fix describes.", placeholders: ["code"] },
|
|
182
199
|
} as const satisfies Readonly<Record<string, CatalogueEntry>>;
|
|
183
200
|
|
|
184
201
|
/** Every code the compiler and the engines may emit. Nothing else is one. */
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
/** What a diagnostic does to the run. An error stops it; a warning never does. */
|
|
7
7
|
export type Severity = 'error' | 'warning';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Which part of the pipeline raised a diagnostic.
|
|
11
|
+
*
|
|
12
|
+
* `import` is not a stage of compiling OpenScript: it is the importer, which
|
|
13
|
+
* reads a script written in another chart language and whose findings point
|
|
14
|
+
* into that script rather than into an OpenScript file.
|
|
15
|
+
*/
|
|
16
|
+
export type Stage = 'lex' | 'parse' | 'check' | 'runtime' | 'host' | 'import';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* What a compiler puts into a message slot.
|
|
@@ -87,6 +87,8 @@ export interface DiagnosticValues {
|
|
|
87
87
|
readonly OS3022: { readonly name: PlaceholderValue; readonly line: PlaceholderValue };
|
|
88
88
|
readonly OS3023: { readonly name: PlaceholderValue; readonly argument: PlaceholderValue };
|
|
89
89
|
readonly OS3024: Record<string, never>;
|
|
90
|
+
readonly OS3025: { readonly option: PlaceholderValue };
|
|
91
|
+
readonly OS3026: { readonly option: PlaceholderValue };
|
|
90
92
|
readonly OS4001: { readonly index: PlaceholderValue };
|
|
91
93
|
readonly OS4002: { readonly index: PlaceholderValue; readonly depth: PlaceholderValue; readonly suggested: PlaceholderValue };
|
|
92
94
|
readonly OS4003: { readonly name: PlaceholderValue; readonly argument: PlaceholderValue; readonly found: PlaceholderValue };
|
|
@@ -133,6 +135,8 @@ export interface DiagnosticValues {
|
|
|
133
135
|
readonly OS6021: { readonly setting: PlaceholderValue; readonly problem: PlaceholderValue };
|
|
134
136
|
readonly OS6022: { readonly found: PlaceholderValue; readonly expected: PlaceholderValue };
|
|
135
137
|
readonly OS6023: { readonly commission: PlaceholderValue; readonly commissionType: PlaceholderValue };
|
|
138
|
+
readonly OS6024: { readonly what: PlaceholderValue; readonly limit: PlaceholderValue };
|
|
139
|
+
readonly OS6025: { readonly index: PlaceholderValue };
|
|
136
140
|
readonly OS7001: { readonly name: PlaceholderValue; readonly line: PlaceholderValue };
|
|
137
141
|
readonly OS7002: { readonly name: PlaceholderValue; readonly argument: PlaceholderValue };
|
|
138
142
|
readonly OS7003: { readonly name: PlaceholderValue };
|
|
@@ -171,4 +175,16 @@ export interface DiagnosticValues {
|
|
|
171
175
|
readonly OS8017: { readonly value: PlaceholderValue };
|
|
172
176
|
readonly OS8018: { readonly title: PlaceholderValue; readonly line: PlaceholderValue };
|
|
173
177
|
readonly OS8019: { readonly name: PlaceholderValue; readonly kind: PlaceholderValue; readonly line: PlaceholderValue };
|
|
178
|
+
readonly OS9001: { readonly found: PlaceholderValue };
|
|
179
|
+
readonly OS9002: { readonly construct: PlaceholderValue };
|
|
180
|
+
readonly OS9003: { readonly name: PlaceholderValue };
|
|
181
|
+
readonly OS9004: { readonly found: PlaceholderValue };
|
|
182
|
+
readonly OS9005: { readonly name: PlaceholderValue; readonly line: PlaceholderValue };
|
|
183
|
+
readonly OS9006: { readonly argument: PlaceholderValue; readonly call: PlaceholderValue };
|
|
184
|
+
readonly OS9007: { readonly call: PlaceholderValue; readonly target: PlaceholderValue };
|
|
185
|
+
readonly OS9008: Record<string, never>;
|
|
186
|
+
readonly OS9009: { readonly argument: PlaceholderValue; readonly call: PlaceholderValue };
|
|
187
|
+
readonly OS9010: { readonly call: PlaceholderValue };
|
|
188
|
+
readonly OS9011: { readonly operator: PlaceholderValue };
|
|
189
|
+
readonly OS9012: { readonly code: PlaceholderValue };
|
|
174
190
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names some line of the file gives a value, wherever that line is.
|
|
3
|
+
*
|
|
4
|
+
* `warmup.ts` answers `never` for a value that is absent on every bar, and
|
|
5
|
+
* OS8009 is built on it. The checker reads a name's warmup at the point of use,
|
|
6
|
+
* in source order, which is right for a plain name: it is recomputed every bar
|
|
7
|
+
* and cannot be read before this bar's line writes it. It is not right for a
|
|
8
|
+
* `var`, which is one name over the whole run: a line near the top can read the
|
|
9
|
+
* value a line near the bottom wrote on the bar before, which is the shape of
|
|
10
|
+
* every rollover that carries a running figure into a held one and restarts it.
|
|
11
|
+
*
|
|
12
|
+
* So the checker asks this set before it calls a `var` never. An assignment of
|
|
13
|
+
* the literal `none` is not counted, because it gives the name nothing, and a
|
|
14
|
+
* placeholder `var x = none` that nothing else writes stays exactly what OS8009
|
|
15
|
+
* was written for.
|
|
16
|
+
*/
|
|
17
|
+
import type { AstNode, Script } from '../ast/index.js';
|
|
18
|
+
import { childrenOf, withoutGrouping } from '../ast/index.js';
|
|
19
|
+
|
|
20
|
+
export function namesGivenAValue(script: Script): ReadonlySet<string> {
|
|
21
|
+
const found = new Set<string>();
|
|
22
|
+
collect(script, found);
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collect(node: AstNode, found: Set<string>): void {
|
|
27
|
+
if (node.kind === 'assignment' && withoutGrouping(node.value).kind !== 'noneLiteral') {
|
|
28
|
+
found.add(node.target.text);
|
|
29
|
+
}
|
|
30
|
+
for (const child of childrenOf(node)) collect(child, found);
|
|
31
|
+
}
|