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.
- package/CHANGELOG.md +929 -0
- package/README.md +69 -16
- package/dist/adapters/charts/driving.d.ts +50 -0
- package/dist/adapters/charts/driving.d.ts.map +1 -0
- package/dist/adapters/charts/driving.js +57 -0
- package/dist/adapters/charts/driving.js.map +1 -0
- package/dist/adapters/charts/run.d.ts +20 -0
- package/dist/adapters/charts/run.d.ts.map +1 -1
- package/dist/adapters/charts/run.js +83 -16
- package/dist/adapters/charts/run.js.map +1 -1
- package/dist/adapters/charts/venue.d.ts +73 -0
- package/dist/adapters/charts/venue.d.ts.map +1 -0
- package/dist/adapters/charts/venue.js +104 -0
- package/dist/adapters/charts/venue.js.map +1 -0
- package/dist/core/accounting/analysis.d.ts +111 -0
- package/dist/core/accounting/analysis.d.ts.map +1 -0
- package/dist/core/accounting/analysis.js +123 -0
- package/dist/core/accounting/analysis.js.map +1 -0
- package/dist/core/accounting/equity.d.ts +33 -0
- package/dist/core/accounting/equity.d.ts.map +1 -1
- package/dist/core/accounting/equity.js +12 -0
- package/dist/core/accounting/equity.js.map +1 -1
- package/dist/core/accounting/index.d.ts +2 -0
- package/dist/core/accounting/index.d.ts.map +1 -1
- package/dist/core/accounting/index.js +1 -0
- package/dist/core/accounting/index.js.map +1 -1
- package/dist/core/accounting/report.d.ts +3 -0
- package/dist/core/accounting/report.d.ts.map +1 -1
- package/dist/core/accounting/report.js +2 -0
- package/dist/core/accounting/report.js.map +1 -1
- package/dist/core/accounting/statistics.d.ts +12 -0
- package/dist/core/accounting/statistics.d.ts.map +1 -1
- package/dist/core/accounting/statistics.js +23 -1
- package/dist/core/accounting/statistics.js.map +1 -1
- package/dist/core/backtest/case.d.ts +60 -0
- package/dist/core/backtest/case.d.ts.map +1 -0
- package/dist/core/backtest/case.js +319 -0
- package/dist/core/backtest/case.js.map +1 -0
- package/dist/core/backtest/compare.d.ts.map +1 -1
- package/dist/core/backtest/compare.js +2 -0
- package/dist/core/backtest/compare.js.map +1 -1
- package/dist/core/backtest/deliver.d.ts +93 -0
- package/dist/core/backtest/deliver.d.ts.map +1 -0
- package/dist/core/backtest/deliver.js +94 -0
- package/dist/core/backtest/deliver.js.map +1 -0
- package/dist/core/backtest/drive.d.ts +66 -8
- package/dist/core/backtest/drive.d.ts.map +1 -1
- package/dist/core/backtest/drive.js +109 -59
- package/dist/core/backtest/drive.js.map +1 -1
- package/dist/core/backtest/index.d.ts +24 -12
- package/dist/core/backtest/index.d.ts.map +1 -1
- package/dist/core/backtest/index.js +21 -10
- package/dist/core/backtest/index.js.map +1 -1
- package/dist/core/backtest/record.d.ts +63 -2
- package/dist/core/backtest/record.d.ts.map +1 -1
- package/dist/core/backtest/record.js +71 -4
- package/dist/core/backtest/record.js.map +1 -1
- package/dist/core/backtest/replay.d.ts.map +1 -1
- package/dist/core/backtest/replay.js +11 -1
- package/dist/core/backtest/replay.js.map +1 -1
- package/dist/core/backtest/simulate.d.ts +113 -1
- package/dist/core/backtest/simulate.d.ts.map +1 -1
- package/dist/core/backtest/simulate.js +123 -5
- package/dist/core/backtest/simulate.js.map +1 -1
- package/dist/core/catalogue/catalogue.generated.d.ts +1 -1
- package/dist/core/catalogue/catalogue.generated.js +1 -1
- package/dist/core/catalogue/catalogue.generated.js.map +1 -1
- package/dist/core/check/library-orders.js +2 -2
- package/dist/core/check/library-orders.js.map +1 -1
- package/dist/core/check/library-prose.generated.js +2 -2
- package/dist/core/check/library-prose.generated.js.map +1 -1
- package/dist/core/emit/canonical.d.ts +22 -8
- package/dist/core/emit/canonical.d.ts.map +1 -1
- package/dist/core/emit/canonical.js +67 -6
- package/dist/core/emit/canonical.js.map +1 -1
- package/dist/core/engine/arithmetic.d.ts +6 -25
- package/dist/core/engine/arithmetic.d.ts.map +1 -1
- package/dist/core/engine/arithmetic.js +48 -3
- package/dist/core/engine/arithmetic.js.map +1 -1
- package/dist/core/engine/index.d.ts +1 -1
- package/dist/core/engine/index.d.ts.map +1 -1
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/index.js.map +1 -1
- package/dist/core/engine/library/arrays.d.ts.map +1 -1
- package/dist/core/engine/library/arrays.js +8 -2
- package/dist/core/engine/library/arrays.js.map +1 -1
- package/dist/core/engine/library/code-points.d.ts +40 -0
- package/dist/core/engine/library/code-points.d.ts.map +1 -0
- package/dist/core/engine/library/code-points.js +74 -0
- package/dist/core/engine/library/code-points.js.map +1 -0
- package/dist/core/engine/library/index.d.ts +5 -0
- package/dist/core/engine/library/index.d.ts.map +1 -1
- package/dist/core/engine/library/index.js +5 -0
- package/dist/core/engine/library/index.js.map +1 -1
- package/dist/core/engine/library/text.d.ts.map +1 -1
- package/dist/core/engine/library/text.js +51 -34
- package/dist/core/engine/library/text.js.map +1 -1
- package/dist/core/engine/load.d.ts +20 -0
- package/dist/core/engine/load.d.ts.map +1 -1
- package/dist/core/engine/load.js +62 -0
- package/dist/core/engine/load.js.map +1 -1
- package/dist/core/engine/verify-tables.d.ts.map +1 -1
- package/dist/core/engine/verify-tables.js +41 -0
- package/dist/core/engine/verify-tables.js.map +1 -1
- package/dist/core/index.d.ts +10 -5
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/stdlib/index.d.ts +1 -1
- package/dist/core/stdlib/index.d.ts.map +1 -1
- package/dist/core/stdlib/index.js +1 -1
- package/dist/core/stdlib/index.js.map +1 -1
- package/dist/core/stdlib/maths/index.d.ts +1 -1
- package/dist/core/stdlib/maths/index.d.ts.map +1 -1
- package/dist/core/stdlib/maths/index.js +1 -1
- package/dist/core/stdlib/maths/index.js.map +1 -1
- package/dist/core/stdlib/maths/rounding.d.ts +5 -0
- package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
- package/dist/core/stdlib/maths/rounding.js +19 -1
- package/dist/core/stdlib/maths/rounding.js.map +1 -1
- package/dist/core/version/version.generated.d.ts +1 -1
- package/dist/core/version/version.generated.js +1 -1
- package/package.json +14 -2
- package/spec/README.md +2 -1
- package/spec/errors.json +3 -3
- package/src/adapters/charts/driving.ts +109 -0
- package/src/adapters/charts/run.ts +120 -28
- package/src/adapters/charts/venue.ts +132 -0
- package/src/core/accounting/analysis.ts +188 -0
- package/src/core/accounting/equity.ts +38 -0
- package/src/core/accounting/index.ts +2 -0
- package/src/core/accounting/report.ts +5 -0
- package/src/core/accounting/statistics.ts +38 -1
- package/src/core/backtest/case.ts +395 -0
- package/src/core/backtest/compare.ts +2 -0
- package/src/core/backtest/deliver.ts +161 -0
- package/src/core/backtest/drive.ts +175 -71
- package/src/core/backtest/index.ts +24 -12
- package/src/core/backtest/record.ts +134 -5
- package/src/core/backtest/replay.ts +11 -1
- package/src/core/backtest/simulate.ts +201 -6
- package/src/core/catalogue/catalogue.generated.ts +1 -1
- package/src/core/check/library-orders.ts +2 -2
- package/src/core/check/library-prose.generated.ts +2 -2
- package/src/core/emit/canonical.ts +67 -9
- package/src/core/engine/arithmetic.ts +23 -3
- package/src/core/engine/index.ts +1 -1
- package/src/core/engine/library/arrays.ts +8 -2
- package/src/core/engine/library/code-points.ts +73 -0
- package/src/core/engine/library/index.ts +6 -0
- package/src/core/engine/library/text.ts +53 -35
- package/src/core/engine/load.ts +68 -0
- package/src/core/engine/verify-tables.ts +43 -0
- package/src/core/index.ts +16 -2
- package/src/core/stdlib/index.ts +1 -0
- package/src/core/stdlib/maths/index.ts +1 -0
- package/src/core/stdlib/maths/rounding.ts +23 -1
- 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
|