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,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a fill costs, as an ordered list of lines the platform brings.
|
|
3
|
+
*
|
|
4
|
+
* **The language has three commission spellings and a slippage in ticks, and a
|
|
5
|
+
* real cost stack is not shaped like that.** It is a flat fee, a percentage, a
|
|
6
|
+
* charge levied on a charge, and a tax that applies to one side of the trade
|
|
7
|
+
* only. Teaching the language one market's stack would be teaching it a market;
|
|
8
|
+
* so the platform passes a schedule instead, and the language keeps the three
|
|
9
|
+
* spellings it has, which turn into a schedule of one line.
|
|
10
|
+
*
|
|
11
|
+
* **Order is part of the result.** The lines are applied in the order they are
|
|
12
|
+
* declared, a line charged on other lines may only name lines declared before
|
|
13
|
+
* it, and that is what makes a schedule evaluable in exactly one order. Two
|
|
14
|
+
* engines that disagree about the order disagree about the money, and a
|
|
15
|
+
* disagreement in the last bit is still a failed conformance comparison
|
|
16
|
+
* (`conformance.md` 6).
|
|
17
|
+
*
|
|
18
|
+
* **Where it is applied is not here.** `stdlib.md` 17.1 puts slippage and
|
|
19
|
+
* commission on the destination: the engine folds the price it is told and
|
|
20
|
+
* never adjusts one, so a cost model inside the ledger would be the engine
|
|
21
|
+
* moving a price, which is the one thing the invariant forbids. This module
|
|
22
|
+
* says what a charge is and works out what one fill came to; the destination is
|
|
23
|
+
* what charges it, and the slippage a schedule carries is measured and applied
|
|
24
|
+
* there, against the tick size this module only refuses the absence of.
|
|
25
|
+
*
|
|
26
|
+
* **What is charged to a fill, and only to a fill.** Every line is measured
|
|
27
|
+
* against this fill and nothing else, so a tier that changes with the month's
|
|
28
|
+
* cumulative volume, a cap counted per day rather than per application, and
|
|
29
|
+
* margin and its interest are outside this model rather than approximated
|
|
30
|
+
* inside it. A cost model that quietly approximates is a report that is wrong
|
|
31
|
+
* in the strategy's favour and says nothing about it.
|
|
32
|
+
*/
|
|
33
|
+
import { diagnosticFor } from '../diagnostics/index.js';
|
|
34
|
+
import type { Diagnostic } from '../diagnostics/index.js';
|
|
35
|
+
import type { Contract, Money, RecordedFill } from './shapes.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a line's rate is measured against.
|
|
39
|
+
*
|
|
40
|
+
* Four bases cover every stack the documentation describes without naming a
|
|
41
|
+
* market: a fraction of turnover, money per unit, money per fill, and a
|
|
42
|
+
* fraction of the lines named before this one, which is the charge on a charge.
|
|
43
|
+
*/
|
|
44
|
+
export type ChargeBase = 'turnover' | 'units' | 'order' | 'charges';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which side of the trade a line applies to.
|
|
48
|
+
*
|
|
49
|
+
* A transaction tax levied on one side is expressed exactly rather than smeared
|
|
50
|
+
* across both fills at half the rate, which is what a reader doing it by hand
|
|
51
|
+
* has to do and what makes their figure disagree with their broker's.
|
|
52
|
+
*/
|
|
53
|
+
export type ChargeSide = 'buy' | 'sell' | 'both';
|
|
54
|
+
|
|
55
|
+
/** One line of a schedule, applied to one fill. */
|
|
56
|
+
export interface ChargeLine {
|
|
57
|
+
/** The platform's own word. */
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly base: ChargeBase;
|
|
60
|
+
readonly side: ChargeSide;
|
|
61
|
+
/**
|
|
62
|
+
* Fraction for turnover and charges, money per unit for units, money for
|
|
63
|
+
* order.
|
|
64
|
+
*/
|
|
65
|
+
readonly rate: number;
|
|
66
|
+
/** Floor per application. */
|
|
67
|
+
readonly min: Money | null;
|
|
68
|
+
/** Cap per application. */
|
|
69
|
+
readonly max: Money | null;
|
|
70
|
+
/** Base 'charges' only: names of lines declared before this one. */
|
|
71
|
+
readonly of: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The whole cost model a run was carried out under. */
|
|
75
|
+
export interface ChargeSchedule {
|
|
76
|
+
readonly currency: string;
|
|
77
|
+
readonly digits: number;
|
|
78
|
+
readonly slippageTicks: number;
|
|
79
|
+
/** Applied in order, and order is part of the result. */
|
|
80
|
+
readonly lines: readonly ChargeLine[];
|
|
81
|
+
/**
|
|
82
|
+
* Where the schedule came from.
|
|
83
|
+
*
|
|
84
|
+
* A schedule derived from the declaration is derived again on replay rather
|
|
85
|
+
* than stored, because what the program states is stored once, as the
|
|
86
|
+
* program. A supplied one is the host's choice and travels with the record.
|
|
87
|
+
*/
|
|
88
|
+
readonly source: 'declaration' | 'supplied';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** What one fill was charged, line by line and in total. */
|
|
92
|
+
export interface ChargeBreakdown {
|
|
93
|
+
readonly lines: readonly { readonly name: string; readonly amount: Money }[];
|
|
94
|
+
readonly total: Money;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The most digits money is rounded to, and why there is a ceiling at all.
|
|
99
|
+
*
|
|
100
|
+
* A rounding scale is a power of ten and a binary64 holds about fifteen
|
|
101
|
+
* significant decimal digits, so past this the scale itself is approximate and
|
|
102
|
+
* the rounding stops being arithmetic and becomes noise. A currency with more
|
|
103
|
+
* than fifteen decimal places is not a currency this module is refusing to
|
|
104
|
+
* support; it is a digit count nobody stated on purpose.
|
|
105
|
+
*/
|
|
106
|
+
const MAX_DIGITS = 15;
|
|
107
|
+
|
|
108
|
+
/** A percentage, as the declaration states one, over the fraction a rate is. */
|
|
109
|
+
const PERCENT = 100;
|
|
110
|
+
|
|
111
|
+
/** What a refusal calls the setting it is about, `errors.md` OS6021. */
|
|
112
|
+
const SETTING = 'The charge schedule';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Where a refusal about a setting points.
|
|
116
|
+
*
|
|
117
|
+
* Nowhere in the script, because a schedule is not something anybody wrote in
|
|
118
|
+
* one: it is what the host stated before the first bar, and a caret drawn under
|
|
119
|
+
* a line of the strategy would blame the one party who did not choose it. The
|
|
120
|
+
* engine gives a load-time failure the same position for the same reason, and
|
|
121
|
+
* the number is written here rather than imported from it because this module
|
|
122
|
+
* imports no engine, which is what lets a stored record be reported again with
|
|
123
|
+
* no engine present.
|
|
124
|
+
*/
|
|
125
|
+
const NO_POSITION: Diagnostic['span'] = { offset: 0, length: 0, line: 0, column: 0 };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What one fill cost, line by line and in total.
|
|
129
|
+
*
|
|
130
|
+
* **The lines are the arithmetic and the total is the money.** Each line's
|
|
131
|
+
* amount is computed in binary64 and left unrounded, and the per-fill total is
|
|
132
|
+
* rounded once, half to even, to the contract's digits. Rounding each line
|
|
133
|
+
* would round once per line, and two engines rounding in two places disagree in
|
|
134
|
+
* the last bit, which is a failed conformance comparison months later on
|
|
135
|
+
* somebody else's engine. So a reader adding the lines up by hand may land a
|
|
136
|
+
* fraction of the last digit away from the total, and that is the honest way
|
|
137
|
+
* round: the total is the figure the report accumulates.
|
|
138
|
+
*
|
|
139
|
+
* **Half to even, and not the language's own rounding.** `round()` in the
|
|
140
|
+
* language is halves away from zero, because a price a trader reads should
|
|
141
|
+
* agree with what they would write down (`stdlib.md` 8.1). Money folded over
|
|
142
|
+
* thousands of fills is a different question: away from zero biases every exact
|
|
143
|
+
* half upward, and half a unit of the last digit per fill is a bias that grows
|
|
144
|
+
* with the length of the backtest. Two rules, two reasons, both written down.
|
|
145
|
+
*
|
|
146
|
+
* **A line that does not apply to this side is not in the breakdown at all.** A
|
|
147
|
+
* name beside a zero reads as a charge that was levied and came to nothing,
|
|
148
|
+
* which is not what happened, and a later line levied on that name is levied on
|
|
149
|
+
* nothing, which is exactly what a tax on one side of the trade does.
|
|
150
|
+
*/
|
|
151
|
+
export function chargeFor(
|
|
152
|
+
schedule: ChargeSchedule,
|
|
153
|
+
fill: RecordedFill,
|
|
154
|
+
contract: Contract,
|
|
155
|
+
): ChargeBreakdown {
|
|
156
|
+
const turnover = fill.units * fill.price * contract.pointValue;
|
|
157
|
+
const applied: { name: string; amount: Money }[] = [];
|
|
158
|
+
|
|
159
|
+
for (const line of schedule.lines) {
|
|
160
|
+
if (line.side !== 'both' && line.side !== fill.side) continue;
|
|
161
|
+
const base = baseOf(line, turnover, fill.units, applied);
|
|
162
|
+
applied.push({ name: line.name, amount: bounded(line.rate * base, line) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let exact = 0;
|
|
166
|
+
for (const one of applied) exact += one.amount;
|
|
167
|
+
|
|
168
|
+
return { lines: applied, total: roundMoney(exact, contract.digits) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* What a line's rate is measured against, for this fill.
|
|
173
|
+
*
|
|
174
|
+
* The earlier lines are searched rather than indexed. An index would be a map,
|
|
175
|
+
* and this module has promised to depend on no map's iteration order; a
|
|
176
|
+
* schedule is a handful of lines, so the search costs nothing and the promise
|
|
177
|
+
* costs one less thing to be careful about.
|
|
178
|
+
*/
|
|
179
|
+
function baseOf(
|
|
180
|
+
line: ChargeLine,
|
|
181
|
+
turnover: number,
|
|
182
|
+
units: number,
|
|
183
|
+
applied: readonly { readonly name: string; readonly amount: Money }[],
|
|
184
|
+
): number {
|
|
185
|
+
if (line.base === 'turnover') return turnover;
|
|
186
|
+
if (line.base === 'units') return units;
|
|
187
|
+
if (line.base === 'order') return 1;
|
|
188
|
+
|
|
189
|
+
// 'charges': the sum of the named earlier lines, as they were charged on this
|
|
190
|
+
// fill. A name whose line did not apply to this side is absent and adds
|
|
191
|
+
// nothing, which is a charge levied on a charge that was never taken.
|
|
192
|
+
let sum = 0;
|
|
193
|
+
for (const named of line.of) {
|
|
194
|
+
for (const one of applied) {
|
|
195
|
+
if (one.name === named) sum += one.amount;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return sum;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The floor and the cap, per application.
|
|
203
|
+
*
|
|
204
|
+
* Both together is the common brokerage plan: a fraction of turnover, never
|
|
205
|
+
* less than one amount and never more than another. Which is applied first does
|
|
206
|
+
* not decide the answer, because a floor above a cap is refused before the
|
|
207
|
+
* first bar rather than resolved here by whichever comparison runs first.
|
|
208
|
+
*/
|
|
209
|
+
function bounded(raw: number, line: ChargeLine): Money {
|
|
210
|
+
let amount = raw;
|
|
211
|
+
if (line.min !== null && amount < line.min) amount = line.min;
|
|
212
|
+
if (line.max !== null && amount > line.max) amount = line.max;
|
|
213
|
+
return amount;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* One money figure, rounded once, halves to even.
|
|
218
|
+
*
|
|
219
|
+
* A digit count this cannot round by is one `scheduleProblem` refuses before
|
|
220
|
+
* the first bar. If one arrives anyway the amount is returned as it stands,
|
|
221
|
+
* because a scale of ten to the power of something impossible turns money into
|
|
222
|
+
* a number JSON carries as null, and an unrounded figure is worth more.
|
|
223
|
+
*/
|
|
224
|
+
function roundMoney(amount: Money, digits: number): Money {
|
|
225
|
+
if (!Number.isFinite(amount)) return amount;
|
|
226
|
+
if (!Number.isInteger(digits) || digits < 0 || digits > MAX_DIGITS) return amount;
|
|
227
|
+
|
|
228
|
+
const scale = 10 ** digits;
|
|
229
|
+
const scaled = amount * scale;
|
|
230
|
+
const below = Math.floor(scaled);
|
|
231
|
+
const fraction = scaled - below;
|
|
232
|
+
|
|
233
|
+
let whole = below;
|
|
234
|
+
if (fraction > 0.5) whole = below + 1;
|
|
235
|
+
else if (fraction === 0.5 && below % 2 !== 0) whole = below + 1;
|
|
236
|
+
|
|
237
|
+
const money = whole / scale;
|
|
238
|
+
// A negative zero is the same money as a zero and a different set of bytes.
|
|
239
|
+
return money === 0 ? 0 : money;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The declaration's own cost model, as the one schedule this module evaluates.
|
|
244
|
+
*
|
|
245
|
+
* **The declaration is not a second cost engine.** Its three commission
|
|
246
|
+
* spellings are a schedule of one line: a flat fee is a line charged per fill,
|
|
247
|
+
* a per unit fee is a line charged per unit, and a percentage is a line charged
|
|
248
|
+
* on turnover. A second evaluator for the declaration would be the same money
|
|
249
|
+
* computed two ways, and the day the two disagreed the report and the
|
|
250
|
+
* platform's own cost panel would both be defensible.
|
|
251
|
+
*
|
|
252
|
+
* **A commission of zero is no line at all**, rather than a line charging
|
|
253
|
+
* nothing. A zero line would put a name in every breakdown, and it would make a
|
|
254
|
+
* declaration that states no commission indistinguishable from one that states
|
|
255
|
+
* a commission, which is the distinction a run has to make before it accepts a
|
|
256
|
+
* schedule from the host as well.
|
|
257
|
+
*
|
|
258
|
+
* **A flat fee is charged per fill**, which is the one place `language.md` 13.3
|
|
259
|
+
* lets a reasonable person read the words two ways: per order, or per completed
|
|
260
|
+
* round trip. A charge is attributed to the fill that incurred it everywhere in
|
|
261
|
+
* this module, because that is what attributes it to a trade, so a round trip of
|
|
262
|
+
* two fills is charged twice.
|
|
263
|
+
*
|
|
264
|
+
* The currency and the digit count are parameters because neither is the
|
|
265
|
+
* declaration's to state: the declaration's currency is a label and is often
|
|
266
|
+
* left blank, and money rounding is a fact about the contract. A default here
|
|
267
|
+
* would be this module inventing a rounding rule for somebody else's market.
|
|
268
|
+
*
|
|
269
|
+
* `commissionType`'s value set is declared once, in `check/declaration.ts`, and
|
|
270
|
+
* is not restated here: what is below is a mapping from each spelling to the
|
|
271
|
+
* line it means. A program reaching this has been checked, so a fourth spelling
|
|
272
|
+
* cannot arrive, and the spelling that falls through is the declaration's own
|
|
273
|
+
* default rather than a shape this module made up.
|
|
274
|
+
*/
|
|
275
|
+
export function scheduleFromDeclaration(
|
|
276
|
+
commission: number,
|
|
277
|
+
commissionType: string,
|
|
278
|
+
slippage: number,
|
|
279
|
+
currency: string,
|
|
280
|
+
digits: number,
|
|
281
|
+
): ChargeSchedule {
|
|
282
|
+
return {
|
|
283
|
+
currency,
|
|
284
|
+
digits,
|
|
285
|
+
slippageTicks: slippage,
|
|
286
|
+
lines: commission === 0 ? [] : [commissionLine(commission, commissionType)],
|
|
287
|
+
source: 'declaration',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The one line a declared commission is, in the base its spelling names. */
|
|
292
|
+
function commissionLine(commission: number, commissionType: string): ChargeLine {
|
|
293
|
+
if (commissionType === 'perUnit') return only('units', commission);
|
|
294
|
+
if (commissionType === 'percent') return only('turnover', commission / PERCENT);
|
|
295
|
+
return only('order', commission);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** A line with no bounds, on both sides, levied on nothing: the declaration's shape. */
|
|
299
|
+
function only(base: ChargeBase, rate: number): ChargeLine {
|
|
300
|
+
return { name: 'commission', base, side: 'both', rate, min: null, max: null, of: [] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Why this schedule cannot be carried out, or null.
|
|
305
|
+
*
|
|
306
|
+
* **Asked before the first bar, and answered once.** Everything here is a fact
|
|
307
|
+
* about the schedule rather than about any fill, so a run that would produce a
|
|
308
|
+
* number nobody can explain is refused while nothing has been computed and the
|
|
309
|
+
* cost of correcting it is one run. The contract is optional because a schedule
|
|
310
|
+
* is checkable on its own: what it adds is the three questions that need both,
|
|
311
|
+
* which are the currency the money is in, the digits it is rounded to, and the
|
|
312
|
+
* tick a slippage in ticks is measured in.
|
|
313
|
+
*
|
|
314
|
+
* The first problem found is the one reported. A list of everything wrong with
|
|
315
|
+
* a schedule reads as a worse schedule than it is, and it is corrected one line
|
|
316
|
+
* at a time regardless.
|
|
317
|
+
*/
|
|
318
|
+
export function scheduleProblem(
|
|
319
|
+
schedule: ChargeSchedule,
|
|
320
|
+
contract: Contract | null = null,
|
|
321
|
+
): Diagnostic | null {
|
|
322
|
+
const problem =
|
|
323
|
+
moneyProblem(schedule, contract) ??
|
|
324
|
+
slippageProblem(schedule, contract) ??
|
|
325
|
+
linesProblem(schedule.lines);
|
|
326
|
+
|
|
327
|
+
if (problem === null) return null;
|
|
328
|
+
return diagnosticFor('OS6021', NO_POSITION, { setting: SETTING, problem });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The currency the money is in and the digits it is rounded to.
|
|
333
|
+
*
|
|
334
|
+
* A schedule states both and so does the contract, and the two are compared
|
|
335
|
+
* here rather than one of them being quietly preferred. A schedule in another
|
|
336
|
+
* currency charges a fill in money the contract is not priced in, and a total
|
|
337
|
+
* nobody can add to the profit is worse than no total. A schedule rounding to
|
|
338
|
+
* other digits is the same fact stated twice and left to disagree.
|
|
339
|
+
*/
|
|
340
|
+
function moneyProblem(schedule: ChargeSchedule, contract: Contract | null): string | null {
|
|
341
|
+
const digits = schedule.digits;
|
|
342
|
+
if (!Number.isInteger(digits) || digits < 0 || digits > MAX_DIGITS) {
|
|
343
|
+
return `it rounds money to ${digits} digits, and a digit count is a whole number from 0 to ${MAX_DIGITS}`;
|
|
344
|
+
}
|
|
345
|
+
if (schedule.currency.trim() === '') {
|
|
346
|
+
return 'it names no currency, so what it charges is a number with no unit on it';
|
|
347
|
+
}
|
|
348
|
+
if (contract === null) return null;
|
|
349
|
+
|
|
350
|
+
if (schedule.currency !== contract.currency) {
|
|
351
|
+
return `it charges in ${schedule.currency} and the contract is priced in ${contract.currency}`;
|
|
352
|
+
}
|
|
353
|
+
if (digits !== contract.digits) {
|
|
354
|
+
return `it rounds money to ${digits} digits and the contract rounds to ${contract.digits}`;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The slippage, which this module refuses and does not apply.
|
|
361
|
+
*
|
|
362
|
+
* A slippage in ticks with no tick size to measure a tick in would charge
|
|
363
|
+
* nothing at all, and a backtest that silently charges nothing is one that lies
|
|
364
|
+
* in the strategy's favour. It is refused here, where the schedule is checked,
|
|
365
|
+
* rather than at the fill, where a zero looks like a cost model that ran.
|
|
366
|
+
*/
|
|
367
|
+
function slippageProblem(schedule: ChargeSchedule, contract: Contract | null): string | null {
|
|
368
|
+
const ticks = schedule.slippageTicks;
|
|
369
|
+
if (!Number.isFinite(ticks) || ticks < 0) {
|
|
370
|
+
return `it states ${ticks} ticks of slippage, and slippage is adverse, so it is never negative`;
|
|
371
|
+
}
|
|
372
|
+
if (ticks === 0 || contract === null) return null;
|
|
373
|
+
|
|
374
|
+
const tick = contract.tickSize;
|
|
375
|
+
if (tick === null || !(tick > 0)) {
|
|
376
|
+
return `it states ${ticks} ticks of slippage and the contract has no tick size to measure a tick in`;
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Every line, in the order the schedule declares them, against the ones before it. */
|
|
382
|
+
function linesProblem(lines: readonly ChargeLine[]): string | null {
|
|
383
|
+
const declared: string[] = [];
|
|
384
|
+
for (const line of lines) {
|
|
385
|
+
const problem = lineProblem(line, declared);
|
|
386
|
+
if (problem !== null) return problem;
|
|
387
|
+
declared.push(line.name);
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One line, against the names declared before it. */
|
|
393
|
+
function lineProblem(line: ChargeLine, declared: readonly string[]): string | null {
|
|
394
|
+
const name = line.name;
|
|
395
|
+
if (name.trim() === '') {
|
|
396
|
+
return 'a line carries no name, and a line levied on charges names the lines it is levied on';
|
|
397
|
+
}
|
|
398
|
+
if (declared.includes(name)) {
|
|
399
|
+
return `two lines are named "${name}", so a line levied on that name is levied on two answers`;
|
|
400
|
+
}
|
|
401
|
+
if (!Number.isFinite(line.rate) || line.rate < 0) {
|
|
402
|
+
return `the line "${name}" charges a rate of ${line.rate}, and a charge is money taken, never given`;
|
|
403
|
+
}
|
|
404
|
+
return boundProblem(line) ?? levyProblem(line, declared);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The floor and the cap: money, not negative, and the floor no higher than the cap. */
|
|
408
|
+
function boundProblem(line: ChargeLine): string | null {
|
|
409
|
+
const { max, min, name } = line;
|
|
410
|
+
if (min !== null && (!Number.isFinite(min) || min < 0)) {
|
|
411
|
+
return `the line "${name}" has a floor of ${min}, and a bound on a charge is money`;
|
|
412
|
+
}
|
|
413
|
+
if (max !== null && (!Number.isFinite(max) || max < 0)) {
|
|
414
|
+
return `the line "${name}" has a cap of ${max}, and a bound on a charge is money`;
|
|
415
|
+
}
|
|
416
|
+
if (min !== null && max !== null && min > max) {
|
|
417
|
+
return `the line "${name}" has a floor of ${min} above its cap of ${max}`;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* What a line is levied on, which is the rule the whole ordering exists for.
|
|
424
|
+
*
|
|
425
|
+
* A line levied on lines not declared before it has no single evaluation order,
|
|
426
|
+
* so two engines would charge two different amounts and both would be
|
|
427
|
+
* defensible. Naming itself, naming a line declared after it and naming a line
|
|
428
|
+
* that is not in the schedule at all are one problem in three spellings, and
|
|
429
|
+
* they are reported as one: none of the three is declared before this line.
|
|
430
|
+
*/
|
|
431
|
+
function levyProblem(line: ChargeLine, declared: readonly string[]): string | null {
|
|
432
|
+
const name = line.name;
|
|
433
|
+
if (line.base !== 'charges') {
|
|
434
|
+
if (line.of.length === 0) return null;
|
|
435
|
+
return `the line "${name}" names ${line.of.length} lines to be levied on and its base is ${line.base}, so the names are read by nothing`;
|
|
436
|
+
}
|
|
437
|
+
if (line.of.length === 0) {
|
|
438
|
+
return `the line "${name}" is levied on charges and names none, so it is levied on nothing`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const seen: string[] = [];
|
|
442
|
+
for (const named of line.of) {
|
|
443
|
+
if (!declared.includes(named)) {
|
|
444
|
+
return `the line "${name}" is levied on "${named}", which is not declared before it`;
|
|
445
|
+
}
|
|
446
|
+
if (seen.includes(named)) {
|
|
447
|
+
return `the line "${name}" is levied on "${named}" twice, so that line is charged on twice over`;
|
|
448
|
+
}
|
|
449
|
+
seen.push(named);
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|