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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,935 @@ nothing, fails the build before it can become permanent.
7
7
 
8
8
  ---
9
9
 
10
+ ## 0.5.0
11
+
12
+ **`pow` has no library vector, and the reason is the same one.** A vector is a
13
+ bit pattern a second engine is written to match, and `pow` returns different
14
+ bits on two runtimes of the same virtual machine: publishing one hands that
15
+ second engine a test it cannot pass and this engine cannot keep. The vectors
16
+ were added after the last release, so this is the first build to check them
17
+ anywhere but where they were made.
18
+
19
+ `compiled-program.md` 8.3 already requires that the transcendental functions not
20
+ use the platform's maths library, and the source records that they do until the
21
+ portable algorithm it names exists. Everything in that group is provisional in
22
+ the last bit; only `pow` has been measured to differ, so only `pow` is held out,
23
+ and the index says why. The rest keep their vectors and the exposure is written
24
+ down where the next one goes when it is measured.
25
+
26
+
27
+ **A figure in the specification was one machine's reading.** Section 20.7 said
28
+ how many powers of ten a host's `pow` gets wrong and which one, and a test
29
+ measured it again on every build, which is the rule this project has for a
30
+ printed figure. Both numbers turned out to belong to the host rather than to the
31
+ language: the same engine misses a single count on one runtime of its virtual
32
+ machine and thirty six on another, and the two sets do not overlap. The claim
33
+ that matters, that a floating point power is not the nearest binary64 and that
34
+ the difference reaches `round`, is true on both and is what is now printed and
35
+ measured. The witnesses the two rounding tests use are found on the host running
36
+ them rather than written down, because a literal witness demonstrates the claim
37
+ on the machine it was written on and nothing on the next one.
38
+
39
+
40
+ **A chart can draw a strategy, not just a study.** `descriptorFor` takes
41
+ `simulateOrders`, and with it a program that places orders runs in the chart
42
+ tier against the venue a backtest uses: its plots draw, its legend row and its
43
+ settings dialog follow, and its position is right because the venue's frames
44
+ reach the engine between bars. Until now a host had two choices, refuse the
45
+ strategy or hand it a destination that answered nothing, and the second draws a
46
+ strategy that never learns it holds anything: every close closes nothing, every
47
+ entry is allowed again on the next signal, and a stop and reverse script
48
+ measured five buys and no sells while looking entirely normal.
49
+
50
+ It is the backtest's own `Simulator` rather than a second one written for
51
+ charts, so the marks a trader sees on the price and the trades in the report of
52
+ the same script are one answer. A test holds the two together: the position the
53
+ chart ends on and the open size the report states are compared directly.
54
+
55
+ **Off unless asked for.** Without `simulateOrders` a strategy with nowhere to
56
+ send an order is still refused at load with OS6006, which is what a host that
57
+ meant to wire a destination and forgot needs to be told. Supplying `orders`
58
+ still wins over it: somewhere real to send an order is a better destination than
59
+ a simulated one. Nothing is placed anywhere by this.
60
+
61
+
62
+ **The Python engine is on the index.** `pip install openscript` gets the engine
63
+ that runs a compiled program, Apache-2.0, zero dependencies, Python 3.12 or
64
+ newer. Until now the only way to have it was to clone this repository and point
65
+ an environment variable at a directory, which meant a platform built from a
66
+ container image could not run a strategy at all, and anyone attempting the
67
+ second half of Phase 6 had nothing to install. `RELEASING.md` carries the
68
+ release, beside the npm one, and the two versions ship together because
69
+ `check-python.mjs` holds them equal.
70
+
71
+ `engine/README.md` is the page the index shows: what the package is, that it
72
+ holds no compiler and is handed a compiled program, that nothing in it builds
73
+ code out of text, and how a host drives it bar by bar. It was written because
74
+ the first build had no long description at all and the page would have been
75
+ blank, which is permanent for a version once it is up.
76
+
77
+ **The Python distribution shipped one package out of six.** `[tool.setuptools]
78
+ packages` named `openscript` alone, so an install carried the machine and none
79
+ of the halves it calls: `import openscript` worked and
80
+ `from openscript.adapter.serving import Serving` did not. A host that followed
81
+ `docs/integrating/the-python-engine.md` and installed the directory got an
82
+ engine that could not run anything, and the failure appeared at their first
83
+ import rather than anywhere in this build.
84
+
85
+ It was found by doing it: installing the engine into a platform and watching the
86
+ adapter go missing. Nothing here could have caught it, because every test in this
87
+ repository runs the package from the tree where all six directories are present
88
+ whether or not the distribution would have carried them.
89
+
90
+ `scripts/check-python.mjs` now compares the package list against the packages
91
+ that exist, in both directions: a directory holding an `__init__.py` that the
92
+ list omits is refused, and a name in the list that is not a package in the tree
93
+ is refused too. A new subpackage is shipped because it exists, not because
94
+ somebody remembered a line.
95
+
96
+ **The phase the language was designed for is now scoped.** A strategy trades one
97
+ instrument today, chosen by the host before the run starts. The surface for more
98
+ than one has been designed and marked planned since `stdlib.md` was written:
99
+ `leg.fixed` and `leg.relative` declare what each leg trades, the `leg.*` rules
100
+ manage one, and the `book.*` rules reason across all of them. None of it
101
+ executes, and a script calling any of it is refused at the call with OS2020.
102
+ `ROADMAP.md` now carries Phase 8, which says what building it involves, in the
103
+ order it has to be built, and the four questions that have to be answered before
104
+ any of it is written. It is placed after Phase 7 deliberately, and the reason is
105
+ in the phase: every rule in it is behaviour a third engine has to reproduce
106
+ exactly, so designing it before the format is fixed means discovering the
107
+ disagreements one at a time in somebody else's engine.
108
+
109
+ The risk rules that a combination of contracts needs are the point of it. A
110
+ position made of two or more derivative contracts has a risk profile belonging to
111
+ the combination and not to any leg, so a stop placed per leg both fires on moves
112
+ the combination absorbed and misses the ones it did not. Two independent
113
+ single-instrument strategies are not a substitute for one multi-leg strategy;
114
+ they are two strategies running at the same time.
115
+
116
+ **Six capabilities that were missing rather than planned now have rows.** The
117
+ feature matrix said nothing at all about an account-level drawdown halt, a
118
+ position size cap, a cap on orders per session, a halt after consecutive losing
119
+ sessions, indexed access to past trades, or a script stating whether it is
120
+ evaluated on every update or only on a closed bar. Every one of them is ordinary
121
+ in the prior art this language is measured against, and a gap nothing records is
122
+ a gap nobody plans. They are `planned` with no section, which is what that status
123
+ is for.
124
+
125
+ **The second engine's host surface is documented, and proved.** `engine/openscript/run.py`
126
+ has held everything a live runner needs since the engine was written, and no
127
+ document mentioned it once: a host reading `docs/integrating/the-python-engine.md`
128
+ concluded that the only way to use this engine was to hand it a case directory,
129
+ because the one entry point that page named was the conformance adapter. Phase 6
130
+ of the roadmap is a server-side engine running the same compiled program, and the
131
+ surface that makes it possible was invisible to the people it exists for.
132
+
133
+ `docs/integrating/running-a-strategy.md` is the page a platform engineer reads
134
+ instead. It states plainly that the engine is handed a compiled program and never
135
+ a script, and what that means for a server that cannot run the compiler: the
136
+ program is compiled where the compiler runs and stored as data. Then `load` and
137
+ `load_text` and what a refusal carries, `execute_bar` argument by argument, what a
138
+ `BarResult` holds, the rollback a still-moving bar rests on and the single
139
+ condition it needs, the order boundary with `adapter/ordering.py` as the worked
140
+ reference, a worked example that runs, and a list of what the surface does not
141
+ give a host: no scheduling, no process isolation, no persistence, no data feed, no
142
+ destination, no compiler and no chart.
143
+
144
+ `engine/tests/test_host_surface.py` is the other half. It drives the engine the way
145
+ a live host does, with no conformance adapter in the loop: a program loaded from
146
+ canonical text, bars pushed one at a time, the order calls read back and placed on
147
+ a ledger, a frame folded in at a bar boundary, and a moving bar executed three
148
+ times that accumulates once. Every existing test drove this engine in batch, so
149
+ nothing held the surface a live host actually uses.
150
+
151
+ **And the record says what is true.** The registry page claimed there was no second
152
+ engine and no case files; both have existed for some time, and it now says the one
153
+ thing that is still true, which is that no engine anybody else wrote has run the
154
+ suite. The trading-mode sense of "paper" is gone from the documentation, the
155
+ specification and the error catalogue, and `docs/running/paper-and-live.md` is now
156
+ `docs/running/sandbox-and-live.md`: this platform maintains sandbox mode and
157
+ analyzer mode and now says so everywhere. The trading sense of "arm" is gone from the language itself, not
158
+ only from the prose: `leg.trail`'s and `book.lockProfit`'s `arm` parameter is
159
+ now `activateAt`, and the events `trailArmed` and `lockProfitArmed` are now
160
+ `trailActivated` and `lockProfitActivated`, which is the word the event table
161
+ already used in `trailToEntryActivated`. Both names are marked planned, so no
162
+ script running today is affected. A `switch` arm and a ternary arm are language
163
+ vocabulary and are untouched.
164
+
165
+ **Three cases where the destination behaves badly.** Every one of the 204 frames
166
+ the suite held was an order working and then filling whole: no partial fill, no
167
+ refusal, no cancellation, no expiry, and no frame arriving later than the bar
168
+ that placed its order. Two engines agreeing over that agreed about the half of a
169
+ day that costs nobody anything. Three cases are harvested from the crossing
170
+ example against a destination told to behave badly on a schedule stated before
171
+ the run, and each one reaches a shape `stdlib.md` 17.7 and 17.8 provide for and
172
+ no case carried.
173
+
174
+ - `order/partial-fill`. The first entry is acknowledged with nothing filled,
175
+ reports 400 of its 1084 units two boundaries later and the rest five
176
+ boundaries after it was taken, and the close that ends the trade arrives in
177
+ two pieces as well. The first trade has two entries and two exits where every
178
+ other trade in the suite has one of each, its entry price is the average over
179
+ two pieces filled at two prices, and the run is charged for fourteen fills
180
+ where the same run against a destination that fills whole is charged for
181
+ twelve. An engine that reads a cumulative quantity as a delta folds 1484 units
182
+ onto a 1084 unit order.
183
+ - `order/ended-unfilled`. The first three entries are refused carrying the
184
+ destination's own text, expire, and are cancelled, each after being
185
+ acknowledged and each with nothing filled. Because nothing fills, no position
186
+ opens on any of the three, the crossing back finds the strategy flat and sends
187
+ nothing, and the run reaches three trades where the plain run reaches six. It
188
+ is the only case in the suite carrying a `rejection`.
189
+ - `order/fold-after-terminal`. The first entry is cancelled a boundary after it
190
+ is taken and reported filled whole the boundary after that. The row ends
191
+ `cancelled` carrying 1084 filled and an average price, which is what 17.8
192
+ describes and why: a cancellation can race a fill at any destination, and an
193
+ engine that refuses the late frame leaves the account holding a position the
194
+ strategy cannot see.
195
+
196
+ The suite is eight cases now, 273 frames, 140 ledger rows and 66 trades. Across
197
+ it a frame says `working` 137 times, `filled` 132, `cancelled` twice, `rejected`
198
+ once and `expired` once, and a ledger row ends `filled` 131 times, `placed` five
199
+ times, `cancelled` twice, `rejected` once and `expired` once. Two of those
200
+ `working` frames carry part of an order rather than none of it, and five of the
201
+ `placed` rows are the order each run was holding when its bars ran out.
202
+
203
+ **A schedule that stops producing its status is refused rather than harvested.**
204
+ An act names an order by its ordinal and the run decides how many orders there
205
+ are, so a schedule can stop reaching its order without anything failing: the
206
+ case is still harvested, still passes, and tests whatever the plain run tests.
207
+ `scripts/lib/venue-schedule.mjs` holds each act to the frames the run recorded,
208
+ by the facts the act itself fixes and by the row it reached, with the status
209
+ vocabulary read out of `stdlib.md` 17.7 rather than copied into it. Twenty four
210
+ wrong schedules and wrong pages were put to it and all twenty four were refused:
211
+ every ordinal moved past the orders the run places, one act moved past the end
212
+ of the run, the plain run asked about a schedule it never ran under, the acts
213
+ written in reverse, a verb no word on the page is the stem of, a page with no
214
+ status table, a rejection text no frame carries, and a piece of 401 where the
215
+ run reported 400. The first version passed one of them, a fill of a whole order
216
+ answered by whichever later order happened to fill, which is why an act is now
217
+ held to the row its own order was answered about.
218
+
219
+ **The second engine now carries a frame's instant, and the suite is eight of
220
+ eight between the engines.** It did not when the three cases landed: three places
221
+ built a frame and left the column off, so every frame carried the instant of the
222
+ bar that placed the order and the three new cases failed on the second engine at
223
+ `orders[].updatedAt` and at nothing else, with the partial quantities, the
224
+ cumulative averages, the refusal text, the trades with two entries and two exits
225
+ and the whole summary agreeing exactly. The three were
226
+ `openscript/adapter/ordering.py`, where the driver builds the frame it delivers;
227
+ `tests/recorded.py`, whose `DeliveredFrame` had no field for the column and read
228
+ seven of the file's eight; and `tests/replaying.py`, where the replay that holds
229
+ the ledger to a case builds its own. Each now sets it, and an absent column still
230
+ leaves the ledger whatever instant the placement carried.
231
+
232
+ And the language's own `cancel(...)` is still exercised by nothing, because no
233
+ shipped example calls it: the cancellation in `order/fold-after-terminal` is the
234
+ destination's own and not the strategy's.
235
+
236
+ **A report says how far the run climbed, and which side made the money.** The
237
+ summary answered twenty six figures and could not answer three questions a
238
+ reader decides on. A run that made ten and gave back nine reports the same net
239
+ profit as one that made one and kept it, and no figure separated them. A run
240
+ whose long trades paid for its short ones reported a healthy net, because every
241
+ figure in the summary is folded over both sides at once. And how many times in a
242
+ row a strategy was wrong, which is the number that actually stops somebody, was
243
+ not derivable from the win rate, the drawdown or the trade count.
244
+
245
+ `maxRunUp`, `maxRunUpPercent` and `maxRunUpAt` join the summary, and `runUp` and
246
+ `runUpPercent` join every point of the equity curve. Run-up is measured from a
247
+ running trough anchored at the run's capital, which is the running peak's rule
248
+ rather than its mirror image: a trough anchored at the first reported point
249
+ would report the gain a run arrived with as having come from nowhere. The
250
+ fraction is **zero wherever that trough is not above zero**, and that asymmetry
251
+ with `maxDrawdownPercent` is deliberate. A peak only rises and stays above zero
252
+ throughout any funded run; a trough only falls, and an open position can lose
253
+ more than the account holds, so it reaches zero and passes it. Against a
254
+ negative basis a positive climb divides to a negative fraction, which is the
255
+ shape that once reported a profit factor of minus a half. `maxRunUp` itself is
256
+ unaffected and is the figure to read on such a run. The height and the depth
257
+ name different bars, and the earliest bar reaching either wins the tie.
258
+
259
+ `analysisOf` and the report's new `analysis` are the trades taken apart: the
260
+ closed trades split long against short with each side's own net and win rate,
261
+ the largest win and the largest loss as nets after charges, and the longest run
262
+ of wins and of losses. The sides partition the closed trades, so their counts
263
+ sum to `tradeCount` and their nets to `netProfit`. A streak is counted **in the
264
+ order the trades closed**, which differs from the order they opened whenever a
265
+ trade is held across another one's whole life, which is every strategy that
266
+ scales in; two trades closing on one bar are ordered by the order they opened,
267
+ so the answer does not depend on the order the list arrived in. A trade whose
268
+ net is exactly zero breaks a streak and extends neither half.
269
+
270
+ **What is proved, and what is not.** Run-up is in the `performance` channel, so
271
+ all five conformance cases compare it between the two engines exactly; that was
272
+ checked by removing it from one engine and watching every case fail. The trade
273
+ analysis is **not** in that channel, because the channel holds one flat object
274
+ and the side split is nested, so it is proved by unit tests on each engine and
275
+ by nothing that compares them. Whether it is flattened into `performance` or
276
+ becomes a channel of its own is left open rather than answered in passing.
277
+
278
+ **And the gap that was invisible is now closed.** Until this release the
279
+ summary's figures had no formula stated in any specification document: the two
280
+ engines agreed on them because one was translated from the other, not because a
281
+ sentence said what they are, so a third engine had nothing to be written
282
+ against. `conformance.md` section 4 now defines every field of the summary: the
283
+ words the formulas are written in, which trades each figure is counted over, the
284
+ equity basis the curve figures are folded from, which figures are absent rather
285
+ than zero and why, and the tie rules. The equity curve, drawdown and win rate
286
+ rows of `feature-matrix.md` section 30 move from `planned` to `specified` on the
287
+ strength of it.
288
+
289
+ The trade list does **not** move, and the section says why in its own text: how a
290
+ run folds the `trades` channel out of its fills is still unspecified, so that
291
+ channel is the boundary of what these formulas promise. An engine checking itself
292
+ against a case is handed every value they need; an engine folding a report out of
293
+ fills alone still has that earlier fold to agree on. `spec/decisions.md` 64 is the
294
+ minute.
295
+
296
+ **A case supplies the frames, and this engine folds them.** `conformance.md`
297
+ section 3 has said since it was written that `frames.csv` supplies order frames
298
+ the way `bars.csv` supplies bars, so that a case asserts the fold against input
299
+ the engine did not choose. This engine's adapter did something else: it re-ran
300
+ the case on its own simulated destination and held the frames that run answered
301
+ to the case's file byte for byte, reporting the case `unsupported` when the two
302
+ differed. So the suite could hold only cases whose frames this engine would have
303
+ produced anyway, which is why all 204 frames in it are an order working and then
304
+ filling whole, and why a partial fill, a rejection, a cancellation, an expiry
305
+ and a fill after a terminal status were all shapes the page provides for and no
306
+ case could carry. Two engines agreeing on that suite agreed about the half of a
307
+ destination's day that costs nobody anything.
308
+
309
+ `backtest` now has a second driver beside it, `backtestSupplied`, which delivers
310
+ the rows a case supplies and answers none of its own: no fill priced off a bar,
311
+ no order resting and no schedule read, so an order the frames say nothing about
312
+ stays where its placement left it. A case with no `frames.csv` still runs
313
+ against the simulated destination, because section 3 ends that such a case is
314
+ handed no frames at all. The two are one function underneath, so the window, the
315
+ refusals before the first bar, the boundary a frame is folded at and the record
316
+ are the same for both. A row naming an order the run never placed is delivered
317
+ and refused by the fold rather than dropped on the way in, which is what section
318
+ 3 hands an engine such a row for, and a row no boundary of the run delivers, one
319
+ after the last bar or one naming no bar, makes the case `unsupported` naming the
320
+ row rather than run with part of its own input passed over. `spec/decisions.md`
321
+ 63 is the minute, and section 3 now says where a frame naming no row is
322
+ answered.
323
+
324
+ The backtest module's door also exports `VenueAct`, `VenueDoes` and
325
+ `VenuePolicy` beside `Simulator` and `SimulatorOptions`. A host writing a
326
+ schedule for the simulated destination had to reach them structurally, through
327
+ `SimulatorOptions['fill']`, and a module's index is its only door.
328
+
329
+ **What this does not close.** The second engine reads a frame's instant out of
330
+ the file and does not put it on the frame it hands its ledger, so a case whose
331
+ destination answered later than the bar that placed the order is answered
332
+ differently by the two engines, by `updatedAt` and by nothing else: every other
333
+ field of the ledger, the trades and the whole performance summary agree exactly
334
+ on a case harvested and measured here. That line belongs to the file that owns
335
+ that delivery. `BacktestSettings` still states no schedule of its own, because
336
+ the field needs a row in a projection this stage does not own, and
337
+ `docs/integrating/running-the-suite.md` still describes the reading this change
338
+ replaces. None of this is a change to what an engine computes for a case that
339
+ supplies the frames its own destination would have answered: the five cases in
340
+ the suite pass in both modes, exactly, as they did.
341
+
342
+ **A frame carries the instant it arrived at.** `frames.csv` gains a `time`
343
+ column: the destination's own instant for that frame, UTC milliseconds, absent
344
+ as `none`, last in the row and optional in the way `orderRef` and `text` are.
345
+ `host-interface.md` 7.2 gives a frame that instant and `stdlib.md` 17.7 folds a
346
+ ledger row's `updatedAt` from it, so while no column carried one an engine
347
+ driven from a case file had nothing to move that field to and left it at
348
+ `placedAt`, while an engine answering its own frames carried the instant it
349
+ spoke. The two readings differ by one field, on exactly the rows whose
350
+ destination answered later than the bar that placed the order, and every frame
351
+ in the suite today arrives at the boundary that placed its own order, which is
352
+ why a suite made of them passed both. The column makes the instant input, like
353
+ every other byte of a case.
354
+
355
+ A record carries it too, because a projection can only write what the run kept:
356
+ `RECORD_VERSION` is 4, a record written before it reads with no instant on any
357
+ frame, and a later revision is still refused rather than read under this one's
358
+ rules. Both readers read the column, this engine's and the second engine's, and
359
+ `conformance.md` section 3 now says what a `time` means, that a case is not
360
+ required to state one, and why. `spec/decisions.md` 62 is the minute.
361
+
362
+ **What this does not close.** The five cases in the suite were harvested before
363
+ the column existed and carry the seven columns of the day, so they are stale
364
+ rather than wrong, and until they are harvested again `npm test` stops on them
365
+ at `scripts/harvest-cases.mjs --check`, which projects each run again and finds
366
+ eight columns where the file on disk has seven. `frames.csv` is the only file it
367
+ names. Measured with the five harvested again and nothing else changed: 1843 of
368
+ 1843 unit tests, the harvest check passes, and the two engines agree on 5 of 5,
369
+ exactly. The second engine reads the instant and does not yet carry it onto the
370
+ frame it hands its ledger, which is one line in the file that owns that
371
+ boundary. None of this is a change to what an engine computes: the ledgers, the
372
+ trades and the money of every case are what they were.
373
+
374
+ **The destination can be told to behave badly, on a schedule stated before the
375
+ run.** `SimulatorOptions.fill` now carries one: a list of acts, each naming the
376
+ nth order this destination took, how many boundaries after the one that took it
377
+ the act falls, and what it does. Four verbs, and between them they reach every
378
+ status of `stdlib.md` 17.7 a host may send. A fill carries a cumulative
379
+ quantity, so one verb covers the acknowledgement before anything has traded, a
380
+ fill of part of the order and a fill of the whole of it, and the other three are
381
+ the three ways an order ends carrying less than it asked for: refused, cancelled
382
+ and expired. An order the schedule names is answered by the schedule and by
383
+ nothing else, and a run that states no schedule is answered exactly as before,
384
+ which is what keeps every case already harvested the bytes it was harvested as.
385
+
386
+ There is no random number in it and there will not be one. `stdlib.md` 8.2 keeps
387
+ a script that answers differently on a second run out of a conformance suite,
388
+ and a venue that rolled a die would make every case harvested from it
389
+ unreproducible in the same breath. The venue works out the average over the
390
+ cumulative quantity itself, because that is the figure `stdlib.md` 17.8 step 3
391
+ says the row takes whole, and a destination reporting the last piece's price as
392
+ an average hands the engine a number that is not one, which the engine may not
393
+ correct because it is forbidden to average two averages of its own.
394
+
395
+ **What the suite had never been handed, measured rather than guessed.** Across
396
+ the five cases in this suite there are 204 frames: 102 that say `working` with
397
+ nothing filled and 102 that say `filled` with the whole order, and nothing else.
398
+ No frame reports part of an order, none arrives at a boundary later than the one
399
+ that took the order, and the words a host may send for a refusal, a cancellation
400
+ and an expiry appear no times at all. So the two engines are held to each other
401
+ over a destination that behaves perfectly, which is not the half of a day that
402
+ costs a trader money. The venue above is the half of the fix that could ship
403
+ here; the cases it can now produce cannot be added to the suite yet, for two
404
+ reasons that this release records rather than hides.
405
+
406
+ The first is this engine's own adapter. `conformance.md` section 3 says
407
+ `frames.csv` supplies frames the way `bars.csv` supplies bars, so a case can
408
+ assert the fold against input the engine did not choose, and a backtest here
409
+ answers its own frames from its own destination and can be handed none. The
410
+ adapter says so honestly, comparing the frames its run answered with the file
411
+ and reporting `unsupported` when they differ, so every case in the suite today
412
+ is one whose frames this engine's destination would have produced anyway. A case
413
+ about a destination that behaves badly is by definition not one of those.
414
+
415
+ The second is in the file. A frame carries a `time` under `host-interface.md`
416
+ 7.2 and `stdlib.md` 17.7 folds `updatedAt` from it, and `frames.csv` has no
417
+ column for one, so an engine folding a case's frames leaves that field at
418
+ `placedAt` where an engine answering its own frames carries the instant it
419
+ spoke. Every ledger row in the suite today has the two equal, because every
420
+ frame in it arrives after the bar that placed its own order, so no case can tell
421
+ the two readings apart. Section 3 now says this, and says what would close it.
422
+ The two engines were driven over three harvested cases carrying a partial fill,
423
+ an order filled over more than one bar, a refusal with the destination's own
424
+ text, an expiry, a cancellation and a fill arriving after a terminal status, and
425
+ they agreed on every figure of all three except that one field.
426
+
427
+ **The second engine has a home, and the gate covers both engines.** `engine/`
428
+ holds a Python distribution: the package `openscript`, importable as one name,
429
+ its tests beside it, and the tools that run them. It requires an interpreter and
430
+ nothing else, so a clone runs the tests as it stands, with no install and
431
+ nothing fetched from an index. The entry point the suite's adapter starts is
432
+ `python -m openscript`, which works from an installed distribution and from the
433
+ directory unchanged. `__init__.py` is the door, and it says what is where: the
434
+ machine, the two halves of the library, the orders, the money and the adapter,
435
+ each of which is an entry of its own below.
436
+
437
+ `npm test` now runs `scripts/check-python.mjs`, which finds an interpreter,
438
+ refuses one older than the distribution requires, reads every import in the tree
439
+ against the module names that interpreter says are its own, and then runs the
440
+ engine's tests under it. A missing interpreter fails the gate rather than
441
+ skipping half of it, because a suite that quietly checks one engine is how two
442
+ engines drift apart. The empty dependency list is therefore a measured fact
443
+ rather than a claim about a file, and inside the package the network, threads,
444
+ randomness and the locale are refused as well, each with the sentence from the
445
+ specification that refuses it.
446
+
447
+ **The no-eval check reads Python.** A `.py` file used to land in the check's
448
+ `unknown` pile and stop the build, deliberately, because code that nothing scans
449
+ is the one thing that check will not allow. It now has an arm of its own: a
450
+ masker that removes comments, marks string literals, reads a formatted string's
451
+ substitutions as code and normalises every identifier the way an interpreter
452
+ does, and rules that refuse the string evaluator, the statement executor, the
453
+ compiler underneath them, the import machinery driven by hand, objects loaded
454
+ out of bytes, function and code objects built at run time, the namespace of the
455
+ built-in names, a namespace taken as a dictionary, a process, and the modules
456
+ whose purpose is running text handed to them. `ast.literal_eval` is safe and is
457
+ allowed, and the rules are written so that it and an ordinary pattern builder
458
+ both pass untouched.
459
+
460
+ The identifier normalisation is the one with no counterpart on the JavaScript
461
+ side. An interpreter normalises a name before resolving it, so a call written in
462
+ mathematical or fullwidth letters is the same call to it and invisible to any
463
+ pattern written against plain letters. Three such spellings are in the attack
464
+ corpus, each one run under an interpreter before it was written down, and every
465
+ form in that corpus goes through the rules before the check opens a file.
466
+
467
+ **A Python test run leaves nothing behind and refuses to prove nothing.**
468
+ Bytecode caching is off before the first test module is imported, so no cache
469
+ directory appears in the tree, and the test count is a result rather than a line
470
+ of output: a discovery that found no tests, which the standard runner reports as
471
+ a pass, fails instead.
472
+
473
+ **A run record becomes a conformance case.** `caseFilesFrom(record, identity)`
474
+ returns the files of one case, keyed by the names `conformance.md` section 2
475
+ gives them: `case.json`, `script.os`, `bars.csv`, `expected.json` and
476
+ `instrument.json`, with `frames.csv` and `settings.json` where the run had
477
+ frames or inputs. It returns text and writes nothing, because core does no I/O,
478
+ so the caller decides where a case lives and the same call works in a browser.
479
+
480
+ This is what the suite the second engine will be measured against is built from.
481
+ A case written by hand asserts what somebody believed a run does; a harvested one
482
+ asserts what an engine actually produced, over bars that existed, under settings
483
+ somebody chose.
484
+
485
+ **Record version 2 carries the script's text**, in a new `sourceText` channel.
486
+ A record identified its script by hash, and a hash settles whether two files are
487
+ the same without yielding either of them, so a case could never be given the
488
+ `script.os` it is required to hold. The text is checked against that hash when
489
+ the record is written: text from a different revision than the one compiled is
490
+ refused, rather than written into a case that could not reproduce its own
491
+ expected output.
492
+
493
+ The text is on the record and not on the compiled program. A program is
494
+ executable data that no engine needs the source to run, and it is the versioned
495
+ artefact other implementations depend on; putting the text there would send a
496
+ script everywhere a program travels and widen the format every engine reads. The
497
+ compiled format is unchanged.
498
+
499
+ **Record version 3 carries the instrument record**, in a new `instrument`
500
+ channel: the twelve facts of `host-interface.md` 4.1 as the engine read them at
501
+ load. A record carried the money layer's contract, which holds six of them, and
502
+ the interval, the timezone, the session and the volume flag were handed to the
503
+ engine and written down nowhere, so `instrument.json`, which section 2 says is
504
+ that record, was the contract instead: a shape with a rounding digit count no
505
+ instrument record has and without the one fact that page requires of every
506
+ host. `backtest` takes `instrument` in its options, the six facts beside the
507
+ contract; the six the contract holds cannot be stated there, so the two cannot
508
+ disagree. A run whose host states no `hasVolume` still runs and still records,
509
+ and is refused a case rather than handed a value, because no derivation
510
+ recovers that flag and a case stating it would give the engine under test a
511
+ study the expected output did not come from.
512
+
513
+ **A record written before this still reads**, with the channels it never
514
+ carried absent: `sourceText` before version 2, `instrument` before version 3. An
515
+ earlier revision only ever has fewer channels, and every one it carries means
516
+ here what it meant when it was written. A later revision is still refused, which
517
+ is the asymmetry that matters: a later one may mean something new by a field this
518
+ version thinks it knows. Such a record replays and reruns as before; the one
519
+ thing it cannot do is become a case.
520
+
521
+ **A harvested case's `expected.json` is the shape section 4 fixes.**
522
+ `performance` is a list of one flat object, the summary statistics. The equity
523
+ curve, the monthly table and the trade markers are no longer written inside it:
524
+ the first two are not conformance channels, and a marker belongs to the
525
+ `markers` channel, which a case about money does not assert. A case now writes
526
+ exactly the channels it declares in `case.json` and no others.
527
+
528
+ **Specification decisions for the second engine.** Three, all in
529
+ `conformance.md` and `stdlib.md`, and each one was a place two implementations
530
+ could not have been written against the same page.
531
+
532
+ *The adapter is invoked once per case*, and the runner assembles the result
533
+ document. The page allowed both readings. Per-case is forced by the `error`
534
+ outcome, which covers a crash, a hang and a timeout: none of the three can be
535
+ reported by the program that suffered it, so only a caller holding a clock and a
536
+ child process can turn them into an outcome.
537
+
538
+ *An adapter also answers `--actual`*, writing what it computed with no
539
+ comparison. Section 10 requires comparing two engines channel by channel, and a
540
+ case result carries an outcome and a first difference rather than the values, so
541
+ two adapters both reporting `pass` proved only that each matched an expected
542
+ file, which is the thing that section says is not enough.
543
+
544
+ *The equity curve is not a conformance channel.* It is one value per bar derived
545
+ from fills and closes that the case already asserts, so it can only fail with the
546
+ channels it comes from or alone, and alone means the engines disagree about
547
+ arithmetic section 6 compares directly. It was also most of the bytes in a case.
548
+ Trade markers move to the `markers` channel, which already existed, and
549
+ `performance` is a list of one flat object.
550
+
551
+ **The transcendental gap is scoped out of conformance rather than solved.**
552
+ `exp`, `log`, `pow`, the trigonometric family and the three indicators built on
553
+ them have no portable reference algorithm, and `compiled-program.md` 8.3 forbids
554
+ answering them from the platform's maths library. No conformance case may assert
555
+ a value reaching them until one is written. They still compute what they always
556
+ did; what they do not carry is a cross-engine guarantee. The deciding fact was
557
+ deployment rather than theory: the first host installs across two processor
558
+ architectures and most common operating systems, so that one library is several
559
+ in practice, and the disagreement is two traders reading two numbers.
560
+
561
+ **A harvested case carries the frames the run was handed.** Without them the
562
+ case was unpassable on every engine, including the one that wrote it:
563
+ `conformance.md` section 3 ends "a case with no `frames.csv` is handed no frames
564
+ at all", and what `expected.json` asserts through its orders channel is what came
565
+ of those frames. An engine handed none folds nothing, disagrees with every row,
566
+ and takes the blame for a hole in the case. A frame names its intent by ordinal
567
+ rather than by an engine's own id, because a case cannot know the id another
568
+ engine minted.
569
+
570
+ `caseFilesFrom` and its types are exported from the package root, so an install
571
+ can reach the one function that turns a run into a case. `DriveOptions` and
572
+ `InstrumentFacts` are exported beside them, so a host can name what `backtest`
573
+ takes.
574
+
575
+ `backtest` takes `sourceText` and `instrument` in its options. A caller that
576
+ has only a compiled program leaves the first out, one that states nothing about
577
+ the instrument beside the contract leaves the second out, and either loses
578
+ nothing but the ability to harvest.
579
+
580
+ **The first conformance cases are in the tree, harvested rather than written.**
581
+ `scripts/harvest-cases.mjs` runs the shipped strategy examples over the Phase 5
582
+ gate's fixture, the placeholder contract and four hundred formula bars, under
583
+ the instrument facts `conformance.md` section 3 assumes of a case that states
584
+ none, read from that page, and writes each run into `cases/<id>/` through
585
+ `caseFilesFrom`, with a `notes.md` saying why the case exists and what it
586
+ defends against. The first two: `order/buy`, from the crossing strategy, and
587
+ `order/sell`, from the opening range strategy. The rows of
588
+ `feature-matrix.md` that name them are the first marked `implemented`, and the
589
+ gate's contract and bars now live in one module the reproducibility check and
590
+ the harvest share, so what the gate reproduces is what the suite holds.
591
+
592
+ **The harvest is a check as well as a writer.** Run with `--check`, which
593
+ `npm test` does, it writes nothing and fails the build when a case on disk is
594
+ not what this engine produces, byte for byte; when a case it wrote names a row
595
+ that does not say `implemented`; when an `implemented` row names a case
596
+ directory that is not there; or when a case directory exists that no row names.
597
+ Every script is harvested twice and the two compared before anything is
598
+ written. A script that reaches a gap of `stdlib.md` section 20.11 is refused by
599
+ name with the call that reached it, by the gate's own reading of that table,
600
+ which now lives in one module the gate's test and the harvest share; and a
601
+ strategy this driver cannot run is named and counted rather than passed over.
602
+ The short premium example reads a second instrument, which a backtest over one
603
+ series of bars cannot supply, so nobody has chosen a case identity for it, and
604
+ that is what the report names it for.
605
+
606
+ **The feature matrix is checked against the tests and the pages it cites.**
607
+ `scripts/check-matrix.mjs` enforces the preamble of `spec/feature-matrix.md`,
608
+ which described a checker nobody had written, and `npm test` runs it. Every
609
+ feature row has five cells and a status the page lists; every citation resolves
610
+ to a Markdown document under `spec/` and, where it carries a locator, to a
611
+ heading matching the shape the preamble gives; every test identifier is well
612
+ formed under a listed area and no two rows share one; an `implemented` row
613
+ names a test that exists, a `unit:` identifier written by a file under `tests/`
614
+ or a case directory holding its `case.json`; and a case directory no row names
615
+ fails the build. Existence is what it proves, and the unit runner and the suite
616
+ prove passing. A `specified` or `planned` row naming a case that is not written
617
+ is not a failure, which is the direction the preamble's paragraph on
618
+ `conformance.md` settles: such rows reserve an identifier, and they are counted
619
+ and printed rather than failed. The status words, the column names, the heading
620
+ shapes and the areas are read out of the page rather than retyped, every rule
621
+ is attacked with a row it must refuse and one it must accept over a fabricated
622
+ document and tree before a row is read, and a run that reads no row refuses.
623
+ It prints the row count, the count per status and the implemented ratio, which
624
+ is the number the page says nobody types. The preamble now names the checker,
625
+ says `none` is written in backticks, says what makes a unit test or a case
626
+ exist, and no longer says every row is unimplemented.
627
+
628
+ **This engine has a conformance adapter, and the suite has a runner.**
629
+ `scripts/adapter.mjs` is the program `conformance.md` section 9 says an
630
+ implementation ships, answering the three invocations that page gives:
631
+ `--describe` for the engine's identity, a case directory for one case result,
632
+ and `--actual` for what the engine computed with no comparison made. It loads
633
+ the built package by its door and nothing behind it, reads a case directory by
634
+ the file names section 2's table gives and refuses a file the table does not
635
+ name, compiles `script.os`, runs it over `bars.csv` under `instrument.json` and
636
+ `settings.json`, and answers the channels the case asserts in the encoding
637
+ section 4 gives them. The channels are read out of the same projection the
638
+ harvest writes a case with, so what this engine can be held to is stated once.
639
+
640
+ `scripts/run-suite.mjs` walks `cases/`, invokes an adapter once per case in a
641
+ child process with a timeout, turns a crash, a hang, a timeout or an answer
642
+ that is not one JSON object into the `error` outcome with the reason in the
643
+ row, and writes the result document of section 9. Both modes of section 10 are
644
+ there: against the expected files, and `--against` a second adapter, where each
645
+ is asked for `--actual` and the runner compares the two channel by channel with
646
+ tolerance zero, whatever the case declares. A case outside the claimed profile
647
+ is skipped and never counted as a pass; any failing, erroring or unsupported
648
+ case exits non-zero. Every harvested case passes on this adapter, alone and
649
+ against itself. `docs/integrating/running-the-suite.md` is the page.
650
+
651
+ **The comparison of section 6 is written once**, in `scripts/lib/compare.mjs`,
652
+ and the adapter and the runner both call it: absence first and never inside a
653
+ tolerance, a non-finite value as its own `nonFinite` outcome, signed zero
654
+ normalised, equality over the binary64 bits rather than a decimal rendering,
655
+ exact by default, and the `max` form of the two bounds with the bound that was
656
+ broken named. A declared tolerance is refused, not clamped, past the cap the
657
+ page prints or without a reason. Every step has a test written against the
658
+ implementation that would get it wrong, and each was run against that mutant.
659
+
660
+ **What the adapter says it does not reach**, reported on the case rather than
661
+ passed over. This engine's backtest answers its own frames from a simulated
662
+ destination and takes none from a file, so the frames it answered are held to
663
+ the case's `frames.csv` byte for byte and a case whose frames the destination
664
+ did not answer is `unsupported`; every harvested case runs. A per-bar or chart
665
+ channel, a warning case, `ticks.csv` and a secondary series are `unsupported`
666
+ by name. A per-column tolerance is not read, because section 6 fixes no shape
667
+ for one. Two facts the page owed a place when the adapter was written: the
668
+ money rounding digit count, which `backtest.json` below now carries and which
669
+ the adapter still takes from the fixture every harvested case ran under until
670
+ it reads that file, and a currency for section 3's default instrument, without
671
+ which the money layer refuses a strategy case that states no `instrument.json`.
672
+ The suite revision has no fixed place either, and the document carries the
673
+ package version until it does.
674
+
675
+ **A case carries what its report was folded under.** `backtest.json` is a new
676
+ file of a strategy case, `conformance.md` sections 2 and 3: the money rounding
677
+ digit count, the charge schedule the host supplied or `null` for the
678
+ declaration's own, and the report window with `null` for an unstated bound. A
679
+ run under a supplied schedule or a narrowed window harvested to a case that said
680
+ nothing about either, so a second engine ran under other values and took the
681
+ blame, and no case file carried the digit count at all. The count is not in
682
+ `instrument.json` because `host-interface.md` 4.1 has no such fact, and the
683
+ three are not in `settings.json` because that file is the script's inputs keyed
684
+ by name. Every field of `BacktestSettings` now has a place in a case,
685
+ `caseFilesFrom` carries the table saying which, and a record whose settings hold
686
+ a field the table does not know is refused by name rather than written into a
687
+ case that ran under something it does not state. The two cases in the tree at
688
+ the time were re-harvested and gain the file; nothing else in them changed. Decision 61 has
689
+ the reasoning.
690
+
691
+ **A record past the tolerance cap makes no case.** Section 6 caps a declared
692
+ tolerance at `rel = 1e-9` and `abs = 1e-12`, and the runner refused a case past
693
+ it while nothing refused a record past it, so a harvest could write a directory
694
+ every runner errors on. `caseFilesFrom` now refuses such a record with OS6021,
695
+ the code the run refuses a setting with, and writes no file. The cap's two
696
+ figures are read out of the page by a test and held to the constants in core,
697
+ which cannot read the page itself. `CaseRefusal` gains `code`: the catalogue
698
+ code a refusal is filed under, or `null` for a refusal about what the record
699
+ holds, which no catalogue entry is about. `reason` is unchanged.
700
+
701
+ **How a number becomes text is one written rule, and one function.**
702
+ `language.md` 5.5 states it completely: the shortest round trip digits, written
703
+ positionally from ten to the minus seventh exclusive up to ten to the twenty
704
+ first exclusive and with an exponent outside that range, spelled `1e21` and
705
+ `1.5e-7` with never a plus, `0` for both zeros, and no spelling for a value that
706
+ is not finite. `canonicalNumber` from the emit module is the writer every number
707
+ goes through: `text(x)`, `text(x, decimals)`, the canonical encoding, and a
708
+ harvested case's csv files. `spec/vectors/number-text.json` carries fifty one
709
+ boundary cases as binary64 bit patterns, decimals and text, so an engine in
710
+ another language can hold its own writer to the rule without parsing this
711
+ repository's source. A new check, `scripts/check-number-writer.mjs`, asks the
712
+ type checker for the type of every operand under `src` and refuses a number
713
+ turned into text by the host anywhere else; the files that still format a
714
+ number for a human are recorded in `spec/number-text-exceptions.json` with an
715
+ exact count each.
716
+
717
+ **`text(x, decimals)` writes the shortest digits at every magnitude.** It used
718
+ to write the exact binary expansion of the rounded whole number inside the
719
+ scaling range and the shortest form past it, so `text(1152921504606846976, 0)`
720
+ gave `1152921504606846976` and now gives `1152921504606847000`, the same digits
721
+ `text(x)` gives the value. Only a scaled whole at or above 2 ** 53 is affected;
722
+ no price shaped value moves by a digit. The scale it multiplies by is now the
723
+ binary64 nearest to the power of ten rather than the host's `pow`, which on this
724
+ host is one ulp off at 23 decimals, so `text(3.0627e-8, 23)` no longer ends in a
725
+ stray 1, and 20.7 prints the measured figures beside the claim.
726
+
727
+ **`str.trim` and `toNumber` use a written whitespace set.** `stdlib.md` section
728
+ 10 now lists the twenty five code points with the Unicode White_Space property,
729
+ and both calls are implemented from the list rather than from the host's trim.
730
+ The one visible change: the byte order mark U+FEFF is no longer removed, and the
731
+ next line character U+0085 now is. A test walks every code point of the basic
732
+ plane against the table read out of the page.
733
+
734
+ **Strings sort by code point, as the specification always said.** `sort` on an
735
+ array of strings orders a symbol outside the basic plane after every code point
736
+ of the plane, where the host's own order put it before U+E000 to U+FFFF.
737
+
738
+ **Two corrections a script can observe, for anybody deciding whether to
739
+ upgrade.** The `<` family of operators on two strings orders by code point, as
740
+ `sort` does and `language.md` 9.3 always said, where it used the host's sixteen
741
+ bit units: a comparison between a symbol outside the basic plane and a code
742
+ point from U+E000 upward changes its answer, and no other pair of strings does.
743
+ `round(x, decimals)` scales by the binary64 nearest to the power of ten, the
744
+ scale `text(x, decimals)` uses, rather than the host's `pow`, so a value rounded
745
+ at 23 decimals can move by one unit in the last place and a value rounded at any
746
+ other count cannot. A stored run that did either reproduces to a different
747
+ number after the upgrade; nothing else changes. Decision 60 has the reasoning
748
+ for both.
749
+
750
+ **A program at a lower minor of the same format major loads.** The engine
751
+ required every table of its own minor, so a program compiled at format 1.0 was
752
+ refused by the 1.1 engine at load, at `requests`, with a message about a
753
+ malformed program. `compiled-program.md` 9.4 step 3 now says what 9.5 always
754
+ promised: a table a later minor added and an earlier program lacks reads as
755
+ empty, never as a refusal. A program at the engine's own minor or a later one
756
+ that omits a table is still refused, because section 2 says an empty table is
757
+ written and never omitted, and the version is what tells an older program from
758
+ a malformed one. Decision 56 has the reasoning.
759
+
760
+ **`loadText(text, options)` is the engine's text boundary.** A program that
761
+ arrives from outside the process as text is parsed, written out again through
762
+ the one canonical writer, and refused with OS6018 naming the character where
763
+ the two part if the text is not the canonical encoding of 2.14; the object then
764
+ goes through `load` so every later refusal applies in the same order.
765
+ `load(object)` is unchanged and is not held to canonicity, because an object
766
+ built beside the engine was never text. Section 13's first checklist line and
767
+ 9.4 step 1 now say the same thing (decision 57). The function lives in
768
+ `src/core/engine/load.ts` and is exported through both doors, the engine's and
769
+ the package's.
770
+
771
+ **The compiled format is held to its page by four checks.**
772
+ `check-format-tables.mjs` reads the instruction table of 4.13 and the tag table
773
+ of 2.2 out of the page and compares them with the compiler's opcode table and
774
+ tag list, probing a formula depth with several counts rather than reading it as
775
+ arithmetic. `spec/format-history.json` records, per released format version,
776
+ the field paths, opcodes and capability tags it defined and the sentence of
777
+ section 9 that justified the bump, and `check-format-additive.mjs` fails on a
778
+ field, opcode or tag the compiler gained that no version records and on one a
779
+ version records that the compiler lost, naming the path. `spec/corpus/` holds
780
+ the canonical encoding and `programHash` of every shipped example, and
781
+ `check-format-corpus.mjs` recompiles each one and compares the bytes, reporting
782
+ the first differing byte offset and the field path at it; the recompiled
783
+ program is given the corpus's own `compiler` stamp first, so a package bump
784
+ alone never fails it. A format change is now a deliberate act: record it in the
785
+ history, rewrite the corpus with `--write`, and review the diff.
786
+
787
+ **The named colours' channel values are published.** `stdlib.md` 11.1 said they
788
+ are fixed in the library manifest and are part of the conformance suite, and
789
+ they lived only in two source files. `spec/colours.json` now holds them, 11.1
790
+ names that file as the authority, and `check-colour-channels.mjs` holds the
791
+ compiler's table and the engine's table to it while stating no value of its
792
+ own. No value changed; what changed is that a second engine can now read them
793
+ from the specification.
794
+
795
+ **The library's arithmetic ships as vectors a second engine can load.**
796
+ `spec/vectors/library/` holds one JSON file per arithmetic function of the
797
+ manifest, keyed by name and argument count, with inputs and results as binary64
798
+ bit patterns (sixteen hex digits, sign bit first) rather than decimals, so
799
+ nothing about this repository's number formatting sits between another
800
+ implementation's arithmetic and this one's. Each function gets several cases:
801
+ the full 80-bar fixture the gate tests already use, the same with holes in every
802
+ series, a history shorter than any length, every constant argument absent, an
803
+ edge table for the stateless calls, and a bar with no volume, no session or no
804
+ tick where the function reads one; every case records the bar facts the
805
+ function read and the warmup index of every output. `index.json` names every
806
+ file and the 135 manifest entries in six groups that hold no arithmetic, each
807
+ with the reason. `docs/integrating/library-vectors.md` says how to decode, drive
808
+ and compare a file from another language, in under a page.
809
+
810
+ **A case that reaches a gap of `stdlib.md` 20.11 is written and marked, not
811
+ left out.** Twenty functions have such a case (the transcendental calls and what
812
+ is built on them, `hma`, `eom`, and the `ma` and `keltner` cases that select
813
+ `hma` by name), and each case carries the gaps its call reaches, read by the
814
+ gate's own reading of the table, so an implementer knows which numbers they are
815
+ not held to. `spec/decisions.md` 59 records why a vector is written where a
816
+ conformance case would be refused. `npm test` regenerates the directory and
817
+ fails on a byte that differs (`scripts/check-library-vectors.mjs`), so a vector
818
+ is never older than the engine, and a regeneration is a deliberate act committed
819
+ with the change that caused it.
820
+
821
+ **The second engine runs a strategy, and the build fails when the two engines
822
+ disagree.** `engine/` now holds an engine rather than a home for one: the
823
+ machine of `compiled-program.md` section 5, both halves of the library in the
824
+ accumulation order `stdlib.md` section 20 fixes, the order ledger and the fold
825
+ of section 17, and the money that turns fills into trades and a summary. The
826
+ conformance adapter joins them over a case directory: it reads `frames.csv` and
827
+ `backtest.json`, delivers each frame after the bar it names and folds it before
828
+ the next execution, applies the order calls a decided bar left behind, and
829
+ answers the `orders`, `trades` and `performance` channels beside `diagnostics`
830
+ and `values`.
831
+
832
+ Every harvested case passes on it, against the expected files and against the
833
+ first engine. That is four hundred bars a case, 104 ledger rows and 51 trades
834
+ folded into five summaries, reproduced to the last bit, with every figure here
835
+ read out of `cases/*/expected.json` rather than remembered.
836
+
837
+ **What was read from the pages, and what was not.** The behaviour is the
838
+ pages': the machine of `compiled-program.md` section 5, the fold and the ledger
839
+ of `stdlib.md` section 17, and the accumulation order section 20 fixes for every
840
+ library function, with the arithmetic held to `spec/vectors/library/` bit for
841
+ bit. The decomposition is not: the modules of `strategy/` and `accounting/`
842
+ fall one to one against the first engine's order ledger and money layer, name
843
+ for name, and much of the prose explaining them is that engine's prose. A reader
844
+ deciding what the gate is worth should have both halves of that. Two engines
845
+ agreeing to the last bit says this one computes what the other computes, which
846
+ is what a host needs and what the release is gated on. It does not say the pages
847
+ alone were enough to build an engine from: a page with a hole in it can be read
848
+ the same wrong way twice by somebody with the other engine open beside them, and
849
+ no suite can tell that apart from two readings that agree because the page is
850
+ complete. Only an implementation from the pages with nothing else to hand can,
851
+ and there has not been one. `docs/integrating/the-python-engine.md` says the
852
+ same on the page somebody reading the engine reaches.
853
+
854
+ **`npm test` runs the two engines against each other.** `npm run suite:agree`
855
+ hands every case to both adapters with `--actual` and compares what each
856
+ computed, channel by channel, at tolerance zero whatever the case declares. A
857
+ difference of one bit fails the build naming the case, the channel, the column
858
+ and both values. `conformance.md` section 10 calls a disagreement between two
859
+ engines a release blocker; this is the sentence made mechanical, and it is the
860
+ gate the phase is measured by. `npm run suite` and `npm run suite:engine` run
861
+ each engine against the expected files on its own.
862
+
863
+ One rule belongs to that comparison alone: a run where every case was skipped
864
+ now exits non-zero saying it compared nothing. A skipped case is one engine
865
+ honestly reporting the profile it claims, which is what the first mode is for,
866
+ but in the second mode it means neither engine was asked about a single case,
867
+ and a green line there is the evidence a suite with no cases in it would
868
+ produce.
869
+
870
+ **The second engine claims the `strategy` profile**, so a strategy case is run
871
+ rather than skipped, and every shortfall is named on the case with the
872
+ `unsupported` outcome: a chart channel it does not draw, a capability it does not
873
+ serve, a library function its manifest does not hold, a `ticks.csv`, a secondary
874
+ series, a calendar in a timezone it cannot read, or a frame delivered after the
875
+ last bar, which no fold boundary reaches. `docs/integrating/running-the-suite.md`
876
+ holds the whole list and says why a cumulative profile table has no word for an
877
+ engine that runs the money and draws nothing.
878
+
879
+ **Three more cases, and a straight answer about how much the two engines
880
+ agreeing proves.** The suite held two cases, and the script was the only thing
881
+ that differed between them: no charge schedule, two rounding digits, the whole
882
+ of the bars reported, no value stored for an input, one instrument and one entry
883
+ at a time, in both. Between them, 47 ledger rows and 23 trades, and every frame
884
+ in both was an order working and then filling whole. Three cases join them,
885
+ harvested the same way from the same shipped strategies:
886
+
887
+ - `perf/report-window` reports 151 of the 400 bars, with a position open at each
888
+ end of the window. The ledger, the trades and the realised profit are
889
+ `order/buy`'s to the bit and the summary is not: the bars reported, the bars
890
+ spent holding a position and the deepest drawdown all change, because every
891
+ bar still executes and only the ones inside the window are reported.
892
+ - `perf/money-digits` folds the opening range run under a rounding count of zero
893
+ instead of two. Every figure is `order/sell`'s, which is the assertion: the
894
+ count reaches the total of one fill's charges, half to even, and no other
895
+ figure of the report. An engine reading `conformance.md` section 3 as every
896
+ money figure being rounded to that count writes a net profit of -654 where
897
+ this case says -653.55.
898
+ - `input/host-values` runs the crossing strategy under three values a host
899
+ stored for its inputs, and is the only case that carries a `settings.json`.
900
+ Ten ledger rows and five trades, where the declared defaults give thirteen and
901
+ six.
902
+
903
+ That harvest made the suite five cases, 104 ledger rows and 51 trades, each passing against
904
+ the expected files on both engines and against the other engine exactly. Each
905
+ one was mutation tested before it was committed: an engine that reports every
906
+ bar supplied fails the window case at the bar count, one that never opens
907
+ `settings.json` fails the input case on the length of the ledger, four rows
908
+ where the case says ten, and one that rounds every money figure fails the digits
909
+ case at the first trade. The one
910
+ mutant that survives is an engine that ignores the digit count and rounds at
911
+ two, and the case says so in its own notes rather than leaving it to be found.
912
+
913
+ **A case is a run, so one example can be more than one case.**
914
+ `scripts/harvest-cases.mjs` now harvests every identity that names an example
915
+ rather than the first one, and an identity carries what the host chose for its
916
+ run: a money rounding digit count, a report window as two bar indices, or values
917
+ for the script's inputs. An identity that chooses none is the run the gate
918
+ drives everywhere else, which is why the two cases already in the tree are byte
919
+ for byte what they were. The harvest's report no longer files a strategy nobody
920
+ has chosen a case identity for under the sentence about gaps, which is what it
921
+ was doing to the short premium example on every run.
922
+
923
+ **What the suite does not reach, written down rather than left to be
924
+ discovered.** No case supplies a charge schedule: a supplied schedule beside a
925
+ declared commission is refused before the first bar (OS6023) and every shipped
926
+ strategy declares one, so a case for it needs a strategy example that declares
927
+ none. No case hands an engine a partial fill, a repeated frame, two frames in
928
+ the wrong order, a fill reported after the order had gone terminal, a rejection,
929
+ a cancellation, or a frame naming no row of the ledger. The frames in a
930
+ harvested case are the ones its own run was answered, the destination a backtest
931
+ runs against fills an order once and in full, and no shipped strategy cancels an
932
+ order or leaves one resting, so none of those shapes can be harvested from what
933
+ is in the tree today. `docs/integrating/running-the-suite.md` carries the list
934
+ beside the commands, because that is where somebody reading a green run is
935
+ standing.
936
+
937
+ ---
938
+
10
939
  ## 0.4.0
11
940
 
12
941
  **A backtest is driven, and what it produces is a document rather than a