openalgo-script 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +19 -10
  3. package/dist/adapters/charts/capabilities.d.ts +48 -0
  4. package/dist/adapters/charts/capabilities.d.ts.map +1 -0
  5. package/dist/adapters/charts/capabilities.js +82 -0
  6. package/dist/adapters/charts/capabilities.js.map +1 -0
  7. package/dist/adapters/charts/columns.d.ts +2 -0
  8. package/dist/adapters/charts/columns.d.ts.map +1 -1
  9. package/dist/adapters/charts/columns.js +4 -0
  10. package/dist/adapters/charts/columns.js.map +1 -1
  11. package/dist/adapters/charts/contract.d.ts +35 -3
  12. package/dist/adapters/charts/contract.d.ts.map +1 -1
  13. package/dist/adapters/charts/descriptor.d.ts +1 -1
  14. package/dist/adapters/charts/descriptor.d.ts.map +1 -1
  15. package/dist/adapters/charts/descriptor.js +17 -5
  16. package/dist/adapters/charts/descriptor.js.map +1 -1
  17. package/dist/adapters/charts/fills.d.ts +26 -5
  18. package/dist/adapters/charts/fills.d.ts.map +1 -1
  19. package/dist/adapters/charts/fills.js +55 -7
  20. package/dist/adapters/charts/fills.js.map +1 -1
  21. package/dist/adapters/charts/index.d.ts +9 -3
  22. package/dist/adapters/charts/index.d.ts.map +1 -1
  23. package/dist/adapters/charts/index.js +7 -1
  24. package/dist/adapters/charts/index.js.map +1 -1
  25. package/dist/adapters/charts/produced.d.ts +3 -2
  26. package/dist/adapters/charts/produced.d.ts.map +1 -1
  27. package/dist/adapters/charts/produced.js +1 -1
  28. package/dist/adapters/charts/produced.js.map +1 -1
  29. package/dist/adapters/charts/run.d.ts +23 -0
  30. package/dist/adapters/charts/run.d.ts.map +1 -1
  31. package/dist/adapters/charts/run.js +2 -1
  32. package/dist/adapters/charts/run.js.map +1 -1
  33. package/dist/adapters/charts/surfaces.d.ts +11 -0
  34. package/dist/adapters/charts/surfaces.d.ts.map +1 -1
  35. package/dist/adapters/charts/tables.d.ts +21 -11
  36. package/dist/adapters/charts/tables.d.ts.map +1 -1
  37. package/dist/adapters/charts/tables.js +17 -6
  38. package/dist/adapters/charts/tables.js.map +1 -1
  39. package/dist/adapters/charts/undrawable.d.ts +2 -1
  40. package/dist/adapters/charts/undrawable.d.ts.map +1 -1
  41. package/dist/adapters/charts/undrawable.js +24 -14
  42. package/dist/adapters/charts/undrawable.js.map +1 -1
  43. package/dist/core/version/version.generated.d.ts +1 -1
  44. package/dist/core/version/version.generated.js +1 -1
  45. package/package.json +1 -1
  46. package/src/adapters/charts/capabilities.ts +103 -0
  47. package/src/adapters/charts/columns.ts +5 -0
  48. package/src/adapters/charts/contract.ts +36 -2
  49. package/src/adapters/charts/descriptor.ts +21 -6
  50. package/src/adapters/charts/fills.ts +95 -11
  51. package/src/adapters/charts/index.ts +9 -1
  52. package/src/adapters/charts/produced.ts +4 -3
  53. package/src/adapters/charts/run.ts +25 -1
  54. package/src/adapters/charts/surfaces.ts +12 -0
  55. package/src/adapters/charts/tables.ts +31 -14
  56. package/src/adapters/charts/undrawable.ts +24 -14
  57. package/src/core/version/version.generated.ts +1 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * What the host's chart can draw, read from the version the host states.
3
+ *
4
+ * The adapter declares the descriptor's shape rather than importing the chart
5
+ * library, for the reason `contract.ts` gives, so it cannot ask the library
6
+ * what it has. Two hooks arrived after the oldest chart the peer range accepts:
7
+ * a band's per-bar colour callback and the list of grids, both in 2.5.4. A
8
+ * chart ignores a hook it does not know, so a descriptor that relied on either
9
+ * would draw, on an older chart, a band in the chart's own default colours or
10
+ * the first grid alone, and say nothing. That is the failure OS6024 exists to
11
+ * end, so where the adapter cannot tell, it refuses (`undrawable.ts`).
12
+ *
13
+ * **The host states the version, and the adapter reads what that version
14
+ * has.** The library exports its own version string, and a host that imports
15
+ * the chart already holds it: `descriptorFor(program, { chartVersion: VERSION })`.
16
+ * A version rather than a switch per hook, because a switch is a fact about the
17
+ * chart that the host has to look up and keep true through every upgrade and
18
+ * downgrade, while the version is the chart it actually installed.
19
+ *
20
+ * **Nothing stated reads as the oldest chart**, which is the descriptor every
21
+ * host got before the option existed: nothing it draws changes, and a program
22
+ * that needs either hook is refused as it was. A string that is not a version
23
+ * reads the same way, and the refusal says the string could not be read rather
24
+ * than guessing at what it meant. A prerelease orders before its release, as
25
+ * version precedence orders it, so `2.5.4-rc.1` reads as a chart without what
26
+ * 2.5.4 added; build metadata after a `+` orders nothing and is ignored.
27
+ */
28
+
29
+ /** What the chart this descriptor is registered with will read. */
30
+ export interface ChartCapabilities {
31
+ /** A band's colour per bar, through the band's own colour callback. */
32
+ readonly bandColours: boolean;
33
+ /** Every declared grid, through the list of grids with an id each. */
34
+ readonly grids: boolean;
35
+ /** What the host stated, for the sentence a refusal carries. */
36
+ readonly stated: string | undefined;
37
+ }
38
+
39
+ /** The two hooks this reads a version for. */
40
+ export type Capability = 'bandColours' | 'grids';
41
+
42
+ /** The chart version each hook arrived in. */
43
+ const SINCE: Readonly<Record<Capability, string>> = {
44
+ bandColours: '2.5.4',
45
+ grids: '2.5.4',
46
+ };
47
+
48
+ /** `major.minor.patch`, an optional prerelease, optional build metadata. */
49
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
50
+
51
+ interface Read {
52
+ readonly release: readonly [number, number, number];
53
+ readonly prerelease: boolean;
54
+ }
55
+
56
+ export function capabilitiesOf(stated: string | undefined): ChartCapabilities {
57
+ const read = stated === undefined ? undefined : readVersion(stated);
58
+ return {
59
+ bandColours: read !== undefined && reaches(read, SINCE.bandColours),
60
+ grids: read !== undefined && reaches(read, SINCE.grids),
61
+ stated,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * The sentence a refusal gives for a hook the chart may not have.
67
+ *
68
+ * `older` is what a chart without the hook does, in the words a reader of the
69
+ * refusal needs; the rest says which version has it and what this host stated,
70
+ * because the fix is the host's and it has to know which of the two to change.
71
+ */
72
+ export function lacking(chart: ChartCapabilities, capability: Capability, older: string): string {
73
+ const since = SINCE[capability];
74
+ const stated = chart.stated;
75
+ const which =
76
+ stated === undefined
77
+ ? 'this host stated no chart version'
78
+ : readVersion(stated) === undefined
79
+ ? `this host stated ${JSON.stringify(stated)}, which is not a version this adapter can read`
80
+ : `this host stated version ${stated}`;
81
+ return `${older} before chart version ${since}, and ${which}`;
82
+ }
83
+
84
+ function readVersion(text: string): Read | undefined {
85
+ const match = VERSION.exec(text);
86
+ if (match === null) return undefined;
87
+ return {
88
+ release: [Number(match[1]), Number(match[2]), Number(match[3])],
89
+ prerelease: match[4] !== undefined,
90
+ };
91
+ }
92
+
93
+ /** Whether a stated version is at or past the one a hook arrived in. */
94
+ function reaches(read: Read, since: string): boolean {
95
+ const needed = readVersion(since);
96
+ if (needed === undefined) return false;
97
+ for (let part = 0; part < 3; part += 1) {
98
+ const have = read.release[part] ?? 0;
99
+ const need = needed.release[part] ?? 0;
100
+ if (have !== need) return have > need;
101
+ }
102
+ return !read.prerelease;
103
+ }
@@ -42,6 +42,11 @@ export function levelKey(index: number): string {
42
42
  return `openscript:level:${index}`;
43
43
  }
44
44
 
45
+ /** The key one side of a band's computed colour travels under. */
46
+ export function bandColourKey(band: number, side: 'up' | 'down'): string {
47
+ return `openscript:fill:${band}:${side}`;
48
+ }
49
+
45
50
  /** The two keys a per-bar colour travels under. */
46
51
  export function colourKeys(key: string): { readonly rgb: string; readonly alpha: string } {
47
52
  return { rgb: `${key}:rgb`, alpha: `${key}:alpha` };
@@ -32,7 +32,7 @@
32
32
  * What is declared is the whole output surface: the inputs and the settings
33
33
  * dialog a chart generates from them, the plots with their styles and scales,
34
34
  * the bands between them, the horizontal levels, the pane's fixed range, the
35
- * markers, the candle and pane painting, the summary grid, the drawing objects,
35
+ * markers, the candle and pane painting, the summary grids, the drawing objects,
36
36
  * the watched conditions, and the lifecycle a read of another instrument fetches
37
37
  * through.
38
38
  *
@@ -56,6 +56,7 @@ import type {
56
56
  ChartGrid,
57
57
  ChartMarker,
58
58
  ChartSurfaceContext,
59
+ ChartTableSpec,
59
60
  } from './surfaces.js';
60
61
 
61
62
  /** One bar as the chart holds it. Its time is UTC seconds, not milliseconds. */
@@ -223,6 +224,20 @@ export interface ChartPlot {
223
224
  colorParts?(ctx: ChartColorContext): ChartBarColor | undefined;
224
225
  }
225
226
 
227
+ /**
228
+ * What a band's per-bar colour callback is handed, for one bar.
229
+ *
230
+ * `a` and `b` are the two columns the band is drawn between, read on that bar,
231
+ * and `null` where either has no value, which is a bar the band is not drawn on.
232
+ */
233
+ export interface ChartFillContext {
234
+ readonly index: number;
235
+ readonly a: number | null;
236
+ readonly b: number | null;
237
+ readonly values: ChartValues;
238
+ readonly settings: ChartSettings;
239
+ }
240
+
226
241
  export interface ChartFill {
227
242
  readonly between: readonly [string, string];
228
243
  readonly colorUp?: string;
@@ -231,6 +246,15 @@ export interface ChartFill {
231
246
  readonly colorDownKey?: string;
232
247
  readonly opacity?: number;
233
248
  readonly overlay?: boolean;
249
+ /**
250
+ * The band's colour on one bar, for a band whose colour the script computes.
251
+ *
252
+ * Present only on such a band, and only where the host states a chart that
253
+ * has the callback (`capabilities.ts`). Nothing where that bar's side is not
254
+ * computed per bar, which leaves the chart drawing the band's own colour for
255
+ * that side.
256
+ */
257
+ colorBy?(ctx: ChartFillContext): string | undefined;
234
258
  }
235
259
 
236
260
  export interface ChartLevel {
@@ -305,8 +329,18 @@ export interface ChartDescriptor {
305
329
  background?(ctx: ChartSurfaceContext): readonly (string | null)[];
306
330
  /** Every marker the last calculation produced, oldest bar first. */
307
331
  markers?(ctx: ChartSurfaceContext): readonly ChartMarker[];
308
- /** The declared grid as the last executed bar left it, or nothing. */
332
+ /** The first declared grid as the last executed bar left it, or nothing. */
309
333
  table?(ctx: ChartSurfaceContext): ChartGrid | null;
334
+ /**
335
+ * Every declared grid as the last executed bar left it, each under its
336
+ * declaration's key.
337
+ *
338
+ * A chart that has this hook reads it in place of `table`, and one that does
339
+ * not reads `table` and draws the first grid. So it is present only where the
340
+ * host states a chart that has it (`capabilities.ts`), and a second grid is
341
+ * refused on a chart that does not.
342
+ */
343
+ tables?(ctx: ChartSurfaceContext): readonly ChartTableSpec[];
310
344
  /** Every drawing object the script currently holds, oldest first. */
311
345
  draws?(ctx: ChartSurfaceContext): readonly ChartDrawing[];
312
346
  /**
@@ -14,7 +14,7 @@
14
14
  * second thing to keep in step with the specification.
15
15
  *
16
16
  * **The hooks that follow a calculation read it and do not repeat it.** Markers,
17
- * the grid, the drawing objects and each alert's message channel are taken once,
17
+ * the grids, the drawing objects and each alert's message channel are taken once,
18
18
  * while the engine is in hand, and left in the run record `produced.ts` keeps.
19
19
  * The chart calls each hook after every calculation, so asking twice gives one
20
20
  * answer and a study with a grid costs the same over a hundred thousand bars as
@@ -43,6 +43,7 @@
43
43
  */
44
44
  import type { CompiledProgram } from '../../core/emit/index.js';
45
45
  import { alertMessages, buildAlerts } from './alerts.js';
46
+ import { capabilitiesOf } from './capabilities.js';
46
47
  import { valuesFrom } from './columns.js';
47
48
  import type {
48
49
  ChartBar,
@@ -70,8 +71,9 @@ import type {
70
71
  ChartGrid,
71
72
  ChartMarker,
72
73
  ChartSurfaceContext,
74
+ ChartTableSpec,
73
75
  } from './surfaces.js';
74
- import { buildTable } from './tables.js';
76
+ import { buildTables, firstGrid } from './tables.js';
75
77
 
76
78
  /** The grey an unnamed marker is drawn in, where the host names no default. */
77
79
  const MARKER_COLOUR = 'rgba(128, 128, 128, 1)';
@@ -87,11 +89,14 @@ export function descriptorFor(
87
89
  // afterwards carry a settings key instead of a value, and the two that cannot
88
90
  // are read again per call: a level's style and the pane's range.
89
91
  const declared = lookupFor(program, options.settings ?? {});
92
+ // Which of the descriptor's newer hooks the host's chart reads. A hook it may
93
+ // not read is left off, and what needs one is refused before a bar runs.
94
+ const chart = capabilitiesOf(options.chartVersion);
90
95
 
91
96
  const overlay = boolField(program.meta.overlay, declared) === true;
92
97
  const plots = buildPlots(program, declared, !overlay);
93
98
  const levels = buildLevels(program);
94
- const fills = buildFills(program, declared);
99
+ const fills = buildFills(program, declared, chart);
95
100
  const paint = buildPaint(program);
96
101
  const alerts = buildAlerts(program, declared);
97
102
  const columns = [
@@ -99,6 +104,7 @@ export function descriptorFor(
99
104
  ...levels.columns,
100
105
  ...paint.columns,
101
106
  ...alerts.columns,
107
+ ...fills.columns,
102
108
  ];
103
109
  const markerColour = options.markerColor ?? MARKER_COLOUR;
104
110
 
@@ -117,7 +123,7 @@ export function descriptorFor(
117
123
  const lookup = lookupFor(program, settings);
118
124
  remember(settings, {
119
125
  markers: buildMarkers(program, lookup, bars, ran.columns, markerColour),
120
- table: buildTable(program, lookup, ran.tables),
126
+ tables: buildTables(program, lookup, ran.tables),
121
127
  drawings: buildDrawings(ran.drawings),
122
128
  messages: alertMessages(program, ran.columns),
123
129
  });
@@ -134,7 +140,7 @@ export function descriptorFor(
134
140
  placement: overlay ? 'onchart' : 'pane',
135
141
  inputs: inputRows(program),
136
142
  plots: plots.plots,
137
- ...(fills.length === 0 ? {} : { fills }),
143
+ ...(fills.fills.length === 0 ? {} : { fills: fills.fills }),
138
144
 
139
145
  ...(alerts.alerts.length === 0 ? {} : { alerts: alerts.alerts }),
140
146
 
@@ -175,7 +181,16 @@ export function descriptorFor(
175
181
  }),
176
182
  ...(program.outputs.tables.length === 0
177
183
  ? {}
178
- : { table: (ctx: ChartSurfaceContext): ChartGrid | null => producedFor(ctx.settings).table }),
184
+ : {
185
+ table: (ctx: ChartSurfaceContext): ChartGrid | null =>
186
+ firstGrid(producedFor(ctx.settings).tables),
187
+ }),
188
+ ...(program.outputs.tables.length === 0 || !chart.grids
189
+ ? {}
190
+ : {
191
+ tables: (ctx: ChartSurfaceContext): readonly ChartTableSpec[] =>
192
+ producedFor(ctx.settings).tables,
193
+ }),
179
194
  ...(program.requires.includes('objects')
180
195
  ? {
181
196
  draws: (ctx: ChartSurfaceContext): readonly ChartDrawing[] =>
@@ -2,23 +2,40 @@
2
2
  * Declared bands into the chart's shaded bands.
3
3
  *
4
4
  * A band is two plot keys and two colours, which is the chart's own spec, so the
5
- * pair of keys crosses unchanged. Two details do not.
5
+ * pair of keys crosses unchanged. Three details do not.
6
6
  *
7
7
  * **Opacity is a dimmer, not a second alpha.** A colour carries its own alpha
8
8
  * everywhere in the language, and the band's `opacity` multiplies whatever the
9
- * colours already are. That is exactly what the chart does with the field, so a
10
- * script that never touches it gets the colour it wrote.
9
+ * colours already are. That is exactly what the chart does with the field, to a
10
+ * colour computed per bar as much as to a constant one, so a script that never
11
+ * touches it gets the colour it wrote.
11
12
  *
12
13
  * **A band with no colour of its own follows the first plot.** `stdlib.md` 14.2
13
14
  * says such a band is that plot's colour faded to twelve percent, and the plot's
14
15
  * colour may be one the host picked from its palette rather than one the script
15
16
  * wrote, so the band is pointed at the plot's settings key rather than at a value
16
17
  * read now. The twelve percent is then the dimmer, multiplied by whatever the
17
- * script asked for.
18
+ * script asked for. A band whose colour is computed per bar has a colour of its
19
+ * own, so neither applies to it.
20
+ *
21
+ * **A colour computed per bar travels as columns and is read back per bar.**
22
+ * It arrives on a channel, rides the values table as the two columns
23
+ * `columns.ts` describes, and the band's colour callback reads them on each bar
24
+ * for the side the band is on there. The side is decided the way the chart
25
+ * decides it, the first plot at or above the second, so the colour answered is
26
+ * the colour of the run the chart is drawing that bar in. An absent colour is a
27
+ * transparent one rather than no answer, because no answer hands the bar to
28
+ * the band's colour for that side, and `docs/visuals/fills.md` teaches an absent
29
+ * colour as how a band switches itself off. The callback exists only on a chart
30
+ * that reads it (`capabilities.ts`); on any other, `undrawable.ts` has refused
31
+ * the program before this is drawn.
18
32
  */
19
33
  import type { Band, CompiledProgram } from '../../core/emit/index.js';
34
+ import type { ChartCapabilities } from './capabilities.js';
20
35
  import { cssColour } from './colours.js';
21
- import type { ChartFill } from './contract.js';
36
+ import { bandColourKey, colourAt, colourColumns } from './columns.js';
37
+ import type { ColumnSpec } from './columns.js';
38
+ import type { ChartFill, ChartFillContext } from './contract.js';
22
39
  import { boolField, colourField, isReference, numberField } from './fields.js';
23
40
  import type { InputLookup } from './fields.js';
24
41
 
@@ -30,17 +47,67 @@ import type { InputLookup } from './fields.js';
30
47
  */
31
48
  const UNCOLOURED_FADE = 0.12;
32
49
 
33
- export function buildFills(program: CompiledProgram, lookup: InputLookup): readonly ChartFill[] {
34
- const out: ChartFill[] = [];
35
- for (const band of program.outputs.fills) out.push(oneBand(program, band, lookup));
36
- return out;
50
+ /** What a bar whose computed colour is absent is painted in: nothing. */
51
+ const UNPAINTED = 'rgba(0, 0, 0, 0)';
52
+
53
+ export interface FillsBuild {
54
+ readonly fills: readonly ChartFill[];
55
+ /** The colour columns the bands' callbacks read, for the values table. */
56
+ readonly columns: readonly ColumnSpec[];
37
57
  }
38
58
 
39
- function oneBand(program: CompiledProgram, band: Band, lookup: InputLookup): ChartFill {
59
+ export function buildFills(
60
+ program: CompiledProgram,
61
+ lookup: InputLookup,
62
+ chart: ChartCapabilities,
63
+ ): FillsBuild {
64
+ const fills: ChartFill[] = [];
65
+ const columns: ColumnSpec[] = [];
66
+ program.outputs.fills.forEach((band, index) => {
67
+ const perBar = chart.bandColours ? perBarColours(band, index) : undefined;
68
+ if (perBar !== undefined) columns.push(...perBar.columns);
69
+ fills.push(oneBand(program, band, lookup, perBar));
70
+ });
71
+ return { fills, columns };
72
+ }
73
+
74
+ /** The keys a band's two computed colours travel under, and their columns. */
75
+ interface PerBar {
76
+ readonly up: string | undefined;
77
+ readonly down: string | undefined;
78
+ readonly columns: readonly ColumnSpec[];
79
+ }
80
+
81
+ /**
82
+ * The columns a band's computed colours need, or nothing for a band with none.
83
+ *
84
+ * `color = ...` computed per bar writes one channel into both sides, and that
85
+ * channel travels once rather than twice.
86
+ */
87
+ function perBarColours(band: Band, index: number): PerBar | undefined {
88
+ const upChannel = band.colorUpChannel;
89
+ const downChannel = band.colorDownChannel;
90
+ if (upChannel === null && downChannel === null) return undefined;
91
+ const up = upChannel === null ? undefined : bandColourKey(index, 'up');
92
+ const shared = downChannel !== null && downChannel === upChannel;
93
+ const down = downChannel === null ? undefined : shared ? up : bandColourKey(index, 'down');
94
+ const columns: ColumnSpec[] = [];
95
+ if (up !== undefined && upChannel !== null) columns.push(...colourColumns(up, upChannel));
96
+ if (down !== undefined && downChannel !== null && !shared) columns.push(...colourColumns(down, downChannel));
97
+ return { up, down, columns };
98
+ }
99
+
100
+ function oneBand(
101
+ program: CompiledProgram,
102
+ band: Band,
103
+ lookup: InputLookup,
104
+ perBar: PerBar | undefined,
105
+ ): ChartFill {
40
106
  const up = colourField(band.colorUp, lookup);
41
107
  const down = colourField(band.colorDown, lookup);
42
108
  const overlay = boolField(band.overlay, lookup);
43
- const declared = up !== undefined || down !== undefined;
109
+ const computed = band.colorUpChannel !== null || band.colorDownChannel !== null;
110
+ const declared = up !== undefined || down !== undefined || computed;
44
111
  const opacity = numberField(band.opacity, lookup, 1) * (declared ? 1 : UNCOLOURED_FADE);
45
112
  const follow = declared ? undefined : plotColourKey(program, band.between[0]);
46
113
 
@@ -53,6 +120,23 @@ function oneBand(program: CompiledProgram, band: Band, lookup: InputLookup): Cha
53
120
  ...(follow === undefined ? {} : { colorUpKey: follow, colorDownKey: follow }),
54
121
  opacity,
55
122
  ...(overlay === undefined ? {} : { overlay }),
123
+ ...(perBar === undefined ? {} : { colorBy: colourBy(perBar) }),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * The band's colour on one bar: the computed colour for the side it is on.
129
+ *
130
+ * Nothing where either column is absent, which is a bar the chart does not
131
+ * draw, and nothing where that side's colour is not computed, which leaves the
132
+ * chart the band's own colour for the side.
133
+ */
134
+ function colourBy(perBar: PerBar): (ctx: ChartFillContext) => string | undefined {
135
+ return (ctx: ChartFillContext): string | undefined => {
136
+ if (typeof ctx.a !== 'number' || typeof ctx.b !== 'number') return undefined;
137
+ const key = ctx.a >= ctx.b ? perBar.up : perBar.down;
138
+ if (key === undefined) return undefined;
139
+ return colourAt(ctx.values, ctx.index, key) ?? UNPAINTED;
56
140
  };
57
141
  }
58
142
 
@@ -16,10 +16,16 @@
16
16
  * What is mapped is the whole output surface: the declared inputs and the
17
17
  * settings dialog a chart generates from them, the plots with their styles and
18
18
  * scales, the bands between them, the horizontal levels, the pane's fixed range,
19
- * the markers, the candle and pane painting, the summary grid, the drawing
19
+ * the markers, the candle and pane painting, the summary grids, the drawing
20
20
  * objects a script mutates over time, the watched conditions, and the lifecycle
21
21
  * a read of another instrument fetches through.
22
22
  *
23
+ * **Two of those depend on the chart the host states.** A band coloured per bar
24
+ * and a study with more than one grid need hooks only a newer chart has, so a
25
+ * host passes the chart library's own `VERSION` as
26
+ * `ChartAdapterOptions.chartVersion`, and without it such a study is refused
27
+ * with OS6024 rather than drawn in part. `capabilities.ts` has the versions.
28
+ *
23
29
  * What a study can express and this descriptor has no field for is recorded,
24
30
  * with its reason, in `spec/chart-narrowings.json`, and each one is also named
25
31
  * in the file that would have written it. `scripts/check-chart-surface.mjs`
@@ -41,6 +47,7 @@ export type {
41
47
  ChartCalcContext,
42
48
  ChartDescriptor,
43
49
  ChartFill,
50
+ ChartFillContext,
44
51
  ChartInput,
45
52
  ChartLevel,
46
53
  ChartLevelContext,
@@ -75,4 +82,5 @@ export type {
75
82
  ChartSurfaceContext,
76
83
  ChartTableOptions,
77
84
  ChartTablePosition,
85
+ ChartTableSpec,
78
86
  } from './surfaces.js';
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { Value } from '../../core/engine/index.js';
34
34
  import type { ChartSettings } from './contract.js';
35
- import type { ChartDrawing, ChartGrid, ChartMarker } from './surfaces.js';
35
+ import type { ChartDrawing, ChartMarker, ChartTableSpec } from './surfaces.js';
36
36
 
37
37
  /**
38
38
  * One declared alert's message channel, whole, as the run left it.
@@ -47,13 +47,14 @@ export type MessageColumn = readonly Value[];
47
47
  /** One run's non-numeric output, as the hooks that follow it read it. */
48
48
  export interface Produced {
49
49
  readonly markers: readonly ChartMarker[];
50
- readonly table: ChartGrid | null;
50
+ /** Every declared grid, in declaration order; the single hook reads the first. */
51
+ readonly tables: readonly ChartTableSpec[];
51
52
  readonly drawings: readonly ChartDrawing[];
52
53
  /** One entry per declared alert, in the order `outputs.alerts` declares them. */
53
54
  readonly messages: readonly MessageColumn[];
54
55
  }
55
56
 
56
- const NOTHING: Produced = { markers: [], table: null, drawings: [], messages: [] };
57
+ const NOTHING: Produced = { markers: [], tables: [], drawings: [], messages: [] };
57
58
 
58
59
  const runs = new WeakMap<ChartSettings, Produced>();
59
60
 
@@ -38,6 +38,7 @@ import type { CompiledProgram } from '../../core/emit/index.js';
38
38
  import type { SourceFile } from '../../core/index.js';
39
39
  import { hostBar, hostNow, stateFor } from './bars.js';
40
40
  import type { ChartBar, ChartCalcContext, ChartSettings, ChartStore } from './contract.js';
41
+ import { capabilitiesOf } from './capabilities.js';
41
42
  import { refused, stopped } from './errors.js';
42
43
  import { undrawable } from './undrawable.js';
43
44
  import { stationIn, stationOf } from './requests.js';
@@ -60,6 +61,29 @@ export interface ChartAdapterOptions {
60
61
  readonly id?: string;
61
62
  /** The category a picker groups the study under, when `meta.group` is empty. */
62
63
  readonly category?: string;
64
+ /**
65
+ * The chart library's own version: the `VERSION` string it exports, which a
66
+ * host that imports the chart already holds, passed on as
67
+ * `descriptorFor(program, { chartVersion: VERSION })`.
68
+ *
69
+ * It says which of the descriptor's hooks the chart this descriptor is
70
+ * registered with will read. Two arrived after the oldest chart the peer
71
+ * range accepts: a band's per-bar colour and the list of grids. From 2.5.4
72
+ * on, a band whose colour the script computes is drawn bar by bar and every
73
+ * declared grid is drawn. Before it, or when nothing is stated, the adapter
74
+ * cannot tell whether the chart will read either hook, and a chart that
75
+ * ignores one draws a band in its own default colours or the first grid
76
+ * alone, with nothing said. So such a program is refused before any bar runs
77
+ * with OS6024, which is what a host that states nothing got before this
78
+ * option existed.
79
+ *
80
+ * The version rather than a switch per hook, because it is one fact the
81
+ * host already holds, it moves with the chart the host actually installed,
82
+ * and a hook the adapter learns to read later needs nothing new from any
83
+ * host. `capabilities.ts` has the rule, prereleases and unreadable strings
84
+ * included.
85
+ */
86
+ readonly chartVersion?: string;
63
87
  /**
64
88
  * What the host has stored for this study's inputs, for the declared shape.
65
89
  *
@@ -338,7 +362,7 @@ function start(
338
362
 
339
363
  // What this chart has no room for is refused before the engine is asked,
340
364
  // because a study drawn without it is a study that looks broken.
341
- const narrower = undrawable(program);
365
+ const narrower = undrawable(program, capabilitiesOf(options.chartVersion));
342
366
  if (narrower !== undefined) throw refused(narrower);
343
367
 
344
368
  const loaded = load(program, {
@@ -125,6 +125,18 @@ export interface ChartGrid {
125
125
  readonly options?: ChartTableOptions;
126
126
  }
127
127
 
128
+ /**
129
+ * One grid of several, under an identity the chart keeps it by.
130
+ *
131
+ * The chart reuses the grid a recompute names again and removes one it no
132
+ * longer names, so the id is the declaration's key: stable across recomputes,
133
+ * never empty, and unique within a program because the compiler writes one per
134
+ * declaration.
135
+ */
136
+ export interface ChartTableSpec extends ChartGrid {
137
+ readonly id: string;
138
+ }
139
+
128
140
  /** One end of a drawing: a time on the shared axis, a price on the pane's scale. */
129
141
  export interface ChartAnchor {
130
142
  readonly time: number;
@@ -27,13 +27,17 @@
27
27
  * placed into it, which is what `language.md` 6.7 asks for from the other
28
28
  * direction: a cell nothing was written into is blank, never a zero.
29
29
  *
30
- * **A chart pane holds one grid and the language declares as many as it likes.**
31
- * The descriptor has one `table` hook, and merging two grids into it would put
32
- * cells somewhere the script never asked for. So a program declaring a second
33
- * grid never reaches this file: `undrawable.ts` refuses it before any bar runs,
34
- * with OS6024, and this reads the one grid a program that runs can have.
35
- * `spec/chart-narrowings.json` records the refusal and
36
- * `scripts/check-chart-surface.mjs` proves it with a study that declares two.
30
+ * **The language declares as many grids as it likes, and a chart with the list
31
+ * of grids draws them all** (`capabilities.ts` says which chart has it). The
32
+ * list holds each grid under an id the chart keeps it by, and every declared
33
+ * grid is built for it here under its declaration's key. The single `table`
34
+ * hook stays beside the list with the first grid in it, because a chart
35
+ * without the list reads that one. Merging two grids into the single hook
36
+ * would put cells somewhere the script never asked for, so on a chart without
37
+ * the list a program declaring a second grid never reaches this file:
38
+ * `undrawable.ts` refuses it before any bar runs, with OS6024.
39
+ * `spec/chart-narrowings.json` records both halves and
40
+ * `scripts/check-chart-surface.mjs` proves both with a study that declares two.
37
41
  */
38
42
  import type { CompiledProgram, Grid as DeclaredGrid } from '../../core/emit/index.js';
39
43
  import type { Grid, GridCell } from '../../core/engine/index.js';
@@ -45,6 +49,7 @@ import type {
45
49
  ChartCellAlign,
46
50
  ChartGrid,
47
51
  ChartTablePosition,
52
+ ChartTableSpec,
48
53
  } from './surfaces.js';
49
54
 
50
55
  /** The language's four corners, in the chart's own words. */
@@ -58,22 +63,34 @@ const CORNERS: Readonly<Record<string, ChartTablePosition>> = {
58
63
  const ALIGNMENTS: readonly string[] = ['left', 'center', 'right'];
59
64
 
60
65
  /**
61
- * The grid a chart draws, from the declaration and the engine's buffer.
66
+ * Every grid a chart draws, from the declarations and the engine's buffers.
62
67
  *
63
- * The buffer is paired with the declaration by the key both of them carry
68
+ * Each buffer is paired with its declaration by the key both of them carry
64
69
  * rather than by position. The engine reads its grids in declaration order, so
65
70
  * the two agree today, and a pairing by position is one reordering away from
66
71
  * drawing one grid's cells into another grid's shape, which would be a wrong
67
72
  * table that looks like a right one.
73
+ *
74
+ * The key is the id as well. The compiler writes one per declaration and never
75
+ * an empty one, and it does not move between recomputes, which is what lets the
76
+ * chart keep a grid rather than build it again on every tick.
68
77
  */
69
- export function buildTable(
78
+ export function buildTables(
70
79
  program: CompiledProgram,
71
80
  lookup: InputLookup,
72
81
  written: readonly Grid[],
73
- ): ChartGrid | null {
74
- const declared = program.outputs.tables[0];
75
- if (declared === undefined) return null;
76
- return oneTable(declared, lookup, written.find((one) => one.key === declared.key));
82
+ ): readonly ChartTableSpec[] {
83
+ return program.outputs.tables.map((declared) => ({
84
+ id: declared.key,
85
+ ...oneTable(declared, lookup, written.find((one) => one.key === declared.key)),
86
+ }));
87
+ }
88
+
89
+ /** The first grid alone, for the single hook a chart without the list reads. */
90
+ export function firstGrid(tables: readonly ChartTableSpec[]): ChartGrid | null {
91
+ const first = tables[0];
92
+ if (first === undefined) return null;
93
+ return { rows: first.rows, ...(first.options === undefined ? {} : { options: first.options }) };
77
94
  }
78
95
 
79
96
  function oneTable(