openalgo-script 0.4.0 → 0.5.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 (158) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/README.md +69 -16
  3. package/dist/adapters/charts/driving.d.ts +50 -0
  4. package/dist/adapters/charts/driving.d.ts.map +1 -0
  5. package/dist/adapters/charts/driving.js +57 -0
  6. package/dist/adapters/charts/driving.js.map +1 -0
  7. package/dist/adapters/charts/run.d.ts +20 -0
  8. package/dist/adapters/charts/run.d.ts.map +1 -1
  9. package/dist/adapters/charts/run.js +83 -16
  10. package/dist/adapters/charts/run.js.map +1 -1
  11. package/dist/adapters/charts/venue.d.ts +73 -0
  12. package/dist/adapters/charts/venue.d.ts.map +1 -0
  13. package/dist/adapters/charts/venue.js +104 -0
  14. package/dist/adapters/charts/venue.js.map +1 -0
  15. package/dist/core/accounting/analysis.d.ts +111 -0
  16. package/dist/core/accounting/analysis.d.ts.map +1 -0
  17. package/dist/core/accounting/analysis.js +123 -0
  18. package/dist/core/accounting/analysis.js.map +1 -0
  19. package/dist/core/accounting/equity.d.ts +33 -0
  20. package/dist/core/accounting/equity.d.ts.map +1 -1
  21. package/dist/core/accounting/equity.js +12 -0
  22. package/dist/core/accounting/equity.js.map +1 -1
  23. package/dist/core/accounting/index.d.ts +2 -0
  24. package/dist/core/accounting/index.d.ts.map +1 -1
  25. package/dist/core/accounting/index.js +1 -0
  26. package/dist/core/accounting/index.js.map +1 -1
  27. package/dist/core/accounting/report.d.ts +3 -0
  28. package/dist/core/accounting/report.d.ts.map +1 -1
  29. package/dist/core/accounting/report.js +2 -0
  30. package/dist/core/accounting/report.js.map +1 -1
  31. package/dist/core/accounting/statistics.d.ts +12 -0
  32. package/dist/core/accounting/statistics.d.ts.map +1 -1
  33. package/dist/core/accounting/statistics.js +23 -1
  34. package/dist/core/accounting/statistics.js.map +1 -1
  35. package/dist/core/backtest/case.d.ts +60 -0
  36. package/dist/core/backtest/case.d.ts.map +1 -0
  37. package/dist/core/backtest/case.js +319 -0
  38. package/dist/core/backtest/case.js.map +1 -0
  39. package/dist/core/backtest/compare.d.ts.map +1 -1
  40. package/dist/core/backtest/compare.js +2 -0
  41. package/dist/core/backtest/compare.js.map +1 -1
  42. package/dist/core/backtest/deliver.d.ts +93 -0
  43. package/dist/core/backtest/deliver.d.ts.map +1 -0
  44. package/dist/core/backtest/deliver.js +94 -0
  45. package/dist/core/backtest/deliver.js.map +1 -0
  46. package/dist/core/backtest/drive.d.ts +66 -8
  47. package/dist/core/backtest/drive.d.ts.map +1 -1
  48. package/dist/core/backtest/drive.js +109 -59
  49. package/dist/core/backtest/drive.js.map +1 -1
  50. package/dist/core/backtest/index.d.ts +24 -12
  51. package/dist/core/backtest/index.d.ts.map +1 -1
  52. package/dist/core/backtest/index.js +21 -10
  53. package/dist/core/backtest/index.js.map +1 -1
  54. package/dist/core/backtest/record.d.ts +63 -2
  55. package/dist/core/backtest/record.d.ts.map +1 -1
  56. package/dist/core/backtest/record.js +71 -4
  57. package/dist/core/backtest/record.js.map +1 -1
  58. package/dist/core/backtest/replay.d.ts.map +1 -1
  59. package/dist/core/backtest/replay.js +11 -1
  60. package/dist/core/backtest/replay.js.map +1 -1
  61. package/dist/core/backtest/simulate.d.ts +113 -1
  62. package/dist/core/backtest/simulate.d.ts.map +1 -1
  63. package/dist/core/backtest/simulate.js +123 -5
  64. package/dist/core/backtest/simulate.js.map +1 -1
  65. package/dist/core/catalogue/catalogue.generated.d.ts +1 -1
  66. package/dist/core/catalogue/catalogue.generated.js +1 -1
  67. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  68. package/dist/core/check/library-orders.js +2 -2
  69. package/dist/core/check/library-orders.js.map +1 -1
  70. package/dist/core/check/library-prose.generated.js +2 -2
  71. package/dist/core/check/library-prose.generated.js.map +1 -1
  72. package/dist/core/emit/canonical.d.ts +22 -8
  73. package/dist/core/emit/canonical.d.ts.map +1 -1
  74. package/dist/core/emit/canonical.js +67 -6
  75. package/dist/core/emit/canonical.js.map +1 -1
  76. package/dist/core/engine/arithmetic.d.ts +6 -25
  77. package/dist/core/engine/arithmetic.d.ts.map +1 -1
  78. package/dist/core/engine/arithmetic.js +48 -3
  79. package/dist/core/engine/arithmetic.js.map +1 -1
  80. package/dist/core/engine/index.d.ts +1 -1
  81. package/dist/core/engine/index.d.ts.map +1 -1
  82. package/dist/core/engine/index.js +1 -1
  83. package/dist/core/engine/index.js.map +1 -1
  84. package/dist/core/engine/library/arrays.d.ts.map +1 -1
  85. package/dist/core/engine/library/arrays.js +8 -2
  86. package/dist/core/engine/library/arrays.js.map +1 -1
  87. package/dist/core/engine/library/code-points.d.ts +40 -0
  88. package/dist/core/engine/library/code-points.d.ts.map +1 -0
  89. package/dist/core/engine/library/code-points.js +74 -0
  90. package/dist/core/engine/library/code-points.js.map +1 -0
  91. package/dist/core/engine/library/index.d.ts +5 -0
  92. package/dist/core/engine/library/index.d.ts.map +1 -1
  93. package/dist/core/engine/library/index.js +5 -0
  94. package/dist/core/engine/library/index.js.map +1 -1
  95. package/dist/core/engine/library/text.d.ts.map +1 -1
  96. package/dist/core/engine/library/text.js +51 -34
  97. package/dist/core/engine/library/text.js.map +1 -1
  98. package/dist/core/engine/load.d.ts +20 -0
  99. package/dist/core/engine/load.d.ts.map +1 -1
  100. package/dist/core/engine/load.js +62 -0
  101. package/dist/core/engine/load.js.map +1 -1
  102. package/dist/core/engine/verify-tables.d.ts.map +1 -1
  103. package/dist/core/engine/verify-tables.js +41 -0
  104. package/dist/core/engine/verify-tables.js.map +1 -1
  105. package/dist/core/index.d.ts +10 -5
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js +8 -3
  108. package/dist/core/index.js.map +1 -1
  109. package/dist/core/stdlib/index.d.ts +1 -1
  110. package/dist/core/stdlib/index.d.ts.map +1 -1
  111. package/dist/core/stdlib/index.js +1 -1
  112. package/dist/core/stdlib/index.js.map +1 -1
  113. package/dist/core/stdlib/maths/index.d.ts +1 -1
  114. package/dist/core/stdlib/maths/index.d.ts.map +1 -1
  115. package/dist/core/stdlib/maths/index.js +1 -1
  116. package/dist/core/stdlib/maths/index.js.map +1 -1
  117. package/dist/core/stdlib/maths/rounding.d.ts +5 -0
  118. package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
  119. package/dist/core/stdlib/maths/rounding.js +19 -1
  120. package/dist/core/stdlib/maths/rounding.js.map +1 -1
  121. package/dist/core/version/version.generated.d.ts +1 -1
  122. package/dist/core/version/version.generated.js +1 -1
  123. package/package.json +14 -2
  124. package/spec/README.md +2 -1
  125. package/spec/errors.json +3 -3
  126. package/src/adapters/charts/driving.ts +109 -0
  127. package/src/adapters/charts/run.ts +120 -28
  128. package/src/adapters/charts/venue.ts +132 -0
  129. package/src/core/accounting/analysis.ts +188 -0
  130. package/src/core/accounting/equity.ts +38 -0
  131. package/src/core/accounting/index.ts +2 -0
  132. package/src/core/accounting/report.ts +5 -0
  133. package/src/core/accounting/statistics.ts +38 -1
  134. package/src/core/backtest/case.ts +395 -0
  135. package/src/core/backtest/compare.ts +2 -0
  136. package/src/core/backtest/deliver.ts +161 -0
  137. package/src/core/backtest/drive.ts +175 -71
  138. package/src/core/backtest/index.ts +24 -12
  139. package/src/core/backtest/record.ts +134 -5
  140. package/src/core/backtest/replay.ts +11 -1
  141. package/src/core/backtest/simulate.ts +201 -6
  142. package/src/core/catalogue/catalogue.generated.ts +1 -1
  143. package/src/core/check/library-orders.ts +2 -2
  144. package/src/core/check/library-prose.generated.ts +2 -2
  145. package/src/core/emit/canonical.ts +67 -9
  146. package/src/core/engine/arithmetic.ts +23 -3
  147. package/src/core/engine/index.ts +1 -1
  148. package/src/core/engine/library/arrays.ts +8 -2
  149. package/src/core/engine/library/code-points.ts +73 -0
  150. package/src/core/engine/library/index.ts +6 -0
  151. package/src/core/engine/library/text.ts +53 -35
  152. package/src/core/engine/load.ts +68 -0
  153. package/src/core/engine/verify-tables.ts +43 -0
  154. package/src/core/index.ts +16 -2
  155. package/src/core/stdlib/index.ts +1 -0
  156. package/src/core/stdlib/maths/index.ts +1 -0
  157. package/src/core/stdlib/maths/rounding.ts +23 -1
  158. package/src/core/version/version.generated.ts +1 -1
@@ -8,34 +8,50 @@
8
8
  * one element, or two engines will disagree about the length of a string
9
9
  * holding a symbol outside the basic plane and about every substring taken
10
10
  * after one. That is what the spread into an array is doing everywhere below:
11
- * it costs a pass over the string and buys agreement.
11
+ * it costs a pass over the string and buys agreement. The trimmed set and the
12
+ * order of two strings are in `code-points.ts`, written from the page.
12
13
  *
13
14
  * **Number to string is specified, not the host's default.** `text(x)` is the
14
- * shortest decimal string that reads back as the same binary64 value, which is
15
- * what this language's own conversion already produces. `text(x, d)` rounds to
16
- * `d` decimals with halves away from zero and always emits exactly `d` digits
17
- * after the point, because this is a display conversion and half up is what a
18
- * reader of a price expects.
15
+ * rule of `language.md` 5.5 and goes through the one writer that implements
16
+ * it, `canonicalNumber`, as does every digit this file writes: a host's own
17
+ * conversion is never asked. `text(x, d)` rounds to `d` decimals with halves
18
+ * away from zero and always emits exactly `d` digits after the point, because
19
+ * this is a display conversion and half up is what a reader of a price expects.
19
20
  */
20
- import { roundHalfAway } from '../../stdlib/index.js';
21
+ import { canonicalNumber } from '../../emit/index.js';
22
+ import { roundHalfAway, scaleOf } from '../../stdlib/index.js';
21
23
  import type { Value } from '../values/index.js';
22
24
  import { isColour, reference } from '../values/index.js';
23
25
  import { entry, numberAt, refAt, stringAt, valueAt, wholeAt } from './binding.js';
24
26
  import type { CallContext, ManifestEntry } from './binding.js';
27
+ import { points, trimmed } from './code-points.js';
25
28
 
26
29
  /** A value as `text(x)` spells it. */
27
30
  export function spell(ctx: CallContext, value: Value): string {
28
31
  if (value === null) return 'none';
29
- if (typeof value === 'number') return String(value);
30
- if (typeof value === 'boolean') return String(value);
32
+ if (typeof value === 'number') return canonicalNumber(value);
33
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
31
34
  if (typeof value === 'string') return value;
32
35
  if (isColour(value)) {
33
- const byte = (x: number): string => x.toString(16).padStart(2, '0');
34
- return `#${byte(value.r)}${byte(value.g)}${byte(value.b)}${byte(roundHalfAway(value.a * 255))}`;
36
+ const alpha = roundHalfAway(value.a * 255);
37
+ return `#${hexByte(value.r)}${hexByte(value.g)}${hexByte(value.b)}${hexByte(alpha)}`;
35
38
  }
36
39
  return ctx.heap.get(value.id)?.kind ?? 'none';
37
40
  }
38
41
 
42
+ /**
43
+ * A whole number from 0 to 255 as two hex digits, from a table.
44
+ *
45
+ * A colour's channels are whole numbers (`compiled-program.md` 3.1), so this
46
+ * is not the decimal rendering rule and asks no host conversion: each nibble
47
+ * indexes a string of sixteen characters.
48
+ */
49
+ const HEX_DIGITS = '0123456789abcdef';
50
+
51
+ function hexByte(channel: number): string {
52
+ return `${HEX_DIGITS[channel >> 4] ?? '0'}${HEX_DIGITS[channel & 15] ?? '0'}`;
53
+ }
54
+
39
55
  /** A magnitude as decimal digits, and how many of them fall before the point. */
40
56
  interface Spread {
41
57
  readonly digits: string;
@@ -108,24 +124,31 @@ function written(of: Spread): string {
108
124
  * least one digit, and exactly `d` digits after the point. `spread` is what
109
125
  * makes that true of every magnitude rather than of the ones below a threshold.
110
126
  *
111
- * **Past the scaling range the digits are the shortest form's, zero filled.**
112
- * Scaling by `10 ** d` leaves binary64 altogether for a large enough magnitude
113
- * or a large enough `d`, and there is nothing to round out there: a binary64 at
114
- * or above 2 ** 53 is a whole number already, and a decimal place that far from
115
- * the leading digit is past every digit the value carries. Two engines write
116
- * the same digits, because the shortest decimal form is what each one's own
117
- * conversion produces. Inside the range the scaling is what it was, so no value
118
- * that had an answer has a different one.
127
+ * **The scale is `round(x, d)`'s**, `scaleOf` from the numeric library, so
128
+ * the display conversion and the rounding call multiply by one value
129
+ * (`stdlib.md` 20.7) and cannot part by an ulp at the one count where the
130
+ * host's power does.
131
+ *
132
+ * **The digits are the shortest form's, zero filled, at every magnitude.**
133
+ * Below 2 ** 53 a rounded whole number has no digits but its own, so nothing
134
+ * ordinary changes; at or above it the shortest form is what both engines
135
+ * write, and it is what this uses inside the scaling range as well as past it,
136
+ * where scaling by ten to the `d` leaves binary64 altogether and there is
137
+ * nothing to round: a binary64 that large is a whole number already, and a
138
+ * decimal place that far from the leading digit is past every digit the value
139
+ * carries. One rule for the digits at every magnitude, rather than the exact
140
+ * binary expansion below a threshold and the shortest form above it.
119
141
  */
120
142
  function fixed(x: number, decimals: number): string {
121
- const scaled = roundHalfAway(x * Math.pow(10, decimals));
143
+ const scaled = roundHalfAway(x * scaleOf(decimals));
122
144
  const usable = Number.isFinite(scaled);
123
145
  const sign = (usable ? scaled < 0 : x < 0) ? '-' : '';
124
- // Within the scaling range the whole number's own digits are asked for and
125
- // not its shortest form, which drops the low digits of a large one.
146
+ // The digits are the shortest form's at every magnitude, through the one
147
+ // writer: a rounded whole below 2 ** 53 has no other digits, and above it
148
+ // the shortest form is the one rule both engines write (language.md 5.5).
126
149
  const magnitude = usable
127
- ? spread(Math.abs(scaled).toFixed(0), 0)
128
- : spread(Math.abs(x).toString(), decimals);
150
+ ? spread(canonicalNumber(Math.abs(scaled)), 0)
151
+ : spread(canonicalNumber(Math.abs(x)), decimals);
129
152
  const digits = written(magnitude).padStart(decimals + 1, '0');
130
153
  if (decimals === 0) return sign + digits;
131
154
  const whole = digits.slice(0, digits.length - decimals);
@@ -143,7 +166,7 @@ function fixed(x: number, decimals: number): string {
143
166
  * have. `str.repeat` measures first for the same reason.
144
167
  */
145
168
  function lengthOf(x: number, decimals: number): number {
146
- const before = Math.max(1, spread(Math.abs(x).toString(), 0).point);
169
+ const before = Math.max(1, spread(canonicalNumber(Math.abs(x)), 0).point);
147
170
  return before + decimals + (decimals > 0 ? 1 : 0);
148
171
  }
149
172
 
@@ -154,22 +177,17 @@ function lengthOf(x: number, decimals: number): number {
154
177
  * which accepts hexadecimal, infinities and a bare leading point in some
155
178
  * languages and not others. Absence rather than zero for text that is not a
156
179
  * number, so a script can tell text that is not a number apart from the number
157
- * zero.
180
+ * zero. The whitespace ignored at either end is `str.trim`'s set and no other.
158
181
  */
159
182
  const NUMERIC = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
160
183
 
161
184
  function parseNumber(text: string): Value {
162
- const trimmed = text.trim();
163
- if (!NUMERIC.test(trimmed)) return null;
164
- const parsed = Number(trimmed);
185
+ const bare = trimmed(text);
186
+ if (!NUMERIC.test(bare)) return null;
187
+ const parsed = Number(bare);
165
188
  return Number.isFinite(parsed) ? (parsed === 0 ? 0 : parsed) : null;
166
189
  }
167
190
 
168
- /** Code points, which is what every index in this file counts. */
169
- function points(text: string): string[] {
170
- return [...text];
171
- }
172
-
173
191
  function makeArray(ctx: CallContext, items: Value[]): Value {
174
192
  ctx.guard.array(ctx.span, 'the array', items.length);
175
193
  return reference(ctx.heap.allocate({ kind: 'array', items }));
@@ -198,7 +216,7 @@ export const TEXT_ENTRIES: readonly ManifestEntry[] = [
198
216
 
199
217
  entry('str.upper', 's', (ctx, args) => map(ctx, args, (s) => s.toUpperCase())),
200
218
  entry('str.lower', 's', (ctx, args) => map(ctx, args, (s) => s.toLowerCase())),
201
- entry('str.trim', 's', (ctx, args) => map(ctx, args, (s) => s.trim())),
219
+ entry('str.trim', 's', (ctx, args) => map(ctx, args, trimmed)),
202
220
 
203
221
  entry('str.contains', 's part', (_ctx, args) => pair(args, (s, part) => s.includes(part))),
204
222
  entry('str.startsWith', 's part', (_ctx, args) => pair(args, (s, part) => s.startsWith(part))),
@@ -10,6 +10,7 @@
10
10
  * bar four thousand with half a study on the screen.
11
11
  */
12
12
  import type { Diagnostic } from '../diagnostics/index.js';
13
+ import { canonicalise } from '../emit/index.js';
13
14
  import type { SourceFile } from '../source/index.js';
14
15
  import { BAR_FIELDS } from './bars.js';
15
16
  import { limitsWith } from './budget.js';
@@ -116,3 +117,70 @@ export function load(program: unknown, options: LoadOptions = {}): LoadResult {
116
117
  inputs: resolved.inputs,
117
118
  };
118
119
  }
120
+
121
+ /**
122
+ * Loads a compiled program from text, which is where canonicity is required.
123
+ *
124
+ * `load` takes a parsed object because a host that compiled in this process
125
+ * never serialised, and there is nothing for such an object to be canonical
126
+ * about. Text is the other boundary, `compiled-program.md` 9.4 step 1 and
127
+ * section 13 (decision 57): a program an engine reads from outside its process
128
+ * arrives as the canonical encoding of 2.14, and text that parses to a program
129
+ * but is not that encoding is refused, because the hash a host recorded was
130
+ * taken over canonical bytes and text in any other spelling is text that hash
131
+ * does not name. So the text is parsed, written out again by the one canonical
132
+ * writer, and the two are compared character for character. Either failure is
133
+ * OS6018 naming where the text stops being readable or stops being canonical,
134
+ * counted in characters of the text, which is where an editor's cursor lands.
135
+ *
136
+ * What passes here is the object `load` verifies from step 2 on, so every
137
+ * refusal that applies to an object applies to text as well and in the same
138
+ * order.
139
+ */
140
+ export function loadText(text: string, options: LoadOptions = {}): LoadResult {
141
+ let parsed: unknown;
142
+ try {
143
+ parsed = JSON.parse(text);
144
+ } catch (thrown) {
145
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
146
+ const at = /position (\d+)/.exec(message);
147
+ return {
148
+ ok: false,
149
+ diagnostic: malformed(
150
+ at === null ? 'the text' : `character ${at[1]}`,
151
+ 'the text stops being the object notation of section 2.14 there, so it is not a program',
152
+ ),
153
+ };
154
+ }
155
+
156
+ let canonical: string;
157
+ try {
158
+ canonical = canonicalise(parsed);
159
+ } catch (thrown) {
160
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
161
+ return {
162
+ ok: false,
163
+ diagnostic: malformed('the text', `it parses but cannot be written in the canonical form: ${message}`),
164
+ };
165
+ }
166
+ if (canonical !== text) {
167
+ return {
168
+ ok: false,
169
+ diagnostic: malformed(
170
+ `character ${firstDifference(text, canonical)}`,
171
+ 'the text is not the canonical encoding of the program it parses to, and a program ' +
172
+ 'read from outside the process has to be, because that is what its hash was taken over',
173
+ ),
174
+ };
175
+ }
176
+ return load(parsed, options);
177
+ }
178
+
179
+ /** Where two texts part, as an index into either, or the shorter one's length. */
180
+ function firstDifference(a: string, b: string): number {
181
+ const shorter = Math.min(a.length, b.length);
182
+ for (let i = 0; i < shorter; i += 1) {
183
+ if (a[i] !== b[i]) return i;
184
+ }
185
+ return shorter;
186
+ }
@@ -21,8 +21,51 @@ const META_KINDS = ['study', 'strategy'] as const;
21
21
  const CHANNEL_TYPES = ['number', 'string', 'color', 'bool'] as const;
22
22
  const EFFECTS = ['none', 'signal', 'order', 'draw', 'log'] as const;
23
23
 
24
+ /**
25
+ * The tables a later minor of this format major added, with the minor that
26
+ * added each.
27
+ *
28
+ * 9.2's rule read from the other side, which 9.4 step 3 states and decision 56
29
+ * settled: a table a later minor added and an earlier program lacks reads as
30
+ * empty, never as a refusal, because 9.5's first line is a promise about that
31
+ * program. A program stamped at this minor or a later one has no such excuse.
32
+ * Section 2 says an empty table is written as an empty array and never
33
+ * omitted, so its absence there is the defect check 1 exists for, and the
34
+ * version is what tells the two apart.
35
+ *
36
+ * `spec/format-history.json` records the same additions, one entry per format
37
+ * version, and `tests/engine/format-minors.test.ts` holds this list to that
38
+ * file: a table the history says a later minor added has to be one a program
39
+ * at the earlier minor may lack.
40
+ */
41
+ const ADDED_AT_MINOR: readonly { readonly table: string; readonly minor: number }[] = [
42
+ { table: 'requests', minor: 1 },
43
+ ];
44
+
45
+ /** The minor of a `major.minor` the version step has already proved is one. */
46
+ function minorOf(raw: Readonly<Record<string, unknown>>): number {
47
+ const version = raw['openscript'] as Readonly<Record<string, unknown>>;
48
+ return Number(String(version['format']).split('.')[1] ?? '0');
49
+ }
50
+
51
+ /**
52
+ * Writes the empty table an earlier minor is owed into the program itself.
53
+ *
54
+ * Into the object rather than into a local, because every later step reads
55
+ * the program as its own type and would find the table missing again: the
56
+ * verifier hands the same object on, and the engine holds it.
57
+ */
58
+ function supplyAddedTables(raw: Readonly<Record<string, unknown>>): void {
59
+ const minor = minorOf(raw);
60
+ for (const added of ADDED_AT_MINOR) {
61
+ if (raw[added.table] !== undefined || minor >= added.minor) continue;
62
+ (raw as Record<string, unknown>)[added.table] = [];
63
+ }
64
+ }
65
+
24
66
  /** Check 1 over every table the machine indexes, and every declaration field. */
25
67
  export function checkTables(shape: ShapeCheck, raw: Readonly<Record<string, unknown>>): boolean {
68
+ supplyAddedTables(raw);
26
69
  for (const name of ['requires', 'inputs', 'channels', 'consts', 'series', 'cells', 'states',
27
70
  'functions', 'callSites', 'loops', 'code', 'requests']) {
28
71
  if (!shape.array(raw[name], name)) return false;
package/src/core/index.ts CHANGED
@@ -250,10 +250,15 @@ export { DECLARATION_CALLS, declarationDefaultText, namedColour } from './emit/i
250
250
  * same check on its own, for a host that keeps compiled programs and wants to
251
251
  * refuse a bad one when it arrives rather than when it is first drawn.
252
252
  *
253
+ * `loadText` is `load` for a program that arrives as text, which is the one
254
+ * boundary where the canonical encoding is required (`compiled-program.md`
255
+ * section 13): text that parses to a program but is not that encoding is
256
+ * refused, because the hash a host recorded was taken over canonical bytes.
257
+ *
253
258
  * `capabilitiesFor` and `LANGUAGE_VERSIONS` are this engine's half of the
254
259
  * compatibility statement in 9.1, beside `COMPILED_FORMAT_VERSION` above.
255
260
  */
256
- export { load } from './engine/index.js';
261
+ export { load, loadText } from './engine/index.js';
257
262
  export type { BarResult, Engine, LoadOptions, LoadResult, RunResult } from './engine/index.js';
258
263
  export type {
259
264
  BarState,
@@ -311,21 +316,25 @@ export type {
311
316
  MonthlyReturn,
312
317
  RecordedFill,
313
318
  Report,
319
+ SideAnalysis,
314
320
  Summary,
315
321
  Trade,
322
+ TradeAnalysis,
316
323
  TradeMarker,
317
324
  } from './accounting/index.js';
318
325
  export { chargeFor, scheduleFromDeclaration, scheduleProblem } from './accounting/index.js';
319
326
  export { markersOf, monthlyOver, reportOf } from './accounting/index.js';
320
327
  export { tradesOf } from './accounting/index.js';
321
- export { equityOver, summaryOf } from './accounting/index.js';
328
+ export { analysisOf, equityOver, summaryOf } from './accounting/index.js';
322
329
  export {
323
330
  DEFAULT_FILL,
324
331
  EXACT,
325
332
  WHOLE_RANGE,
326
333
  RECORD_VERSION,
327
334
  backtest,
335
+ backtestSupplied,
328
336
  barsHash,
337
+ caseFilesFrom,
329
338
  checkSettings,
330
339
  compareRuns,
331
340
  recordFromJson,
@@ -341,8 +350,13 @@ export type { BacktestResult, ReplayResult, ReportWindow } from './backtest/inde
341
350
  export type {
342
351
  BacktestSettings,
343
352
  BarsInRecord,
353
+ CaseFiles,
354
+ CaseIdentity,
355
+ CaseResult,
344
356
  DateRange,
357
+ DriveOptions,
345
358
  FillPolicy,
359
+ InstrumentFacts,
346
360
  RecordedBar,
347
361
  RecordedDiagnostic,
348
362
  RecordedFrame,
@@ -105,6 +105,7 @@ export {
105
105
  roundTo,
106
106
  roundToStep,
107
107
  roundToTick,
108
+ scaleOf,
108
109
  sign,
109
110
  sin,
110
111
  sqrt,
@@ -18,6 +18,7 @@ export {
18
18
  roundTo,
19
19
  roundToStep,
20
20
  roundToTick,
21
+ scaleOf,
21
22
  trunc,
22
23
  } from './rounding.js';
23
24
 
@@ -31,6 +31,28 @@ export function round(x: Value): Value {
31
31
  return isPresent(x) ? result(roundHalfAway(x)) : NONE;
32
32
  }
33
33
 
34
+ /**
35
+ * Ten to each whole power a binary64 can hold, as the binary64 nearest to it.
36
+ *
37
+ * `stdlib.md` 20.7: the scale a fixed decimal rounding or conversion multiplies
38
+ * by is the nearest binary64 to the power of ten, the value the literal `1e23`
39
+ * reads as, and not what a floating point power returns, which is an ulp away
40
+ * from it for one count on this engine's host. Built once from exact integer
41
+ * arithmetic, and it is the one table: `text(x, decimals)` in the engine's
42
+ * library scales by it through `scaleOf` rather than holding a copy.
43
+ */
44
+ const POWERS_OF_TEN: readonly number[] = Array.from({ length: 309 }, (_, count) =>
45
+ Number(10n ** BigInt(count)),
46
+ );
47
+
48
+ /**
49
+ * The scale for a digit count: the nearest binary64 to ten to that power, and
50
+ * past the last finite power the infinity the power would have been.
51
+ */
52
+ export function scaleOf(decimals: number): number {
53
+ return POWERS_OF_TEN[decimals] ?? Infinity;
54
+ }
55
+
34
56
  /**
35
57
  * `round(x, decimals)`: to a fixed number of decimals, halves away from zero.
36
58
  *
@@ -40,7 +62,7 @@ export function round(x: Value): Value {
40
62
  export function roundTo(x: Value, decimals: number): Value {
41
63
  if (!isPresent(x)) return NONE;
42
64
  if (!Number.isInteger(decimals) || decimals < 0) return NONE;
43
- const scale = Math.pow(10, decimals);
65
+ const scale = scaleOf(decimals);
44
66
  return result(roundHalfAway(x * scale) / scale);
45
67
  }
46
68
 
@@ -4,7 +4,7 @@
4
4
  // spec/compiled-program.md, so neither can drift from the thing it names.
5
5
 
6
6
  /** This build's package version. The release refuses to publish unless the tag, the manifest and this agree. */
7
- export const VERSION = "0.4.0";
7
+ export const VERSION = "0.5.0";
8
8
 
9
9
  /**
10
10
  * The compiled program format this compiler emits.