openalgo-script 0.4.0 → 0.6.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 (359) hide show
  1. package/CHANGELOG.md +1050 -0
  2. package/README.md +111 -27
  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 +89 -16
  10. package/dist/adapters/charts/run.js.map +1 -1
  11. package/dist/adapters/charts/tables.d.ts +6 -19
  12. package/dist/adapters/charts/tables.d.ts.map +1 -1
  13. package/dist/adapters/charts/tables.js.map +1 -1
  14. package/dist/adapters/charts/undrawable.d.ts +4 -0
  15. package/dist/adapters/charts/undrawable.d.ts.map +1 -0
  16. package/dist/adapters/charts/undrawable.js +47 -0
  17. package/dist/adapters/charts/undrawable.js.map +1 -0
  18. package/dist/adapters/charts/venue.d.ts +73 -0
  19. package/dist/adapters/charts/venue.d.ts.map +1 -0
  20. package/dist/adapters/charts/venue.js +104 -0
  21. package/dist/adapters/charts/venue.js.map +1 -0
  22. package/dist/core/accounting/analysis.d.ts +111 -0
  23. package/dist/core/accounting/analysis.d.ts.map +1 -0
  24. package/dist/core/accounting/analysis.js +123 -0
  25. package/dist/core/accounting/analysis.js.map +1 -0
  26. package/dist/core/accounting/equity.d.ts +33 -0
  27. package/dist/core/accounting/equity.d.ts.map +1 -1
  28. package/dist/core/accounting/equity.js +12 -0
  29. package/dist/core/accounting/equity.js.map +1 -1
  30. package/dist/core/accounting/index.d.ts +2 -0
  31. package/dist/core/accounting/index.d.ts.map +1 -1
  32. package/dist/core/accounting/index.js +1 -0
  33. package/dist/core/accounting/index.js.map +1 -1
  34. package/dist/core/accounting/report.d.ts +3 -0
  35. package/dist/core/accounting/report.d.ts.map +1 -1
  36. package/dist/core/accounting/report.js +2 -0
  37. package/dist/core/accounting/report.js.map +1 -1
  38. package/dist/core/accounting/statistics.d.ts +12 -0
  39. package/dist/core/accounting/statistics.d.ts.map +1 -1
  40. package/dist/core/accounting/statistics.js +23 -1
  41. package/dist/core/accounting/statistics.js.map +1 -1
  42. package/dist/core/backtest/case.d.ts +60 -0
  43. package/dist/core/backtest/case.d.ts.map +1 -0
  44. package/dist/core/backtest/case.js +319 -0
  45. package/dist/core/backtest/case.js.map +1 -0
  46. package/dist/core/backtest/compare.d.ts.map +1 -1
  47. package/dist/core/backtest/compare.js +2 -0
  48. package/dist/core/backtest/compare.js.map +1 -1
  49. package/dist/core/backtest/deliver.d.ts +93 -0
  50. package/dist/core/backtest/deliver.d.ts.map +1 -0
  51. package/dist/core/backtest/deliver.js +94 -0
  52. package/dist/core/backtest/deliver.js.map +1 -0
  53. package/dist/core/backtest/drive.d.ts +101 -8
  54. package/dist/core/backtest/drive.d.ts.map +1 -1
  55. package/dist/core/backtest/drive.js +114 -158
  56. package/dist/core/backtest/drive.js.map +1 -1
  57. package/dist/core/backtest/index.d.ts +26 -12
  58. package/dist/core/backtest/index.d.ts.map +1 -1
  59. package/dist/core/backtest/index.js +21 -10
  60. package/dist/core/backtest/index.js.map +1 -1
  61. package/dist/core/backtest/record.d.ts +63 -2
  62. package/dist/core/backtest/record.d.ts.map +1 -1
  63. package/dist/core/backtest/record.js +71 -4
  64. package/dist/core/backtest/record.js.map +1 -1
  65. package/dist/core/backtest/replay.d.ts.map +1 -1
  66. package/dist/core/backtest/replay.js +11 -1
  67. package/dist/core/backtest/replay.js.map +1 -1
  68. package/dist/core/backtest/simulate.d.ts +113 -1
  69. package/dist/core/backtest/simulate.d.ts.map +1 -1
  70. package/dist/core/backtest/simulate.js +123 -5
  71. package/dist/core/backtest/simulate.js.map +1 -1
  72. package/dist/core/backtest/walk.d.ts +39 -0
  73. package/dist/core/backtest/walk.d.ts.map +1 -0
  74. package/dist/core/backtest/walk.js +125 -0
  75. package/dist/core/backtest/walk.js.map +1 -0
  76. package/dist/core/catalogue/catalogue.generated.d.ts +179 -2
  77. package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -1
  78. package/dist/core/catalogue/catalogue.generated.js +19 -2
  79. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  80. package/dist/core/catalogue/types.d.ts +8 -2
  81. package/dist/core/catalogue/types.d.ts.map +1 -1
  82. package/dist/core/catalogue/values.generated.d.ts +51 -0
  83. package/dist/core/catalogue/values.generated.d.ts.map +1 -1
  84. package/dist/core/check/assigned.d.ts +19 -0
  85. package/dist/core/check/assigned.d.ts.map +1 -0
  86. package/dist/core/check/assigned.js +14 -0
  87. package/dist/core/check/assigned.js.map +1 -0
  88. package/dist/core/check/call-sites.d.ts.map +1 -1
  89. package/dist/core/check/call-sites.js +9 -4
  90. package/dist/core/check/call-sites.js.map +1 -1
  91. package/dist/core/check/calls.d.ts.map +1 -1
  92. package/dist/core/check/calls.js +12 -4
  93. package/dist/core/check/calls.js.map +1 -1
  94. package/dist/core/check/check.d.ts.map +1 -1
  95. package/dist/core/check/check.js +15 -2
  96. package/dist/core/check/check.js.map +1 -1
  97. package/dist/core/check/checker.d.ts +26 -0
  98. package/dist/core/check/checker.d.ts.map +1 -1
  99. package/dist/core/check/checker.js +38 -2
  100. package/dist/core/check/checker.js.map +1 -1
  101. package/dist/core/check/constant.d.ts +16 -0
  102. package/dist/core/check/constant.d.ts.map +1 -1
  103. package/dist/core/check/constant.js +52 -0
  104. package/dist/core/check/constant.js.map +1 -1
  105. package/dist/core/check/deleted.d.ts +3 -0
  106. package/dist/core/check/deleted.d.ts.map +1 -0
  107. package/dist/core/check/deleted.js +109 -0
  108. package/dist/core/check/deleted.js.map +1 -0
  109. package/dist/core/check/expressions.js +1 -1
  110. package/dist/core/check/expressions.js.map +1 -1
  111. package/dist/core/check/library-orders.js +2 -2
  112. package/dist/core/check/library-orders.js.map +1 -1
  113. package/dist/core/check/library-output.d.ts.map +1 -1
  114. package/dist/core/check/library-output.js +1 -0
  115. package/dist/core/check/library-output.js.map +1 -1
  116. package/dist/core/check/library-prose.generated.js +12 -12
  117. package/dist/core/check/library-prose.generated.js.map +1 -1
  118. package/dist/core/check/library.d.ts +7 -0
  119. package/dist/core/check/library.d.ts.map +1 -1
  120. package/dist/core/check/library.js +1 -0
  121. package/dist/core/check/library.js.map +1 -1
  122. package/dist/core/check/surface.d.ts +9 -0
  123. package/dist/core/check/surface.d.ts.map +1 -1
  124. package/dist/core/check/surface.js +13 -0
  125. package/dist/core/check/surface.js.map +1 -1
  126. package/dist/core/emit/canonical.d.ts +22 -8
  127. package/dist/core/emit/canonical.d.ts.map +1 -1
  128. package/dist/core/emit/canonical.js +67 -6
  129. package/dist/core/emit/canonical.js.map +1 -1
  130. package/dist/core/engine/arithmetic.d.ts +6 -25
  131. package/dist/core/engine/arithmetic.d.ts.map +1 -1
  132. package/dist/core/engine/arithmetic.js +48 -3
  133. package/dist/core/engine/arithmetic.js.map +1 -1
  134. package/dist/core/engine/bar-source.d.ts +65 -0
  135. package/dist/core/engine/bar-source.d.ts.map +1 -0
  136. package/dist/core/engine/bar-source.js +58 -0
  137. package/dist/core/engine/bar-source.js.map +1 -0
  138. package/dist/core/engine/engine.d.ts +9 -11
  139. package/dist/core/engine/engine.d.ts.map +1 -1
  140. package/dist/core/engine/engine.js +24 -24
  141. package/dist/core/engine/engine.js.map +1 -1
  142. package/dist/core/engine/guard.d.ts.map +1 -1
  143. package/dist/core/engine/guard.js +10 -0
  144. package/dist/core/engine/guard.js.map +1 -1
  145. package/dist/core/engine/host.d.ts +4 -2
  146. package/dist/core/engine/host.d.ts.map +1 -1
  147. package/dist/core/engine/host.js.map +1 -1
  148. package/dist/core/engine/index.d.ts +2 -1
  149. package/dist/core/engine/index.d.ts.map +1 -1
  150. package/dist/core/engine/index.js +1 -1
  151. package/dist/core/engine/index.js.map +1 -1
  152. package/dist/core/engine/library/arrays.d.ts.map +1 -1
  153. package/dist/core/engine/library/arrays.js +8 -2
  154. package/dist/core/engine/library/arrays.js.map +1 -1
  155. package/dist/core/engine/library/binding.d.ts +2 -0
  156. package/dist/core/engine/library/binding.d.ts.map +1 -1
  157. package/dist/core/engine/library/binding.js.map +1 -1
  158. package/dist/core/engine/library/code-points.d.ts +40 -0
  159. package/dist/core/engine/library/code-points.d.ts.map +1 -0
  160. package/dist/core/engine/library/code-points.js +74 -0
  161. package/dist/core/engine/library/code-points.js.map +1 -0
  162. package/dist/core/engine/library/index.d.ts +13 -0
  163. package/dist/core/engine/library/index.d.ts.map +1 -1
  164. package/dist/core/engine/library/index.js +15 -0
  165. package/dist/core/engine/library/index.js.map +1 -1
  166. package/dist/core/engine/library/objects.d.ts.map +1 -1
  167. package/dist/core/engine/library/objects.js +1 -1
  168. package/dist/core/engine/library/objects.js.map +1 -1
  169. package/dist/core/engine/library/text.d.ts.map +1 -1
  170. package/dist/core/engine/library/text.js +51 -34
  171. package/dist/core/engine/library/text.js.map +1 -1
  172. package/dist/core/engine/load.d.ts +20 -0
  173. package/dist/core/engine/load.d.ts.map +1 -1
  174. package/dist/core/engine/load.js +62 -0
  175. package/dist/core/engine/load.js.map +1 -1
  176. package/dist/core/engine/request-body.d.ts +3 -1
  177. package/dist/core/engine/request-body.d.ts.map +1 -1
  178. package/dist/core/engine/request-body.js +3 -1
  179. package/dist/core/engine/request-body.js.map +1 -1
  180. package/dist/core/engine/request-plan.d.ts +3 -3
  181. package/dist/core/engine/request-plan.js +3 -3
  182. package/dist/core/engine/requests.d.ts +2 -2
  183. package/dist/core/engine/requests.d.ts.map +1 -1
  184. package/dist/core/engine/requests.js +5 -4
  185. package/dist/core/engine/requests.js.map +1 -1
  186. package/dist/core/engine/series.d.ts +18 -6
  187. package/dist/core/engine/series.d.ts.map +1 -1
  188. package/dist/core/engine/series.js +24 -5
  189. package/dist/core/engine/series.js.map +1 -1
  190. package/dist/core/engine/session/record.d.ts +4 -4
  191. package/dist/core/engine/session/record.js +4 -4
  192. package/dist/core/engine/verify-tables.d.ts.map +1 -1
  193. package/dist/core/engine/verify-tables.js +41 -0
  194. package/dist/core/engine/verify-tables.js.map +1 -1
  195. package/dist/core/importer/calls.d.ts +29 -0
  196. package/dist/core/importer/calls.d.ts.map +1 -0
  197. package/dist/core/importer/calls.js +267 -0
  198. package/dist/core/importer/calls.js.map +1 -0
  199. package/dist/core/importer/context.d.ts +118 -0
  200. package/dist/core/importer/context.d.ts.map +1 -0
  201. package/dist/core/importer/context.js +120 -0
  202. package/dist/core/importer/context.js.map +1 -0
  203. package/dist/core/importer/expressions.d.ts +44 -0
  204. package/dist/core/importer/expressions.d.ts.map +1 -0
  205. package/dist/core/importer/expressions.js +223 -0
  206. package/dist/core/importer/expressions.js.map +1 -0
  207. package/dist/core/importer/importer.d.ts +13 -0
  208. package/dist/core/importer/importer.d.ts.map +1 -0
  209. package/dist/core/importer/importer.js +220 -0
  210. package/dist/core/importer/importer.js.map +1 -0
  211. package/dist/core/importer/index.d.ts +20 -0
  212. package/dist/core/importer/index.d.ts.map +1 -0
  213. package/dist/core/importer/index.js +18 -0
  214. package/dist/core/importer/index.js.map +1 -0
  215. package/dist/core/importer/lexer.d.ts +68 -0
  216. package/dist/core/importer/lexer.d.ts.map +1 -0
  217. package/dist/core/importer/lexer.js +207 -0
  218. package/dist/core/importer/lexer.js.map +1 -0
  219. package/dist/core/importer/names.d.ts +41 -0
  220. package/dist/core/importer/names.d.ts.map +1 -0
  221. package/dist/core/importer/names.js +78 -0
  222. package/dist/core/importer/names.js.map +1 -0
  223. package/dist/core/importer/orders.d.ts +8 -0
  224. package/dist/core/importer/orders.d.ts.map +1 -0
  225. package/dist/core/importer/orders.js +332 -0
  226. package/dist/core/importer/orders.js.map +1 -0
  227. package/dist/core/importer/outputs.d.ts +19 -0
  228. package/dist/core/importer/outputs.d.ts.map +1 -0
  229. package/dist/core/importer/outputs.js +266 -0
  230. package/dist/core/importer/outputs.js.map +1 -0
  231. package/dist/core/importer/parser.d.ts +5 -0
  232. package/dist/core/importer/parser.d.ts.map +1 -0
  233. package/dist/core/importer/parser.js +439 -0
  234. package/dist/core/importer/parser.js.map +1 -0
  235. package/dist/core/importer/presence.d.ts +32 -0
  236. package/dist/core/importer/presence.d.ts.map +1 -0
  237. package/dist/core/importer/presence.js +146 -0
  238. package/dist/core/importer/presence.js.map +1 -0
  239. package/dist/core/importer/settle.d.ts +23 -0
  240. package/dist/core/importer/settle.d.ts.map +1 -0
  241. package/dist/core/importer/settle.js +135 -0
  242. package/dist/core/importer/settle.js.map +1 -0
  243. package/dist/core/importer/statements.d.ts +4 -0
  244. package/dist/core/importer/statements.d.ts.map +1 -0
  245. package/dist/core/importer/statements.js +339 -0
  246. package/dist/core/importer/statements.js.map +1 -0
  247. package/dist/core/importer/syntax.d.ts +182 -0
  248. package/dist/core/importer/syntax.d.ts.map +1 -0
  249. package/dist/core/importer/syntax.js +14 -0
  250. package/dist/core/importer/syntax.js.map +1 -0
  251. package/dist/core/importer/table.d.ts +52 -0
  252. package/dist/core/importer/table.d.ts.map +1 -0
  253. package/dist/core/importer/table.js +156 -0
  254. package/dist/core/importer/table.js.map +1 -0
  255. package/dist/core/importer/values.d.ts +27 -0
  256. package/dist/core/importer/values.d.ts.map +1 -0
  257. package/dist/core/importer/values.js +236 -0
  258. package/dist/core/importer/values.js.map +1 -0
  259. package/dist/core/index.d.ts +22 -5
  260. package/dist/core/index.d.ts.map +1 -1
  261. package/dist/core/index.js +19 -3
  262. package/dist/core/index.js.map +1 -1
  263. package/dist/core/stdlib/calendar/civil.d.ts.map +1 -1
  264. package/dist/core/stdlib/calendar/civil.js +5 -2
  265. package/dist/core/stdlib/calendar/civil.js.map +1 -1
  266. package/dist/core/stdlib/index.d.ts +1 -1
  267. package/dist/core/stdlib/index.d.ts.map +1 -1
  268. package/dist/core/stdlib/index.js +1 -1
  269. package/dist/core/stdlib/index.js.map +1 -1
  270. package/dist/core/stdlib/maths/index.d.ts +1 -1
  271. package/dist/core/stdlib/maths/index.d.ts.map +1 -1
  272. package/dist/core/stdlib/maths/index.js +1 -1
  273. package/dist/core/stdlib/maths/index.js.map +1 -1
  274. package/dist/core/stdlib/maths/rounding.d.ts +5 -0
  275. package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
  276. package/dist/core/stdlib/maths/rounding.js +19 -1
  277. package/dist/core/stdlib/maths/rounding.js.map +1 -1
  278. package/dist/core/version/version.generated.d.ts +1 -1
  279. package/dist/core/version/version.generated.js +1 -1
  280. package/package.json +17 -2
  281. package/spec/README.md +2 -1
  282. package/spec/errors.json +383 -23
  283. package/src/adapters/charts/driving.ts +109 -0
  284. package/src/adapters/charts/run.ts +126 -28
  285. package/src/adapters/charts/tables.ts +6 -19
  286. package/src/adapters/charts/undrawable.ts +51 -0
  287. package/src/adapters/charts/venue.ts +132 -0
  288. package/src/core/accounting/analysis.ts +188 -0
  289. package/src/core/accounting/equity.ts +38 -0
  290. package/src/core/accounting/index.ts +2 -0
  291. package/src/core/accounting/report.ts +5 -0
  292. package/src/core/accounting/statistics.ts +38 -1
  293. package/src/core/backtest/case.ts +395 -0
  294. package/src/core/backtest/compare.ts +2 -0
  295. package/src/core/backtest/deliver.ts +161 -0
  296. package/src/core/backtest/drive.ts +233 -217
  297. package/src/core/backtest/index.ts +26 -12
  298. package/src/core/backtest/record.ts +134 -5
  299. package/src/core/backtest/replay.ts +11 -1
  300. package/src/core/backtest/simulate.ts +201 -6
  301. package/src/core/backtest/walk.ts +182 -0
  302. package/src/core/catalogue/catalogue.generated.ts +19 -2
  303. package/src/core/catalogue/types.ts +8 -2
  304. package/src/core/catalogue/values.generated.ts +16 -0
  305. package/src/core/check/assigned.ts +31 -0
  306. package/src/core/check/call-sites.ts +11 -4
  307. package/src/core/check/calls.ts +13 -4
  308. package/src/core/check/check.ts +14 -2
  309. package/src/core/check/checker.ts +39 -2
  310. package/src/core/check/constant.ts +57 -0
  311. package/src/core/check/deleted.ts +135 -0
  312. package/src/core/check/expressions.ts +1 -1
  313. package/src/core/check/library-orders.ts +2 -2
  314. package/src/core/check/library-output.ts +1 -0
  315. package/src/core/check/library-prose.generated.ts +12 -12
  316. package/src/core/check/library.ts +8 -0
  317. package/src/core/check/surface.ts +16 -0
  318. package/src/core/emit/canonical.ts +67 -9
  319. package/src/core/engine/arithmetic.ts +23 -3
  320. package/src/core/engine/bar-source.ts +113 -0
  321. package/src/core/engine/engine.ts +26 -25
  322. package/src/core/engine/guard.ts +12 -1
  323. package/src/core/engine/host.ts +4 -2
  324. package/src/core/engine/index.ts +2 -1
  325. package/src/core/engine/library/arrays.ts +8 -2
  326. package/src/core/engine/library/binding.ts +2 -0
  327. package/src/core/engine/library/code-points.ts +73 -0
  328. package/src/core/engine/library/index.ts +17 -0
  329. package/src/core/engine/library/objects.ts +1 -6
  330. package/src/core/engine/library/text.ts +53 -35
  331. package/src/core/engine/load.ts +68 -0
  332. package/src/core/engine/request-body.ts +5 -7
  333. package/src/core/engine/request-plan.ts +3 -3
  334. package/src/core/engine/requests.ts +11 -8
  335. package/src/core/engine/series.ts +30 -6
  336. package/src/core/engine/session/record.ts +4 -4
  337. package/src/core/engine/verify-tables.ts +43 -0
  338. package/src/core/importer/calls.ts +274 -0
  339. package/src/core/importer/context.ts +197 -0
  340. package/src/core/importer/expressions.ts +247 -0
  341. package/src/core/importer/importer.ts +226 -0
  342. package/src/core/importer/index.ts +19 -0
  343. package/src/core/importer/lexer.ts +285 -0
  344. package/src/core/importer/names.ts +104 -0
  345. package/src/core/importer/orders.ts +307 -0
  346. package/src/core/importer/outputs.ts +245 -0
  347. package/src/core/importer/parser.ts +436 -0
  348. package/src/core/importer/presence.ts +164 -0
  349. package/src/core/importer/settle.ts +141 -0
  350. package/src/core/importer/statements.ts +331 -0
  351. package/src/core/importer/syntax.ts +144 -0
  352. package/src/core/importer/table.ts +191 -0
  353. package/src/core/importer/values.ts +241 -0
  354. package/src/core/index.ts +29 -2
  355. package/src/core/stdlib/calendar/civil.ts +5 -2
  356. package/src/core/stdlib/index.ts +1 -0
  357. package/src/core/stdlib/maths/index.ts +1 -0
  358. package/src/core/stdlib/maths/rounding.ts +23 -1
  359. package/src/core/version/version.generated.ts +1 -1
@@ -26,9 +26,9 @@
26
26
  */
27
27
  import type { RecordedFill, Report } from '../accounting/index.js';
28
28
  import type { Diagnostic } from '../diagnostics/index.js';
29
- import { canonicalise, programHash, sha256 } from '../emit/index.js';
29
+ import { canonicalNumber, canonicalise, programHash, sha256, sourceHash } from '../emit/index.js';
30
30
  import type { CompiledProgram, SourceStamp } from '../emit/index.js';
31
- import type { LedgerRow } from '../engine/index.js';
31
+ import type { Instrument, LedgerRow } from '../engine/index.js';
32
32
  import { VERSION } from '../version/index.js';
33
33
  import type { BacktestSettings } from './settings.js';
34
34
 
@@ -75,6 +75,20 @@ export interface RecordedFrame {
75
75
  readonly avgFillPrice: number | null;
76
76
  readonly orderRef: string | null;
77
77
  readonly text: string | null;
78
+ /**
79
+ * The destination's own instant for this frame, or null where it stated none.
80
+ *
81
+ * **One field of the ledger is folded from it.** `host-interface.md` 7.2
82
+ * gives a frame a `time` and `stdlib.md` 17.7 moves `updatedAt` to it, so a
83
+ * record that dropped it wrote a ledger no engine reading the case back could
84
+ * fold to: handed frames with no instant, that engine leaves every
85
+ * `updatedAt` at `placedAt`, and the two disagree on exactly the rows whose
86
+ * destination answered later than the bar that placed the order.
87
+ *
88
+ * Absent rather than substituted, because 7.2 lets a destination state none
89
+ * and a row whose frame stated none keeps the instant it had.
90
+ */
91
+ readonly time: number | null;
78
92
  }
79
93
 
80
94
  /** A ledger row of `stdlib.md` 17.7, flattened: the `orders` channel of expected.json. */
@@ -121,7 +135,43 @@ export interface RunRecord {
121
135
  readonly programHash: string;
122
136
  /** Hash, lines, file: the script revision. */
123
137
  readonly source: SourceStamp;
138
+ /**
139
+ * The source text itself, or null on a record written before version 2.
140
+ *
141
+ * **A record is the conformance case, and a case has to hold `script.os`.**
142
+ * `source` above identifies the script and cannot reproduce it: a hash is a
143
+ * fingerprint, so it settles whether two files are the same and yields
144
+ * neither of them. Without the text a stored record could be replayed months
145
+ * later and still not be handed to anybody else to run, which is the whole
146
+ * claim the suite exists to test.
147
+ *
148
+ * It lives here and not on the compiled program on purpose. A program is
149
+ * executable data that no engine needs the source to run, and it is the
150
+ * versioned artefact adopters depend on; putting the text there would send a
151
+ * script everywhere its program travels and widen the format every engine
152
+ * has to read. The record is the thing that wants to be self-contained.
153
+ */
154
+ readonly sourceText: string | null;
124
155
  readonly settings: BacktestSettings;
156
+ /**
157
+ * The instrument record of `host-interface.md` 4.1, as the engine was handed
158
+ * it, or null on a record written before version 3.
159
+ *
160
+ * **A case holds `instrument.json`, and `conformance.md` section 2 says that
161
+ * file is this record.** The contract in `settings` is the money layer's
162
+ * snapshot of the same instrument and holds six of the twelve facts; the
163
+ * ones a script reads and the money never does, the interval, the timezone,
164
+ * the session and the volume flag, were handed to the engine and written
165
+ * down nowhere. A case harvested from such a record either omitted a fact
166
+ * the page requires or stated one the run never had, and either way the
167
+ * second engine ran a different study than the one the expected output
168
+ * came from.
169
+ *
170
+ * It is the whole record and not the six facts beside the contract, because
171
+ * what is recorded is what the engine read at load, verbatim, and a reader
172
+ * should not have to compose it.
173
+ */
174
+ readonly instrument: Instrument | null;
125
175
  readonly bars: BarsInRecord;
126
176
  /** What the destination answered, in delivery order. */
127
177
  readonly frames: readonly RecordedFrame[];
@@ -141,7 +191,22 @@ export interface RunRecord {
141
191
  * file alone. It moves when a channel is added or a meaning changes, never when
142
192
  * a figure in a report does.
143
193
  */
144
- export const RECORD_VERSION = 1;
194
+ export const RECORD_VERSION = 4;
195
+
196
+ /**
197
+ * The revision each later channel arrived in.
198
+ *
199
+ * A record written before a channel existed reads with that channel absent,
200
+ * and this table is what `recordFromJson` reads it from: one row per channel
201
+ * added since version 1, so the rule for an old record is stated once and
202
+ * grows by a line when the next channel does.
203
+ *
204
+ * `frameTime` is a field of a row rather than a channel of the document, and it
205
+ * is a row here for the same reason the other two are: what a reader has to
206
+ * know is which revision it arrived in. Where the absence is written differs,
207
+ * and that is the reader's business below, not this table's.
208
+ */
209
+ const ADDED_IN = { sourceText: 2, instrument: 3, frameTime: 4 } as const;
145
210
 
146
211
  /** What this engine calls itself in a record it wrote. */
147
212
  const ENGINE_NAME = 'openscript';
@@ -149,7 +214,18 @@ const ENGINE_NAME = 'openscript';
149
214
  /** The parts a run hands over, each already in the shape the record holds. */
150
215
  export interface RecordParts {
151
216
  readonly program: CompiledProgram;
217
+ /**
218
+ * The script's own text, so the record can become a conformance case.
219
+ *
220
+ * Optional because a caller that has only a compiled program cannot invent
221
+ * it, and a run is still worth recording without it. `caseFilesFrom` is the
222
+ * one thing that then cannot be served, and it says so rather than writing a
223
+ * case with a hole in it.
224
+ */
225
+ readonly sourceText?: string;
152
226
  readonly settings: BacktestSettings;
227
+ /** The instrument record the engine was handed, `host-interface.md` 4.1. */
228
+ readonly instrument: Instrument;
153
229
  readonly bars: readonly RecordedBar[];
154
230
  /**
155
231
  * Whether the bars travel in the record or are pointed at.
@@ -180,11 +256,13 @@ export function recordOf(parts: RecordParts): RunRecord {
180
256
  return {
181
257
  recordVersion: RECORD_VERSION,
182
258
  engine: { name: ENGINE_NAME, version: VERSION },
183
- languageVersion: String(parts.program.openscript.language),
259
+ languageVersion: canonicalNumber(parts.program.openscript.language),
184
260
  program: parts.program,
185
261
  programHash: programHash(parts.program),
186
262
  source: parts.program.source,
263
+ sourceText: textFor(parts),
187
264
  settings: parts.settings,
265
+ instrument: parts.instrument,
188
266
  bars: barsIn(parts.bars, parts.form),
189
267
  frames: parts.frames,
190
268
  fills: parts.fills,
@@ -194,6 +272,26 @@ export function recordOf(parts: RecordParts): RunRecord {
194
272
  };
195
273
  }
196
274
 
275
+ /**
276
+ * The source text, checked against the hash the program already carries.
277
+ *
278
+ * A check rather than a promise, and it costs one hash of a few kilobytes. The
279
+ * failure it exists for is quiet: a caller that passes the text of a different
280
+ * revision than the one it compiled produces a case whose script does not make
281
+ * its own expected output, and the engine being tested gets the blame for a
282
+ * disagreement that was in the case all along.
283
+ */
284
+ function textFor(parts: RecordParts): string | null {
285
+ const text = parts.sourceText;
286
+ if (text === undefined) return null;
287
+ if (sourceHash(text) !== parts.program.source.hash) {
288
+ throw new Error(
289
+ 'openscript: the source text does not hash to the source hash the program carries',
290
+ );
291
+ }
292
+ return text;
293
+ }
294
+
197
295
  /**
198
296
  * The bars, held or pointed at, and the same hash either way.
199
297
  *
@@ -283,7 +381,38 @@ export function recordFromJson(text: string): RunRecord | null {
283
381
  const parsed: unknown = JSON.parse(text);
284
382
  if (parsed === null || typeof parsed !== 'object') return null;
285
383
  const record = parsed as RunRecord;
286
- return record.recordVersion === RECORD_VERSION ? record : null;
384
+ const version = record.recordVersion;
385
+ if (typeof version !== 'number' || version < 1 || version > RECORD_VERSION) return null;
386
+ // An earlier revision is readable and a later one is not, and the asymmetry
387
+ // is the point. A later revision may mean something by a field this one
388
+ // thinks it knows, which is how a record silently becomes a different run. An
389
+ // earlier one only ever has fewer: every channel it carries means here what
390
+ // it meant there, and the ones added since are absent rather than wrong. So a
391
+ // run stored months ago still reads, which is the whole of what it was stored
392
+ // for, and a channel it never carried reads as absent.
393
+ if (version === RECORD_VERSION) return record;
394
+ return {
395
+ ...record,
396
+ sourceText: version >= ADDED_IN.sourceText ? record.sourceText : null,
397
+ instrument: version >= ADDED_IN.instrument ? record.instrument : null,
398
+ frames: version >= ADDED_IN.frameTime ? record.frames : timeless(record.frames),
399
+ };
400
+ }
401
+
402
+ /**
403
+ * The frames of a record written before a frame carried an instant.
404
+ *
405
+ * The absence is written on every frame rather than on the record, because the
406
+ * channel is a field of a row: a reader that left the field undefined would
407
+ * hand the case projection an undefined where the column's absent spelling
408
+ * belongs, and the file would read `undefined` back to the next engine. The
409
+ * frames are left as they are when they are not an array, because this is a
410
+ * parse and not a validation and a document this engine did not write is the
411
+ * caller's to trust or not.
412
+ */
413
+ function timeless(frames: readonly RecordedFrame[]): readonly RecordedFrame[] {
414
+ if (!Array.isArray(frames)) return frames;
415
+ return frames.map((frame) => ({ ...frame, time: null }));
287
416
  }
288
417
 
289
418
  /**
@@ -88,7 +88,17 @@ export function replay(record: RunRecord, bars: readonly RecordedBar[] | null =
88
88
  export function rerun(record: RunRecord, bars: readonly RecordedBar[] | null = null): BacktestResult {
89
89
  const held = barsOf(record, bars);
90
90
  if (!held.ok) return { ok: false, diagnostic: held.diagnostic };
91
- return backtest(record.program, held.bars, record.settings, { form: record.bars.form });
91
+ // Everything the run depended on, and that includes the two channels that
92
+ // arrived after the sentence above was written. The instrument record is what
93
+ // the engine read at load, so a rerun handed the contract alone runs under
94
+ // different session facts and does not reproduce the bytes; the text is what
95
+ // makes the rerun's record harvestable, and dropping it would turn a record
96
+ // that could become a case into one that cannot, by being rerun.
97
+ return backtest(record.program, held.bars, record.settings, {
98
+ form: record.bars.form,
99
+ ...(record.instrument === null ? {} : { instrument: record.instrument }),
100
+ ...(record.sourceText === null ? {} : { sourceText: record.sourceText }),
101
+ });
92
102
  }
93
103
 
94
104
  type BarsResult =
@@ -30,6 +30,21 @@
30
30
  * intent no row holds and the fold would refuse it. The row a bracket wants is
31
31
  * decision 55 and it is not in this release, so a stop cannot fill here and a
32
32
  * page that says it can is ahead of the engine.
33
+ *
34
+ * **A destination that behaves badly does it on a schedule, stated before the
35
+ * run and never by chance.** Every case harvested before this one ran against a
36
+ * venue that filled every order whole and on time, so two engines were proved
37
+ * to agree about the half of a day that costs nobody anything. The other half
38
+ * is a partial fill, an order arriving in pieces, a rejection, a cancellation,
39
+ * an expiry and a fill that turns up after the order has ended, and
40
+ * `SimulatorOptions.fill` carries the schedule saying which order each of those
41
+ * happens to and at which boundary. There is no random number here and there
42
+ * will not be one: `stdlib.md` 8.2 keeps a script that answers differently on a
43
+ * second run out of a conformance suite, and a venue rolling a die would make
44
+ * every case it wrote unreproducible in the same breath. **An order the
45
+ * schedule names is answered by the schedule and by nothing else**, so a run
46
+ * stating none is the run the cases before this one were harvested from, to the
47
+ * bit, which is what lets this exist at all.
33
48
  */
34
49
  import type { Contract } from '../accounting/index.js';
35
50
  import type { OrderFrame, OrderIntent, OrderSide, RoutedEffect } from '../engine/index.js';
@@ -38,11 +53,70 @@ import { testResting } from './resting.js';
38
53
  import type { RestingOrder } from './resting.js';
39
54
  import type { FillPolicy } from './settings.js';
40
55
 
56
+ /**
57
+ * What this destination does to one order, at one boundary.
58
+ *
59
+ * Four words reaching every status of `stdlib.md` 17.7 a host may send. `fill`
60
+ * carries the cumulative quantity, so one word covers the acknowledgement
61
+ * before anything has traded, a fill of part of the order and a fill of the
62
+ * whole of it: `working` and `filled` are that quantity read against the
63
+ * order's own rather than two instructions. The other three are the three ways
64
+ * an order ends carrying less than it asked for, and an engine never handed
65
+ * one has never been asked what it does with the quantity still working.
66
+ */
67
+ export type VenueDoes = 'fill' | 'reject' | 'cancel' | 'expire';
68
+
69
+ /** One act of the schedule: what the destination does, to which order, when. */
70
+ export interface VenueAct {
71
+ /**
72
+ * The nth order this destination took, counting from one.
73
+ *
74
+ * Orders and not intents: a bracket is never taken and a cancellation is
75
+ * answered without being held, so neither is counted and neither can be named
76
+ * here. An ordinal for the reason `conformance.md` section 3 gives a case's
77
+ * own frames: nobody writing a schedule knows the id an engine will mint.
78
+ */
79
+ readonly order: number;
80
+ /**
81
+ * Boundaries after the one the order arrived at, `0` being that boundary.
82
+ *
83
+ * Counted from the order rather than stated as a bar index, because the bar
84
+ * a strategy decides an order on moves the moment anything else about the run
85
+ * does, and a schedule in bar indices is one nobody can read back.
86
+ */
87
+ readonly afterBars: number;
88
+ readonly does: VenueDoes;
89
+ /**
90
+ * The cumulative quantity a fill reports, in units, or absent for all of it.
91
+ *
92
+ * Cumulative because a frame is (`stdlib.md` 17.8): `0` is the
93
+ * acknowledgement a venue sends before anything has traded, a number below
94
+ * the order's own is a partial fill, and one at or above it completes the
95
+ * order. A quantity below what this venue has already reported is a stale
96
+ * frame, which a venue really sends and the fold has to swallow, so it is
97
+ * stated here rather than refused.
98
+ */
99
+ readonly units?: number | null;
100
+ /** The destination's own text, which the ledger records against the row. */
101
+ readonly text?: string;
102
+ }
103
+
104
+ /**
105
+ * The fill policy, and what this destination does to the orders it names.
106
+ *
107
+ * The schedule sits on the policy because it is the same kind of fact: how this
108
+ * destination decides a fill, chosen before the first bar and carried in the
109
+ * record beside the policy's own version, so a stored run replays as it ran.
110
+ */
111
+ export interface VenuePolicy extends FillPolicy {
112
+ readonly schedule?: readonly VenueAct[];
113
+ }
114
+
41
115
  /** What the venue prices against and how it decides a fill. */
42
116
  export interface SimulatorOptions {
43
117
  readonly bars: readonly RecordedBar[];
44
118
  readonly contract: Contract;
45
- readonly fill: FillPolicy;
119
+ readonly fill: VenuePolicy;
46
120
  /** Adverse always, in ticks, applied to a market fill and to a stop. */
47
121
  readonly slippageTicks: number;
48
122
  /** The declaration's own fill rule: where a market order is priced. */
@@ -64,7 +138,11 @@ interface Order {
64
138
  readonly placedOn: number;
65
139
  /** Absent on a market order, which rests on nothing. */
66
140
  readonly rest: RestingOrder | null;
141
+ /** The acts the schedule states about this order, in the order they were stated. */
142
+ readonly acts: readonly VenueAct[];
67
143
  filledQty: number;
144
+ /** This venue's average over `filledQty`, absent while nothing has filled. */
145
+ avgPrice: number | null;
68
146
  live: boolean;
69
147
  /** Whether a stop limit has reached its trigger and is now a limit. */
70
148
  triggered: boolean;
@@ -73,6 +151,20 @@ interface Order {
73
151
  /** `language.md` 13.3: a market order priced at the close of its own bar. */
74
152
  const AT_CLOSE = 'close';
75
153
 
154
+ /**
155
+ * The word this venue reports each ending under.
156
+ *
157
+ * A destination with words of its own maps them onto the vocabulary in its
158
+ * adapter, which is where `stdlib.md` 17.7 puts the mapping because a status
159
+ * vocabulary differs per destination. This is this one's, and the whole of what
160
+ * a schedule's verb means.
161
+ */
162
+ const ENDED: Readonly<Record<Exclude<VenueDoes, 'fill'>, string>> = {
163
+ reject: 'rejected',
164
+ cancel: 'cancelled',
165
+ expire: 'expired',
166
+ };
167
+
76
168
  /**
77
169
  * A venue, for one run.
78
170
  *
@@ -123,6 +215,13 @@ export class Simulator {
123
215
  const bar = this.options.bars[barIndex];
124
216
  if (bar !== undefined) {
125
217
  for (const order of this.orders) {
218
+ // An order the schedule names is answered by the schedule, whether or
219
+ // not it is still live: a fill after a cancellation is the one thing
220
+ // this exists for and the order is dead by then.
221
+ if (order.acts.length > 0) {
222
+ if (order.placedOn < barIndex) this.perform(order, bar, barIndex);
223
+ continue;
224
+ }
126
225
  if (!order.live || order.rest === null || order.placedOn >= barIndex) continue;
127
226
  this.decide(order, bar, barIndex);
128
227
  }
@@ -145,12 +244,17 @@ export class Simulator {
145
244
  /** An order the strategy sent, which this venue now holds. */
146
245
  private accept(intent: OrderIntent, barIndex: number): void {
147
246
  this.refs += 1;
247
+ // The ordinal of this order among the orders taken, which is what an act
248
+ // names. Read before the order joins them, so the first is one.
249
+ const ordinal = this.orders.length + 1;
148
250
  const order: Order = {
149
251
  intent,
150
252
  ref: refOf(this.refs),
151
253
  placedOn: barIndex,
152
254
  rest: restingFor(intent),
255
+ acts: (this.options.fill.schedule ?? []).filter((act) => act.order === ordinal),
153
256
  filledQty: 0,
257
+ avgPrice: null,
154
258
  live: true,
155
259
  triggered: false,
156
260
  };
@@ -188,6 +292,15 @@ export class Simulator {
188
292
  * where it does not.
189
293
  */
190
294
  private open(order: Order, barIndex: number): void {
295
+ // A scheduled order is answered by its schedule from its first breath. The
296
+ // acknowledgement below is a thing this venue chooses to say, so a schedule
297
+ // that wants one states it, and one that wants the destination to sit on an
298
+ // order and say nothing gets that instead.
299
+ if (order.acts.length > 0) {
300
+ const bar = this.options.bars[barIndex];
301
+ if (bar !== undefined) this.perform(order, bar, barIndex);
302
+ return;
303
+ }
191
304
  this.say(order.intent, order.ref, 'working', 0, null, barIndex);
192
305
  if (order.rest !== null) return;
193
306
 
@@ -227,15 +340,96 @@ export class Simulator {
227
340
  * than guessed at here, so by this point the unit is one of two.
228
341
  */
229
342
  private complete(order: Order, price: number, barIndex: number): void {
230
- const stated = order.intent.qty ?? 0;
231
- const lot = this.options.contract.lotSize;
232
- const qty =
233
- this.options.qtyType === 'lots' && lot !== null && lot > 0 ? stated * lot : stated;
343
+ const qty = this.unitsOf(order.intent);
234
344
  order.filledQty = qty;
345
+ order.avgPrice = price;
235
346
  order.live = false;
236
347
  this.say(order.intent, order.ref, 'filled', qty, price, barIndex);
237
348
  }
238
349
 
350
+ /**
351
+ * The acts due at this boundary, in the order the schedule stated them.
352
+ *
353
+ * Due is counted from the bar that sent the order, so an act names a moment
354
+ * in the life of its own order rather than a bar of the run. Two acts due
355
+ * together are answered as written, which is how a schedule states a
356
+ * cancellation and the fill that raced it.
357
+ *
358
+ * Liveness is not consulted. An order that has ended can still be spoken
359
+ * about, because the frame that arrives after it ended is the whole reason
360
+ * this is here: `stdlib.md` 17.8's fill after a terminal status, which a
361
+ * venue sends whenever a cancel races a fill and which an engine refusing it
362
+ * loses, leaving a position the strategy cannot see.
363
+ */
364
+ private perform(order: Order, bar: RecordedBar, barIndex: number): void {
365
+ for (const act of order.acts) {
366
+ if (order.placedOn + act.afterBars !== barIndex) continue;
367
+ if (act.does === 'fill') {
368
+ this.report(order, act, bar, barIndex);
369
+ continue;
370
+ }
371
+ // The order ends, reporting what it filled before it ended, because a
372
+ // frame is cumulative: `stdlib.md` 17.8 has the row keeping the terminal
373
+ // word and the quantity recording what traded, both true at once.
374
+ order.live = false;
375
+ const ended = ENDED[act.does];
376
+ const price = order.filledQty > 0 ? order.avgPrice : null;
377
+ this.say(order.intent, order.ref, ended, order.filledQty, price, barIndex, act.text ?? '');
378
+ }
379
+ }
380
+
381
+ /**
382
+ * A fill the schedule stated, at this bar's close and worsened like any other.
383
+ *
384
+ * **The average is this venue's own, over the cumulative quantity**, the
385
+ * figure `stdlib.md` 17.8 step 3 says the row takes whole. A venue reporting
386
+ * the last piece's price and calling it an average hands the engine a number
387
+ * that is not one, and the engine may not work its own out from two of them,
388
+ * so the lie settles into the ledger and into every trade folded from it.
389
+ *
390
+ * A stated quantity at or below what this venue has already reported adds
391
+ * nothing and moves nothing: that is a repeated or a stale frame, which a real
392
+ * destination sends and the fold has to swallow. It carries the average this
393
+ * venue holds now rather than then, because it keeps no history of its own
394
+ * averages and the fold ignores the price of a frame adding no quantity. A
395
+ * bar with no close prices nothing, so an act due at one says nothing rather
396
+ * than raising a quantity with no price against it, which step 3 refuses.
397
+ */
398
+ private report(order: Order, act: VenueAct, bar: RecordedBar, barIndex: number): void {
399
+ const whole = this.unitsOf(order.intent);
400
+ const stated = act.units ?? whole;
401
+ const delta = stated - order.filledQty;
402
+ if (delta > 0) {
403
+ if (bar.close === null) return;
404
+ // Written in this order and left in it: the source order of a sum is
405
+ // what decides its last bit, and a case harvested from this venue is
406
+ // asserted to the bit.
407
+ const price = this.worsen(bar.close, order.intent.side);
408
+ order.avgPrice = ((order.avgPrice ?? 0) * order.filledQty + price * delta) / stated;
409
+ order.filledQty = stated;
410
+ if (order.filledQty >= whole) order.live = false;
411
+ }
412
+ // `working` is live and not completely filled, `filled` is the whole
413
+ // quantity: 17.7's two words read off the quantity rather than stated
414
+ // twice by a schedule that could disagree with the number beside them.
415
+ const status = order.filledQty >= whole ? 'filled' : 'working';
416
+ const price = stated > 0 ? order.avgPrice : null;
417
+ this.say(order.intent, order.ref, status, stated, price, barIndex, act.text ?? '');
418
+ }
419
+
420
+ /**
421
+ * The order's quantity in units, which is what a fill is counted in.
422
+ *
423
+ * The conversion `complete` did inline, wanted in two places the moment a
424
+ * schedule can fill an order in pieces: the whole a piece is measured
425
+ * against has to be the number the fill path would have reported.
426
+ */
427
+ private unitsOf(intent: OrderIntent): number {
428
+ const stated = intent.qty ?? 0;
429
+ const lot = this.options.contract.lotSize;
430
+ return this.options.qtyType === 'lots' && lot !== null && lot > 0 ? stated * lot : stated;
431
+ }
432
+
239
433
  /**
240
434
  * A price worsened by the slippage the run was carried out under.
241
435
  *
@@ -270,6 +464,7 @@ export class Simulator {
270
464
  filledQty: number,
271
465
  avgFillPrice: number | null,
272
466
  barIndex: number,
467
+ text = '',
273
468
  ): void {
274
469
  this.seq += 1;
275
470
  this.answered.push({
@@ -281,7 +476,7 @@ export class Simulator {
281
476
  sentInstrument: intent.instrument,
282
477
  sentProduct: intent.product,
283
478
  time: this.options.bars[barIndex]?.time ?? null,
284
- text: '',
479
+ text,
285
480
  seq: this.seq,
286
481
  });
287
482
  }