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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The equity curve: one point per bar in the report window.
|
|
3
|
+
*
|
|
4
|
+
* **Marked to the close and to nothing else.** An intrabar extreme is a price
|
|
5
|
+
* the strategy could not have acted on, so it is not a profit it had, and a
|
|
6
|
+
* curve drawn through the extremes flatters every run that ever held a losing
|
|
7
|
+
* position. Where a bar's close is absent the previous mark carries and the
|
|
8
|
+
* point says so, rather than a zero a reader would compare against.
|
|
9
|
+
*
|
|
10
|
+
* **Drawdown is stated on equity including open profit**, and the basis is
|
|
11
|
+
* carried on every point rather than left to the reader: `drawdown` is the
|
|
12
|
+
* distance below the running peak, zero or negative, and `drawdownPercent` is
|
|
13
|
+
* that distance against the peak. A report whose drawdown basis is not written
|
|
14
|
+
* down is a report whose worst figure means a different thing to each reader.
|
|
15
|
+
*
|
|
16
|
+
* ## What the curve is folded from, and when each figure lands
|
|
17
|
+
*
|
|
18
|
+
* A trade list and a list of bar closes, and nothing else. A trade carries its
|
|
19
|
+
* own gross, its own charges and the two bars it lived between, so the fold is
|
|
20
|
+
* a sweep: bars in the order they arrived, trades in the order they opened.
|
|
21
|
+
*
|
|
22
|
+
* - **A trade's charges land on the bar it opened.** The trade list does not
|
|
23
|
+
* carry the timing of the individual fills underneath it, so the cost has to
|
|
24
|
+
* land somewhere, and the open is the one place that is never later than the
|
|
25
|
+
* truth: an entry charge is paid the moment the trade is taken on, and an
|
|
26
|
+
* exit charge cannot be paid before it. The alternative, landing the whole
|
|
27
|
+
* cost at the close, shows a run carrying a position for two hundred bars as
|
|
28
|
+
* having paid nothing for it, and leaves a trade that never closed paying
|
|
29
|
+
* nothing at all.
|
|
30
|
+
* - **A trade's gross lands on the bar it closed**, because that is the bar it
|
|
31
|
+
* stopped being an opinion and became a number. While it is open it is in
|
|
32
|
+
* `openProfit` instead, marked to the close, and the two never overlap.
|
|
33
|
+
*
|
|
34
|
+
* So the last point of a full run carries every charge the run paid, open
|
|
35
|
+
* trades included, and every gross it realised, which is an identity the
|
|
36
|
+
* summary is asserted against rather than assumed to share.
|
|
37
|
+
*
|
|
38
|
+
* ## What a trade list cannot say, said here rather than discovered later
|
|
39
|
+
*
|
|
40
|
+
* A trade holds one entry price weighted over its entry fills and one exit
|
|
41
|
+
* price weighted over its exit fills, so a trade whose size changed while it
|
|
42
|
+
* was open is not visible in it: the size a trade held between its open and its
|
|
43
|
+
* close is the size it ended up entering, at the average price it ended up
|
|
44
|
+
* entering at. Both directions are wrong and they are wrong differently.
|
|
45
|
+
*
|
|
46
|
+
* - **A partial close** is marked at the full size from the bar the reduction
|
|
47
|
+
* settled on, so the open profit of the part already closed is counted twice
|
|
48
|
+
* over, once here and once in the realised total.
|
|
49
|
+
* - **A scale-in is the worse of the two**, because it is wrong from the
|
|
50
|
+
* beginning rather than from the middle. A trade that buys a hundred at ten
|
|
51
|
+
* and another hundred at twenty is marked, from the bar it first opened, as
|
|
52
|
+
* two hundred units bought at fifteen. On the bars before the second entry it
|
|
53
|
+
* is therefore marked five points under water on units it did not hold, and
|
|
54
|
+
* the curve reports a drawdown the account never had. The summary's
|
|
55
|
+
* `maxDrawdown` and `maxDrawdownPercent` are folded from this curve, so a
|
|
56
|
+
* pyramiding strategy is reported as having risked more than it did.
|
|
57
|
+
*
|
|
58
|
+
* Neither reaches the realised total, which is folded from the fills: what is
|
|
59
|
+
* affected is `openProfit`, `exposure`, `equity` and every drawdown figure
|
|
60
|
+
* taken off the curve, on the bars a trade's size was not what it ended as.
|
|
61
|
+
*
|
|
62
|
+
* A curve folded from the fills rather than from the trades would have neither,
|
|
63
|
+
* and that is what fixes it: it needs each entry, exit and charge to land on
|
|
64
|
+
* the bar it settled on, which is a different fold from this one rather than a
|
|
65
|
+
* correction to it. It is written here because a limitation nobody wrote down
|
|
66
|
+
* is a limitation somebody finds inside a report they had already believed.
|
|
67
|
+
*
|
|
68
|
+
* ## Two preconditions, both of them the caller's
|
|
69
|
+
*
|
|
70
|
+
* The trades arrive in the order they opened, which is the order `Trade.index`
|
|
71
|
+
* states, and the bars arrive in the order they were loaded. Both are swept
|
|
72
|
+
* with a pointer rather than searched, because a report over fifty thousand
|
|
73
|
+
* bars that rescans its trade list on every one of them is a report nobody
|
|
74
|
+
* waits for.
|
|
75
|
+
*
|
|
76
|
+
* ## And a percentage here is a fraction of its basis
|
|
77
|
+
*
|
|
78
|
+
* `drawdownPercent` is `drawdown / runningPeak`, which is a fraction: a
|
|
79
|
+
* hundredth of a percent down is written `-0.0001` and not `-0.01`. One
|
|
80
|
+
* convention for every figure in this module that divides, because two figures
|
|
81
|
+
* spelled the same way in two units is a number a reader gets wrong once and
|
|
82
|
+
* never trusts again. The multiplication by a hundred belongs to whatever
|
|
83
|
+
* prints it.
|
|
84
|
+
*/
|
|
85
|
+
import type { BarMark, Contract, Money } from './shapes.js';
|
|
86
|
+
import type { Trade } from './trades.js';
|
|
87
|
+
|
|
88
|
+
/** One report bar's standing, folded from the fills settled up to it. */
|
|
89
|
+
export interface EquityPoint {
|
|
90
|
+
readonly barIndex: number;
|
|
91
|
+
readonly time: number | null;
|
|
92
|
+
/** Cumulative gross of closed trades. */
|
|
93
|
+
readonly realised: Money;
|
|
94
|
+
/** Cumulative. */
|
|
95
|
+
readonly charges: Money;
|
|
96
|
+
/** Marked to this bar's close. */
|
|
97
|
+
readonly openProfit: Money;
|
|
98
|
+
/** capital + realised - charges. */
|
|
99
|
+
readonly cash: Money;
|
|
100
|
+
/** cash + openProfit. */
|
|
101
|
+
readonly equity: Money;
|
|
102
|
+
/** The open position's magnitude at this close. */
|
|
103
|
+
readonly exposure: Money;
|
|
104
|
+
/** equity - runningPeak, zero or negative. */
|
|
105
|
+
readonly drawdown: Money;
|
|
106
|
+
/** `drawdown / runningPeak`, a fraction and not a figure times a hundred. */
|
|
107
|
+
readonly drawdownPercent: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether this trade was still held at the close of this bar.
|
|
112
|
+
*
|
|
113
|
+
* The boundaries are the whole of the rule and both are decisions. A trade is
|
|
114
|
+
* held from the close of the bar it opened on, because an entry that settled
|
|
115
|
+
* during a bar is a position that bar ended holding. It is not held at the
|
|
116
|
+
* close of the bar it closed on, because that bar ended flat. So a trade
|
|
117
|
+
* opened and closed inside one bar is held at no close at all, which is what a
|
|
118
|
+
* run that was flat at every mark should report.
|
|
119
|
+
*
|
|
120
|
+
* Every figure in this module that asks which trades are open asks it here, so
|
|
121
|
+
* the curve, the exposure and the count of bars in the market cannot come to
|
|
122
|
+
* three different answers about one bar.
|
|
123
|
+
*/
|
|
124
|
+
export function openOnBar(trade: Trade, barIndex: number): boolean {
|
|
125
|
+
if (barIndex < trade.openedOnBar) return false;
|
|
126
|
+
return trade.closedOnBar === null || barIndex < trade.closedOnBar;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A ratio against a basis that may not be there to divide by.
|
|
131
|
+
*
|
|
132
|
+
* Capital of zero and a peak of zero are both reachable, and both turn an
|
|
133
|
+
* honest division into a value JSON cannot carry: a report whose worst figure
|
|
134
|
+
* comes back `null` from a round trip is worse than one that says zero. A basis
|
|
135
|
+
* that is not positive has no ratio to state, so the figure beside it, which is
|
|
136
|
+
* money and is always true, is the one a reader is left with.
|
|
137
|
+
*/
|
|
138
|
+
export function ratioOf(value: number, basis: number): number {
|
|
139
|
+
return basis > 0 ? value / basis : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The curve, one point per report bar, in the order the bars arrived.
|
|
144
|
+
*
|
|
145
|
+
* Warmup bars are swept and not reported: their orders were real, so a trade
|
|
146
|
+
* opened during the warmup is already in the fold at the first point, with its
|
|
147
|
+
* charges already paid and its position already marked. A curve that began its
|
|
148
|
+
* fold at the first report bar would lose both, and would lose them silently.
|
|
149
|
+
*
|
|
150
|
+
* The running peak starts at the capital rather than at the first point, so a
|
|
151
|
+
* run that is down from its first bar is in drawdown at its first bar. Starting
|
|
152
|
+
* it at the first point would report every run as having begun at its high.
|
|
153
|
+
*/
|
|
154
|
+
export function equityOver(
|
|
155
|
+
trades: readonly Trade[],
|
|
156
|
+
marks: readonly BarMark[],
|
|
157
|
+
contract: Contract,
|
|
158
|
+
capital: Money,
|
|
159
|
+
): readonly EquityPoint[] {
|
|
160
|
+
const points: EquityPoint[] = [];
|
|
161
|
+
let held: Trade[] = [];
|
|
162
|
+
let next = 0;
|
|
163
|
+
let realised = 0;
|
|
164
|
+
let charges = 0;
|
|
165
|
+
let peak = capital;
|
|
166
|
+
let mark: number | null = null;
|
|
167
|
+
|
|
168
|
+
for (const bar of marks) {
|
|
169
|
+
// Opened by this bar, charges and all. The pointer is why the trades have
|
|
170
|
+
// to arrive in the order they opened.
|
|
171
|
+
while (next < trades.length) {
|
|
172
|
+
const opening = trades[next];
|
|
173
|
+
if (opening === undefined || opening.openedOnBar > bar.barIndex) break;
|
|
174
|
+
charges += opening.charges;
|
|
175
|
+
held.push(opening);
|
|
176
|
+
next += 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Closed by this bar, gross and all. A trade that opened and closed inside
|
|
180
|
+
// one bar is taken on and given up here in that order, so its cost and its
|
|
181
|
+
// gross are both in this point and its position is in none.
|
|
182
|
+
let closedHere = false;
|
|
183
|
+
for (const trade of held) {
|
|
184
|
+
if (closedBy(trade, bar.barIndex)) {
|
|
185
|
+
realised += trade.grossProfit;
|
|
186
|
+
closedHere = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (closedHere) held = held.filter((trade) => !closedBy(trade, bar.barIndex));
|
|
190
|
+
|
|
191
|
+
// A close the host did not have leaves the previous mark standing. It is
|
|
192
|
+
// carried across the warmup boundary too, so the first report bar of a run
|
|
193
|
+
// whose close is absent is marked at the last price there was.
|
|
194
|
+
if (bar.close !== null) mark = bar.close;
|
|
195
|
+
if (!bar.inReport) continue;
|
|
196
|
+
|
|
197
|
+
let openProfit = 0;
|
|
198
|
+
let exposure = 0;
|
|
199
|
+
for (const trade of held) {
|
|
200
|
+
// Before the first close there has ever been, a trade is marked at its
|
|
201
|
+
// own entry: no profit, and the position still visible in the exposure.
|
|
202
|
+
const at = mark ?? trade.entryPrice;
|
|
203
|
+
const direction = trade.side === 'long' ? 1 : -1;
|
|
204
|
+
openProfit += direction * (at - trade.entryPrice) * trade.units * contract.pointValue;
|
|
205
|
+
exposure += Math.abs(trade.units * at * contract.pointValue);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const cash = capital + realised - charges;
|
|
209
|
+
const equity = cash + openProfit;
|
|
210
|
+
if (equity > peak) peak = equity;
|
|
211
|
+
const drawdown = equity - peak;
|
|
212
|
+
points.push({
|
|
213
|
+
barIndex: bar.barIndex,
|
|
214
|
+
time: bar.time,
|
|
215
|
+
realised,
|
|
216
|
+
charges,
|
|
217
|
+
openProfit,
|
|
218
|
+
cash,
|
|
219
|
+
equity,
|
|
220
|
+
exposure,
|
|
221
|
+
drawdown,
|
|
222
|
+
drawdownPercent: ratioOf(drawdown, peak),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return points;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* How many of these bars ended with something held.
|
|
231
|
+
*
|
|
232
|
+
* Swept rather than searched: a sorted list of the bars trades opened on, a
|
|
233
|
+
* sorted list of the bars they closed on, and the running difference between
|
|
234
|
+
* how many of each have gone by. That is the boundary rule `openOnBar` states,
|
|
235
|
+
* arrived at from the other side, and the two are asserted to agree over a
|
|
236
|
+
* generated corpus rather than trusted to. A count that disagrees with the
|
|
237
|
+
* curve beside it about which bars were in the market is exactly the kind of
|
|
238
|
+
* defect a reader finds by adding two of the printed figures up.
|
|
239
|
+
*/
|
|
240
|
+
export function barsInMarketOver(
|
|
241
|
+
trades: readonly Trade[],
|
|
242
|
+
equity: readonly EquityPoint[],
|
|
243
|
+
): number {
|
|
244
|
+
const opens = trades.map((trade) => trade.openedOnBar).sort(ascending);
|
|
245
|
+
const closes = trades
|
|
246
|
+
.filter((trade) => trade.closedOnBar !== null)
|
|
247
|
+
.map((trade) => trade.closedOnBar ?? 0)
|
|
248
|
+
.sort(ascending);
|
|
249
|
+
|
|
250
|
+
let opened = 0;
|
|
251
|
+
let closed = 0;
|
|
252
|
+
let live = 0;
|
|
253
|
+
let bars = 0;
|
|
254
|
+
for (const point of equity) {
|
|
255
|
+
while (opened < opens.length && (opens[opened] ?? 0) <= point.barIndex) {
|
|
256
|
+
live += 1;
|
|
257
|
+
opened += 1;
|
|
258
|
+
}
|
|
259
|
+
while (closed < closes.length && (closes[closed] ?? 0) <= point.barIndex) {
|
|
260
|
+
live -= 1;
|
|
261
|
+
closed += 1;
|
|
262
|
+
}
|
|
263
|
+
if (live > 0) bars += 1;
|
|
264
|
+
}
|
|
265
|
+
return bars;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Whether this bar is the bar the trade closed on, or one after it. */
|
|
269
|
+
function closedBy(trade: Trade, barIndex: number): boolean {
|
|
270
|
+
return trade.closedOnBar !== null && trade.closedOnBar <= barIndex;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Smallest first, said once, because a sort without a comparison sorts text. */
|
|
274
|
+
function ascending(left: number, right: number): number {
|
|
275
|
+
return left - right;
|
|
276
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The money: what a run made, what it cost, and what that is worth knowing.
|
|
3
|
+
*
|
|
4
|
+
* **This module imports no engine and no emitter, and it never will.** It is
|
|
5
|
+
* arithmetic over portable data: a list of settled fills, a list of bar closes,
|
|
6
|
+
* a charge schedule and the contract the run was carried out under. Two things
|
|
7
|
+
* follow from that and both of them are the reason for it.
|
|
8
|
+
*
|
|
9
|
+
* A stored record can be reported again with no engine present, which is what
|
|
10
|
+
* makes a run record a conformance case rather than a souvenir. And the engine
|
|
11
|
+
* can call this, when the day comes that a script may read its own equity,
|
|
12
|
+
* without a cycle and without a second implementation of any of these formulas
|
|
13
|
+
* sitting inside the execution path disagreeing with this one.
|
|
14
|
+
*
|
|
15
|
+
* The door names what a caller may use. The fold itself, the cost basis, the
|
|
16
|
+
* round trip state machine and the statistics stay behind it: they are one
|
|
17
|
+
* algorithm with one caller.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour lands beside these shapes, each call in the file its type is in.
|
|
20
|
+
* The cost model is here: `chargeFor` is what one fill came to,
|
|
21
|
+
* `scheduleFromDeclaration` is the declaration's own commission as the one kind
|
|
22
|
+
* of schedule this module evaluates, and `scheduleProblem` is what a schedule
|
|
23
|
+
* is refused for before the first bar. The round trips, the equity curve, the
|
|
24
|
+
* statistics and the report land beside their own shapes the same way.
|
|
25
|
+
*
|
|
26
|
+
* The shapes were agreed before any of it computed, because they are what the
|
|
27
|
+
* engine, the backtest driver and a second engine all have to agree about.
|
|
28
|
+
*/
|
|
29
|
+
export { chargeFor, scheduleFromDeclaration, scheduleProblem } from './charges.js';
|
|
30
|
+
export type { BarMark, Contract, Money, RecordedFill } from './shapes.js';
|
|
31
|
+
export type {
|
|
32
|
+
ChargeBase,
|
|
33
|
+
ChargeBreakdown,
|
|
34
|
+
ChargeLine,
|
|
35
|
+
ChargeSchedule,
|
|
36
|
+
ChargeSide,
|
|
37
|
+
} from './charges.js';
|
|
38
|
+
export { tradesOf } from './trades.js';
|
|
39
|
+
export type { Trade } from './trades.js';
|
|
40
|
+
export { equityOver } from './equity.js';
|
|
41
|
+
export type { EquityPoint } from './equity.js';
|
|
42
|
+
export { monthlyOver } from './monthly.js';
|
|
43
|
+
export type { MonthlyReturn } from './monthly.js';
|
|
44
|
+
export { markersOf } from './markers.js';
|
|
45
|
+
export type { TradeMarker } from './markers.js';
|
|
46
|
+
export { summaryOf } from './statistics.js';
|
|
47
|
+
export type { Summary } from './statistics.js';
|
|
48
|
+
export { reportOf } from './report.js';
|
|
49
|
+
export type { Report } from './report.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One marker per entry and exit fill, which is the chart's whole input.
|
|
3
|
+
*
|
|
4
|
+
* A chart draws a trade as two marks and a line between them, and everything it
|
|
5
|
+
* needs to do that is in the fills: the bar, the trade the fill belongs to, the
|
|
6
|
+
* side, the units, the price and the tag the order carried. So the markers are
|
|
7
|
+
* produced here, by the module that already knows which fill belongs to which
|
|
8
|
+
* trade, and no chart is needed to produce them and none is imported to.
|
|
9
|
+
*/
|
|
10
|
+
import type { RecordedFill } from './shapes.js';
|
|
11
|
+
import { closedBy, openedBy } from './trades.js';
|
|
12
|
+
import type { Trade } from './trades.js';
|
|
13
|
+
|
|
14
|
+
/** One entry or exit fill, addressed as a chart addresses it. */
|
|
15
|
+
export interface TradeMarker {
|
|
16
|
+
readonly barIndex: number;
|
|
17
|
+
readonly time: number | null;
|
|
18
|
+
readonly tradeIndex: number;
|
|
19
|
+
readonly kind: 'entry' | 'exit';
|
|
20
|
+
readonly side: 'buy' | 'sell';
|
|
21
|
+
readonly units: number;
|
|
22
|
+
readonly price: number;
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One marker per entry and exit fill, against the trades those fills made up.
|
|
28
|
+
*
|
|
29
|
+
* **The fill is read with the same rule the trade list read it with.**
|
|
30
|
+
* `closedBy` and `openedBy` are imported from `trades.ts` rather than restated,
|
|
31
|
+
* because a marker that called an exit an entry would draw the chart the report
|
|
32
|
+
* contradicts. A fill that carried a reference through zero is both: it closes
|
|
33
|
+
* the trade the reference held and opens the next one, so it produces two
|
|
34
|
+
* markers, in that order, which is the order the trade list folded it in.
|
|
35
|
+
*
|
|
36
|
+
* **A trade is found by its reference and by the order it opened**, never by
|
|
37
|
+
* the bar it opened on: a reference that went to zero and was entered again on
|
|
38
|
+
* the same bar is two trades, and addressing them by bar would put both
|
|
39
|
+
* markers on the first. The trades are walked once per reference, oldest first,
|
|
40
|
+
* which is the order `tradesOf` built them in.
|
|
41
|
+
*
|
|
42
|
+
* The count is an identity rather than a claim: the markers of one trade are
|
|
43
|
+
* its `entries` plus its `exits`, and a test asserts it over the same fills.
|
|
44
|
+
*/
|
|
45
|
+
export function markersOf(
|
|
46
|
+
fills: readonly RecordedFill[],
|
|
47
|
+
trades: readonly Trade[],
|
|
48
|
+
): readonly TradeMarker[] {
|
|
49
|
+
const ordered = fills.slice().sort((a, b) => a.seq - b.seq);
|
|
50
|
+
const queues = new Map<number, Trade[]>();
|
|
51
|
+
for (const trade of trades) {
|
|
52
|
+
const held = queues.get(trade.positionRef) ?? [];
|
|
53
|
+
held.push(trade);
|
|
54
|
+
queues.set(trade.positionRef, held);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const out: TradeMarker[] = [];
|
|
58
|
+
for (const fill of ordered) {
|
|
59
|
+
const queue = queues.get(fill.positionRef) ?? [];
|
|
60
|
+
const closing = closedBy(fill.refSizeBefore, fill.refSizeAfter);
|
|
61
|
+
const opening = openedBy(fill.refSizeBefore, fill.refSizeAfter);
|
|
62
|
+
|
|
63
|
+
if (closing > 0) {
|
|
64
|
+
const held = queue[0];
|
|
65
|
+
if (held !== undefined) out.push(markerFor(fill, held.index, 'exit', closing));
|
|
66
|
+
// The trade the reference held is finished by a fill that closed the
|
|
67
|
+
// whole of it, and the next trade on that reference is the one the fills
|
|
68
|
+
// after this belong to.
|
|
69
|
+
if (held !== undefined && (opening > 0 || fill.refSizeAfter === 0)) queue.shift();
|
|
70
|
+
}
|
|
71
|
+
if (opening > 0) {
|
|
72
|
+
const held = queue[0];
|
|
73
|
+
if (held !== undefined) out.push(markerFor(fill, held.index, 'entry', opening));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function markerFor(
|
|
80
|
+
fill: RecordedFill,
|
|
81
|
+
tradeIndex: number,
|
|
82
|
+
kind: 'entry' | 'exit',
|
|
83
|
+
units: number,
|
|
84
|
+
): TradeMarker {
|
|
85
|
+
return {
|
|
86
|
+
barIndex: fill.barIndex,
|
|
87
|
+
time: fill.barTime,
|
|
88
|
+
tradeIndex,
|
|
89
|
+
kind,
|
|
90
|
+
side: fill.side,
|
|
91
|
+
units,
|
|
92
|
+
price: fill.price,
|
|
93
|
+
tag: fill.tag,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The month by month table, bucketed in UTC from bar times.
|
|
3
|
+
*
|
|
4
|
+
* **UTC, and it says so on the type.** A calendar month in the instrument's own
|
|
5
|
+
* timezone would need the session machinery and the instrument's calendar, and
|
|
6
|
+
* a table that silently buckets a trade into the wrong month is worse than one
|
|
7
|
+
* that states the basis it used. No timezone library, and no pretence of the
|
|
8
|
+
* instrument's calendar.
|
|
9
|
+
*/
|
|
10
|
+
import type { EquityPoint } from './equity.js';
|
|
11
|
+
import type { Money } from './shapes.js';
|
|
12
|
+
import type { Trade } from './trades.js';
|
|
13
|
+
|
|
14
|
+
/** One calendar month of the run, in UTC. */
|
|
15
|
+
export interface MonthlyReturn {
|
|
16
|
+
readonly year: number;
|
|
17
|
+
/** 1 to 12, UTC. */
|
|
18
|
+
readonly month: number;
|
|
19
|
+
readonly netProfit: Money;
|
|
20
|
+
/** Against the equity at the month's first bar. */
|
|
21
|
+
readonly returnPercent: number;
|
|
22
|
+
readonly trades: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The months a run passed through, in the order it passed through them.
|
|
27
|
+
*
|
|
28
|
+
* **A month holds the change in equity across the closes inside it.** The first
|
|
29
|
+
* point of the curve has nothing before it to be compared with, so it opens the
|
|
30
|
+
* table rather than contributing to it, and every later point contributes what
|
|
31
|
+
* it moved from the point before. The consequence is an identity rather than a
|
|
32
|
+
* claim: the months add up to the equity at the last point less the equity at
|
|
33
|
+
* the first, and nothing that happened during the warmup is attributed to a
|
|
34
|
+
* month it did not happen in.
|
|
35
|
+
*
|
|
36
|
+
* `returnPercent` is that change over the equity at the month's first bar,
|
|
37
|
+
* which is the basis this table states and the reason the figure is a fraction
|
|
38
|
+
* rather than a fraction times a hundred: `drawdownPercent` beside it is
|
|
39
|
+
* `drawdown / peak`, and one report with two conventions is a report a reader
|
|
40
|
+
* has to check every figure of.
|
|
41
|
+
*
|
|
42
|
+
* `trades` counts the trades that closed inside the month, by the time they
|
|
43
|
+
* closed. An open trade is in no month, because the month it will be counted in
|
|
44
|
+
* is not decided yet.
|
|
45
|
+
*
|
|
46
|
+
* **A point with no time is in no month.** A bucket is a calendar fact and a
|
|
47
|
+
* bar with no time states no calendar, so such a point carries its change into
|
|
48
|
+
* the month in force rather than opening one, and where none is in force it is
|
|
49
|
+
* outside the table altogether. That is the honest reading: bucketing it by the
|
|
50
|
+
* month that happened to come before would put money in a month on the strength
|
|
51
|
+
* of nothing.
|
|
52
|
+
*/
|
|
53
|
+
export function monthlyOver(
|
|
54
|
+
equity: readonly EquityPoint[],
|
|
55
|
+
trades: readonly Trade[],
|
|
56
|
+
): readonly MonthlyReturn[] {
|
|
57
|
+
const buckets: Bucket[] = [];
|
|
58
|
+
let previous: EquityPoint | undefined;
|
|
59
|
+
let current: Bucket | undefined;
|
|
60
|
+
|
|
61
|
+
for (const point of equity) {
|
|
62
|
+
const at = monthOf(point.time);
|
|
63
|
+
if (at !== null && (current === undefined || current.year !== at.year || current.month !== at.month)) {
|
|
64
|
+
// The equity this month started from, which is where the previous month
|
|
65
|
+
// left the account and not where this one's first bar closed. Taking the
|
|
66
|
+
// opening point's own equity put the month's first move into the
|
|
67
|
+
// numerator and into the denominator at once: a February that doubled a
|
|
68
|
+
// thousand pounds reported fifty percent, because the gain was divided by
|
|
69
|
+
// the two thousand it had already produced.
|
|
70
|
+
current = {
|
|
71
|
+
year: at.year,
|
|
72
|
+
month: at.month,
|
|
73
|
+
netProfit: 0,
|
|
74
|
+
basis: previous === undefined ? point.equity : previous.equity,
|
|
75
|
+
trades: 0,
|
|
76
|
+
};
|
|
77
|
+
buckets.push(current);
|
|
78
|
+
}
|
|
79
|
+
if (previous !== undefined && current !== undefined) {
|
|
80
|
+
current.netProfit += point.equity - previous.equity;
|
|
81
|
+
}
|
|
82
|
+
previous = point;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const trade of trades) {
|
|
86
|
+
const at = monthOf(trade.closedAt);
|
|
87
|
+
if (at === null) continue;
|
|
88
|
+
const bucket = buckets.find((one) => one.year === at.year && one.month === at.month);
|
|
89
|
+
if (bucket !== undefined) bucket.trades += 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return buckets.map((one) => ({
|
|
93
|
+
year: one.year,
|
|
94
|
+
month: one.month,
|
|
95
|
+
netProfit: one.netProfit,
|
|
96
|
+
returnPercent: one.basis > 0 ? one.netProfit / one.basis : 0,
|
|
97
|
+
trades: one.trades,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One month while it is being filled. */
|
|
102
|
+
interface Bucket {
|
|
103
|
+
readonly year: number;
|
|
104
|
+
readonly month: number;
|
|
105
|
+
netProfit: Money;
|
|
106
|
+
/** The equity at the month's first bar, which the return is measured against. */
|
|
107
|
+
readonly basis: Money;
|
|
108
|
+
trades: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The calendar month an instant falls in, in UTC and in no other zone.
|
|
113
|
+
*
|
|
114
|
+
* Decomposed rather than formatted, so no locale, no zone table and no library
|
|
115
|
+
* is involved: the same instant produces the same month on every machine this
|
|
116
|
+
* ever runs on.
|
|
117
|
+
*/
|
|
118
|
+
function monthOf(time: number | null): { readonly year: number; readonly month: number } | null {
|
|
119
|
+
if (time === null || !Number.isFinite(time) || Math.abs(time) > LATEST_INSTANT) return null;
|
|
120
|
+
const at = new Date(time);
|
|
121
|
+
return { year: at.getUTCFullYear(), month: at.getUTCMonth() + 1 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The furthest either way an instant can be and still name a month.
|
|
126
|
+
*
|
|
127
|
+
* A finite number outside it decomposes to NaN rather than throwing, and a NaN
|
|
128
|
+
* year never equals the next one, so every point opened a bucket of its own and
|
|
129
|
+
* a fifty thousand bar run produced fifty thousand rows of NaN. The canonical
|
|
130
|
+
* writer then threw a bare error with no code on them, out of core, on a path a
|
|
131
|
+
* host could not tell from an internal fault. The mistake that gets here is
|
|
132
|
+
* ordinary: bar times supplied in nanoseconds rather than milliseconds.
|
|
133
|
+
*
|
|
134
|
+
* A point whose time names no month contributes to no month, which is what an
|
|
135
|
+
* absent time already did.
|
|
136
|
+
*/
|
|
137
|
+
const LATEST_INSTANT = 8.64e15;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report: what a run came to, folded from its fills and its bar closes.
|
|
3
|
+
*
|
|
4
|
+
* **The evaluation order is fixed here and nowhere else**, because a report
|
|
5
|
+
* that is right to eleven digits and different in the twelfth fails a
|
|
6
|
+
* conformance comparison months later on somebody else's engine, and the cause
|
|
7
|
+
* is always a line nobody thought was arithmetic. Fills in `seq` order, charge
|
|
8
|
+
* lines in declaration order, one rounding per fill total, no collection
|
|
9
|
+
* re-summed in another order, no dependence on a map's iteration order, no wall
|
|
10
|
+
* clock and no random number generator anywhere in this module.
|
|
11
|
+
*
|
|
12
|
+
* **What the report is not is read by a script.** The money entries of the
|
|
13
|
+
* `pos` namespace stay planned and go on refusing at the call. The moment a
|
|
14
|
+
* script can read its own equity mid-run, the money layer joins the execution
|
|
15
|
+
* path, and therefore joins the conformance surface, and every formula in this
|
|
16
|
+
* module has to be agreed by a second engine before a script may branch on it.
|
|
17
|
+
* This module computes the money after the fact from a record; whether a script
|
|
18
|
+
* may read it is a later decision. That this module imports no engine is the
|
|
19
|
+
* structural half of keeping that decision open: when it is taken, the engine
|
|
20
|
+
* calls this, and there is no second implementation to disagree with.
|
|
21
|
+
*/
|
|
22
|
+
import { chargeFor } from './charges.js';
|
|
23
|
+
import type { ChargeSchedule } from './charges.js';
|
|
24
|
+
import { equityOver } from './equity.js';
|
|
25
|
+
import type { EquityPoint } from './equity.js';
|
|
26
|
+
import { monthlyOver } from './monthly.js';
|
|
27
|
+
import type { MonthlyReturn } from './monthly.js';
|
|
28
|
+
import { markersOf } from './markers.js';
|
|
29
|
+
import type { TradeMarker } from './markers.js';
|
|
30
|
+
import type { BarMark, Contract, Money, RecordedFill } from './shapes.js';
|
|
31
|
+
import { summaryOf } from './statistics.js';
|
|
32
|
+
import type { Summary } from './statistics.js';
|
|
33
|
+
import { tradesOf } from './trades.js';
|
|
34
|
+
import type { Trade } from './trades.js';
|
|
35
|
+
|
|
36
|
+
/** Everything a run is reported as, and nothing a chart has to compute. */
|
|
37
|
+
export interface Report {
|
|
38
|
+
readonly summary: Summary;
|
|
39
|
+
readonly trades: readonly Trade[];
|
|
40
|
+
readonly equity: readonly EquityPoint[];
|
|
41
|
+
readonly monthly: readonly MonthlyReturn[];
|
|
42
|
+
readonly markers: readonly TradeMarker[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The whole report, folded from the fills, the bar closes and the schedule.
|
|
47
|
+
*
|
|
48
|
+
* **One pass, in one order, and the order is the result.** The fills are put in
|
|
49
|
+
* `seq` order once, here, and every fold below reads that same list: the
|
|
50
|
+
* charges are computed in it, the trades are built from it, the curve is marked
|
|
51
|
+
* along it and the markers come off it. A second ordering anywhere would be a
|
|
52
|
+
* report that is right to eleven digits and different in the twelfth on
|
|
53
|
+
* somebody else's engine, which is the failure this module is shaped to make
|
|
54
|
+
* impossible rather than unlikely.
|
|
55
|
+
*
|
|
56
|
+
* **A charge belongs to the fill that incurred it.** `chargeFor` rounds one
|
|
57
|
+
* fill's total once, and nothing here rounds it again or re-sums the collection
|
|
58
|
+
* in another order, so `sum(trade.charges)` and `summary.charges` are the same
|
|
59
|
+
* money and a test asserts it rather than a page claiming it.
|
|
60
|
+
*
|
|
61
|
+
* A run carried out under no schedule at all is charged nothing, which is a
|
|
62
|
+
* study of the strategy before costs and is a thing worth being able to ask
|
|
63
|
+
* for. It is not a default: `scheduleFromDeclaration` is what a run under the
|
|
64
|
+
* declaration's own commission uses, and the caller states which it wants.
|
|
65
|
+
*/
|
|
66
|
+
export function reportOf(
|
|
67
|
+
fills: readonly RecordedFill[],
|
|
68
|
+
marks: readonly BarMark[],
|
|
69
|
+
schedule: ChargeSchedule | null,
|
|
70
|
+
contract: Contract,
|
|
71
|
+
capital: Money,
|
|
72
|
+
): Report {
|
|
73
|
+
const ordered = fills.slice().sort((a, b) => a.seq - b.seq);
|
|
74
|
+
const charges = ordered.map((fill) =>
|
|
75
|
+
schedule === null ? 0 : chargeFor(schedule, fill, contract).total,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const trades = tradesOf(ordered, charges, marks, contract);
|
|
79
|
+
const equity = equityOver(trades, marks, contract, capital);
|
|
80
|
+
const summary = summaryOf(trades, equity, contract, capital);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
summary,
|
|
84
|
+
trades,
|
|
85
|
+
equity,
|
|
86
|
+
monthly: monthlyOver(equity, trades),
|
|
87
|
+
markers: markersOf(ordered, trades),
|
|
88
|
+
};
|
|
89
|
+
}
|