openalgo-script 0.2.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 (357) hide show
  1. package/CHANGELOG.md +1198 -0
  2. package/README.md +127 -40
  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/surfaces.d.ts +17 -0
  12. package/dist/adapters/charts/surfaces.d.ts.map +1 -1
  13. package/dist/adapters/charts/tables.js +82 -0
  14. package/dist/adapters/charts/tables.js.map +1 -1
  15. package/dist/adapters/charts/venue.d.ts +73 -0
  16. package/dist/adapters/charts/venue.d.ts.map +1 -0
  17. package/dist/adapters/charts/venue.js +104 -0
  18. package/dist/adapters/charts/venue.js.map +1 -0
  19. package/dist/adapters/codemirror/commands.d.ts +14 -0
  20. package/dist/adapters/codemirror/commands.d.ts.map +1 -0
  21. package/dist/adapters/codemirror/commands.js +52 -0
  22. package/dist/adapters/codemirror/commands.js.map +1 -0
  23. package/dist/adapters/codemirror/completion.d.ts +15 -0
  24. package/dist/adapters/codemirror/completion.d.ts.map +1 -0
  25. package/dist/adapters/codemirror/completion.js +64 -0
  26. package/dist/adapters/codemirror/completion.js.map +1 -0
  27. package/dist/adapters/codemirror/contract.d.ts +156 -0
  28. package/dist/adapters/codemirror/contract.d.ts.map +1 -0
  29. package/dist/adapters/codemirror/contract.js +42 -0
  30. package/dist/adapters/codemirror/contract.js.map +1 -0
  31. package/dist/adapters/codemirror/index.d.ts +43 -0
  32. package/dist/adapters/codemirror/index.d.ts.map +1 -0
  33. package/dist/adapters/codemirror/index.js +8 -0
  34. package/dist/adapters/codemirror/index.js.map +1 -0
  35. package/dist/adapters/codemirror/lint.d.ts +17 -0
  36. package/dist/adapters/codemirror/lint.d.ts.map +1 -0
  37. package/dist/adapters/codemirror/lint.js +62 -0
  38. package/dist/adapters/codemirror/lint.js.map +1 -0
  39. package/dist/adapters/codemirror/positions.d.ts +16 -0
  40. package/dist/adapters/codemirror/positions.d.ts.map +1 -0
  41. package/dist/adapters/codemirror/positions.js +65 -0
  42. package/dist/adapters/codemirror/positions.js.map +1 -0
  43. package/dist/adapters/codemirror/stream.d.ts +23 -0
  44. package/dist/adapters/codemirror/stream.d.ts.map +1 -0
  45. package/dist/adapters/codemirror/stream.js +70 -0
  46. package/dist/adapters/codemirror/stream.js.map +1 -0
  47. package/dist/adapters/codemirror/tokens.d.ts +44 -0
  48. package/dist/adapters/codemirror/tokens.d.ts.map +1 -0
  49. package/dist/adapters/codemirror/tokens.js +34 -0
  50. package/dist/adapters/codemirror/tokens.js.map +1 -0
  51. package/dist/adapters/codemirror/tooltips.d.ts +35 -0
  52. package/dist/adapters/codemirror/tooltips.d.ts.map +1 -0
  53. package/dist/adapters/codemirror/tooltips.js +108 -0
  54. package/dist/adapters/codemirror/tooltips.js.map +1 -0
  55. package/dist/core/accounting/analysis.d.ts +111 -0
  56. package/dist/core/accounting/analysis.d.ts.map +1 -0
  57. package/dist/core/accounting/analysis.js +123 -0
  58. package/dist/core/accounting/analysis.js.map +1 -0
  59. package/dist/core/accounting/charges.d.ts +136 -0
  60. package/dist/core/accounting/charges.d.ts.map +1 -0
  61. package/dist/core/accounting/charges.js +362 -0
  62. package/dist/core/accounting/charges.js.map +1 -0
  63. package/dist/core/accounting/equity.d.ts +191 -0
  64. package/dist/core/accounting/equity.d.ts.map +1 -0
  65. package/dist/core/accounting/equity.js +167 -0
  66. package/dist/core/accounting/equity.js.map +1 -0
  67. package/dist/core/accounting/index.d.ts +46 -0
  68. package/dist/core/accounting/index.d.ts.map +1 -0
  69. package/dist/core/accounting/index.js +37 -0
  70. package/dist/core/accounting/index.js.map +1 -0
  71. package/dist/core/accounting/markers.d.ts +43 -0
  72. package/dist/core/accounting/markers.d.ts.map +1 -0
  73. package/dist/core/accounting/markers.js +64 -0
  74. package/dist/core/accounting/markers.js.map +1 -0
  75. package/dist/core/accounting/monthly.d.ts +52 -0
  76. package/dist/core/accounting/monthly.d.ts.map +1 -0
  77. package/dist/core/accounting/monthly.js +99 -0
  78. package/dist/core/accounting/monthly.js.map +1 -0
  79. package/dist/core/accounting/report.d.ts +41 -0
  80. package/dist/core/accounting/report.d.ts.map +1 -0
  81. package/dist/core/accounting/report.js +65 -0
  82. package/dist/core/accounting/report.js.map +1 -0
  83. package/dist/core/accounting/shapes.d.ts +70 -0
  84. package/dist/core/accounting/shapes.d.ts.map +1 -0
  85. package/dist/core/accounting/shapes.js +21 -0
  86. package/dist/core/accounting/shapes.js.map +1 -0
  87. package/dist/core/accounting/statistics.d.ts +87 -0
  88. package/dist/core/accounting/statistics.d.ts.map +1 -0
  89. package/dist/core/accounting/statistics.js +268 -0
  90. package/dist/core/accounting/statistics.js.map +1 -0
  91. package/dist/core/accounting/trades.d.ts +118 -0
  92. package/dist/core/accounting/trades.d.ts.map +1 -0
  93. package/dist/core/accounting/trades.js +186 -0
  94. package/dist/core/accounting/trades.js.map +1 -0
  95. package/dist/core/backtest/case.d.ts +60 -0
  96. package/dist/core/backtest/case.d.ts.map +1 -0
  97. package/dist/core/backtest/case.js +319 -0
  98. package/dist/core/backtest/case.js.map +1 -0
  99. package/dist/core/backtest/compare.d.ts +38 -0
  100. package/dist/core/backtest/compare.d.ts.map +1 -0
  101. package/dist/core/backtest/compare.js +191 -0
  102. package/dist/core/backtest/compare.js.map +1 -0
  103. package/dist/core/backtest/declaration.d.ts +52 -0
  104. package/dist/core/backtest/declaration.d.ts.map +1 -0
  105. package/dist/core/backtest/declaration.js +48 -0
  106. package/dist/core/backtest/declaration.js.map +1 -0
  107. package/dist/core/backtest/deliver.d.ts +93 -0
  108. package/dist/core/backtest/deliver.d.ts.map +1 -0
  109. package/dist/core/backtest/deliver.js +94 -0
  110. package/dist/core/backtest/deliver.js.map +1 -0
  111. package/dist/core/backtest/drive.d.ts +87 -0
  112. package/dist/core/backtest/drive.d.ts.map +1 -0
  113. package/dist/core/backtest/drive.js +312 -0
  114. package/dist/core/backtest/drive.js.map +1 -0
  115. package/dist/core/backtest/index.d.ts +58 -0
  116. package/dist/core/backtest/index.d.ts.map +1 -0
  117. package/dist/core/backtest/index.js +47 -0
  118. package/dist/core/backtest/index.js.map +1 -0
  119. package/dist/core/backtest/range.d.ts +84 -0
  120. package/dist/core/backtest/range.d.ts.map +1 -0
  121. package/dist/core/backtest/range.js +90 -0
  122. package/dist/core/backtest/range.js.map +1 -0
  123. package/dist/core/backtest/record.d.ts +299 -0
  124. package/dist/core/backtest/record.d.ts.map +1 -0
  125. package/dist/core/backtest/record.js +243 -0
  126. package/dist/core/backtest/record.js.map +1 -0
  127. package/dist/core/backtest/replay.d.ts +48 -0
  128. package/dist/core/backtest/replay.d.ts.map +1 -0
  129. package/dist/core/backtest/replay.js +137 -0
  130. package/dist/core/backtest/replay.js.map +1 -0
  131. package/dist/core/backtest/resting.d.ts +62 -0
  132. package/dist/core/backtest/resting.d.ts.map +1 -0
  133. package/dist/core/backtest/resting.js +59 -0
  134. package/dist/core/backtest/resting.js.map +1 -0
  135. package/dist/core/backtest/settings.d.ts +117 -0
  136. package/dist/core/backtest/settings.d.ts.map +1 -0
  137. package/dist/core/backtest/settings.js +207 -0
  138. package/dist/core/backtest/settings.js.map +1 -0
  139. package/dist/core/backtest/simulate.d.ts +258 -0
  140. package/dist/core/backtest/simulate.d.ts.map +1 -0
  141. package/dist/core/backtest/simulate.js +335 -0
  142. package/dist/core/backtest/simulate.js.map +1 -0
  143. package/dist/core/catalogue/catalogue.generated.d.ts +66 -0
  144. package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -1
  145. package/dist/core/catalogue/catalogue.generated.js +6 -0
  146. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  147. package/dist/core/catalogue/values.generated.d.ts +24 -0
  148. package/dist/core/catalogue/values.generated.d.ts.map +1 -1
  149. package/dist/core/check/index.d.ts +2 -1
  150. package/dist/core/check/index.d.ts.map +1 -1
  151. package/dist/core/check/index.js +1 -1
  152. package/dist/core/check/index.js.map +1 -1
  153. package/dist/core/check/library-orders.js +2 -2
  154. package/dist/core/check/library-orders.js.map +1 -1
  155. package/dist/core/check/library-prose.generated.d.ts +16 -0
  156. package/dist/core/check/library-prose.generated.d.ts.map +1 -0
  157. package/dist/core/check/library-prose.generated.js +353 -0
  158. package/dist/core/check/library-prose.generated.js.map +1 -0
  159. package/dist/core/check/surface.d.ts +24 -0
  160. package/dist/core/check/surface.d.ts.map +1 -1
  161. package/dist/core/check/surface.js +29 -0
  162. package/dist/core/check/surface.js.map +1 -1
  163. package/dist/core/emit/canonical.d.ts +22 -8
  164. package/dist/core/emit/canonical.d.ts.map +1 -1
  165. package/dist/core/emit/canonical.js +67 -6
  166. package/dist/core/emit/canonical.js.map +1 -1
  167. package/dist/core/emit/defaults.d.ts +35 -16
  168. package/dist/core/emit/defaults.d.ts.map +1 -1
  169. package/dist/core/emit/defaults.js +66 -0
  170. package/dist/core/emit/defaults.js.map +1 -1
  171. package/dist/core/emit/index.d.ts +2 -0
  172. package/dist/core/emit/index.d.ts.map +1 -1
  173. package/dist/core/emit/index.js +2 -0
  174. package/dist/core/emit/index.js.map +1 -1
  175. package/dist/core/engine/arithmetic.d.ts +6 -25
  176. package/dist/core/engine/arithmetic.d.ts.map +1 -1
  177. package/dist/core/engine/arithmetic.js +48 -3
  178. package/dist/core/engine/arithmetic.js.map +1 -1
  179. package/dist/core/engine/index.d.ts +2 -2
  180. package/dist/core/engine/index.d.ts.map +1 -1
  181. package/dist/core/engine/index.js +2 -2
  182. package/dist/core/engine/index.js.map +1 -1
  183. package/dist/core/engine/library/arrays.d.ts.map +1 -1
  184. package/dist/core/engine/library/arrays.js +8 -2
  185. package/dist/core/engine/library/arrays.js.map +1 -1
  186. package/dist/core/engine/library/code-points.d.ts +40 -0
  187. package/dist/core/engine/library/code-points.d.ts.map +1 -0
  188. package/dist/core/engine/library/code-points.js +74 -0
  189. package/dist/core/engine/library/code-points.js.map +1 -0
  190. package/dist/core/engine/library/index.d.ts +5 -0
  191. package/dist/core/engine/library/index.d.ts.map +1 -1
  192. package/dist/core/engine/library/index.js +5 -0
  193. package/dist/core/engine/library/index.js.map +1 -1
  194. package/dist/core/engine/library/text.d.ts.map +1 -1
  195. package/dist/core/engine/library/text.js +51 -34
  196. package/dist/core/engine/library/text.js.map +1 -1
  197. package/dist/core/engine/load.d.ts +35 -1
  198. package/dist/core/engine/load.d.ts.map +1 -1
  199. package/dist/core/engine/load.js +63 -0
  200. package/dist/core/engine/load.js.map +1 -1
  201. package/dist/core/engine/verify-tables.d.ts.map +1 -1
  202. package/dist/core/engine/verify-tables.js +41 -0
  203. package/dist/core/engine/verify-tables.js.map +1 -1
  204. package/dist/core/index.d.ts +43 -4
  205. package/dist/core/index.d.ts.map +1 -1
  206. package/dist/core/index.js +23 -2
  207. package/dist/core/index.js.map +1 -1
  208. package/dist/core/stdlib/index.d.ts +1 -1
  209. package/dist/core/stdlib/index.d.ts.map +1 -1
  210. package/dist/core/stdlib/index.js +1 -1
  211. package/dist/core/stdlib/index.js.map +1 -1
  212. package/dist/core/stdlib/maths/index.d.ts +1 -1
  213. package/dist/core/stdlib/maths/index.d.ts.map +1 -1
  214. package/dist/core/stdlib/maths/index.js +1 -1
  215. package/dist/core/stdlib/maths/index.js.map +1 -1
  216. package/dist/core/stdlib/maths/rounding.d.ts +5 -0
  217. package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
  218. package/dist/core/stdlib/maths/rounding.js +19 -1
  219. package/dist/core/stdlib/maths/rounding.js.map +1 -1
  220. package/dist/core/version/version.generated.d.ts +1 -1
  221. package/dist/core/version/version.generated.js +1 -1
  222. package/dist/editor/complete.d.ts +40 -0
  223. package/dist/editor/complete.d.ts.map +1 -0
  224. package/dist/editor/complete.js +206 -0
  225. package/dist/editor/complete.js.map +1 -0
  226. package/dist/editor/diagnose.d.ts +18 -0
  227. package/dist/editor/diagnose.d.ts.map +1 -0
  228. package/dist/editor/diagnose.js +70 -0
  229. package/dist/editor/diagnose.js.map +1 -0
  230. package/dist/editor/format.d.ts +11 -0
  231. package/dist/editor/format.d.ts.map +1 -0
  232. package/dist/editor/format.js +50 -0
  233. package/dist/editor/format.js.map +1 -0
  234. package/dist/editor/highlight.d.ts +28 -0
  235. package/dist/editor/highlight.d.ts.map +1 -0
  236. package/dist/editor/highlight.js +65 -0
  237. package/dist/editor/highlight.js.map +1 -0
  238. package/dist/editor/hover.d.ts +40 -0
  239. package/dist/editor/hover.d.ts.map +1 -0
  240. package/dist/editor/hover.js +147 -0
  241. package/dist/editor/hover.js.map +1 -0
  242. package/dist/editor/index.d.ts +60 -0
  243. package/dist/editor/index.d.ts.map +1 -0
  244. package/dist/editor/index.js +7 -0
  245. package/dist/editor/index.js.map +1 -0
  246. package/dist/editor/kinds.d.ts +27 -0
  247. package/dist/editor/kinds.d.ts.map +1 -0
  248. package/dist/editor/kinds.js +118 -0
  249. package/dist/editor/kinds.js.map +1 -0
  250. package/dist/editor/layout.d.ts +47 -0
  251. package/dist/editor/layout.d.ts.map +1 -0
  252. package/dist/editor/layout.js +135 -0
  253. package/dist/editor/layout.js.map +1 -0
  254. package/dist/editor/manifest.d.ts +47 -0
  255. package/dist/editor/manifest.d.ts.map +1 -0
  256. package/dist/editor/manifest.js +93 -0
  257. package/dist/editor/manifest.js.map +1 -0
  258. package/dist/editor/reading.d.ts +41 -0
  259. package/dist/editor/reading.d.ts.map +1 -0
  260. package/dist/editor/reading.js +51 -0
  261. package/dist/editor/reading.js.map +1 -0
  262. package/dist/editor/scan.d.ts +26 -0
  263. package/dist/editor/scan.d.ts.map +1 -0
  264. package/dist/editor/scan.js +139 -0
  265. package/dist/editor/scan.js.map +1 -0
  266. package/dist/editor/scope.d.ts +19 -0
  267. package/dist/editor/scope.d.ts.map +1 -0
  268. package/dist/editor/scope.js +99 -0
  269. package/dist/editor/scope.js.map +1 -0
  270. package/dist/editor/signature.d.ts +38 -0
  271. package/dist/editor/signature.d.ts.map +1 -0
  272. package/dist/editor/signature.js +112 -0
  273. package/dist/editor/signature.js.map +1 -0
  274. package/dist/editor/site.d.ts +46 -0
  275. package/dist/editor/site.d.ts.map +1 -0
  276. package/dist/editor/site.js +184 -0
  277. package/dist/editor/site.js.map +1 -0
  278. package/dist/editor/spacing.d.ts +29 -0
  279. package/dist/editor/spacing.d.ts.map +1 -0
  280. package/dist/editor/spacing.js +116 -0
  281. package/dist/editor/spacing.js.map +1 -0
  282. package/package.json +42 -3
  283. package/spec/README.md +2 -1
  284. package/spec/errors.json +141 -1
  285. package/src/adapters/charts/driving.ts +109 -0
  286. package/src/adapters/charts/run.ts +120 -28
  287. package/src/adapters/charts/surfaces.ts +17 -0
  288. package/src/adapters/charts/tables.ts +88 -0
  289. package/src/adapters/charts/venue.ts +132 -0
  290. package/src/adapters/codemirror/commands.ts +53 -0
  291. package/src/adapters/codemirror/completion.ts +74 -0
  292. package/src/adapters/codemirror/contract.ts +156 -0
  293. package/src/adapters/codemirror/index.ts +66 -0
  294. package/src/adapters/codemirror/lint.ts +66 -0
  295. package/src/adapters/codemirror/positions.ts +79 -0
  296. package/src/adapters/codemirror/stream.ts +87 -0
  297. package/src/adapters/codemirror/tokens.ts +64 -0
  298. package/src/adapters/codemirror/tooltips.ts +113 -0
  299. package/src/core/accounting/analysis.ts +188 -0
  300. package/src/core/accounting/charges.ts +452 -0
  301. package/src/core/accounting/equity.ts +314 -0
  302. package/src/core/accounting/index.ts +51 -0
  303. package/src/core/accounting/markers.ts +95 -0
  304. package/src/core/accounting/monthly.ts +137 -0
  305. package/src/core/accounting/report.ts +94 -0
  306. package/src/core/accounting/shapes.ts +73 -0
  307. package/src/core/accounting/statistics.ts +387 -0
  308. package/src/core/accounting/trades.ts +313 -0
  309. package/src/core/backtest/case.ts +395 -0
  310. package/src/core/backtest/compare.ts +246 -0
  311. package/src/core/backtest/declaration.ts +97 -0
  312. package/src/core/backtest/deliver.ts +161 -0
  313. package/src/core/backtest/drive.ts +468 -0
  314. package/src/core/backtest/index.ts +64 -0
  315. package/src/core/backtest/range.ts +137 -0
  316. package/src/core/backtest/record.ts +470 -0
  317. package/src/core/backtest/replay.ts +168 -0
  318. package/src/core/backtest/resting.ts +125 -0
  319. package/src/core/backtest/settings.ts +280 -0
  320. package/src/core/backtest/simulate.ts +499 -0
  321. package/src/core/catalogue/catalogue.generated.ts +6 -0
  322. package/src/core/catalogue/values.generated.ts +6 -0
  323. package/src/core/check/index.ts +3 -0
  324. package/src/core/check/library-orders.ts +2 -2
  325. package/src/core/check/library-prose.generated.ts +367 -0
  326. package/src/core/check/surface.ts +34 -0
  327. package/src/core/emit/canonical.ts +67 -9
  328. package/src/core/emit/defaults.ts +70 -0
  329. package/src/core/emit/index.ts +2 -0
  330. package/src/core/engine/arithmetic.ts +23 -3
  331. package/src/core/engine/index.ts +2 -2
  332. package/src/core/engine/library/arrays.ts +8 -2
  333. package/src/core/engine/library/code-points.ts +73 -0
  334. package/src/core/engine/library/index.ts +6 -0
  335. package/src/core/engine/library/text.ts +53 -35
  336. package/src/core/engine/load.ts +84 -2
  337. package/src/core/engine/verify-tables.ts +43 -0
  338. package/src/core/index.ts +100 -2
  339. package/src/core/stdlib/index.ts +1 -0
  340. package/src/core/stdlib/maths/index.ts +1 -0
  341. package/src/core/stdlib/maths/rounding.ts +23 -1
  342. package/src/core/version/version.generated.ts +1 -1
  343. package/src/editor/complete.ts +266 -0
  344. package/src/editor/diagnose.ts +71 -0
  345. package/src/editor/format.ts +85 -0
  346. package/src/editor/highlight.ts +110 -0
  347. package/src/editor/hover.ts +199 -0
  348. package/src/editor/index.ts +65 -0
  349. package/src/editor/kinds.ts +157 -0
  350. package/src/editor/layout.ts +189 -0
  351. package/src/editor/manifest.ts +119 -0
  352. package/src/editor/reading.ts +69 -0
  353. package/src/editor/scan.ts +162 -0
  354. package/src/editor/scope.ts +122 -0
  355. package/src/editor/signature.ts +150 -0
  356. package/src/editor/site.ts +233 -0
  357. package/src/editor/spacing.ts +132 -0
package/spec/errors.json CHANGED
@@ -2605,6 +2605,99 @@
2605
2605
  "refines": null,
2606
2606
  "test": "tests/engine/verify.test.ts"
2607
2607
  },
2608
+ {
2609
+ "code": "OS6020",
2610
+ "title": "The report window holds no bars",
2611
+ "severity": "error",
2612
+ "stage": "host",
2613
+ "since": 1,
2614
+ "message": "The window from {from} to {to} holds none of the {count} bars supplied.",
2615
+ "placeholders": {
2616
+ "from": "the first moment the window covers, or the first bar supplied where the host named none",
2617
+ "to": "the last moment it covers",
2618
+ "count": "how many bars were supplied"
2619
+ },
2620
+ "cause": "A report is about the bars inside the window the host chose, and a window holding none of them has nothing to report: no equity point, no trade and no summary. The bars outside it are warmup, which execute and whose orders are real, so an empty window is not an empty run, and reporting one as a flat curve would tell a reader that nothing happened when something did.",
2621
+ "fix": "Widen the window until it covers bars, or supply the bars it covers. Both bounds are inclusive and are compared against the times of the bars supplied rather than against a calendar, so a window that falls inside a gap in the data is empty however wide it looks.",
2622
+ "autofix": false,
2623
+ "example": {
2624
+ "kind": "transcript",
2625
+ "before": "bars supplied: 1240, first day 1, last day 1240\nreport window: day 1300 to day 1400, bars inside 0",
2626
+ "after": "bars supplied: 1240, first day 1, last day 1240\nreport window: day 1100 to day 1240, bars inside 141"
2627
+ },
2628
+ "spec": "language.md 7.1",
2629
+ "refines": null,
2630
+ "test": "tests/backtest/range.test.ts"
2631
+ },
2632
+ {
2633
+ "code": "OS6021",
2634
+ "title": "A run setting cannot be applied as stated",
2635
+ "severity": "error",
2636
+ "stage": "host",
2637
+ "since": 1,
2638
+ "message": "{setting} cannot be applied: {problem}.",
2639
+ "placeholders": {
2640
+ "setting": "the setting that cannot be applied, such as the charge schedule or the comparison tolerance",
2641
+ "problem": "what makes it unusable, such as a charge line levied on a line declared after it"
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, 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
+ "autofix": false,
2646
+ "example": {
2647
+ "kind": "transcript",
2648
+ "before": "charge lines: tax on levy, levy\nslippage: 2 ticks, tick size not supplied",
2649
+ "after": "charge lines: levy, tax on levy\nslippage: 2 ticks, tick size 0.05"
2650
+ },
2651
+ "spec": "stdlib.md 17.1",
2652
+ "refines": null,
2653
+ "test": "tests/accounting/schedules.test.ts"
2654
+ },
2655
+ {
2656
+ "code": "OS6022",
2657
+ "title": "The bars are not the bars the record was made from",
2658
+ "severity": "error",
2659
+ "stage": "host",
2660
+ "since": 1,
2661
+ "message": "The bars supplied hash to {found}, and the record was made from {expected}.",
2662
+ "placeholders": {
2663
+ "found": "the hash of the bars supplied now",
2664
+ "expected": "the hash the record names"
2665
+ },
2666
+ "cause": "A record names the bars it was made from by a hash over their canonical form, so a replay can prove it is replaying the same run rather than producing a different study under the same name. Bars are revised: a feed corrects a print, a session is extended, a split is applied to history. A replay over revised bars that reported the original figures would be the most convincing wrong answer this system can produce.",
2667
+ "fix": "Replay against the bars the record names. Where the revision is the point, make a second record over the revised bars and compare the two runs, rather than overwriting one run with the other under one name.",
2668
+ "autofix": false,
2669
+ "example": {
2670
+ "kind": "transcript",
2671
+ "before": "record: bars 1240, hash 9f2c4e...\nsupplied: bars 1240, hash 4ab70d...",
2672
+ "after": "record: bars 1240, hash 9f2c4e...\nsupplied: bars 1240, hash 9f2c4e..."
2673
+ },
2674
+ "spec": "conformance.md 3",
2675
+ "refines": null,
2676
+ "test": "tests/backtest/record.test.ts"
2677
+ },
2678
+ {
2679
+ "code": "OS6023",
2680
+ "title": "Two cost models are stated at once",
2681
+ "severity": "error",
2682
+ "stage": "host",
2683
+ "since": 1,
2684
+ "message": "A charge schedule was supplied, and the declaration states a commission of {commission} in {commissionType}.",
2685
+ "placeholders": {
2686
+ "commission": "the commission the declaration states",
2687
+ "commissionType": "the unit that commission is stated in"
2688
+ },
2689
+ "cause": "The declaration's commission is the script's own statement of what trading costs and a supplied schedule is the platform's, and the two describe the same money. Applied together they charge it twice; applied one at a time they charge whichever an engine happened to prefer, which is a rule nobody wrote down and a figure nobody can explain afterwards. So exactly one of the two is stated for a run, and stating both is refused before the first bar rather than reconciled behind the reader.",
2690
+ "fix": "Supply the schedule and leave the declaration's commission at its default of zero, or state the commission in the declaration and supply no schedule. A schedule is the one of the two that can carry a floor, a cap, a charge levied on a charge, and a cost that falls on one side of the trade only.",
2691
+ "autofix": false,
2692
+ "example": {
2693
+ "kind": "transcript",
2694
+ "before": "declaration: commission 20, per trade\nhost: charge schedule supplied, 4 lines",
2695
+ "after": "declaration: commission 0, per trade\nhost: charge schedule supplied, 4 lines"
2696
+ },
2697
+ "spec": "language.md 13.3; stdlib.md 17.1",
2698
+ "refines": null,
2699
+ "test": "tests/backtest/settings.test.ts"
2700
+ },
2608
2701
  {
2609
2702
  "code": "OS7001",
2610
2703
  "title": "Only a strategy can do that",
@@ -2928,7 +3021,7 @@
2928
3021
  "since": 1,
2929
3022
  "message": "This strategy placed an order and the host supplied no destination.",
2930
3023
  "placeholders": {},
2931
- "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.",
2932
3025
  "fix": "Connect a destination in the host, or run the file as a study(): replace buy() with signal(\"BUY\").",
2933
3026
  "autofix": false,
2934
3027
  "example": {
@@ -2984,6 +3077,53 @@
2984
3077
  "refines": null,
2985
3078
  "test": "tests/engine/closing.test.ts"
2986
3079
  },
3080
+ {
3081
+ "code": "OS7018",
3082
+ "title": "A frame names an order this strategy did not place",
3083
+ "severity": "error",
3084
+ "stage": "host",
3085
+ "since": 1,
3086
+ "message": "The frame names intent {intent}, and this strategy holds no such order.",
3087
+ "placeholders": {
3088
+ "intent": "the intent id the frame named"
3089
+ },
3090
+ "cause": "Step 1 of the fold locates the row a frame is about, and a frame naming no row cannot be folded into anything. It is a fact about the host rather than about the strategy: a destination answering for an order another strategy placed, or answering for a run that has already ended. The frame changes nothing and is reported, because a fold that passed over it in silence would leave a host's mistake invisible to the only party who can correct it.",
3091
+ "fix": "Answer with the intent id the engine sent. A destination's own reference is carried in the frame's reference field, where the engine records it and never parses it, and it is not what an answer is addressed by.",
3092
+ "autofix": false,
3093
+ "example": {
3094
+ "kind": "transcript",
3095
+ "before": "engine sent: intent 7, intent 8\nframe: intent 11, status filled, filled qty 1",
3096
+ "after": "engine sent: intent 7, intent 8\nframe: intent 8, status filled, filled qty 1"
3097
+ },
3098
+ "spec": "stdlib.md 17.8, 17.14",
3099
+ "refines": null,
3100
+ "test": null,
3101
+ "deferred": "Nothing raises this yet. The fold refuses the frame and records the refusal as a word on the outcome it hands back, and a word is not a catalogue code, so nothing reports it against the run and a host answering for orders it was never handed stays invisible. Raised when the outcome of a fold carries a code and a bar carries a channel for a diagnostic that does not stop it, stdlib.md 17.8."
3102
+ },
3103
+ {
3104
+ "code": "OS7019",
3105
+ "title": "A fill was reported with no price",
3106
+ "severity": "error",
3107
+ "stage": "host",
3108
+ "since": 1,
3109
+ "message": "The frame reports {qty} filled for intent {intent}, and no average fill price.",
3110
+ "placeholders": {
3111
+ "qty": "the cumulative filled quantity the frame reports",
3112
+ "intent": "the intent id the frame named"
3113
+ },
3114
+ "cause": "Step 3 of the fold takes the destination's own average over the cumulative quantity, because the engine never averages two averages of its own. A frame reporting more filled than the row holds and no price to take is a fill that can be marked against nothing: no position average, no realised profit, no equity point. It is refused whole rather than folded for its quantity alone, because a position holding a size and no price is worse than no position at all.",
3115
+ "fix": "Report the average fill price the destination computed over the cumulative quantity, on every frame that reports a quantity greater than the last one. A frame carrying no new quantity needs no price.",
3116
+ "autofix": false,
3117
+ "example": {
3118
+ "kind": "transcript",
3119
+ "before": "frame: intent 8, status filled, filled qty 3, average fill price absent",
3120
+ "after": "frame: intent 8, status filled, filled qty 3, average fill price 104.25"
3121
+ },
3122
+ "spec": "stdlib.md 17.8, 17.14",
3123
+ "refines": null,
3124
+ "test": null,
3125
+ "deferred": "Nothing raises this yet. The fold refuses the frame and records the refusal as a word on the outcome it hands back, and a word is not a catalogue code, so a destination reporting a fill that can be marked against nothing is refused in silence. Raised when the outcome of a fold carries a code and a bar carries a channel for a diagnostic that does not stop it, stdlib.md 17.8."
3126
+ },
2987
3127
  {
2988
3128
  "code": "OS8001",
2989
3129
  "title": "A stateful call inside a branch",
@@ -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,
@@ -100,6 +100,23 @@ export interface ChartTableOptions {
100
100
  readonly borderWidth: number;
101
101
  /** The backdrop behind the whole grid, which is the grid's own `bgColor`. */
102
102
  readonly background?: string;
103
+ /**
104
+ * Column widths in media pixels, one per column.
105
+ *
106
+ * The chart's own default is one flat width for every column, which is a
107
+ * width chosen without seeing the text. A grid whose first column says
108
+ * "Moving averages" and whose second says "RSI: 27.22" has two columns that
109
+ * want different room, and one number cannot give it to both.
110
+ */
111
+ readonly cellWidth?: readonly number[];
112
+ /**
113
+ * `'auto'` to shrink a cell's type until its text fits the cell it is in.
114
+ *
115
+ * The chart measures the text it is about to draw, which nothing upstream of
116
+ * it can do, so this is what makes a column width that is slightly wrong
117
+ * merely slightly loose instead of two cells written over each other.
118
+ */
119
+ readonly fontSize?: number | 'auto';
103
120
  }
104
121
 
105
122
  /** A grid of cells pinned to a corner, and the options it is drawn with. */
@@ -118,10 +118,98 @@ function oneTable(
118
118
  position: CORNERS[position] ?? 'top-right',
119
119
  borderWidth: numberField(declared.options.borderWidth, lookup, 0),
120
120
  ...(bgColour === undefined ? {} : { background: cssColour(bgColour) }),
121
+ ...(cols > 0 ? { cellWidth: columnWidths(grid, cols) } : {}),
122
+ // The chart measures the text it is about to draw and shrinks it to fit,
123
+ // which is the half of this that cannot be done from here.
124
+ fontSize: 'auto',
121
125
  },
122
126
  };
123
127
  }
124
128
 
129
+ /**
130
+ * How wide each column has to be, in media pixels.
131
+ *
132
+ * The chart's default is one width for every column, chosen without seeing the
133
+ * text, and a grid whose header says "Moving averages" over cells saying
134
+ * "RSI: 27.22" has one column bleeding into the next. Nothing downstream can
135
+ * choose better, because by the time the chart has the grid it has lost which
136
+ * cells belong together; and nothing upstream can, because the script declares
137
+ * a size and writes text, not a layout.
138
+ *
139
+ * **This is an estimate, and it is allowed to be.** A real width needs the font
140
+ * the chart will draw with, and only the chart has it. So the width is counted
141
+ * off the characters and the chart's own `fontSize: 'auto'` corrects whatever
142
+ * this gets wrong: too narrow and the type shrinks a little, too wide and the
143
+ * column is a little loose. Either is a table that reads. The failure this
144
+ * replaces was neither.
145
+ *
146
+ * Delete it when the chart can size a column from its own measurements. It
147
+ * exists because that is not a thing it can be asked to do yet.
148
+ */
149
+ function columnWidths(grid: readonly (readonly ChartCell[])[], cols: number): number[] {
150
+ const widths: number[] = [];
151
+ for (let col = 0; col < cols; col += 1) {
152
+ let widest = 0;
153
+ for (const row of grid) {
154
+ const text = row[col]?.text ?? '';
155
+ if (text.length > 0) widest = Math.max(widest, textWidth(text));
156
+ }
157
+ widths.push(Math.min(MAX_COLUMN, Math.max(MIN_COLUMN, Math.ceil(widest) + CELL_PADDING * 2)));
158
+ }
159
+ return widths;
160
+ }
161
+
162
+ /**
163
+ * Roughly how wide a string is at the type size the chart starts from.
164
+ *
165
+ * Counted rather than measured, and counted in two weights rather than one: a
166
+ * column of "Histogram: -7.01" is half punctuation and narrow digits, and
167
+ * treating every character as an em-and-a-bit makes that column half again as
168
+ * wide as it needs to be while "Moving averages" stays too narrow. Two weights
169
+ * is not typography, but it is enough to tell those two apart.
170
+ */
171
+ function textWidth(text: string): number {
172
+ let width = 0;
173
+ for (const character of text) {
174
+ width += NARROW.has(character) ? BASE_SIZE * NARROW_EM : BASE_SIZE * WIDE_EM;
175
+ }
176
+ return width;
177
+ }
178
+
179
+ /** The characters that take noticeably less than an average advance. */
180
+ const NARROW = new Set([...' .,:;!|ijlt1IJfr()[]{}-']);
181
+
182
+ /**
183
+ * The type size the widths are counted at.
184
+ *
185
+ * The chart's own default row is 18 media pixels and it draws at 62% of the
186
+ * row, so this is the size a cell gets before anything shrinks it. Counting at
187
+ * a larger size would reserve room the text never uses.
188
+ */
189
+ const BASE_SIZE = 11;
190
+ const NARROW_EM = 0.32;
191
+ const WIDE_EM = 0.56;
192
+
193
+ /** What the chart insets a cell's text by, on each side. */
194
+ const CELL_PADDING = 4;
195
+
196
+ /**
197
+ * The narrowest a column is drawn.
198
+ *
199
+ * A column of one-character cells sized to its content is a sliver, and a grid
200
+ * of slivers reads as a rendering fault rather than as a narrow column.
201
+ */
202
+ const MIN_COLUMN = 56;
203
+
204
+ /**
205
+ * The widest a column is drawn.
206
+ *
207
+ * A cell holding a sentence would otherwise push the grid past the pane it is
208
+ * pinned inside, taking the chart with it. Past this the type shrinks instead,
209
+ * which is the chart's job and it is better at it.
210
+ */
211
+ const MAX_COLUMN = 220;
212
+
125
213
  /**
126
214
  * One written cell, or none.
127
215
  *