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
|
@@ -30,6 +30,21 @@
|
|
|
30
30
|
* intent no row holds and the fold would refuse it. The row a bracket wants is
|
|
31
31
|
* decision 55 and it is not in this release, so a stop cannot fill here and a
|
|
32
32
|
* page that says it can is ahead of the engine.
|
|
33
|
+
*
|
|
34
|
+
* **A destination that behaves badly does it on a schedule, stated before the
|
|
35
|
+
* run and never by chance.** Every case harvested before this one ran against a
|
|
36
|
+
* venue that filled every order whole and on time, so two engines were proved
|
|
37
|
+
* to agree about the half of a day that costs nobody anything. The other half
|
|
38
|
+
* is a partial fill, an order arriving in pieces, a rejection, a cancellation,
|
|
39
|
+
* an expiry and a fill that turns up after the order has ended, and
|
|
40
|
+
* `SimulatorOptions.fill` carries the schedule saying which order each of those
|
|
41
|
+
* happens to and at which boundary. There is no random number here and there
|
|
42
|
+
* will not be one: `stdlib.md` 8.2 keeps a script that answers differently on a
|
|
43
|
+
* second run out of a conformance suite, and a venue rolling a die would make
|
|
44
|
+
* every case it wrote unreproducible in the same breath. **An order the
|
|
45
|
+
* schedule names is answered by the schedule and by nothing else**, so a run
|
|
46
|
+
* stating none is the run the cases before this one were harvested from, to the
|
|
47
|
+
* bit, which is what lets this exist at all.
|
|
33
48
|
*/
|
|
34
49
|
import type { Contract } from '../accounting/index.js';
|
|
35
50
|
import type { OrderFrame, OrderIntent, OrderSide, RoutedEffect } from '../engine/index.js';
|
|
@@ -38,11 +53,70 @@ import { testResting } from './resting.js';
|
|
|
38
53
|
import type { RestingOrder } from './resting.js';
|
|
39
54
|
import type { FillPolicy } from './settings.js';
|
|
40
55
|
|
|
56
|
+
/**
|
|
57
|
+
* What this destination does to one order, at one boundary.
|
|
58
|
+
*
|
|
59
|
+
* Four words reaching every status of `stdlib.md` 17.7 a host may send. `fill`
|
|
60
|
+
* carries the cumulative quantity, so one word covers the acknowledgement
|
|
61
|
+
* before anything has traded, a fill of part of the order and a fill of the
|
|
62
|
+
* whole of it: `working` and `filled` are that quantity read against the
|
|
63
|
+
* order's own rather than two instructions. The other three are the three ways
|
|
64
|
+
* an order ends carrying less than it asked for, and an engine never handed
|
|
65
|
+
* one has never been asked what it does with the quantity still working.
|
|
66
|
+
*/
|
|
67
|
+
export type VenueDoes = 'fill' | 'reject' | 'cancel' | 'expire';
|
|
68
|
+
|
|
69
|
+
/** One act of the schedule: what the destination does, to which order, when. */
|
|
70
|
+
export interface VenueAct {
|
|
71
|
+
/**
|
|
72
|
+
* The nth order this destination took, counting from one.
|
|
73
|
+
*
|
|
74
|
+
* Orders and not intents: a bracket is never taken and a cancellation is
|
|
75
|
+
* answered without being held, so neither is counted and neither can be named
|
|
76
|
+
* here. An ordinal for the reason `conformance.md` section 3 gives a case's
|
|
77
|
+
* own frames: nobody writing a schedule knows the id an engine will mint.
|
|
78
|
+
*/
|
|
79
|
+
readonly order: number;
|
|
80
|
+
/**
|
|
81
|
+
* Boundaries after the one the order arrived at, `0` being that boundary.
|
|
82
|
+
*
|
|
83
|
+
* Counted from the order rather than stated as a bar index, because the bar
|
|
84
|
+
* a strategy decides an order on moves the moment anything else about the run
|
|
85
|
+
* does, and a schedule in bar indices is one nobody can read back.
|
|
86
|
+
*/
|
|
87
|
+
readonly afterBars: number;
|
|
88
|
+
readonly does: VenueDoes;
|
|
89
|
+
/**
|
|
90
|
+
* The cumulative quantity a fill reports, in units, or absent for all of it.
|
|
91
|
+
*
|
|
92
|
+
* Cumulative because a frame is (`stdlib.md` 17.8): `0` is the
|
|
93
|
+
* acknowledgement a venue sends before anything has traded, a number below
|
|
94
|
+
* the order's own is a partial fill, and one at or above it completes the
|
|
95
|
+
* order. A quantity below what this venue has already reported is a stale
|
|
96
|
+
* frame, which a venue really sends and the fold has to swallow, so it is
|
|
97
|
+
* stated here rather than refused.
|
|
98
|
+
*/
|
|
99
|
+
readonly units?: number | null;
|
|
100
|
+
/** The destination's own text, which the ledger records against the row. */
|
|
101
|
+
readonly text?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The fill policy, and what this destination does to the orders it names.
|
|
106
|
+
*
|
|
107
|
+
* The schedule sits on the policy because it is the same kind of fact: how this
|
|
108
|
+
* destination decides a fill, chosen before the first bar and carried in the
|
|
109
|
+
* record beside the policy's own version, so a stored run replays as it ran.
|
|
110
|
+
*/
|
|
111
|
+
export interface VenuePolicy extends FillPolicy {
|
|
112
|
+
readonly schedule?: readonly VenueAct[];
|
|
113
|
+
}
|
|
114
|
+
|
|
41
115
|
/** What the venue prices against and how it decides a fill. */
|
|
42
116
|
export interface SimulatorOptions {
|
|
43
117
|
readonly bars: readonly RecordedBar[];
|
|
44
118
|
readonly contract: Contract;
|
|
45
|
-
readonly fill:
|
|
119
|
+
readonly fill: VenuePolicy;
|
|
46
120
|
/** Adverse always, in ticks, applied to a market fill and to a stop. */
|
|
47
121
|
readonly slippageTicks: number;
|
|
48
122
|
/** The declaration's own fill rule: where a market order is priced. */
|
|
@@ -64,7 +138,11 @@ interface Order {
|
|
|
64
138
|
readonly placedOn: number;
|
|
65
139
|
/** Absent on a market order, which rests on nothing. */
|
|
66
140
|
readonly rest: RestingOrder | null;
|
|
141
|
+
/** The acts the schedule states about this order, in the order they were stated. */
|
|
142
|
+
readonly acts: readonly VenueAct[];
|
|
67
143
|
filledQty: number;
|
|
144
|
+
/** This venue's average over `filledQty`, absent while nothing has filled. */
|
|
145
|
+
avgPrice: number | null;
|
|
68
146
|
live: boolean;
|
|
69
147
|
/** Whether a stop limit has reached its trigger and is now a limit. */
|
|
70
148
|
triggered: boolean;
|
|
@@ -73,6 +151,20 @@ interface Order {
|
|
|
73
151
|
/** `language.md` 13.3: a market order priced at the close of its own bar. */
|
|
74
152
|
const AT_CLOSE = 'close';
|
|
75
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The word this venue reports each ending under.
|
|
156
|
+
*
|
|
157
|
+
* A destination with words of its own maps them onto the vocabulary in its
|
|
158
|
+
* adapter, which is where `stdlib.md` 17.7 puts the mapping because a status
|
|
159
|
+
* vocabulary differs per destination. This is this one's, and the whole of what
|
|
160
|
+
* a schedule's verb means.
|
|
161
|
+
*/
|
|
162
|
+
const ENDED: Readonly<Record<Exclude<VenueDoes, 'fill'>, string>> = {
|
|
163
|
+
reject: 'rejected',
|
|
164
|
+
cancel: 'cancelled',
|
|
165
|
+
expire: 'expired',
|
|
166
|
+
};
|
|
167
|
+
|
|
76
168
|
/**
|
|
77
169
|
* A venue, for one run.
|
|
78
170
|
*
|
|
@@ -123,6 +215,13 @@ export class Simulator {
|
|
|
123
215
|
const bar = this.options.bars[barIndex];
|
|
124
216
|
if (bar !== undefined) {
|
|
125
217
|
for (const order of this.orders) {
|
|
218
|
+
// An order the schedule names is answered by the schedule, whether or
|
|
219
|
+
// not it is still live: a fill after a cancellation is the one thing
|
|
220
|
+
// this exists for and the order is dead by then.
|
|
221
|
+
if (order.acts.length > 0) {
|
|
222
|
+
if (order.placedOn < barIndex) this.perform(order, bar, barIndex);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
126
225
|
if (!order.live || order.rest === null || order.placedOn >= barIndex) continue;
|
|
127
226
|
this.decide(order, bar, barIndex);
|
|
128
227
|
}
|
|
@@ -145,12 +244,17 @@ export class Simulator {
|
|
|
145
244
|
/** An order the strategy sent, which this venue now holds. */
|
|
146
245
|
private accept(intent: OrderIntent, barIndex: number): void {
|
|
147
246
|
this.refs += 1;
|
|
247
|
+
// The ordinal of this order among the orders taken, which is what an act
|
|
248
|
+
// names. Read before the order joins them, so the first is one.
|
|
249
|
+
const ordinal = this.orders.length + 1;
|
|
148
250
|
const order: Order = {
|
|
149
251
|
intent,
|
|
150
252
|
ref: refOf(this.refs),
|
|
151
253
|
placedOn: barIndex,
|
|
152
254
|
rest: restingFor(intent),
|
|
255
|
+
acts: (this.options.fill.schedule ?? []).filter((act) => act.order === ordinal),
|
|
153
256
|
filledQty: 0,
|
|
257
|
+
avgPrice: null,
|
|
154
258
|
live: true,
|
|
155
259
|
triggered: false,
|
|
156
260
|
};
|
|
@@ -188,6 +292,15 @@ export class Simulator {
|
|
|
188
292
|
* where it does not.
|
|
189
293
|
*/
|
|
190
294
|
private open(order: Order, barIndex: number): void {
|
|
295
|
+
// A scheduled order is answered by its schedule from its first breath. The
|
|
296
|
+
// acknowledgement below is a thing this venue chooses to say, so a schedule
|
|
297
|
+
// that wants one states it, and one that wants the destination to sit on an
|
|
298
|
+
// order and say nothing gets that instead.
|
|
299
|
+
if (order.acts.length > 0) {
|
|
300
|
+
const bar = this.options.bars[barIndex];
|
|
301
|
+
if (bar !== undefined) this.perform(order, bar, barIndex);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
191
304
|
this.say(order.intent, order.ref, 'working', 0, null, barIndex);
|
|
192
305
|
if (order.rest !== null) return;
|
|
193
306
|
|
|
@@ -227,15 +340,96 @@ export class Simulator {
|
|
|
227
340
|
* than guessed at here, so by this point the unit is one of two.
|
|
228
341
|
*/
|
|
229
342
|
private complete(order: Order, price: number, barIndex: number): void {
|
|
230
|
-
const
|
|
231
|
-
const lot = this.options.contract.lotSize;
|
|
232
|
-
const qty =
|
|
233
|
-
this.options.qtyType === 'lots' && lot !== null && lot > 0 ? stated * lot : stated;
|
|
343
|
+
const qty = this.unitsOf(order.intent);
|
|
234
344
|
order.filledQty = qty;
|
|
345
|
+
order.avgPrice = price;
|
|
235
346
|
order.live = false;
|
|
236
347
|
this.say(order.intent, order.ref, 'filled', qty, price, barIndex);
|
|
237
348
|
}
|
|
238
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The acts due at this boundary, in the order the schedule stated them.
|
|
352
|
+
*
|
|
353
|
+
* Due is counted from the bar that sent the order, so an act names a moment
|
|
354
|
+
* in the life of its own order rather than a bar of the run. Two acts due
|
|
355
|
+
* together are answered as written, which is how a schedule states a
|
|
356
|
+
* cancellation and the fill that raced it.
|
|
357
|
+
*
|
|
358
|
+
* Liveness is not consulted. An order that has ended can still be spoken
|
|
359
|
+
* about, because the frame that arrives after it ended is the whole reason
|
|
360
|
+
* this is here: `stdlib.md` 17.8's fill after a terminal status, which a
|
|
361
|
+
* venue sends whenever a cancel races a fill and which an engine refusing it
|
|
362
|
+
* loses, leaving a position the strategy cannot see.
|
|
363
|
+
*/
|
|
364
|
+
private perform(order: Order, bar: RecordedBar, barIndex: number): void {
|
|
365
|
+
for (const act of order.acts) {
|
|
366
|
+
if (order.placedOn + act.afterBars !== barIndex) continue;
|
|
367
|
+
if (act.does === 'fill') {
|
|
368
|
+
this.report(order, act, bar, barIndex);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// The order ends, reporting what it filled before it ended, because a
|
|
372
|
+
// frame is cumulative: `stdlib.md` 17.8 has the row keeping the terminal
|
|
373
|
+
// word and the quantity recording what traded, both true at once.
|
|
374
|
+
order.live = false;
|
|
375
|
+
const ended = ENDED[act.does];
|
|
376
|
+
const price = order.filledQty > 0 ? order.avgPrice : null;
|
|
377
|
+
this.say(order.intent, order.ref, ended, order.filledQty, price, barIndex, act.text ?? '');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A fill the schedule stated, at this bar's close and worsened like any other.
|
|
383
|
+
*
|
|
384
|
+
* **The average is this venue's own, over the cumulative quantity**, the
|
|
385
|
+
* figure `stdlib.md` 17.8 step 3 says the row takes whole. A venue reporting
|
|
386
|
+
* the last piece's price and calling it an average hands the engine a number
|
|
387
|
+
* that is not one, and the engine may not work its own out from two of them,
|
|
388
|
+
* so the lie settles into the ledger and into every trade folded from it.
|
|
389
|
+
*
|
|
390
|
+
* A stated quantity at or below what this venue has already reported adds
|
|
391
|
+
* nothing and moves nothing: that is a repeated or a stale frame, which a real
|
|
392
|
+
* destination sends and the fold has to swallow. It carries the average this
|
|
393
|
+
* venue holds now rather than then, because it keeps no history of its own
|
|
394
|
+
* averages and the fold ignores the price of a frame adding no quantity. A
|
|
395
|
+
* bar with no close prices nothing, so an act due at one says nothing rather
|
|
396
|
+
* than raising a quantity with no price against it, which step 3 refuses.
|
|
397
|
+
*/
|
|
398
|
+
private report(order: Order, act: VenueAct, bar: RecordedBar, barIndex: number): void {
|
|
399
|
+
const whole = this.unitsOf(order.intent);
|
|
400
|
+
const stated = act.units ?? whole;
|
|
401
|
+
const delta = stated - order.filledQty;
|
|
402
|
+
if (delta > 0) {
|
|
403
|
+
if (bar.close === null) return;
|
|
404
|
+
// Written in this order and left in it: the source order of a sum is
|
|
405
|
+
// what decides its last bit, and a case harvested from this venue is
|
|
406
|
+
// asserted to the bit.
|
|
407
|
+
const price = this.worsen(bar.close, order.intent.side);
|
|
408
|
+
order.avgPrice = ((order.avgPrice ?? 0) * order.filledQty + price * delta) / stated;
|
|
409
|
+
order.filledQty = stated;
|
|
410
|
+
if (order.filledQty >= whole) order.live = false;
|
|
411
|
+
}
|
|
412
|
+
// `working` is live and not completely filled, `filled` is the whole
|
|
413
|
+
// quantity: 17.7's two words read off the quantity rather than stated
|
|
414
|
+
// twice by a schedule that could disagree with the number beside them.
|
|
415
|
+
const status = order.filledQty >= whole ? 'filled' : 'working';
|
|
416
|
+
const price = stated > 0 ? order.avgPrice : null;
|
|
417
|
+
this.say(order.intent, order.ref, status, stated, price, barIndex, act.text ?? '');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The order's quantity in units, which is what a fill is counted in.
|
|
422
|
+
*
|
|
423
|
+
* The conversion `complete` did inline, wanted in two places the moment a
|
|
424
|
+
* schedule can fill an order in pieces: the whole a piece is measured
|
|
425
|
+
* against has to be the number the fill path would have reported.
|
|
426
|
+
*/
|
|
427
|
+
private unitsOf(intent: OrderIntent): number {
|
|
428
|
+
const stated = intent.qty ?? 0;
|
|
429
|
+
const lot = this.options.contract.lotSize;
|
|
430
|
+
return this.options.qtyType === 'lots' && lot !== null && lot > 0 ? stated * lot : stated;
|
|
431
|
+
}
|
|
432
|
+
|
|
239
433
|
/**
|
|
240
434
|
* A price worsened by the slippage the run was carried out under.
|
|
241
435
|
*
|
|
@@ -270,6 +464,7 @@ export class Simulator {
|
|
|
270
464
|
filledQty: number,
|
|
271
465
|
avgFillPrice: number | null,
|
|
272
466
|
barIndex: number,
|
|
467
|
+
text = '',
|
|
273
468
|
): void {
|
|
274
469
|
this.seq += 1;
|
|
275
470
|
this.answered.push({
|
|
@@ -281,7 +476,7 @@ export class Simulator {
|
|
|
281
476
|
sentInstrument: intent.instrument,
|
|
282
477
|
sentProduct: intent.product,
|
|
283
478
|
time: this.options.bars[barIndex]?.time ?? null,
|
|
284
|
-
text
|
|
479
|
+
text,
|
|
285
480
|
seq: this.seq,
|
|
286
481
|
});
|
|
287
482
|
}
|
|
@@ -138,7 +138,7 @@ export const ENTRIES = {
|
|
|
138
138
|
OS6018: { code: "OS6018", title: "The compiled program is malformed", severity: "error", stage: "host", since: 1, autofix: false, message: "The program failed verification at {location}: {reason}.", fix: "Recompile the script from its source; a program that fails verification came from a broken compiler or was edited after it was written, and neither is repairable by hand.", placeholders: ["location", "reason"] },
|
|
139
139
|
OS6019: { code: "OS6019", title: "A host setting fails the input's validation", severity: "error", stage: "host", since: 1, autofix: false, message: "The host supplied {value} for {key}, and {validation}.", fix: "Correct the value in the settings dialog, or widen the input's own min, max or options so the value is allowed.", placeholders: ["value", "key", "validation"] },
|
|
140
140
|
OS6020: { code: "OS6020", title: "The report window holds no bars", severity: "error", stage: "host", since: 1, autofix: false, message: "The window from {from} to {to} holds none of the {count} bars supplied.", fix: "Widen the window until it covers bars, or supply the bars it covers. Both bounds are inclusive and are compared against the times of the bars supplied rather than against a calendar, so a window that falls inside a gap in the data is empty however wide it looks.", placeholders: ["from", "to", "count"] },
|
|
141
|
-
OS6021: { code: "OS6021", title: "A run setting cannot be applied as stated", severity: "error", stage: "host", since: 1, autofix: false, message: "{setting} cannot be applied: {problem}.", fix: "State the setting so it can be carried out: declare a charge line after every line it is levied on, supply the tick size a slippage in ticks is measured in,
|
|
141
|
+
OS6021: { code: "OS6021", title: "A run setting cannot be applied as stated", severity: "error", stage: "host", since: 1, autofix: false, message: "{setting} cannot be applied: {problem}.", fix: "State the setting so it can be carried out: declare a charge line after every line it is levied on, supply the tick size a slippage in ticks is measured in, write down the reason a tolerance needs a bound, or bring a bound past the cap back inside it. Nothing has been computed at the point this is refused, so correcting the setting and running again costs one run.", placeholders: ["setting", "problem"] },
|
|
142
142
|
OS6022: { code: "OS6022", title: "The bars are not the bars the record was made from", severity: "error", stage: "host", since: 1, autofix: false, message: "The bars supplied hash to {found}, and the record was made from {expected}.", fix: "Replay against the bars the record names. Where the revision is the point, make a second record over the revised bars and compare the two runs, rather than overwriting one run with the other under one name.", placeholders: ["found", "expected"] },
|
|
143
143
|
OS6023: { code: "OS6023", title: "Two cost models are stated at once", severity: "error", stage: "host", since: 1, autofix: false, message: "A charge schedule was supplied, and the declaration states a commission of {commission} in {commissionType}.", fix: "Supply the schedule and leave the declaration's commission at its default of zero, or state the commission in the declaration and supply no schedule. A schedule is the one of the two that can carry a floor, a cap, a charge levied on a charge, and a cost that falls on one side of the trade only.", placeholders: ["commission", "commissionType"] },
|
|
144
144
|
OS7001: { code: "OS7001", title: "Only a strategy can do that", severity: "error", stage: "check", since: 1, autofix: false, message: "{name} is available only in a file declared with strategy().", fix: "Change study(...) on line {line} to strategy(...), or replace {name} with signal(\"...\") to mark the bar without trading.", placeholders: ["name", "line"] },
|
|
@@ -240,7 +240,7 @@ const legs: readonly LibraryEntry[] = [
|
|
|
240
240
|
}),
|
|
241
241
|
entry('leg.stop(name: string, price: number) -> nothing', { ...strategyOnly, planned: true }),
|
|
242
242
|
entry('leg.target(name: string, price: number) -> nothing', { ...strategyOnly, planned: true }),
|
|
243
|
-
entry('leg.trail(name: string, distance: number,
|
|
243
|
+
entry('leg.trail(name: string, distance: number, activateAt?: number = none) -> nothing', {
|
|
244
244
|
...strategyOnly,
|
|
245
245
|
planned: true,
|
|
246
246
|
}),
|
|
@@ -258,7 +258,7 @@ const book: readonly LibraryEntry[] = [
|
|
|
258
258
|
entry('book.stop(amount: number) -> nothing', { ...strategyOnly, planned: true }),
|
|
259
259
|
entry('book.target(amount: number) -> nothing', { ...strategyOnly, planned: true }),
|
|
260
260
|
entry(
|
|
261
|
-
'book.lockProfit(
|
|
261
|
+
'book.lockProfit(activateAt: number, lock: number, step?: number = none, advance?: number = none) -> nothing',
|
|
262
262
|
{ ...strategyOnly, planned: true },
|
|
263
263
|
),
|
|
264
264
|
entry('book.trailStopsToEntry(at: number) -> nothing', { ...strategyOnly, planned: true }),
|
|
@@ -49,7 +49,7 @@ export const LIBRARY_PROSE: Readonly<Record<string, LibraryProse>> = {
|
|
|
49
49
|
"book.exit": {"summary":"Exit the whole book as a unit"},
|
|
50
50
|
"book.exitAt": {"summary":"Square off every leg at this `\"HHMM\"` in the chart's timezone"},
|
|
51
51
|
"book.isOpen": {"summary":"Whether any leg holds a position","warmup":"bar 0"},
|
|
52
|
-
"book.lockProfit": {"summary":"
|
|
52
|
+
"book.lockProfit": {"summary":"Activate a floor at a profit, then advance it as profit grows"},
|
|
53
53
|
"book.profit": {"summary":"The book's profit in money, open and realised since it was last flat","warmup":"bar 0"},
|
|
54
54
|
"book.squareOffAtExpiry": {"summary":"Square off a leg this many minutes before its contract expires"},
|
|
55
55
|
"book.stop": {"summary":"Square off every leg when the book's profit falls to `-amount`"},
|
|
@@ -332,7 +332,7 @@ export const LIBRARY_PROSE: Readonly<Record<string, LibraryProse>> = {
|
|
|
332
332
|
"str.split": {"summary":"Split into parts"},
|
|
333
333
|
"str.startsWith": {"summary":"Prefix test"},
|
|
334
334
|
"str.substring": {"summary":"`from` inclusive, `to` exclusive, to the end when `to` is absent"},
|
|
335
|
-
"str.trim": {"summary":"Leading and trailing
|
|
335
|
+
"str.trim": {"summary":"Leading and trailing whitespace removed: the code points listed below"},
|
|
336
336
|
"str.upper": {"summary":"Upper case, invariant, not locale dependent"},
|
|
337
337
|
"sum": {"summary":"Total over the window","warmup":"bar `len - 1`"},
|
|
338
338
|
"sumSkip": {"summary":"Total over the window, ignoring absent bars","warmup":"bar `len - 1`"},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The canonical encoding, `compiled-program.md` 2.14.
|
|
2
|
+
* The canonical encoding, `compiled-program.md` 2.14, and the one number writer.
|
|
3
3
|
*
|
|
4
4
|
* It exists so that a hash of a program means something: two compilers, in two
|
|
5
5
|
* languages, handed the same source, produce the same bytes. So none of the
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - Object keys sorted ascending by Unicode code point. Sorted rather than in
|
|
10
10
|
* the specification's listed order, because a sort is a rule an emitter in any
|
|
11
11
|
* language can follow without a table.
|
|
12
|
-
* - A number is the
|
|
13
|
-
*
|
|
12
|
+
* - A number is written by the rule of `language.md` 5.5, which is the same
|
|
13
|
+
* rule for every number that becomes text anywhere in the language.
|
|
14
14
|
* - A string escapes only the quote, the backslash and the code points below
|
|
15
15
|
* 0x20, the last as `\u00XX` except for `\n`, `\r` and `\t`.
|
|
16
16
|
*/
|
|
@@ -18,16 +18,74 @@ import type { CompiledProgram } from './program.js';
|
|
|
18
18
|
import { sha256 } from './sha256.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* A number
|
|
21
|
+
* A number as text: the one writer.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Two engines compare numbers as bits, but they compare text in a case file, an
|
|
24
|
+
* expected column, a table cell and `text(x)`, so how a number becomes text has
|
|
25
|
+
* to be one rule that both implement, and in this repository it has to be one
|
|
26
|
+
* function. This is that function. `scripts/check-number-writer.mjs` reads
|
|
27
|
+
* every file under `src` and refuses a conversion outside this module, so a
|
|
28
|
+
* number cannot reach text by a host's default somewhere else and disagree with
|
|
29
|
+
* this one by a plus sign.
|
|
30
|
+
*
|
|
31
|
+
* **The digits are the host's and the layout is not.** The shortest decimal
|
|
32
|
+
* digit string that reads back as the same binary64 is what every host this
|
|
33
|
+
* engine runs on produces, and what a second engine's host produces too; that
|
|
34
|
+
* part is taken from the host's own shortest form. Where the two hosts part
|
|
35
|
+
* company is the layout, when a value is written positionally and when with an
|
|
36
|
+
* exponent, and how the exponent is spelled, so that half is written here from
|
|
37
|
+
* the rule in `language.md` 5.5 and not left to the host. The rule is the one
|
|
38
|
+
* the first host follows natively, minus the `+` it writes on a positive
|
|
39
|
+
* exponent, and `spec/vectors/number-text.json` holds the boundary cases a
|
|
40
|
+
* second engine checks itself against.
|
|
27
41
|
*/
|
|
28
42
|
export function canonicalNumber(value: number): string {
|
|
29
43
|
if (!Number.isFinite(value)) throw new Error('a compiled program holds finite numbers only');
|
|
30
|
-
|
|
44
|
+
// Zero and negative zero are one value to the language (compiled-program.md
|
|
45
|
+
// 3.1) and one spelling here.
|
|
46
|
+
if (value === 0) return '0';
|
|
47
|
+
const { digits, point } = shortestDigits(Math.abs(value));
|
|
48
|
+
return (value < 0 ? '-' : '') + layout(digits, point);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The shortest round trip digits of a positive finite magnitude, and where the
|
|
53
|
+
* decimal point falls: the value is `0.d1d2...dk` times ten to the `point`.
|
|
54
|
+
*
|
|
55
|
+
* Read out of the host's own shortest form rather than computed here, because
|
|
56
|
+
* the search for the shortest digit string is the one part of the conversion
|
|
57
|
+
* every host already agrees on. Whatever layout the host chose is undone: the
|
|
58
|
+
* digits and the point are all that is kept.
|
|
59
|
+
*/
|
|
60
|
+
function shortestDigits(magnitude: number): { readonly digits: string; readonly point: number } {
|
|
61
|
+
const shown = String(magnitude);
|
|
62
|
+
const e = shown.indexOf('e');
|
|
63
|
+
const mantissa = e < 0 ? shown : shown.slice(0, e);
|
|
64
|
+
const exponent = e < 0 ? 0 : Number(shown.slice(e + 1));
|
|
65
|
+
const dot = mantissa.indexOf('.');
|
|
66
|
+
const whole = dot < 0 ? mantissa : mantissa.slice(0, dot);
|
|
67
|
+
const fraction = dot < 0 ? '' : mantissa.slice(dot + 1);
|
|
68
|
+
let digits = whole + fraction;
|
|
69
|
+
let point = whole.length + exponent;
|
|
70
|
+
// A positional form below one carries leading zeros that are not digits of
|
|
71
|
+
// the value, and a whole number carries trailing zeros that are its layout.
|
|
72
|
+
while (digits.startsWith('0')) {
|
|
73
|
+
digits = digits.slice(1);
|
|
74
|
+
point -= 1;
|
|
75
|
+
}
|
|
76
|
+
while (digits.endsWith('0')) digits = digits.slice(0, -1);
|
|
77
|
+
return { digits, point };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `language.md` 5.5: positional between the two thresholds, an exponent outside them. */
|
|
81
|
+
function layout(digits: string, point: number): string {
|
|
82
|
+
const count = digits.length;
|
|
83
|
+
if (count <= point && point <= 21) return digits + '0'.repeat(point - count);
|
|
84
|
+
if (0 < point && point <= 21) return `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
85
|
+
if (-6 < point && point <= 0) return `0.${'0'.repeat(-point)}${digits}`;
|
|
86
|
+
const exponent = point - 1;
|
|
87
|
+
const lead = count === 1 ? digits : `${digits[0]}.${digits.slice(1)}`;
|
|
88
|
+
return `${lead}e${exponent < 0 ? '-' : ''}${String(Math.abs(exponent))}`;
|
|
31
89
|
}
|
|
32
90
|
|
|
33
91
|
export function canonicalString(value: string): string {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* what a script wants when it wraps an index. Collapsing them would silently
|
|
24
24
|
* change one of the two.
|
|
25
25
|
*/
|
|
26
|
+
import { compareStrings } from './library/index.js';
|
|
26
27
|
import type { Value } from './values/index.js';
|
|
27
28
|
import { ABSENT, numberValue, valuesEqual } from './values/index.js';
|
|
28
29
|
|
|
@@ -99,12 +100,17 @@ function numeric(
|
|
|
99
100
|
* absence is treated two ways, and it is deliberate: an operator that can
|
|
100
101
|
* itself be absent gives a script no way to ask whether a value is absent at
|
|
101
102
|
* all, so equality is the exception and every other comparison is not.
|
|
103
|
+
*
|
|
104
|
+
* **Two strings are ordered by code point** (`language.md` 9.3, `stdlib.md`
|
|
105
|
+
* section 10), through the one order `sort` uses, and not by the host's own
|
|
106
|
+
* operator, which orders by sixteen bit unit and puts a symbol outside the
|
|
107
|
+
* basic plane below the last thousands of the plane. Numbers take the host's
|
|
108
|
+
* operator, which is the binary64 order every engine shares.
|
|
102
109
|
*/
|
|
103
110
|
export function compare(opcode: 'LT' | 'LE' | 'GT' | 'GE', a: Value, b: Value): Value {
|
|
104
111
|
if (a === null || b === null) return ABSENT;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!bothNumbers && !bothStrings) throw new OperandMismatch(opcode);
|
|
112
|
+
if (typeof a === 'string' && typeof b === 'string') return ordered(opcode, compareStrings(a, b));
|
|
113
|
+
if (typeof a !== 'number' || typeof b !== 'number') throw new OperandMismatch(opcode);
|
|
108
114
|
switch (opcode) {
|
|
109
115
|
case 'LT':
|
|
110
116
|
return a < b;
|
|
@@ -117,6 +123,20 @@ export function compare(opcode: 'LT' | 'LE' | 'GT' | 'GE', a: Value, b: Value):
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
/** An ordering comparison read off a three way order: negative, zero or positive. */
|
|
127
|
+
function ordered(opcode: 'LT' | 'LE' | 'GT' | 'GE', order: number): boolean {
|
|
128
|
+
switch (opcode) {
|
|
129
|
+
case 'LT':
|
|
130
|
+
return order < 0;
|
|
131
|
+
case 'LE':
|
|
132
|
+
return order <= 0;
|
|
133
|
+
case 'GT':
|
|
134
|
+
return order > 0;
|
|
135
|
+
default:
|
|
136
|
+
return order >= 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
/** `EQ` and `NE` are total: they always produce a boolean. */
|
|
121
141
|
export function equals(a: Value, b: Value): boolean {
|
|
122
142
|
return valuesEqual(a, b);
|
package/src/core/engine/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
export { Engine } from './engine.js';
|
|
25
25
|
export type { BarResult, RunResult } from './results.js';
|
|
26
26
|
|
|
27
|
-
export { load } from './load.js';
|
|
27
|
+
export { load, loadText } from './load.js';
|
|
28
28
|
export type { LoadOptions, LoadResult } from './load.js';
|
|
29
29
|
|
|
30
30
|
export type { AlertFiring } from './alerts.js';
|
|
@@ -22,6 +22,7 @@ import type { Value } from '../values/index.js';
|
|
|
22
22
|
import { isNumber, reference, valuesEqual } from '../values/index.js';
|
|
23
23
|
import { entry, refAt, stringAt, valueAt, wholeAt } from './binding.js';
|
|
24
24
|
import type { CallContext, ManifestEntry } from './binding.js';
|
|
25
|
+
import { compareStrings } from './code-points.js';
|
|
25
26
|
import { result } from '../../stdlib/index.js';
|
|
26
27
|
|
|
27
28
|
/** The array a handle names, or nothing when the handle is absent or stale. */
|
|
@@ -68,12 +69,17 @@ function within(
|
|
|
68
69
|
return index;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The order `sort` uses, written down so two engines cannot differ.
|
|
74
|
+
*
|
|
75
|
+
* Two strings order by code point (`language.md` 9.3), which is not the order
|
|
76
|
+
* the host's `<` gives once a string holds a symbol outside the basic plane.
|
|
77
|
+
*/
|
|
72
78
|
function rank(a: Value, b: Value): number {
|
|
73
79
|
if (a === null) return b === null ? 0 : 1;
|
|
74
80
|
if (b === null) return -1;
|
|
75
81
|
if (typeof a === 'number' && typeof b === 'number') return a < b ? -1 : a > b ? 1 : 0;
|
|
76
|
-
if (typeof a === 'string' && typeof b === 'string') return a
|
|
82
|
+
if (typeof a === 'string' && typeof b === 'string') return compareStrings(a, b);
|
|
77
83
|
if (typeof a === 'boolean' && typeof b === 'boolean') return a === b ? 0 : a ? 1 : -1;
|
|
78
84
|
return 0;
|
|
79
85
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two string rules two engines have to share, written from the page.
|
|
3
|
+
*
|
|
4
|
+
* **A string is a sequence of code points** (`compiled-program.md` 3.1), and the
|
|
5
|
+
* host this engine is written in stores one as sixteen bit units. Every rule
|
|
6
|
+
* here counts code points and not units, because the two disagree the moment a
|
|
7
|
+
* string holds a symbol outside the basic plane, and a rule written on the
|
|
8
|
+
* host's units would be a rule a second engine could not follow.
|
|
9
|
+
*
|
|
10
|
+
* **Ordering.** Two strings are ordered by comparing code points from the
|
|
11
|
+
* front, the first difference deciding, and a string that runs out first
|
|
12
|
+
* ordering first (`language.md` 9.3, `stdlib.md` section 10). The host's own
|
|
13
|
+
* `<` orders by unit, which puts every symbol outside the basic plane, stored
|
|
14
|
+
* as a surrogate pair from U+D800, below the code points from U+E000 to U+FFFF
|
|
15
|
+
* that come after it, so it is not used.
|
|
16
|
+
*
|
|
17
|
+
* **Whitespace.** `str.trim` removes, and `toNumber` ignores, exactly the code
|
|
18
|
+
* points `stdlib.md` section 10 lists, which are the ones with the Unicode
|
|
19
|
+
* White_Space property. The host's own `trim` removes a set that is nearly this
|
|
20
|
+
* one: it takes the byte order mark U+FEFF as well, and a second engine's host
|
|
21
|
+
* takes the four separators U+001C to U+001F and leaves the byte order mark, so
|
|
22
|
+
* neither host's default is the set, and the set is implemented from the list.
|
|
23
|
+
* `tests/engine/strings.test.ts` walks every code point of the basic plane
|
|
24
|
+
* against the list read out of the page.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The code points `str.trim` removes, as `stdlib.md` section 10 lists them. */
|
|
28
|
+
const WHITESPACE: ReadonlySet<number> = new Set([
|
|
29
|
+
0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x0020, 0x0085, 0x00a0, 0x1680,
|
|
30
|
+
0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a,
|
|
31
|
+
0x2028, 0x2029, 0x202f, 0x205f, 0x3000,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Code points, which is what every index over a string counts. */
|
|
35
|
+
export function points(text: string): string[] {
|
|
36
|
+
return [...text];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether one code point is in the trimmed set. */
|
|
40
|
+
export function isWhitespace(point: number): boolean {
|
|
41
|
+
return WHITESPACE.has(point);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `text` with the whitespace of the written set taken off both ends. */
|
|
45
|
+
export function trimmed(text: string): string {
|
|
46
|
+
const all = points(text);
|
|
47
|
+
let from = 0;
|
|
48
|
+
let to = all.length;
|
|
49
|
+
while (from < to && isWhitespace(all[from]?.codePointAt(0) ?? -1)) from += 1;
|
|
50
|
+
while (to > from && isWhitespace(all[to - 1]?.codePointAt(0) ?? -1)) to -= 1;
|
|
51
|
+
return from === 0 && to === all.length ? text : all.slice(from, to).join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The order of two strings, negative when `a` comes first, by code point.
|
|
56
|
+
*
|
|
57
|
+
* Walked with the string iterator so a surrogate pair is one code point, and
|
|
58
|
+
* compared as numbers so U+10000 comes after U+FFFF, which is the order the
|
|
59
|
+
* page states and the one an engine with four byte strings gets for free.
|
|
60
|
+
*/
|
|
61
|
+
export function compareStrings(a: string, b: string): number {
|
|
62
|
+
const left = a[Symbol.iterator]();
|
|
63
|
+
const right = b[Symbol.iterator]();
|
|
64
|
+
for (;;) {
|
|
65
|
+
const one = left.next();
|
|
66
|
+
const other = right.next();
|
|
67
|
+
if (one.done) return other.done ? 0 : -1;
|
|
68
|
+
if (other.done) return 1;
|
|
69
|
+
const x = one.value.codePointAt(0) ?? 0;
|
|
70
|
+
const y = other.value.codePointAt(0) ?? 0;
|
|
71
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -37,6 +37,12 @@ import { TREND_ENTRIES } from './trend.js';
|
|
|
37
37
|
import { VOLUME_ENTRIES } from './volume.js';
|
|
38
38
|
import type { ManifestEntry } from './binding.js';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The two string rules, for the operators outside this module and the tests:
|
|
42
|
+
* `code-points.ts` says why neither is the host's own.
|
|
43
|
+
*/
|
|
44
|
+
export { compareStrings, isWhitespace, trimmed } from './code-points.js';
|
|
45
|
+
|
|
40
46
|
const ENTRIES: readonly ManifestEntry[] = [
|
|
41
47
|
...COLOUR_ENTRIES,
|
|
42
48
|
...MATHS_ENTRIES,
|