openalgo-script 0.2.0 → 0.4.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 +269 -0
- package/README.md +65 -31
- package/dist/adapters/charts/surfaces.d.ts +17 -0
- package/dist/adapters/charts/surfaces.d.ts.map +1 -1
- package/dist/adapters/charts/tables.js +82 -0
- package/dist/adapters/charts/tables.js.map +1 -1
- package/dist/adapters/codemirror/commands.d.ts +14 -0
- package/dist/adapters/codemirror/commands.d.ts.map +1 -0
- package/dist/adapters/codemirror/commands.js +52 -0
- package/dist/adapters/codemirror/commands.js.map +1 -0
- package/dist/adapters/codemirror/completion.d.ts +15 -0
- package/dist/adapters/codemirror/completion.d.ts.map +1 -0
- package/dist/adapters/codemirror/completion.js +64 -0
- package/dist/adapters/codemirror/completion.js.map +1 -0
- package/dist/adapters/codemirror/contract.d.ts +156 -0
- package/dist/adapters/codemirror/contract.d.ts.map +1 -0
- package/dist/adapters/codemirror/contract.js +42 -0
- package/dist/adapters/codemirror/contract.js.map +1 -0
- package/dist/adapters/codemirror/index.d.ts +43 -0
- package/dist/adapters/codemirror/index.d.ts.map +1 -0
- package/dist/adapters/codemirror/index.js +8 -0
- package/dist/adapters/codemirror/index.js.map +1 -0
- package/dist/adapters/codemirror/lint.d.ts +17 -0
- package/dist/adapters/codemirror/lint.d.ts.map +1 -0
- package/dist/adapters/codemirror/lint.js +62 -0
- package/dist/adapters/codemirror/lint.js.map +1 -0
- package/dist/adapters/codemirror/positions.d.ts +16 -0
- package/dist/adapters/codemirror/positions.d.ts.map +1 -0
- package/dist/adapters/codemirror/positions.js +65 -0
- package/dist/adapters/codemirror/positions.js.map +1 -0
- package/dist/adapters/codemirror/stream.d.ts +23 -0
- package/dist/adapters/codemirror/stream.d.ts.map +1 -0
- package/dist/adapters/codemirror/stream.js +70 -0
- package/dist/adapters/codemirror/stream.js.map +1 -0
- package/dist/adapters/codemirror/tokens.d.ts +44 -0
- package/dist/adapters/codemirror/tokens.d.ts.map +1 -0
- package/dist/adapters/codemirror/tokens.js +34 -0
- package/dist/adapters/codemirror/tokens.js.map +1 -0
- package/dist/adapters/codemirror/tooltips.d.ts +35 -0
- package/dist/adapters/codemirror/tooltips.d.ts.map +1 -0
- package/dist/adapters/codemirror/tooltips.js +108 -0
- package/dist/adapters/codemirror/tooltips.js.map +1 -0
- package/dist/core/accounting/charges.d.ts +136 -0
- package/dist/core/accounting/charges.d.ts.map +1 -0
- package/dist/core/accounting/charges.js +362 -0
- package/dist/core/accounting/charges.js.map +1 -0
- package/dist/core/accounting/equity.d.ts +158 -0
- package/dist/core/accounting/equity.d.ts.map +1 -0
- package/dist/core/accounting/equity.js +155 -0
- package/dist/core/accounting/equity.js.map +1 -0
- package/dist/core/accounting/index.d.ts +44 -0
- package/dist/core/accounting/index.d.ts.map +1 -0
- package/dist/core/accounting/index.js +36 -0
- package/dist/core/accounting/index.js.map +1 -0
- package/dist/core/accounting/markers.d.ts +43 -0
- package/dist/core/accounting/markers.d.ts.map +1 -0
- package/dist/core/accounting/markers.js +64 -0
- package/dist/core/accounting/markers.js.map +1 -0
- package/dist/core/accounting/monthly.d.ts +52 -0
- package/dist/core/accounting/monthly.d.ts.map +1 -0
- package/dist/core/accounting/monthly.js +99 -0
- package/dist/core/accounting/monthly.js.map +1 -0
- package/dist/core/accounting/report.d.ts +38 -0
- package/dist/core/accounting/report.d.ts.map +1 -0
- package/dist/core/accounting/report.js +63 -0
- package/dist/core/accounting/report.js.map +1 -0
- package/dist/core/accounting/shapes.d.ts +70 -0
- package/dist/core/accounting/shapes.d.ts.map +1 -0
- package/dist/core/accounting/shapes.js +21 -0
- package/dist/core/accounting/shapes.js.map +1 -0
- package/dist/core/accounting/statistics.d.ts +75 -0
- package/dist/core/accounting/statistics.d.ts.map +1 -0
- package/dist/core/accounting/statistics.js +246 -0
- package/dist/core/accounting/statistics.js.map +1 -0
- package/dist/core/accounting/trades.d.ts +118 -0
- package/dist/core/accounting/trades.d.ts.map +1 -0
- package/dist/core/accounting/trades.js +186 -0
- package/dist/core/accounting/trades.js.map +1 -0
- package/dist/core/backtest/compare.d.ts +38 -0
- package/dist/core/backtest/compare.d.ts.map +1 -0
- package/dist/core/backtest/compare.js +189 -0
- package/dist/core/backtest/compare.js.map +1 -0
- package/dist/core/backtest/declaration.d.ts +52 -0
- package/dist/core/backtest/declaration.d.ts.map +1 -0
- package/dist/core/backtest/declaration.js +48 -0
- package/dist/core/backtest/declaration.js.map +1 -0
- package/dist/core/backtest/drive.d.ts +29 -0
- package/dist/core/backtest/drive.d.ts.map +1 -0
- package/dist/core/backtest/drive.js +262 -0
- package/dist/core/backtest/drive.js.map +1 -0
- package/dist/core/backtest/index.d.ts +46 -0
- package/dist/core/backtest/index.d.ts.map +1 -0
- package/dist/core/backtest/index.js +36 -0
- package/dist/core/backtest/index.js.map +1 -0
- package/dist/core/backtest/range.d.ts +84 -0
- package/dist/core/backtest/range.d.ts.map +1 -0
- package/dist/core/backtest/range.js +90 -0
- package/dist/core/backtest/range.js.map +1 -0
- package/dist/core/backtest/record.d.ts +238 -0
- package/dist/core/backtest/record.d.ts.map +1 -0
- package/dist/core/backtest/record.js +176 -0
- package/dist/core/backtest/record.js.map +1 -0
- package/dist/core/backtest/replay.d.ts +48 -0
- package/dist/core/backtest/replay.d.ts.map +1 -0
- package/dist/core/backtest/replay.js +127 -0
- package/dist/core/backtest/replay.js.map +1 -0
- package/dist/core/backtest/resting.d.ts +62 -0
- package/dist/core/backtest/resting.d.ts.map +1 -0
- package/dist/core/backtest/resting.js +59 -0
- package/dist/core/backtest/resting.js.map +1 -0
- package/dist/core/backtest/settings.d.ts +117 -0
- package/dist/core/backtest/settings.d.ts.map +1 -0
- package/dist/core/backtest/settings.js +207 -0
- package/dist/core/backtest/settings.js.map +1 -0
- package/dist/core/backtest/simulate.d.ts +146 -0
- package/dist/core/backtest/simulate.d.ts.map +1 -0
- package/dist/core/backtest/simulate.js +217 -0
- package/dist/core/backtest/simulate.js.map +1 -0
- package/dist/core/catalogue/catalogue.generated.d.ts +66 -0
- package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -1
- package/dist/core/catalogue/catalogue.generated.js +6 -0
- package/dist/core/catalogue/catalogue.generated.js.map +1 -1
- package/dist/core/catalogue/values.generated.d.ts +24 -0
- package/dist/core/catalogue/values.generated.d.ts.map +1 -1
- package/dist/core/check/index.d.ts +2 -1
- package/dist/core/check/index.d.ts.map +1 -1
- package/dist/core/check/index.js +1 -1
- package/dist/core/check/index.js.map +1 -1
- package/dist/core/check/library-prose.generated.d.ts +16 -0
- package/dist/core/check/library-prose.generated.d.ts.map +1 -0
- package/dist/core/check/library-prose.generated.js +353 -0
- package/dist/core/check/library-prose.generated.js.map +1 -0
- package/dist/core/check/surface.d.ts +24 -0
- package/dist/core/check/surface.d.ts.map +1 -1
- package/dist/core/check/surface.js +29 -0
- package/dist/core/check/surface.js.map +1 -1
- package/dist/core/emit/defaults.d.ts +35 -16
- package/dist/core/emit/defaults.d.ts.map +1 -1
- package/dist/core/emit/defaults.js +66 -0
- package/dist/core/emit/defaults.js.map +1 -1
- package/dist/core/emit/index.d.ts +2 -0
- package/dist/core/emit/index.d.ts.map +1 -1
- package/dist/core/emit/index.js +2 -0
- package/dist/core/emit/index.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/load.d.ts +15 -1
- package/dist/core/engine/load.d.ts.map +1 -1
- package/dist/core/engine/load.js +1 -0
- package/dist/core/engine/load.js.map +1 -1
- package/dist/core/index.d.ts +37 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +17 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/version/version.generated.d.ts +1 -1
- package/dist/core/version/version.generated.js +1 -1
- package/dist/editor/complete.d.ts +40 -0
- package/dist/editor/complete.d.ts.map +1 -0
- package/dist/editor/complete.js +206 -0
- package/dist/editor/complete.js.map +1 -0
- package/dist/editor/diagnose.d.ts +18 -0
- package/dist/editor/diagnose.d.ts.map +1 -0
- package/dist/editor/diagnose.js +70 -0
- package/dist/editor/diagnose.js.map +1 -0
- package/dist/editor/format.d.ts +11 -0
- package/dist/editor/format.d.ts.map +1 -0
- package/dist/editor/format.js +50 -0
- package/dist/editor/format.js.map +1 -0
- package/dist/editor/highlight.d.ts +28 -0
- package/dist/editor/highlight.d.ts.map +1 -0
- package/dist/editor/highlight.js +65 -0
- package/dist/editor/highlight.js.map +1 -0
- package/dist/editor/hover.d.ts +40 -0
- package/dist/editor/hover.d.ts.map +1 -0
- package/dist/editor/hover.js +147 -0
- package/dist/editor/hover.js.map +1 -0
- package/dist/editor/index.d.ts +60 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +7 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/kinds.d.ts +27 -0
- package/dist/editor/kinds.d.ts.map +1 -0
- package/dist/editor/kinds.js +118 -0
- package/dist/editor/kinds.js.map +1 -0
- package/dist/editor/layout.d.ts +47 -0
- package/dist/editor/layout.d.ts.map +1 -0
- package/dist/editor/layout.js +135 -0
- package/dist/editor/layout.js.map +1 -0
- package/dist/editor/manifest.d.ts +47 -0
- package/dist/editor/manifest.d.ts.map +1 -0
- package/dist/editor/manifest.js +93 -0
- package/dist/editor/manifest.js.map +1 -0
- package/dist/editor/reading.d.ts +41 -0
- package/dist/editor/reading.d.ts.map +1 -0
- package/dist/editor/reading.js +51 -0
- package/dist/editor/reading.js.map +1 -0
- package/dist/editor/scan.d.ts +26 -0
- package/dist/editor/scan.d.ts.map +1 -0
- package/dist/editor/scan.js +139 -0
- package/dist/editor/scan.js.map +1 -0
- package/dist/editor/scope.d.ts +19 -0
- package/dist/editor/scope.d.ts.map +1 -0
- package/dist/editor/scope.js +99 -0
- package/dist/editor/scope.js.map +1 -0
- package/dist/editor/signature.d.ts +38 -0
- package/dist/editor/signature.d.ts.map +1 -0
- package/dist/editor/signature.js +112 -0
- package/dist/editor/signature.js.map +1 -0
- package/dist/editor/site.d.ts +46 -0
- package/dist/editor/site.d.ts.map +1 -0
- package/dist/editor/site.js +184 -0
- package/dist/editor/site.js.map +1 -0
- package/dist/editor/spacing.d.ts +29 -0
- package/dist/editor/spacing.d.ts.map +1 -0
- package/dist/editor/spacing.js +116 -0
- package/dist/editor/spacing.js.map +1 -0
- package/package.json +30 -3
- package/spec/errors.json +140 -0
- package/src/adapters/charts/surfaces.ts +17 -0
- package/src/adapters/charts/tables.ts +88 -0
- package/src/adapters/codemirror/commands.ts +53 -0
- package/src/adapters/codemirror/completion.ts +74 -0
- package/src/adapters/codemirror/contract.ts +156 -0
- package/src/adapters/codemirror/index.ts +66 -0
- package/src/adapters/codemirror/lint.ts +66 -0
- package/src/adapters/codemirror/positions.ts +79 -0
- package/src/adapters/codemirror/stream.ts +87 -0
- package/src/adapters/codemirror/tokens.ts +64 -0
- package/src/adapters/codemirror/tooltips.ts +113 -0
- package/src/core/accounting/charges.ts +452 -0
- package/src/core/accounting/equity.ts +276 -0
- package/src/core/accounting/index.ts +49 -0
- package/src/core/accounting/markers.ts +95 -0
- package/src/core/accounting/monthly.ts +137 -0
- package/src/core/accounting/report.ts +89 -0
- package/src/core/accounting/shapes.ts +73 -0
- package/src/core/accounting/statistics.ts +350 -0
- package/src/core/accounting/trades.ts +313 -0
- package/src/core/backtest/compare.ts +244 -0
- package/src/core/backtest/declaration.ts +97 -0
- package/src/core/backtest/drive.ts +364 -0
- package/src/core/backtest/index.ts +52 -0
- package/src/core/backtest/range.ts +137 -0
- package/src/core/backtest/record.ts +341 -0
- package/src/core/backtest/replay.ts +158 -0
- package/src/core/backtest/resting.ts +125 -0
- package/src/core/backtest/settings.ts +280 -0
- package/src/core/backtest/simulate.ts +304 -0
- package/src/core/catalogue/catalogue.generated.ts +6 -0
- package/src/core/catalogue/values.generated.ts +6 -0
- package/src/core/check/index.ts +3 -0
- package/src/core/check/library-prose.generated.ts +367 -0
- package/src/core/check/surface.ts +34 -0
- package/src/core/emit/defaults.ts +70 -0
- package/src/core/emit/index.ts +2 -0
- package/src/core/engine/index.ts +1 -1
- package/src/core/engine/load.ts +16 -2
- package/src/core/index.ts +85 -1
- package/src/core/version/version.generated.ts +1 -1
- package/src/editor/complete.ts +266 -0
- package/src/editor/diagnose.ts +71 -0
- package/src/editor/format.ts +85 -0
- package/src/editor/highlight.ts +110 -0
- package/src/editor/hover.ts +199 -0
- package/src/editor/index.ts +65 -0
- package/src/editor/kinds.ts +157 -0
- package/src/editor/layout.ts +189 -0
- package/src/editor/manifest.ts +119 -0
- package/src/editor/reading.ts +69 -0
- package/src/editor/scan.ts +162 -0
- package/src/editor/scope.ts +122 -0
- package/src/editor/signature.ts +150 -0
- package/src/editor/site.ts +233 -0
- package/src/editor/spacing.ts +132 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the host chose, which is the half of a run that is not the program.
|
|
3
|
+
*
|
|
4
|
+
* **The dividing rule, and everything in this module follows from it: the
|
|
5
|
+
* record stores what the host chose, and what the program states is stored
|
|
6
|
+
* once, as the program.** So there is no capital here, no slippage and no
|
|
7
|
+
* commission: the declaration states those and the declaration is in the
|
|
8
|
+
* record. A charge schedule the host supplied is here because the host supplied
|
|
9
|
+
* it; a schedule derived from the declaration is derived again on replay.
|
|
10
|
+
*
|
|
11
|
+
* **The fill policy carries its own version** precisely so that a record made
|
|
12
|
+
* before the policy changed replays as it ran, rather than being quietly re-run
|
|
13
|
+
* under today's rules and reported as the same study. A policy that could not
|
|
14
|
+
* say which revision it was would make every stored run a claim about whichever
|
|
15
|
+
* engine happened to read it last.
|
|
16
|
+
*
|
|
17
|
+
* `fillOn` is deliberately not here: it is the declaration's, and the
|
|
18
|
+
* declaration is the program.
|
|
19
|
+
*
|
|
20
|
+
* Two refusals live in this module when the behaviour lands. A setting the run
|
|
21
|
+
* cannot be carried out under is OS6021, and a supplied schedule beside a
|
|
22
|
+
* declared commission that is not the default is OS6023, because two cost
|
|
23
|
+
* models stated at once is a number nobody can explain afterwards.
|
|
24
|
+
*/
|
|
25
|
+
import { scheduleFromDeclaration, scheduleProblem } from '../accounting/index.js';
|
|
26
|
+
import type { ChargeSchedule, Contract } from '../accounting/index.js';
|
|
27
|
+
import { diagnosticFor } from '../diagnostics/index.js';
|
|
28
|
+
import type { Diagnostic } from '../diagnostics/index.js';
|
|
29
|
+
import type { Value } from '../engine/index.js';
|
|
30
|
+
import type { RunDeclaration } from './declaration.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Where a refusal about a setting points.
|
|
34
|
+
*
|
|
35
|
+
* Nowhere in the script. A run setting is what the host stated before the first
|
|
36
|
+
* bar, and a caret under a line of the strategy would blame the one party that
|
|
37
|
+
* did not choose it. The money layer states the same position for the same
|
|
38
|
+
* reason.
|
|
39
|
+
*/
|
|
40
|
+
const NO_POSITION: Diagnostic['span'] = { offset: 0, length: 0, line: 0, column: 0 };
|
|
41
|
+
|
|
42
|
+
/** The window of the supplied bars a report is about. */
|
|
43
|
+
export interface DateRange {
|
|
44
|
+
/** Inclusive, UTC ms; null means the first bar supplied. */
|
|
45
|
+
readonly from: number | null;
|
|
46
|
+
/** Inclusive; null means the last. */
|
|
47
|
+
readonly to: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** How a resting order is decided against a bar, and who holds a level. */
|
|
51
|
+
export interface FillPolicy {
|
|
52
|
+
/** A limit fills only when the bar traded through it. */
|
|
53
|
+
readonly limitNeedsThrough: boolean;
|
|
54
|
+
readonly stopFillsAtOpenOnGap: boolean;
|
|
55
|
+
/** Who holds a bracket's levels; 'destination' until 17.9 exists. */
|
|
56
|
+
readonly levels: 'destination' | 'engine';
|
|
57
|
+
/** This policy's own revision, so an old record replays as it ran. */
|
|
58
|
+
readonly version: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How closely two numbers have to agree, and why they are allowed not to.
|
|
63
|
+
*
|
|
64
|
+
* Exact by default. A bound that is not zero without a reason beside it is
|
|
65
|
+
* refused, because a tolerance with no reason is a failed comparison somebody
|
|
66
|
+
* turned off. The reason a tolerance exists at all is a second implementation,
|
|
67
|
+
* never this one's own re-run: a rerun of the same record on this engine is
|
|
68
|
+
* bit-identical or it is a defect.
|
|
69
|
+
*/
|
|
70
|
+
export interface Tolerance {
|
|
71
|
+
readonly abs: number;
|
|
72
|
+
readonly rel: number;
|
|
73
|
+
/** Required whenever either bound is non-zero. */
|
|
74
|
+
readonly reason: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Everything the host decided about one run. */
|
|
78
|
+
export interface BacktestSettings {
|
|
79
|
+
readonly range: DateRange;
|
|
80
|
+
readonly contract: Contract;
|
|
81
|
+
/** Null derives one from the declaration. */
|
|
82
|
+
readonly costs: ChargeSchedule | null;
|
|
83
|
+
readonly fill: FillPolicy;
|
|
84
|
+
readonly inputs: Readonly<Record<string, Value>>;
|
|
85
|
+
readonly now: number | null;
|
|
86
|
+
readonly tolerance: Tolerance;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* How a resting order is decided when the host states nothing.
|
|
91
|
+
*
|
|
92
|
+
* Conservative on both counts, because a backtest that is wrong is wrong in the
|
|
93
|
+
* strategy's favour by default: a limit is only filled where the bar traded
|
|
94
|
+
* through it, so an order resting exactly at the extreme of a bar is not
|
|
95
|
+
* credited with a fill nobody can prove happened, and a stop that gapped is
|
|
96
|
+
* filled at the open rather than at its trigger, which is the price a trader
|
|
97
|
+
* would actually have been given.
|
|
98
|
+
*
|
|
99
|
+
* `levels` is `destination` because there is nowhere else for it to be: a
|
|
100
|
+
* bracket reaches a destination as a protective instruction attached to a tag
|
|
101
|
+
* and the engine holds no level of its own. The other spelling exists so that a
|
|
102
|
+
* record made today says which of the two it ran under.
|
|
103
|
+
*
|
|
104
|
+
* `version` is this policy's own revision. It travels in the record so that a
|
|
105
|
+
* run stored before the rules changed replays as it ran rather than being
|
|
106
|
+
* quietly re-decided under today's.
|
|
107
|
+
*/
|
|
108
|
+
export const DEFAULT_FILL: FillPolicy = {
|
|
109
|
+
limitNeedsThrough: true,
|
|
110
|
+
stopFillsAtOpenOnGap: true,
|
|
111
|
+
levels: 'destination',
|
|
112
|
+
version: 1,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Exact, which is what a comparison is until somebody writes down why it is not. */
|
|
116
|
+
export const EXACT: Tolerance = { abs: 0, rel: 0, reason: null };
|
|
117
|
+
|
|
118
|
+
/** The whole window of whatever bars were supplied. */
|
|
119
|
+
export const WHOLE_RANGE: DateRange = { from: null, to: null };
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The settings a run takes when the host states only the contract.
|
|
123
|
+
*
|
|
124
|
+
* Every default here is the absence of a choice rather than a choice made on
|
|
125
|
+
* the host's behalf: the whole of the bars supplied, no schedule, exact
|
|
126
|
+
* comparison, no inputs overridden and no clock. A host that wants any of it
|
|
127
|
+
* different says so, and what it said is what the record stores.
|
|
128
|
+
*
|
|
129
|
+
* **The contract is the first argument and cannot be in the second.** The
|
|
130
|
+
* second was `Partial<BacktestSettings>`, which names a `contract` field that
|
|
131
|
+
* this function then ignored in favour of the positional one: a caller passing
|
|
132
|
+
* a contract there silently got the other, and a test written that way passed
|
|
133
|
+
* while proving nothing. Excluding the field makes it a compiler error at the
|
|
134
|
+
* call rather than a wrong answer at the end of a run.
|
|
135
|
+
*/
|
|
136
|
+
export function settingsFor(
|
|
137
|
+
contract: Contract,
|
|
138
|
+
chosen: Omit<Partial<BacktestSettings>, 'contract'> = {},
|
|
139
|
+
): BacktestSettings {
|
|
140
|
+
return {
|
|
141
|
+
range: chosen.range ?? WHOLE_RANGE,
|
|
142
|
+
contract,
|
|
143
|
+
costs: chosen.costs ?? null,
|
|
144
|
+
fill: chosen.fill ?? DEFAULT_FILL,
|
|
145
|
+
inputs: chosen.inputs ?? {},
|
|
146
|
+
now: chosen.now ?? null,
|
|
147
|
+
tolerance: chosen.tolerance ?? EXACT,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* What a run cannot be carried out under, before its first bar.
|
|
153
|
+
*
|
|
154
|
+
* Four questions, and every one of them is a figure nobody could explain
|
|
155
|
+
* afterwards rather than a tidiness rule:
|
|
156
|
+
*
|
|
157
|
+
* - **Two cost models at once**, OS6023. A supplied schedule and a declared
|
|
158
|
+
* commission describe the same money. Applied together they charge it twice
|
|
159
|
+
* and applied one at a time they charge whichever an engine preferred, which
|
|
160
|
+
* is a rule nobody wrote down.
|
|
161
|
+
* - **A schedule that cannot be evaluated**, OS6021, which `scheduleProblem`
|
|
162
|
+
* decides, because the schedule is the money layer's and the rule for it is
|
|
163
|
+
* written once, there. **Whichever schedule the run will be charged under**,
|
|
164
|
+
* which is the declaration's own when the host supplied none: asking only
|
|
165
|
+
* about a supplied one left every refusal in the money layer unreachable on
|
|
166
|
+
* the path almost every run takes.
|
|
167
|
+
* - **A quantity in a unit this destination cannot fill**, OS6021. A backtest
|
|
168
|
+
* fills in units and works out no running equity, so a quantity in cash or in
|
|
169
|
+
* a percentage of equity is one it cannot convert, and a lot needs a lot size
|
|
170
|
+
* the instrument may not state.
|
|
171
|
+
* - **A tolerance with a bound and no reason**, OS6021. A comparison allowed to
|
|
172
|
+
* pass by a margin nobody justified is a failed comparison somebody switched
|
|
173
|
+
* off, and the reason a tolerance exists at all is a second implementation:
|
|
174
|
+
* a rerun of one record on this engine is bit-identical or it is a defect.
|
|
175
|
+
*
|
|
176
|
+
* Nothing has been computed when this is asked, so a refusal costs one run
|
|
177
|
+
* rather than a report a reader has to be told to distrust.
|
|
178
|
+
*/
|
|
179
|
+
export function checkSettings(
|
|
180
|
+
settings: BacktestSettings,
|
|
181
|
+
declared: RunDeclaration,
|
|
182
|
+
): Diagnostic | null {
|
|
183
|
+
if (settings.costs !== null && declared.isStrategy && declared.commission !== 0) {
|
|
184
|
+
return diagnosticFor('OS6023', NO_POSITION, {
|
|
185
|
+
commission: declared.commission,
|
|
186
|
+
commissionType: declared.commissionType,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Whichever schedule the run will actually be charged under, which is the
|
|
191
|
+
// declaration's own when the host supplied none. Guarding this on
|
|
192
|
+
// `settings.costs !== null` left every refusal in the money layer unreachable
|
|
193
|
+
// on the default path: a declared commission of -5 was charged as a credit
|
|
194
|
+
// and turned a loss into a gain, with nothing raised anywhere.
|
|
195
|
+
const schedule = settings.costs ?? scheduleForDeclaration(declared, settings.contract);
|
|
196
|
+
if (schedule !== null) {
|
|
197
|
+
const problem = scheduleProblem(schedule, settings.contract);
|
|
198
|
+
if (problem !== null) return problem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sizing = sizingProblem(declared, settings.contract);
|
|
202
|
+
if (sizing !== null) return sizing;
|
|
203
|
+
|
|
204
|
+
return toleranceProblem(settings.tolerance);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The schedule a run with no host schedule is charged under, or none. */
|
|
208
|
+
function scheduleForDeclaration(
|
|
209
|
+
declared: RunDeclaration,
|
|
210
|
+
contract: Contract,
|
|
211
|
+
): ChargeSchedule | null {
|
|
212
|
+
if (!declared.isStrategy) return null;
|
|
213
|
+
return scheduleFromDeclaration(
|
|
214
|
+
declared.commission,
|
|
215
|
+
declared.commissionType,
|
|
216
|
+
declared.slippage,
|
|
217
|
+
contract.currency,
|
|
218
|
+
contract.digits,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Whether this destination can fill the unit the strategy sizes in.
|
|
224
|
+
*
|
|
225
|
+
* **A quantity is stated in the declaration's own unit and the destination is
|
|
226
|
+
* the party that converts it** (`host-interface.md` 7.1). This one filled every
|
|
227
|
+
* order at the number the script wrote, whatever unit it was written in, so a
|
|
228
|
+
* strategy sizing in lots on a lot of sixty five traded one sixty fifth of what
|
|
229
|
+
* it asked for and every money figure in the record was out by that factor,
|
|
230
|
+
* with nothing refused and nothing said. A record like that is worse than a
|
|
231
|
+
* refused run twice over: somebody trades on the number, and a second engine is
|
|
232
|
+
* handed it as a conformance case and taught the wrong quantity.
|
|
233
|
+
*
|
|
234
|
+
* So: units and lots are converted, and the two that need a running equity this
|
|
235
|
+
* destination does not hold are refused by name. A refusal costs one run. The
|
|
236
|
+
* alternative cost a whole report that looked right.
|
|
237
|
+
*/
|
|
238
|
+
function sizingProblem(declared: RunDeclaration, contract: Contract): Diagnostic | null {
|
|
239
|
+
if (!declared.isStrategy) return null;
|
|
240
|
+
if (declared.qtyType === 'units' || declared.qtyType === '') return null;
|
|
241
|
+
if (declared.qtyType === 'lots') {
|
|
242
|
+
if (contract.lotSize !== null && contract.lotSize > 0) return null;
|
|
243
|
+
return diagnosticFor('OS6021', NO_POSITION, {
|
|
244
|
+
setting: 'A quantity stated in lots',
|
|
245
|
+
problem:
|
|
246
|
+
'this instrument states no lot size, so there is nothing to convert a lot into',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return diagnosticFor('OS6021', NO_POSITION, {
|
|
250
|
+
setting: 'A quantity stated in ' + declared.qtyType,
|
|
251
|
+
problem:
|
|
252
|
+
'a backtest fills in units and works out no running equity to size against, so it ' +
|
|
253
|
+
'cannot convert one. State the quantity in units or in lots',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A bound, and the reason it is there.
|
|
259
|
+
*
|
|
260
|
+
* Both bounds are checked rather than the first one found, because a tolerance
|
|
261
|
+
* carrying two bounds and one reason is two allowances and one justification.
|
|
262
|
+
*/
|
|
263
|
+
function toleranceProblem(tolerance: Tolerance): Diagnostic | null {
|
|
264
|
+
const stated = tolerance.reason !== null && tolerance.reason.trim() !== '';
|
|
265
|
+
const bounded = tolerance.abs !== 0 || tolerance.rel !== 0;
|
|
266
|
+
if (bounded && !stated) {
|
|
267
|
+
return diagnosticFor('OS6021', NO_POSITION, {
|
|
268
|
+
setting: 'The comparison tolerance',
|
|
269
|
+
problem: 'a bound of ' + String(tolerance.abs) + ' absolute and ' + String(tolerance.rel) +
|
|
270
|
+
' relative is stated with no reason beside it',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (tolerance.abs < 0 || tolerance.rel < 0) {
|
|
274
|
+
return diagnosticFor('OS6021', NO_POSITION, {
|
|
275
|
+
setting: 'The comparison tolerance',
|
|
276
|
+
problem: 'a bound below zero admits nothing and refuses what is exact',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The destination a backtest runs against: intents in, frames out.
|
|
3
|
+
*
|
|
4
|
+
* **This is a host, not part of the engine**, and the division is the one thing
|
|
5
|
+
* to keep straight while reading it. `stdlib.md` 17.1 puts slippage and
|
|
6
|
+
* commission on the destination: the engine folds the price it is told and
|
|
7
|
+
* never adjusts one, so the price worsening below is a venue giving a worse
|
|
8
|
+
* fill, which is what a venue does, and not an engine moving a number, which is
|
|
9
|
+
* what the invariant forbids. Everything here could be a broker.
|
|
10
|
+
*
|
|
11
|
+
* **A frame is cumulative and reaches the engine between bars.** Every frame
|
|
12
|
+
* restates the whole life of one order from its beginning, so a repeat costs
|
|
13
|
+
* nothing, and none of them is handed over during an execution: they are
|
|
14
|
+
* answered here and collected at the boundary, which is where
|
|
15
|
+
* `host-interface.md` 7.4 puts the intake.
|
|
16
|
+
*
|
|
17
|
+
* **When a fill is known is not when it happened.** A market order decided at
|
|
18
|
+
* the close of a bar and filled at the next bar's open is known at that open,
|
|
19
|
+
* so its frame is delivered before that bar executes and the strategy holds the
|
|
20
|
+
* position for the whole of it. An order that rested and traded somewhere
|
|
21
|
+
* inside a bar is only known to have traded once that bar is complete, so its
|
|
22
|
+
* frame is delivered before the bar after it. Both are the truthful reading of
|
|
23
|
+
* what a venue could have told anybody at the time, and the difference between
|
|
24
|
+
* them is why they are not folded together.
|
|
25
|
+
*
|
|
26
|
+
* **A bracket is answered with nothing at all**, exactly as a venue with no
|
|
27
|
+
* order to report would answer. A bracket reaches a destination as a protective
|
|
28
|
+
* instruction attached to a tag and the engine appends no row for it, so there
|
|
29
|
+
* is nothing for a frame to be about: a fill reported against one would name an
|
|
30
|
+
* intent no row holds and the fold would refuse it. The row a bracket wants is
|
|
31
|
+
* decision 55 and it is not in this release, so a stop cannot fill here and a
|
|
32
|
+
* page that says it can is ahead of the engine.
|
|
33
|
+
*/
|
|
34
|
+
import type { Contract } from '../accounting/index.js';
|
|
35
|
+
import type { OrderFrame, OrderIntent, OrderSide, RoutedEffect } from '../engine/index.js';
|
|
36
|
+
import type { RecordedBar } from './record.js';
|
|
37
|
+
import { testResting } from './resting.js';
|
|
38
|
+
import type { RestingOrder } from './resting.js';
|
|
39
|
+
import type { FillPolicy } from './settings.js';
|
|
40
|
+
|
|
41
|
+
/** What the venue prices against and how it decides a fill. */
|
|
42
|
+
export interface SimulatorOptions {
|
|
43
|
+
readonly bars: readonly RecordedBar[];
|
|
44
|
+
readonly contract: Contract;
|
|
45
|
+
readonly fill: FillPolicy;
|
|
46
|
+
/** Adverse always, in ticks, applied to a market fill and to a stop. */
|
|
47
|
+
readonly slippageTicks: number;
|
|
48
|
+
/** The declaration's own fill rule: where a market order is priced. */
|
|
49
|
+
readonly fillOn: string;
|
|
50
|
+
/**
|
|
51
|
+
* The unit the declaration's quantity is stated in, `language.md` 13.3.
|
|
52
|
+
*
|
|
53
|
+
* Here because converting it is the destination's job and a fill is counted
|
|
54
|
+
* in units. Only the units this destination can arrive at reach it: the rest
|
|
55
|
+
* are refused before the first bar by `checkSettings`.
|
|
56
|
+
*/
|
|
57
|
+
readonly qtyType: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One order this venue holds, and what it has said about it. */
|
|
61
|
+
interface Order {
|
|
62
|
+
readonly intent: OrderIntent;
|
|
63
|
+
readonly ref: string;
|
|
64
|
+
readonly placedOn: number;
|
|
65
|
+
/** Absent on a market order, which rests on nothing. */
|
|
66
|
+
readonly rest: RestingOrder | null;
|
|
67
|
+
filledQty: number;
|
|
68
|
+
live: boolean;
|
|
69
|
+
/** Whether a stop limit has reached its trigger and is now a limit. */
|
|
70
|
+
triggered: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `language.md` 13.3: a market order priced at the close of its own bar. */
|
|
74
|
+
const AT_CLOSE = 'close';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A venue, for one run.
|
|
78
|
+
*
|
|
79
|
+
* It holds the orders it was handed and nothing else: no position, no money and
|
|
80
|
+
* no view of what the strategy is doing, because a destination has none of
|
|
81
|
+
* those about somebody else's strategy.
|
|
82
|
+
*/
|
|
83
|
+
export class Simulator {
|
|
84
|
+
private readonly options: SimulatorOptions;
|
|
85
|
+
private readonly orders: Order[] = [];
|
|
86
|
+
/** The intents handed over, in the order they were, whatever became of them. */
|
|
87
|
+
readonly intents: OrderIntent[] = [];
|
|
88
|
+
/** Brackets, held so that a run can say how many it was handed and answered. */
|
|
89
|
+
readonly brackets: OrderIntent[] = [];
|
|
90
|
+
private queued: Order[] = [];
|
|
91
|
+
private answered: OrderFrame[] = [];
|
|
92
|
+
private refs = 0;
|
|
93
|
+
private seq = 0;
|
|
94
|
+
|
|
95
|
+
constructor(options: SimulatorOptions) {
|
|
96
|
+
this.options = options;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Step 9: what the strategy decided reaches the venue. */
|
|
100
|
+
route(effect: RoutedEffect, barIndex: number): void {
|
|
101
|
+
for (const intent of effect.intents) {
|
|
102
|
+
this.intents.push(intent);
|
|
103
|
+
if (intent.kind === 'bracket') {
|
|
104
|
+
this.brackets.push(intent);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (intent.kind === 'cancel') {
|
|
108
|
+
this.withdraw(intent, barIndex);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
this.accept(intent, barIndex);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The frames a boundary hands over: everything this venue can say now.
|
|
117
|
+
*
|
|
118
|
+
* The orders that were already resting are decided against this bar first and
|
|
119
|
+
* the orders this bar sent are answered after, which is the order the venue
|
|
120
|
+
* learned of them in.
|
|
121
|
+
*/
|
|
122
|
+
framesFor(barIndex: number): readonly OrderFrame[] {
|
|
123
|
+
const bar = this.options.bars[barIndex];
|
|
124
|
+
if (bar !== undefined) {
|
|
125
|
+
for (const order of this.orders) {
|
|
126
|
+
if (!order.live || order.rest === null || order.placedOn >= barIndex) continue;
|
|
127
|
+
this.decide(order, bar, barIndex);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sent = this.queued;
|
|
132
|
+
this.queued = [];
|
|
133
|
+
for (const order of sent) this.open(order, barIndex);
|
|
134
|
+
|
|
135
|
+
const out = this.answered;
|
|
136
|
+
this.answered = [];
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** How many orders this venue is still holding, which a run reports. */
|
|
141
|
+
get working(): number {
|
|
142
|
+
return this.orders.filter((order) => order.live).length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** An order the strategy sent, which this venue now holds. */
|
|
146
|
+
private accept(intent: OrderIntent, barIndex: number): void {
|
|
147
|
+
this.refs += 1;
|
|
148
|
+
const order: Order = {
|
|
149
|
+
intent,
|
|
150
|
+
ref: refOf(this.refs),
|
|
151
|
+
placedOn: barIndex,
|
|
152
|
+
rest: restingFor(intent),
|
|
153
|
+
filledQty: 0,
|
|
154
|
+
live: true,
|
|
155
|
+
triggered: false,
|
|
156
|
+
};
|
|
157
|
+
this.orders.push(order);
|
|
158
|
+
this.queued.push(order);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A cancellation, which is about the orders a tag names and not about itself.
|
|
163
|
+
*
|
|
164
|
+
* Every live order carrying the tag is withdrawn and answered, which is what
|
|
165
|
+
* releases the position they were claiming, and the cancellation is then
|
|
166
|
+
* answered as an order of its own, because the engine appended a row for it
|
|
167
|
+
* and a row nothing ever answers stays at `placed` for the life of the run.
|
|
168
|
+
*/
|
|
169
|
+
private withdraw(intent: OrderIntent, barIndex: number): void {
|
|
170
|
+
for (const order of this.orders) {
|
|
171
|
+
if (!order.live || order.intent.tag !== intent.tag) continue;
|
|
172
|
+
order.live = false;
|
|
173
|
+
this.say(order.intent, order.ref, 'cancelled', order.filledQty, null, barIndex);
|
|
174
|
+
}
|
|
175
|
+
// The cancellation is confirmed after what it withdrew, which is the order
|
|
176
|
+
// a venue does it in: the request is answered once it has been carried out.
|
|
177
|
+
this.refs += 1;
|
|
178
|
+
this.say(intent, refOf(this.refs), 'cancelled', 0, null, barIndex);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The first thing a venue says about an order it has taken.
|
|
183
|
+
*
|
|
184
|
+
* A working frame before any fill, because 7.4 asks for a frame on every
|
|
185
|
+
* change of status and a script waiting for a working order to clear waits
|
|
186
|
+
* blind without one. A market order is then filled in the same breath where
|
|
187
|
+
* the declaration prices it at this bar's close, and at the next bar's open
|
|
188
|
+
* where it does not.
|
|
189
|
+
*/
|
|
190
|
+
private open(order: Order, barIndex: number): void {
|
|
191
|
+
this.say(order.intent, order.ref, 'working', 0, null, barIndex);
|
|
192
|
+
if (order.rest !== null) return;
|
|
193
|
+
|
|
194
|
+
const atClose = this.options.fillOn === AT_CLOSE;
|
|
195
|
+
const bar = this.options.bars[atClose ? barIndex : barIndex + 1];
|
|
196
|
+
const price = atClose ? (bar?.close ?? null) : (bar?.open ?? null);
|
|
197
|
+
// A market order sent on the last bar of a run and priced at the next open
|
|
198
|
+
// has no next open. It stays working and is reported as an order the run
|
|
199
|
+
// ended holding, which is what it is.
|
|
200
|
+
if (price === null) return;
|
|
201
|
+
this.complete(order, this.worsen(price, order.intent.side), barIndex);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** One resting order against one bar. */
|
|
205
|
+
private decide(order: Order, bar: RecordedBar, barIndex: number): void {
|
|
206
|
+
const rest = order.rest;
|
|
207
|
+
if (rest === null) return;
|
|
208
|
+
const outcome = testResting(order.triggered ? asLimit(rest) : rest, bar, this.options.fill);
|
|
209
|
+
if (!outcome.filled) {
|
|
210
|
+
if (outcome.triggered) order.triggered = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const price = outcome.slips ? this.worsen(outcome.price, order.intent.side) : outcome.price;
|
|
214
|
+
this.complete(order, price, barIndex);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The whole of an order, filled at one price.
|
|
219
|
+
*
|
|
220
|
+
* **The quantity is converted into units here, because that is what a fill
|
|
221
|
+
* is counted in and what every money figure is folded over.** A quantity
|
|
222
|
+
* travels in the declaration's own unit (`host-interface.md` 7.1) and the
|
|
223
|
+
* destination is the party that converts it. This filled the number verbatim,
|
|
224
|
+
* so a strategy sizing in lots on a lot of sixty five traded one sixty fifth
|
|
225
|
+
* of what it asked for and the whole report was out by that factor. The units
|
|
226
|
+
* this destination cannot arrive at are refused before the first bar rather
|
|
227
|
+
* than guessed at here, so by this point the unit is one of two.
|
|
228
|
+
*/
|
|
229
|
+
private complete(order: Order, price: number, barIndex: number): void {
|
|
230
|
+
const stated = order.intent.qty ?? 0;
|
|
231
|
+
const lot = this.options.contract.lotSize;
|
|
232
|
+
const qty =
|
|
233
|
+
this.options.qtyType === 'lots' && lot !== null && lot > 0 ? stated * lot : stated;
|
|
234
|
+
order.filledQty = qty;
|
|
235
|
+
order.live = false;
|
|
236
|
+
this.say(order.intent, order.ref, 'filled', qty, price, barIndex);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A price worsened by the slippage the run was carried out under.
|
|
241
|
+
*
|
|
242
|
+
* **Adverse always: a buy pays more and a sell receives less.** The sign is
|
|
243
|
+
* taken as written rather than as a magnitude, because a slippage below zero
|
|
244
|
+
* is refused before the first bar and taking its magnitude here would be this
|
|
245
|
+
* function quietly covering for a hole in that refusal. It went uncovered for
|
|
246
|
+
* a while: `scheduleProblem` has always refused a negative rate, and
|
|
247
|
+
* `checkSettings` asked it only about a schedule the host supplied, so a
|
|
248
|
+
* declaration stating a slippage of minus one improved both sides of every
|
|
249
|
+
* fill and the backtest paid the strategy to trade.
|
|
250
|
+
*
|
|
251
|
+
* A slippage in ticks with no tick size to measure a tick in charges nothing.
|
|
252
|
+
* The comment here once said that was refused before the first bar and it was
|
|
253
|
+
* not; it still is not, because an instrument with no tick is a fact a host
|
|
254
|
+
* may legitimately not hold, and a run that charges no slippage is a study
|
|
255
|
+
* before slippage rather than a wrong answer. What has changed is that this
|
|
256
|
+
* says so instead of claiming a refusal that never existed.
|
|
257
|
+
*/
|
|
258
|
+
private worsen(price: number, side: OrderSide | null): number {
|
|
259
|
+
const tick = this.options.contract.tickSize;
|
|
260
|
+
if (tick === null || this.options.slippageTicks === 0) return price;
|
|
261
|
+
const move = this.options.slippageTicks * tick;
|
|
262
|
+
return side === 'sell' ? price - move : price + move;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** One frame, cumulative, in the order this venue spoke. */
|
|
266
|
+
private say(
|
|
267
|
+
intent: OrderIntent,
|
|
268
|
+
ref: string,
|
|
269
|
+
status: string,
|
|
270
|
+
filledQty: number,
|
|
271
|
+
avgFillPrice: number | null,
|
|
272
|
+
barIndex: number,
|
|
273
|
+
): void {
|
|
274
|
+
this.seq += 1;
|
|
275
|
+
this.answered.push({
|
|
276
|
+
intentId: intent.intentId,
|
|
277
|
+
status,
|
|
278
|
+
filledQty,
|
|
279
|
+
avgFillPrice,
|
|
280
|
+
orderRef: ref,
|
|
281
|
+
sentInstrument: intent.instrument,
|
|
282
|
+
sentProduct: intent.product,
|
|
283
|
+
time: this.options.bars[barIndex]?.time ?? null,
|
|
284
|
+
text: '',
|
|
285
|
+
seq: this.seq,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** This venue's own reference for an order, which the engine records and never parses. */
|
|
291
|
+
function refOf(count: number): string {
|
|
292
|
+
return 'ORD-' + String(count);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The resting order an intent is, or nothing where it is a market order. */
|
|
296
|
+
function restingFor(intent: OrderIntent): RestingOrder | null {
|
|
297
|
+
if (intent.type === null || intent.type === 'market' || intent.side === null) return null;
|
|
298
|
+
return { side: intent.side, type: intent.type, limit: intent.limit, trigger: intent.trigger };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A stop limit that has triggered, which is a limit from that moment on. */
|
|
302
|
+
function asLimit(rest: RestingOrder): RestingOrder {
|
|
303
|
+
return { side: rest.side, type: 'limit', limit: rest.limit, trigger: null };
|
|
304
|
+
}
|
|
@@ -137,6 +137,10 @@ export const ENTRIES = {
|
|
|
137
137
|
OS6017: { code: "OS6017", title: "The program's language version is not one this engine implements", severity: "error", stage: "host", since: 1, autofix: false, message: "This program was compiled from language version {found}, and this engine implements {versions}.", fix: "Upgrade the engine to one that implements language version {found}, or recompile the source against a version it has.", placeholders: ["found", "versions"] },
|
|
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
|
+
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, or write down the reason a tolerance needs a bound. Nothing has been computed at the point this is refused, so correcting the setting and running again costs one run.", placeholders: ["setting", "problem"] },
|
|
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
|
+
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"] },
|
|
140
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"] },
|
|
141
145
|
OS7002: { code: "OS7002", title: "An order argument is absent", severity: "error", stage: "runtime", since: 1, autofix: false, message: "{name}'s {argument} is absent on this bar.", fix: "Guard the call with isNone({argument}), or supply a fallback with orElse() where one is genuinely correct.", placeholders: ["name", "argument"] },
|
|
142
146
|
OS7003: { code: "OS7003", title: "An order function inside a request expression", severity: "error", stage: "check", since: 1, autofix: false, message: "{name} inside a request expression would place an order from another instrument's bars.", fix: "Read the value with the request, and place the order at the top level from the result.", placeholders: ["name"] },
|
|
@@ -154,6 +158,8 @@ export const ENTRIES = {
|
|
|
154
158
|
OS7015: { code: "OS7015", title: "The strategy has no order destination", severity: "error", stage: "host", since: 1, autofix: false, message: "This strategy placed an order and the host supplied no destination.", fix: "Connect a destination in the host, or run the file as a study(): replace buy() with signal(\"BUY\").", placeholders: [] },
|
|
155
159
|
OS7016: { code: "OS7016", title: "A close names a tag nothing places", severity: "error", stage: "check", since: 1, autofix: false, message: "No order in this file is placed with the tag {tag}.", fix: "Use the tag the entry was placed with, or leave the tag out to flatten the whole leg.", placeholders: ["tag"] },
|
|
156
160
|
OS7017: { code: "OS7017", title: "A close states more than it is closing", severity: "error", stage: "runtime", since: 1, autofix: false, message: "close was given a quantity of {qty}, and {part} has {held} left to close.", fix: "Leave the quantity out and close() flattens what is left, or size the part from pos.size and keep the quantity at or under {held}.", placeholders: ["qty", "part", "held"] },
|
|
161
|
+
OS7018: { code: "OS7018", title: "A frame names an order this strategy did not place", severity: "error", stage: "host", since: 1, autofix: false, message: "The frame names intent {intent}, and this strategy holds no such order.", fix: "Answer with the intent id the engine sent. A destination's own reference is carried in the frame's reference field, where the engine records it and never parses it, and it is not what an answer is addressed by.", placeholders: ["intent"] },
|
|
162
|
+
OS7019: { code: "OS7019", title: "A fill was reported with no price", severity: "error", stage: "host", since: 1, autofix: false, message: "The frame reports {qty} filled for intent {intent}, and no average fill price.", fix: "Report the average fill price the destination computed over the cumulative quantity, on every frame that reports a quantity greater than the last one. A frame carrying no new quantity needs no price.", placeholders: ["qty", "intent"] },
|
|
157
163
|
OS8001: { code: "OS8001", title: "A stateful call inside a branch", severity: "warning", stage: "check", since: 1, autofix: false, message: "{name} advances only on the bars where this branch runs, and is absent on the rest.", fix: "Compute it unconditionally at the top level and use the result inside the branch.", placeholders: ["name"] },
|
|
158
164
|
OS8002: { code: "OS8002", title: "A higher timeframe read with onUnconfirmed", severity: "warning", stage: "check", since: 1, autofix: false, message: "This file sets onUnconfirmed = true and reads {timeframe}; together they repaint.", fix: "Drop onUnconfirmed = true, or guard every use of the read with bar.isConfirmed.", placeholders: ["timeframe"] },
|
|
159
165
|
OS8003: { code: "OS8003", title: "No version declaration", severity: "warning", stage: "check", since: 1, autofix: true, message: "This file declares no language version; it was compiled as version {version}.", fix: "Add version {version} as the first line of the file.", placeholders: ["version"] },
|
|
@@ -129,6 +129,10 @@ export interface DiagnosticValues {
|
|
|
129
129
|
readonly OS6017: { readonly found: PlaceholderValue; readonly versions: PlaceholderValue };
|
|
130
130
|
readonly OS6018: { readonly location: PlaceholderValue; readonly reason: PlaceholderValue };
|
|
131
131
|
readonly OS6019: { readonly value: PlaceholderValue; readonly key: PlaceholderValue; readonly validation: PlaceholderValue };
|
|
132
|
+
readonly OS6020: { readonly from: PlaceholderValue; readonly to: PlaceholderValue; readonly count: PlaceholderValue };
|
|
133
|
+
readonly OS6021: { readonly setting: PlaceholderValue; readonly problem: PlaceholderValue };
|
|
134
|
+
readonly OS6022: { readonly found: PlaceholderValue; readonly expected: PlaceholderValue };
|
|
135
|
+
readonly OS6023: { readonly commission: PlaceholderValue; readonly commissionType: PlaceholderValue };
|
|
132
136
|
readonly OS7001: { readonly name: PlaceholderValue; readonly line: PlaceholderValue };
|
|
133
137
|
readonly OS7002: { readonly name: PlaceholderValue; readonly argument: PlaceholderValue };
|
|
134
138
|
readonly OS7003: { readonly name: PlaceholderValue };
|
|
@@ -146,6 +150,8 @@ export interface DiagnosticValues {
|
|
|
146
150
|
readonly OS7015: Record<string, never>;
|
|
147
151
|
readonly OS7016: { readonly tag: PlaceholderValue };
|
|
148
152
|
readonly OS7017: { readonly qty: PlaceholderValue; readonly part: PlaceholderValue; readonly held: PlaceholderValue };
|
|
153
|
+
readonly OS7018: { readonly intent: PlaceholderValue };
|
|
154
|
+
readonly OS7019: { readonly qty: PlaceholderValue; readonly intent: PlaceholderValue };
|
|
149
155
|
readonly OS8001: { readonly name: PlaceholderValue };
|
|
150
156
|
readonly OS8002: { readonly timeframe: PlaceholderValue };
|
|
151
157
|
readonly OS8003: { readonly version: PlaceholderValue };
|
package/src/core/check/index.ts
CHANGED
|
@@ -55,13 +55,16 @@ export type { Warmup } from './warmup.js';
|
|
|
55
55
|
export { BAR_ZERO, allOf, atBar, atLeastBar, delayed, earlier, isNever, later } from './warmup.js';
|
|
56
56
|
|
|
57
57
|
export type { LibraryEntry, LibraryParameter, WarmupRule } from './library.js';
|
|
58
|
+
export type { LibraryProse } from './surface.js';
|
|
58
59
|
export {
|
|
59
60
|
NAMESPACES,
|
|
60
61
|
REQUEST_NAMES,
|
|
61
62
|
STRATEGY_NAMESPACES,
|
|
63
|
+
describedNames,
|
|
62
64
|
isLibraryName,
|
|
63
65
|
isNamespace,
|
|
64
66
|
libraryEntries,
|
|
65
67
|
libraryNames,
|
|
66
68
|
membersOf,
|
|
69
|
+
proseFor,
|
|
67
70
|
} from './surface.js';
|