openalgo-script 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/README.md +69 -16
  3. package/dist/adapters/charts/driving.d.ts +50 -0
  4. package/dist/adapters/charts/driving.d.ts.map +1 -0
  5. package/dist/adapters/charts/driving.js +57 -0
  6. package/dist/adapters/charts/driving.js.map +1 -0
  7. package/dist/adapters/charts/run.d.ts +20 -0
  8. package/dist/adapters/charts/run.d.ts.map +1 -1
  9. package/dist/adapters/charts/run.js +83 -16
  10. package/dist/adapters/charts/run.js.map +1 -1
  11. package/dist/adapters/charts/venue.d.ts +73 -0
  12. package/dist/adapters/charts/venue.d.ts.map +1 -0
  13. package/dist/adapters/charts/venue.js +104 -0
  14. package/dist/adapters/charts/venue.js.map +1 -0
  15. package/dist/core/accounting/analysis.d.ts +111 -0
  16. package/dist/core/accounting/analysis.d.ts.map +1 -0
  17. package/dist/core/accounting/analysis.js +123 -0
  18. package/dist/core/accounting/analysis.js.map +1 -0
  19. package/dist/core/accounting/equity.d.ts +33 -0
  20. package/dist/core/accounting/equity.d.ts.map +1 -1
  21. package/dist/core/accounting/equity.js +12 -0
  22. package/dist/core/accounting/equity.js.map +1 -1
  23. package/dist/core/accounting/index.d.ts +2 -0
  24. package/dist/core/accounting/index.d.ts.map +1 -1
  25. package/dist/core/accounting/index.js +1 -0
  26. package/dist/core/accounting/index.js.map +1 -1
  27. package/dist/core/accounting/report.d.ts +3 -0
  28. package/dist/core/accounting/report.d.ts.map +1 -1
  29. package/dist/core/accounting/report.js +2 -0
  30. package/dist/core/accounting/report.js.map +1 -1
  31. package/dist/core/accounting/statistics.d.ts +12 -0
  32. package/dist/core/accounting/statistics.d.ts.map +1 -1
  33. package/dist/core/accounting/statistics.js +23 -1
  34. package/dist/core/accounting/statistics.js.map +1 -1
  35. package/dist/core/backtest/case.d.ts +60 -0
  36. package/dist/core/backtest/case.d.ts.map +1 -0
  37. package/dist/core/backtest/case.js +319 -0
  38. package/dist/core/backtest/case.js.map +1 -0
  39. package/dist/core/backtest/compare.d.ts.map +1 -1
  40. package/dist/core/backtest/compare.js +2 -0
  41. package/dist/core/backtest/compare.js.map +1 -1
  42. package/dist/core/backtest/deliver.d.ts +93 -0
  43. package/dist/core/backtest/deliver.d.ts.map +1 -0
  44. package/dist/core/backtest/deliver.js +94 -0
  45. package/dist/core/backtest/deliver.js.map +1 -0
  46. package/dist/core/backtest/drive.d.ts +66 -8
  47. package/dist/core/backtest/drive.d.ts.map +1 -1
  48. package/dist/core/backtest/drive.js +109 -59
  49. package/dist/core/backtest/drive.js.map +1 -1
  50. package/dist/core/backtest/index.d.ts +24 -12
  51. package/dist/core/backtest/index.d.ts.map +1 -1
  52. package/dist/core/backtest/index.js +21 -10
  53. package/dist/core/backtest/index.js.map +1 -1
  54. package/dist/core/backtest/record.d.ts +63 -2
  55. package/dist/core/backtest/record.d.ts.map +1 -1
  56. package/dist/core/backtest/record.js +71 -4
  57. package/dist/core/backtest/record.js.map +1 -1
  58. package/dist/core/backtest/replay.d.ts.map +1 -1
  59. package/dist/core/backtest/replay.js +11 -1
  60. package/dist/core/backtest/replay.js.map +1 -1
  61. package/dist/core/backtest/simulate.d.ts +113 -1
  62. package/dist/core/backtest/simulate.d.ts.map +1 -1
  63. package/dist/core/backtest/simulate.js +123 -5
  64. package/dist/core/backtest/simulate.js.map +1 -1
  65. package/dist/core/catalogue/catalogue.generated.d.ts +1 -1
  66. package/dist/core/catalogue/catalogue.generated.js +1 -1
  67. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  68. package/dist/core/check/library-orders.js +2 -2
  69. package/dist/core/check/library-orders.js.map +1 -1
  70. package/dist/core/check/library-prose.generated.js +2 -2
  71. package/dist/core/check/library-prose.generated.js.map +1 -1
  72. package/dist/core/emit/canonical.d.ts +22 -8
  73. package/dist/core/emit/canonical.d.ts.map +1 -1
  74. package/dist/core/emit/canonical.js +67 -6
  75. package/dist/core/emit/canonical.js.map +1 -1
  76. package/dist/core/engine/arithmetic.d.ts +6 -25
  77. package/dist/core/engine/arithmetic.d.ts.map +1 -1
  78. package/dist/core/engine/arithmetic.js +48 -3
  79. package/dist/core/engine/arithmetic.js.map +1 -1
  80. package/dist/core/engine/index.d.ts +1 -1
  81. package/dist/core/engine/index.d.ts.map +1 -1
  82. package/dist/core/engine/index.js +1 -1
  83. package/dist/core/engine/index.js.map +1 -1
  84. package/dist/core/engine/library/arrays.d.ts.map +1 -1
  85. package/dist/core/engine/library/arrays.js +8 -2
  86. package/dist/core/engine/library/arrays.js.map +1 -1
  87. package/dist/core/engine/library/code-points.d.ts +40 -0
  88. package/dist/core/engine/library/code-points.d.ts.map +1 -0
  89. package/dist/core/engine/library/code-points.js +74 -0
  90. package/dist/core/engine/library/code-points.js.map +1 -0
  91. package/dist/core/engine/library/index.d.ts +5 -0
  92. package/dist/core/engine/library/index.d.ts.map +1 -1
  93. package/dist/core/engine/library/index.js +5 -0
  94. package/dist/core/engine/library/index.js.map +1 -1
  95. package/dist/core/engine/library/text.d.ts.map +1 -1
  96. package/dist/core/engine/library/text.js +51 -34
  97. package/dist/core/engine/library/text.js.map +1 -1
  98. package/dist/core/engine/load.d.ts +20 -0
  99. package/dist/core/engine/load.d.ts.map +1 -1
  100. package/dist/core/engine/load.js +62 -0
  101. package/dist/core/engine/load.js.map +1 -1
  102. package/dist/core/engine/verify-tables.d.ts.map +1 -1
  103. package/dist/core/engine/verify-tables.js +41 -0
  104. package/dist/core/engine/verify-tables.js.map +1 -1
  105. package/dist/core/index.d.ts +10 -5
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js +8 -3
  108. package/dist/core/index.js.map +1 -1
  109. package/dist/core/stdlib/index.d.ts +1 -1
  110. package/dist/core/stdlib/index.d.ts.map +1 -1
  111. package/dist/core/stdlib/index.js +1 -1
  112. package/dist/core/stdlib/index.js.map +1 -1
  113. package/dist/core/stdlib/maths/index.d.ts +1 -1
  114. package/dist/core/stdlib/maths/index.d.ts.map +1 -1
  115. package/dist/core/stdlib/maths/index.js +1 -1
  116. package/dist/core/stdlib/maths/index.js.map +1 -1
  117. package/dist/core/stdlib/maths/rounding.d.ts +5 -0
  118. package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
  119. package/dist/core/stdlib/maths/rounding.js +19 -1
  120. package/dist/core/stdlib/maths/rounding.js.map +1 -1
  121. package/dist/core/version/version.generated.d.ts +1 -1
  122. package/dist/core/version/version.generated.js +1 -1
  123. package/package.json +14 -2
  124. package/spec/README.md +2 -1
  125. package/spec/errors.json +3 -3
  126. package/src/adapters/charts/driving.ts +109 -0
  127. package/src/adapters/charts/run.ts +120 -28
  128. package/src/adapters/charts/venue.ts +132 -0
  129. package/src/core/accounting/analysis.ts +188 -0
  130. package/src/core/accounting/equity.ts +38 -0
  131. package/src/core/accounting/index.ts +2 -0
  132. package/src/core/accounting/report.ts +5 -0
  133. package/src/core/accounting/statistics.ts +38 -1
  134. package/src/core/backtest/case.ts +395 -0
  135. package/src/core/backtest/compare.ts +2 -0
  136. package/src/core/backtest/deliver.ts +161 -0
  137. package/src/core/backtest/drive.ts +175 -71
  138. package/src/core/backtest/index.ts +24 -12
  139. package/src/core/backtest/record.ts +134 -5
  140. package/src/core/backtest/replay.ts +11 -1
  141. package/src/core/backtest/simulate.ts +201 -6
  142. package/src/core/catalogue/catalogue.generated.ts +1 -1
  143. package/src/core/check/library-orders.ts +2 -2
  144. package/src/core/check/library-prose.generated.ts +2 -2
  145. package/src/core/emit/canonical.ts +67 -9
  146. package/src/core/engine/arithmetic.ts +23 -3
  147. package/src/core/engine/index.ts +1 -1
  148. package/src/core/engine/library/arrays.ts +8 -2
  149. package/src/core/engine/library/code-points.ts +73 -0
  150. package/src/core/engine/library/index.ts +6 -0
  151. package/src/core/engine/library/text.ts +53 -35
  152. package/src/core/engine/load.ts +68 -0
  153. package/src/core/engine/verify-tables.ts +43 -0
  154. package/src/core/index.ts +16 -2
  155. package/src/core/stdlib/index.ts +1 -0
  156. package/src/core/stdlib/maths/index.ts +1 -0
  157. package/src/core/stdlib/maths/rounding.ts +23 -1
  158. package/src/core/version/version.generated.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalgo-script",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "An open trading language. Write a study or a strategy once, plot it, backtest it, trade it.",
5
5
  "keywords": [
6
6
  "trading",
@@ -62,11 +62,23 @@
62
62
  "check:reproducible": "npm run build && node --disallow-code-generation-from-strings scripts/check-reproducible.mjs",
63
63
  "check:section-20": "node --disallow-code-generation-from-strings scripts/check-section-20.mjs",
64
64
  "check:entry-points": "npm run build && node --disallow-code-generation-from-strings scripts/check-entry-points.mjs",
65
- "test": "node --disallow-code-generation-from-strings scripts/check-duplication.mjs && node --disallow-code-generation-from-strings scripts/check-error-codes.mjs && node --disallow-code-generation-from-strings scripts/check-examples.mjs && node --disallow-code-generation-from-strings scripts/check-catalogue-tests.mjs && node --disallow-code-generation-from-strings scripts/check-layering.mjs && node --disallow-code-generation-from-strings scripts/check-limits.mjs && node --disallow-code-generation-from-strings scripts/check-modularity.mjs && node --disallow-code-generation-from-strings scripts/check-names.mjs && node --disallow-code-generation-from-strings scripts/check-raises.mjs && node --disallow-code-generation-from-strings scripts/check-section-20.mjs && npm run build && node --disallow-code-generation-from-strings scripts/check-defaults.mjs && node --disallow-code-generation-from-strings scripts/check-entry-points.mjs && node --disallow-code-generation-from-strings scripts/check-chart-surface.mjs && npm run test:unit && node --disallow-code-generation-from-strings scripts/check-reproducible.mjs && node --disallow-code-generation-from-strings scripts/check-examples-compile.mjs && node --disallow-code-generation-from-strings scripts/check-no-eval.mjs && npm run bench",
65
+ "check:number-writer": "node --disallow-code-generation-from-strings scripts/check-number-writer.mjs",
66
+ "check:python": "node --disallow-code-generation-from-strings scripts/check-python.mjs",
67
+ "check:format-tables": "npm run build && node --disallow-code-generation-from-strings scripts/check-format-tables.mjs",
68
+ "check:format-additive": "npm run build && node --disallow-code-generation-from-strings scripts/check-format-additive.mjs",
69
+ "check:format-corpus": "npm run build && node --disallow-code-generation-from-strings scripts/check-format-corpus.mjs",
70
+ "check:colour-channels": "npm run build && node --disallow-code-generation-from-strings scripts/check-colour-channels.mjs",
71
+ "check:vectors": "npm run build && npm run build:test && node --disallow-code-generation-from-strings scripts/check-library-vectors.mjs",
72
+ "suite": "npm run build && node --disallow-code-generation-from-strings scripts/run-suite.mjs",
73
+ "suite:engine": "npm run build && node --disallow-code-generation-from-strings scripts/run-suite.mjs --adapter engine/adapter.mjs",
74
+ "suite:agree": "node --disallow-code-generation-from-strings scripts/run-suite.mjs --adapter scripts/adapter.mjs --against engine/adapter.mjs",
75
+ "generate:vectors": "npm run build && npm run build:test && node --disallow-code-generation-from-strings scripts/generate-library-vectors.mjs",
76
+ "test": "node --disallow-code-generation-from-strings scripts/check-duplication.mjs && node --disallow-code-generation-from-strings scripts/check-error-codes.mjs && node --disallow-code-generation-from-strings scripts/check-examples.mjs && node --disallow-code-generation-from-strings scripts/check-catalogue-tests.mjs && node --disallow-code-generation-from-strings scripts/check-layering.mjs && node --disallow-code-generation-from-strings scripts/check-limits.mjs && node --disallow-code-generation-from-strings scripts/check-matrix.mjs && node --disallow-code-generation-from-strings scripts/check-modularity.mjs && node --disallow-code-generation-from-strings scripts/check-names.mjs && node --disallow-code-generation-from-strings scripts/check-raises.mjs && node --disallow-code-generation-from-strings scripts/check-section-20.mjs && node --disallow-code-generation-from-strings scripts/check-number-writer.mjs && node --disallow-code-generation-from-strings scripts/check-python.mjs && npm run build && node --disallow-code-generation-from-strings scripts/check-defaults.mjs && node --disallow-code-generation-from-strings scripts/check-entry-points.mjs && node --disallow-code-generation-from-strings scripts/check-chart-surface.mjs && npm run test:unit && node --disallow-code-generation-from-strings scripts/check-library-vectors.mjs && node --disallow-code-generation-from-strings scripts/check-reproducible.mjs && node --disallow-code-generation-from-strings scripts/check-format-tables.mjs && node --disallow-code-generation-from-strings scripts/check-format-additive.mjs && node --disallow-code-generation-from-strings scripts/check-format-corpus.mjs && node --disallow-code-generation-from-strings scripts/check-colour-channels.mjs && node --disallow-code-generation-from-strings scripts/check-examples-compile.mjs && node --disallow-code-generation-from-strings scripts/harvest-cases.mjs --check && npm run suite:agree && node --disallow-code-generation-from-strings scripts/check-no-eval.mjs && npm run bench",
66
77
  "test:unit": "npm run build:test && node --disallow-code-generation-from-strings scripts/run-tests.mjs",
67
78
  "check:layering": "node --disallow-code-generation-from-strings scripts/check-layering.mjs",
68
79
  "check:duplication": "node --disallow-code-generation-from-strings scripts/check-duplication.mjs",
69
80
  "check:limits": "node --disallow-code-generation-from-strings scripts/check-limits.mjs",
81
+ "check:matrix": "node --disallow-code-generation-from-strings scripts/check-matrix.mjs",
70
82
  "check:modularity": "node --disallow-code-generation-from-strings scripts/check-modularity.mjs",
71
83
  "generate:errors": "node --disallow-code-generation-from-strings scripts/generate-error-catalogue.mjs",
72
84
  "typecheck": "npm run generate && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
package/spec/README.md CHANGED
@@ -13,13 +13,14 @@ round.
13
13
  | [`host-interface.md`](./host-interface.md) | Everything a platform supplies so an engine can run, and everything the engine hands back: the boundary |
14
14
  | [`conformance.md`](./conformance.md) | How the suite is run, and what a passing result entitles an implementation to claim |
15
15
 
16
- Six documents, and the list above is the whole of it. Two more files sit beside
16
+ Six documents, and the list above is the whole of it. Three more sit beside
17
17
  them and are not the specification:
18
18
 
19
19
  | File | Holds |
20
20
  |---|---|
21
21
  | [`decisions.md`](./decisions.md) | The minutes of every settled cross-document question, and the home register that says which document owns each fact |
22
22
  | [`feature-matrix.md`](./feature-matrix.md) | One row per feature: whether it is specified, implemented or planned, the section that defines it, and the test that proves it |
23
+ | [`vectors/`](./vectors/) | Bit patterns a second engine holds itself to without reading this repository's source: `number-text.json`, the boundary cases of `language.md` 5.5 as bit patterns, decimals and text, and `library/`, one file per arithmetic function of the manifest with inputs and results as binary64 bit patterns |
23
24
 
24
25
  `errors.json` is `errors.md` in machine form, generated from the same entries.
25
26
  It is not a document of its own, and a change to one is the same change to the
package/spec/errors.json CHANGED
@@ -2640,8 +2640,8 @@
2640
2640
  "setting": "the setting that cannot be applied, such as the charge schedule or the comparison tolerance",
2641
2641
  "problem": "what makes it unusable, such as a charge line levied on a line declared after it"
2642
2642
  },
2643
- "cause": "A run states the settings it is carried out under before its first bar, and a setting that cannot be carried out is refused there rather than quietly producing a figure. A charge line levied on lines not declared before it has no single evaluation order, so two engines would charge two different amounts and both would be defensible. A slippage stated in ticks with no tick size to measure a tick in would charge nothing at all, which is a backtest that lies in the strategy's favour. A comparison tolerance carrying a bound and no reason is a failed comparison somebody switched off.",
2644
- "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.",
2643
+ "cause": "A run states the settings it is carried out under before its first bar, and a setting that cannot be carried out is refused there rather than quietly producing a figure. A charge line levied on lines not declared before it has no single evaluation order, so two engines would charge two different amounts and both would be defensible. A slippage stated in ticks with no tick size to measure a tick in would charge nothing at all, which is a backtest that lies in the strategy's favour. A comparison tolerance carrying a bound and no reason is a failed comparison somebody switched off, and one past the cap conformance.md section 6 puts on a declared tolerance is a comparison the suite will not accept, so a run that declared one is refused a case rather than written into one that fails every runner it meets.",
2644
+ "fix": "State the setting so it can be carried out: declare a charge line after every line it is levied on, supply the tick size a slippage in ticks is measured in, write down the reason a tolerance needs a bound, or bring a bound past the cap back inside it. Nothing has been computed at the point this is refused, so correcting the setting and running again costs one run.",
2645
2645
  "autofix": false,
2646
2646
  "example": {
2647
2647
  "kind": "transcript",
@@ -3021,7 +3021,7 @@
3021
3021
  "since": 1,
3022
3022
  "message": "This strategy placed an order and the host supplied no destination.",
3023
3023
  "placeholders": {},
3024
- "cause": "A strategy needs somewhere for orders to go: a paper engine, a backtest simulator or a broker connection. Running one with no destination would compute a position nothing ever took.",
3024
+ "cause": "A strategy needs somewhere for orders to go: a sandbox engine, a backtest simulator or a broker connection. Running one with no destination would compute a position nothing ever took.",
3025
3025
  "fix": "Connect a destination in the host, or run the file as a study(): replace buy() with signal(\"BUY\").",
3026
3026
  "autofix": false,
3027
3027
  "example": {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Walking the bars, for a study and for a strategy.
3
+ *
4
+ * Split from `run.ts` because they are two things. That file decides what an
5
+ * engine is loaded with: the host record, the clock, the settings, the venue.
6
+ * This one decides how the bars are then handed over, and the two differ by
7
+ * exactly one thing, whether frames have to reach the engine between them.
8
+ */
9
+
10
+ import type { CompiledProgram } from '../../core/emit/index.js';
11
+ import type { Engine } from '../../core/engine/index.js';
12
+ import { hostBar, stateFor } from './bars.js';
13
+ import type { ChartBar, ChartCalcContext, ChartSettings } from './contract.js';
14
+ import { stopped } from './errors.js';
15
+ import { signatureOf } from './settings.js';
16
+ import { answersFor } from './venue.js';
17
+ import type { HeldVenue } from './venue.js';
18
+
19
+ /**
20
+ * A run in hand, held between a chart's recomputes.
21
+ *
22
+ * The signature and the bar times are what a tail run is checked against before
23
+ * it is trusted: splicing a tail onto a history that has changed underneath it
24
+ * draws a wrong study that looks entirely plausible.
25
+ */
26
+ export interface Held {
27
+ readonly engine: Engine;
28
+ readonly signature: string;
29
+ count: number;
30
+ firstTime: number;
31
+ lastTime: number;
32
+ /**
33
+ * The destination this run's orders go to, for a strategy.
34
+ *
35
+ * Held with the engine because the two are one run: the venue holds the
36
+ * orders that have not filled yet, and an engine continued onto a new bar
37
+ * against a fresh venue would have its working orders silently forgotten.
38
+ */
39
+ readonly venue: HeldVenue | null;
40
+ }
41
+
42
+ /** A study: every bar in one hand-over, which is what `run` is for. */
43
+ export function runWhole(
44
+ engine: Engine,
45
+ bars: readonly ChartBar[],
46
+ ctx: ChartCalcContext | undefined,
47
+ program: CompiledProgram,
48
+ settings: ChartSettings,
49
+ ): Held {
50
+ const result = engine.run(
51
+ bars.map(hostBar),
52
+ bars.map((_, index) => stateFor(index, bars, ctx)),
53
+ );
54
+ if (result.diagnostic !== undefined) throw stopped(result.diagnostic);
55
+ return heldFrom(engine, null, bars, program, settings);
56
+ }
57
+
58
+ /**
59
+ * A strategy: one bar at a time, with the venue answering between them.
60
+ *
61
+ * The order is deliver, execute, then ask. A driver that asked the venue before
62
+ * executing would price a fill against a bar the strategy had not seen yet, and
63
+ * one that delivered after executing would let a script read a position its own
64
+ * order on this bar had just created.
65
+ *
66
+ * `supplied` is the whole count rather than the index reached, so `bar.isLast`
67
+ * means the same on this path as on the one above it: a strategy written to act
68
+ * on the final bar acts on the final bar, not on every bar in turn.
69
+ */
70
+ export function walkWith(
71
+ engine: Engine,
72
+ held: HeldVenue,
73
+ bars: readonly ChartBar[],
74
+ ctx: ChartCalcContext | undefined,
75
+ program: CompiledProgram,
76
+ settings: ChartSettings,
77
+ ): Held {
78
+ for (let index = 0; index < bars.length; index += 1) {
79
+ const bar = bars[index];
80
+ if (bar === undefined) continue;
81
+
82
+ for (const frame of held.pending) engine.deliver(frame);
83
+ held.pending = [];
84
+
85
+ const result = engine.append(hostBar(bar), stateFor(index, bars, ctx), bars.length);
86
+ if (result.diagnostic !== undefined) throw stopped(result.diagnostic);
87
+
88
+ answersFor(held, index);
89
+ }
90
+ return heldFrom(engine, held, bars, program, settings);
91
+ }
92
+
93
+ function heldFrom(
94
+ engine: Engine,
95
+ venue: HeldVenue | null,
96
+ bars: readonly ChartBar[],
97
+ program: CompiledProgram,
98
+ settings: ChartSettings,
99
+ ): Held {
100
+ return {
101
+ engine,
102
+ venue,
103
+ signature: signatureOf(program, settings),
104
+ count: bars.length,
105
+ firstTime: bars[0]?.time ?? 0,
106
+ lastTime: bars[bars.length - 1]?.time ?? 0,
107
+ };
108
+ }
109
+
@@ -41,6 +41,11 @@ import type { ChartBar, ChartCalcContext, ChartSettings, ChartStore } from './co
41
41
  import { refused, stopped } from './errors.js';
42
42
  import { stationIn, stationOf } from './requests.js';
43
43
  import { engineSettings, signatureOf } from './settings.js';
44
+ import { answersFor, needsVenue, routeInto, venueFor } from './venue.js';
45
+ import type { HeldVenue } from './venue.js';
46
+ import { runWhole, walkWith } from './driving.js';
47
+ import type { Held } from './driving.js';
48
+ import type { Simulator } from '../../core/backtest/index.js';
44
49
 
45
50
  /** What a host tells the adapter that neither the chart nor the program says. */
46
51
  export interface ChartAdapterOptions {
@@ -116,6 +121,26 @@ export interface ChartAdapterOptions {
116
121
  * (`stdlib.md` 17.1), never handed over by the chart.
117
122
  */
118
123
  readonly orders?: EffectRoute;
124
+ /**
125
+ * Run a strategy against a simulated destination where the host gives none.
126
+ *
127
+ * **Off by default, because the refusal it replaces is useful.** A strategy
128
+ * with nowhere to send an order is refused at load with OS6006 naming the
129
+ * capability, which is exactly what a host that meant to wire a destination
130
+ * and forgot needs to be told. A chart that quietly filled one in for them
131
+ * would draw a convincing strategy that routed nothing.
132
+ *
133
+ * **On, it is the venue a backtest uses.** A chart drawing a strategy then
134
+ * fills the same orders at the same prices as the report of the same script,
135
+ * so the marks on the price and the trades in the report are one answer
136
+ * rather than two. This is what a host turns on to draw a strategy the way it
137
+ * draws a study: the plots, the legend row and the settings all follow from
138
+ * the program running at all.
139
+ *
140
+ * It does not place an order anywhere. A host that wants that supplies
141
+ * `orders`, which wins over this.
142
+ */
143
+ readonly simulateOrders?: boolean;
119
144
  readonly limits?: Partial<EngineLimits>;
120
145
  readonly clock?: Clock;
121
146
  /**
@@ -130,14 +155,6 @@ export interface ChartAdapterOptions {
130
155
  }
131
156
 
132
157
  /** The engine one chart instance is holding, between recomputes. */
133
- interface Held {
134
- readonly engine: Engine;
135
- readonly signature: string;
136
- count: number;
137
- firstTime: number;
138
- lastTime: number;
139
- }
140
-
141
158
  /** The store key. Namespaced, because the store belongs to the host as well. */
142
159
  const HELD = 'openscript';
143
160
 
@@ -168,20 +185,20 @@ export function fullRun(
168
185
  options: ChartAdapterOptions,
169
186
  ): RunOutput {
170
187
  const station = stationIn(store);
171
- const engine = start(program, settings, ctx, options, station.provider(bars));
172
- const result = engine.run(
173
- bars.map(hostBar),
174
- bars.map((_, index) => stateFor(index, bars, ctx)),
175
- );
176
- if (result.diagnostic !== undefined) throw stopped(result.diagnostic);
188
+ const started = start(program, settings, ctx, options, station.provider(bars), bars);
189
+ const engine = started.engine;
177
190
 
178
- const held: Held = {
179
- engine,
180
- signature: signatureOf(program, settings),
181
- count: bars.length,
182
- firstTime: bars[0]?.time ?? 0,
183
- lastTime: bars[bars.length - 1]?.time ?? 0,
184
- };
191
+ // **A strategy is walked bar by bar and a study is handed the lot.** The
192
+ // difference is the venue: its frames reach the engine between bars, which is
193
+ // the only place they can, so a strategy's position is right on the bar after
194
+ // the one it traded on. `run()` has no gap to put them in.
195
+ //
196
+ // It is also the loop the backtest uses, which is the point: a chart drawing a
197
+ // strategy and a report of the same strategy walk the same bars in the same
198
+ // order against the same venue, so they cannot disagree about what filled.
199
+ const held: Held = started.venue === null
200
+ ? runWhole(engine, bars, ctx, program, settings)
201
+ : walkWith(engine, started.venue, bars, ctx, program, settings);
185
202
  store[HELD] = held;
186
203
  station.settle();
187
204
  return outputOf(program, engine);
@@ -218,11 +235,29 @@ export function tailRun(
218
235
  const bar = bars[index];
219
236
  if (bar === undefined) return null;
220
237
  const state = stateFor(index, bars, ctx);
238
+
239
+ // The venue's answers about the bar before this one, delivered before this
240
+ // one runs, exactly as the full walk does. A tail that skipped this would
241
+ // draw the first bars of a strategy correctly and then quietly stop folding
242
+ // its fills the moment the chart went live, which is the half of the run
243
+ // nobody re-checks.
244
+ //
245
+ // Only when the bar is new. Re-executing the bar that moved must not
246
+ // deliver again: a frame is cumulative and folding one twice is harmless,
247
+ // but the bar's own orders have been rolled back and answering them a
248
+ // second time would fill an order the engine no longer knows it sent.
249
+ if (held.venue !== null && index !== from) {
250
+ for (const frame of held.venue.pending) held.engine.deliver(frame);
251
+ held.venue.pending = [];
252
+ }
253
+
221
254
  const result =
222
255
  index === from
223
256
  ? held.engine.update(hostBar(bar), state)
224
257
  : held.engine.append(hostBar(bar), state);
225
258
  if (result.diagnostic !== undefined) throw stopped(result.diagnostic);
259
+
260
+ if (held.venue !== null) answersFor(held.venue, index);
226
261
  }
227
262
 
228
263
  held.count = bars.length;
@@ -257,23 +292,65 @@ function outputOf(program: CompiledProgram, engine: Engine): RunOutput {
257
292
  return { columns, tables: engine.tables(), drawings: engine.drawings() };
258
293
  }
259
294
 
295
+ /** A loaded engine, and the destination its orders go to where it has one. */
296
+ interface Started {
297
+ readonly engine: Engine;
298
+ readonly venue: HeldVenue | null;
299
+ }
300
+
301
+ /**
302
+ * Load the program, and give a strategy somewhere for its orders to go.
303
+ *
304
+ * **The venue is built after the load and reached through a holder, because
305
+ * neither can come first.** The route has to be handed to `load`, since that is
306
+ * what declares the `orders` capability and a program needing one is otherwise
307
+ * refused. The venue has to be built after it, because the declaration it reads
308
+ * may state its fill rule through an `input()`, and inputs are not resolved
309
+ * until `load` has run. The route is only ever called from inside an execution,
310
+ * which is after both, so the holder is always filled by the time anything
311
+ * reaches it.
312
+ *
313
+ * **A host that supplied its own route keeps it.** Somewhere real to send an
314
+ * order is a better destination than a simulated one, and a host that wired one
315
+ * up meant it.
316
+ *
317
+ * **And simulation is asked for rather than assumed.** Without
318
+ * `simulateOrders` a strategy with nowhere to send an order is still refused at
319
+ * load, which is the answer a host that meant to wire a destination and forgot
320
+ * needs to see. Filling in a venue for them would turn that mistake into a
321
+ * chart that draws convincingly and routes nothing, discovered whenever
322
+ * somebody next looked for the orders.
323
+ */
260
324
  function start(
261
325
  program: CompiledProgram,
262
326
  settings: ChartSettings,
263
327
  ctx: ChartCalcContext | undefined,
264
328
  options: ChartAdapterOptions,
265
329
  requests: RequestProvider,
266
- ): Engine {
330
+ bars: readonly ChartBar[],
331
+ ): Started {
332
+ const holder: { current: Simulator | null } = { current: null };
333
+ const simulate = options.simulateOrders === true && options.orders === undefined && needsVenue(program);
334
+ const withRoute: ChartAdapterOptions = simulate
335
+ ? { ...options, orders: routeInto(holder) }
336
+ : options;
337
+
267
338
  const loaded = load(program, {
268
339
  settings: engineSettings(program, settings),
269
- host: hostFor(ctx, options, requests),
340
+ host: hostFor(ctx, withRoute, requests),
270
341
  time: timeFor(options, ctx?.timezone ?? ''),
271
342
  ...(options.limits === undefined ? {} : { limits: options.limits }),
272
343
  ...(options.source === undefined ? {} : { source: options.source }),
273
344
  ...(options.clock === undefined ? {} : { clock: options.clock }),
274
345
  });
275
346
  if (!loaded.ok) throw refused(loaded.diagnostic);
276
- return loaded.engine;
347
+
348
+ if (!simulate) return { engine: loaded.engine, venue: null };
349
+
350
+ const venue = venueFor(program, bars, loaded.inputs, instrumentFor(ctx, options));
351
+ if (venue === null) return { engine: loaded.engine, venue: null };
352
+ holder.current = venue;
353
+ return { engine: loaded.engine, venue: { venue, pending: [] } };
277
354
  }
278
355
 
279
356
  /**
@@ -286,12 +363,19 @@ function start(
286
363
  * the chart's own sentence in `req.error`, instead of being refused at load
287
364
  * with OS6006 naming a capability the chart does have.
288
365
  */
289
- function hostFor(
366
+ /**
367
+ * The instrument record, from what the host stated and what the chart knows.
368
+ *
369
+ * Its own function because two callers need it and they must not each build
370
+ * one: the engine is loaded with this record, and the venue prices fills
371
+ * against the tick size in it. Two spellings of the same record is how a fill
372
+ * gets rounded to a tick the strategy was never told about.
373
+ */
374
+ function instrumentFor(
290
375
  ctx: ChartCalcContext | undefined,
291
376
  options: ChartAdapterOptions,
292
- requests: RequestProvider,
293
- ): EngineHost {
294
- const instrument: Instrument = {
377
+ ): Instrument {
378
+ return {
295
379
  ...(options.instrument ?? {}),
296
380
  ...(ctx?.symbol === undefined ? {} : { symbol: ctx.symbol }),
297
381
  ...(ctx?.interval === undefined ? {} : { interval: ctx.interval }),
@@ -302,6 +386,14 @@ function hostFor(
302
386
  // the chart it was drawn on.
303
387
  ...(ctx === undefined || ctx.timezone === '' ? {} : { timezone: ctx.timezone }),
304
388
  };
389
+ }
390
+
391
+ function hostFor(
392
+ ctx: ChartCalcContext | undefined,
393
+ options: ChartAdapterOptions,
394
+ requests: RequestProvider,
395
+ ): EngineHost {
396
+ const instrument = instrumentFor(ctx, options);
305
397
  return {
306
398
  instrument,
307
399
  requestBars: requests,
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The destination a strategy runs against when a chart draws one.
3
+ *
4
+ * **Why a chart needs one at all.** A strategy asks a destination to do things
5
+ * and folds its position from what comes back. Handed no destination it is
6
+ * refused at load with OS6006, and handed one that answers nothing it runs but
7
+ * never learns it is in a position: every `close()` closes nothing, every entry
8
+ * is allowed again on the next signal, and the study draws a strategy that is
9
+ * wrong about itself while looking entirely normal. Measured on a stop and
10
+ * reverse script, that shape produced five buys and no sells.
11
+ *
12
+ * **It is the backtest's own venue, not a second one.** `Simulator` is what
13
+ * `backtest()` runs against, so a chart running a strategy through it fills the
14
+ * same orders at the same prices as the report the trader ran a moment ago. A
15
+ * venue written here to be simpler would be a second answer to "what would this
16
+ * have filled at", and the two would disagree the first time either changed:
17
+ * the chart would draw one set of trades and the report would list another.
18
+ *
19
+ * **What a chart supplies and what it invents.** The bars, the instrument and
20
+ * the declaration are the chart's own and are passed through. Money is not: a
21
+ * chart draws a strategy, it does not report one, so the currency, the point
22
+ * value and the rounding are stated here as the neutral values that make the
23
+ * accounting a no-op. Nothing a chart draws reads them, and a chart that
24
+ * guessed a point value would put a wrong profit in front of somebody.
25
+ *
26
+ * **Nothing here decides when a fill happens.** The declaration does, through
27
+ * `fillOn`, and the venue reads it. A market order priced at the next bar's
28
+ * open is known at that open; one that rested and traded inside a bar is known
29
+ * once the bar is complete. Both are what a venue could have told anybody at
30
+ * the time, and that is the whole reason the frames arrive between bars rather
31
+ * than inside the execution that sent them.
32
+ */
33
+
34
+ import { DEFAULT_FILL, Simulator, declarationOf } from '../../core/backtest/index.js';
35
+ import type { RecordedBar } from '../../core/backtest/index.js';
36
+ import type { CompiledProgram } from '../../core/emit/index.js';
37
+ import type { Instrument, OrderFrame, ResolvedInput, RoutedEffect } from '../../core/engine/index.js';
38
+ import type { ChartBar } from './contract.js';
39
+
40
+ /**
41
+ * Whether this program needs a destination before it can run at all.
42
+ *
43
+ * Read from the program's own declared capabilities rather than from whether
44
+ * its source said `strategy()`, because `requires` is the same fact the engine
45
+ * tests at load. A strategy that places no orders needs none, and anything that
46
+ * does need one is refused without it.
47
+ */
48
+ export function needsVenue(program: CompiledProgram): boolean {
49
+ return program.requires.includes('orders');
50
+ }
51
+
52
+ /** One chart bar as the venue reads it. */
53
+ function recorded(bar: ChartBar): RecordedBar {
54
+ return {
55
+ time: bar.time,
56
+ open: bar.open,
57
+ high: bar.high,
58
+ low: bar.low,
59
+ close: bar.close,
60
+ volume: bar.volume ?? null,
61
+ oi: bar.oi ?? null,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * The venue for one chart run, or nothing where the program does not need one.
67
+ *
68
+ * `inputs` are the ones the engine resolved at load, because a declaration
69
+ * field may be written as an `input()`: a script whose `fillOn` comes from a
70
+ * setting would otherwise be filled the way its source happened to be written
71
+ * rather than the way the trader is running it.
72
+ */
73
+ export function venueFor(
74
+ program: CompiledProgram,
75
+ bars: readonly ChartBar[],
76
+ inputs: readonly ResolvedInput[],
77
+ instrument: Instrument | undefined,
78
+ ): Simulator | null {
79
+ if (!needsVenue(program)) return null;
80
+
81
+ const declared = declarationOf(program, inputs);
82
+
83
+ return new Simulator({
84
+ bars: bars.map(recorded),
85
+ contract: {
86
+ symbol: instrument?.symbol ?? null,
87
+ exchange: instrument?.exchange ?? null,
88
+ // Neutral money. A chart draws a strategy and does not report one, so
89
+ // nothing it puts on screen reads these; stating a point value a host
90
+ // never gave would be inventing the size of somebody's position.
91
+ currency: declared.currency,
92
+ tickSize: instrument?.tickSize ?? null,
93
+ lotSize: null,
94
+ pointValue: 1,
95
+ digits: 2,
96
+ },
97
+ fill: DEFAULT_FILL,
98
+ // The declaration's own, so slippage is applied the way the backtest
99
+ // applies it. Ticks, against the instrument's tick size.
100
+ slippageTicks: declared.slippage,
101
+ fillOn: declared.fillOn,
102
+ qtyType: declared.qtyType,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * A venue that can be asked for frames, held across a chart's recomputes.
108
+ *
109
+ * The bars a `Simulator` prices against are the ones it was built with, so a
110
+ * chart that has grown a bar needs a venue that knows about it. `extend` is how
111
+ * the tail path says so without rebuilding the orders it is already holding.
112
+ */
113
+ export interface HeldVenue {
114
+ readonly venue: Simulator;
115
+ /** Frames the venue answered for a bar, waiting to be delivered before the next. */
116
+ pending: readonly OrderFrame[];
117
+ }
118
+
119
+ /** What a venue has to say after one bar, which the next bar's engine reads. */
120
+ export function answersFor(held: HeldVenue, barIndex: number): void {
121
+ held.pending = held.venue.framesFor(barIndex);
122
+ }
123
+
124
+ /** The route an engine is loaded with, pointing at a venue built after it. */
125
+ export function routeInto(holder: { current: Simulator | null }): (effect: RoutedEffect, bar: number) => void {
126
+ // Late bound on purpose. The route is handed to `load`, and the venue cannot
127
+ // be built until `load` has resolved the inputs the declaration reads. The
128
+ // route is only ever called from inside an execution, which is after both.
129
+ return (effect: RoutedEffect, bar: number) => {
130
+ holder.current?.route(effect, bar);
131
+ };
132
+ }