openscript 0.4.0__tar.gz → 0.6.0__tar.gz

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 (164) hide show
  1. {openscript-0.4.0 → openscript-0.6.0}/PKG-INFO +82 -82
  2. {openscript-0.4.0 → openscript-0.6.0}/openscript/__init__.py +3 -1
  3. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/answers.py +87 -16
  4. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/facts.py +47 -11
  5. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/running.py +54 -29
  6. openscript-0.6.0/openscript/adapter/secondary.py +72 -0
  7. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/serving.py +72 -16
  8. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/sessions.py +12 -8
  9. openscript-0.6.0/openscript/adapter/surface.py +54 -0
  10. openscript-0.6.0/openscript/arrays.py +283 -0
  11. openscript-0.6.0/openscript/buckets.py +152 -0
  12. {openscript-0.4.0 → openscript-0.6.0}/openscript/budget.py +3 -0
  13. {openscript-0.4.0 → openscript-0.6.0}/openscript/contracts.py +11 -0
  14. {openscript-0.4.0 → openscript-0.6.0}/openscript/diagnostics.py +6 -0
  15. {openscript-0.4.0 → openscript-0.6.0}/openscript/intervals.py +10 -22
  16. {openscript-0.4.0 → openscript-0.6.0}/openscript/machine.py +11 -25
  17. openscript-0.6.0/openscript/objects.py +297 -0
  18. openscript-0.6.0/openscript/reasons.py +49 -0
  19. openscript-0.6.0/openscript/request_body.py +229 -0
  20. openscript-0.6.0/openscript/request_plan.py +226 -0
  21. openscript-0.6.0/openscript/requests.py +284 -0
  22. {openscript-0.4.0 → openscript-0.6.0}/openscript/run.py +125 -12
  23. {openscript-0.4.0 → openscript-0.6.0}/openscript/values.py +12 -7
  24. {openscript-0.4.0 → openscript-0.6.0}/openscript/verify.py +14 -8
  25. {openscript-0.4.0 → openscript-0.6.0}/openscript.egg-info/PKG-INFO +82 -82
  26. {openscript-0.4.0 → openscript-0.6.0}/openscript.egg-info/SOURCES.txt +15 -0
  27. {openscript-0.4.0 → openscript-0.6.0}/pyproject.toml +1 -1
  28. {openscript-0.4.0 → openscript-0.6.0}/setup.cfg +4 -4
  29. {openscript-0.4.0 → openscript-0.6.0}/tests/test_adapter_cases.py +7 -5
  30. openscript-0.6.0/tests/test_adapter_identity.py +125 -0
  31. openscript-0.6.0/tests/test_arrays.py +144 -0
  32. {openscript-0.4.0 → openscript-0.6.0}/tests/test_diagnostics.py +12 -7
  33. {openscript-0.4.0 → openscript-0.6.0}/tests/test_engine_facts.py +4 -1
  34. openscript-0.6.0/tests/test_hand_over.py +71 -0
  35. {openscript-0.4.0 → openscript-0.6.0}/tests/test_host_surface.py +441 -441
  36. openscript-0.6.0/tests/test_objects.py +173 -0
  37. {openscript-0.4.0 → openscript-0.6.0}/tests/test_programs.py +26 -12
  38. openscript-0.6.0/tests/test_request_host.py +273 -0
  39. openscript-0.6.0/tests/test_requests.py +160 -0
  40. {openscript-0.4.0 → openscript-0.6.0}/tests/test_values.py +11 -11
  41. {openscript-0.4.0 → openscript-0.6.0}/tests/test_verify.py +4 -2
  42. {openscript-0.4.0 → openscript-0.6.0}/README.md +0 -0
  43. {openscript-0.4.0 → openscript-0.6.0}/openscript/__main__.py +0 -0
  44. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/__init__.py +0 -0
  45. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/analysis.py +0 -0
  46. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/charges.py +0 -0
  47. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/equity.py +0 -0
  48. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/report.py +0 -0
  49. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/shapes.py +0 -0
  50. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/statistics.py +0 -0
  51. {openscript-0.4.0 → openscript-0.6.0}/openscript/accounting/trades.py +0 -0
  52. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/__init__.py +0 -0
  53. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/channels.py +0 -0
  54. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/expectations.py +0 -0
  55. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/matching.py +0 -0
  56. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/ordering.py +0 -0
  57. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/page.py +0 -0
  58. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/reading.py +0 -0
  59. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/reporting.py +0 -0
  60. {openscript-0.4.0 → openscript-0.6.0}/openscript/adapter/spellings.py +0 -0
  61. {openscript-0.4.0 → openscript-0.6.0}/openscript/bars.py +0 -0
  62. {openscript-0.4.0 → openscript-0.6.0}/openscript/canonical.py +0 -0
  63. {openscript-0.4.0 → openscript-0.6.0}/openscript/civil.py +0 -0
  64. {openscript-0.4.0 → openscript-0.6.0}/openscript/dates.py +0 -0
  65. {openscript-0.4.0 → openscript-0.6.0}/openscript/hours.py +0 -0
  66. {openscript-0.4.0 → openscript-0.6.0}/openscript/inputs.py +0 -0
  67. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/__init__.py +0 -0
  68. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/arithmetic.py +0 -0
  69. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/averages.py +0 -0
  70. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/bars.py +0 -0
  71. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/bookkeeping.py +0 -0
  72. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/code_points.py +0 -0
  73. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/colour.py +0 -0
  74. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/composites.py +0 -0
  75. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/counting.py +0 -0
  76. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/deviation.py +0 -0
  77. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/elementary.py +0 -0
  78. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/extremes.py +0 -0
  79. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/flows.py +0 -0
  80. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/momentum.py +0 -0
  81. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/number_text.py +0 -0
  82. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/prices.py +0 -0
  83. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/ranges.py +0 -0
  84. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/rounding.py +0 -0
  85. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/series.py +0 -0
  86. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/stateful.py +0 -0
  87. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/stateless.py +0 -0
  88. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/strength.py +0 -0
  89. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/strings.py +0 -0
  90. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/trend.py +0 -0
  91. {openscript-0.4.0 → openscript-0.6.0}/openscript/library/values.py +0 -0
  92. {openscript-0.4.0 → openscript-0.6.0}/openscript/logbook.py +0 -0
  93. {openscript-0.4.0 → openscript-0.6.0}/openscript/memory.py +0 -0
  94. {openscript-0.4.0 → openscript-0.6.0}/openscript/opcodes.py +0 -0
  95. {openscript-0.4.0 → openscript-0.6.0}/openscript/program.py +0 -0
  96. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/__init__.py +0 -0
  97. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/calls.py +0 -0
  98. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/closable.py +0 -0
  99. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/fills.py +0 -0
  100. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/holdings.py +0 -0
  101. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/intents.py +0 -0
  102. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/ledger.py +0 -0
  103. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/placing.py +0 -0
  104. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/positions.py +0 -0
  105. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/refusals.py +0 -0
  106. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/rows.py +0 -0
  107. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/sizing.py +0 -0
  108. {openscript-0.4.0 → openscript-0.6.0}/openscript/strategy/statuses.py +0 -0
  109. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/__init__.py +0 -0
  110. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/bands.py +0 -0
  111. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/levels.py +0 -0
  112. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/marks.py +0 -0
  113. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/paints.py +0 -0
  114. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/plots.py +0 -0
  115. {openscript-0.4.0 → openscript-0.6.0}/openscript/surface/published.py +0 -0
  116. {openscript-0.4.0 → openscript-0.6.0}/openscript/verify_code.py +0 -0
  117. {openscript-0.4.0 → openscript-0.6.0}/openscript/verify_requests.py +0 -0
  118. {openscript-0.4.0 → openscript-0.6.0}/openscript/verify_shape.py +0 -0
  119. {openscript-0.4.0 → openscript-0.6.0}/openscript/verify_tables.py +0 -0
  120. {openscript-0.4.0 → openscript-0.6.0}/openscript/version.py +0 -0
  121. {openscript-0.4.0 → openscript-0.6.0}/openscript/zones.py +0 -0
  122. {openscript-0.4.0 → openscript-0.6.0}/openscript.egg-info/dependency_links.txt +0 -0
  123. {openscript-0.4.0 → openscript-0.6.0}/openscript.egg-info/top_level.txt +0 -0
  124. {openscript-0.4.0 → openscript-0.6.0}/tests/test_adapter_matching.py +0 -0
  125. {openscript-0.4.0 → openscript-0.6.0}/tests/test_adapter_page.py +0 -0
  126. {openscript-0.4.0 → openscript-0.6.0}/tests/test_adapter_spellings.py +0 -0
  127. {openscript-0.4.0 → openscript-0.6.0}/tests/test_analysis_and_runup.py +0 -0
  128. {openscript-0.4.0 → openscript-0.6.0}/tests/test_bar_cycle.py +0 -0
  129. {openscript-0.4.0 → openscript-0.6.0}/tests/test_bars.py +0 -0
  130. {openscript-0.4.0 → openscript-0.6.0}/tests/test_calendar.py +0 -0
  131. {openscript-0.4.0 → openscript-0.6.0}/tests/test_calendar_reads.py +0 -0
  132. {openscript-0.4.0 → openscript-0.6.0}/tests/test_calls.py +0 -0
  133. {openscript-0.4.0 → openscript-0.6.0}/tests/test_canonical.py +0 -0
  134. {openscript-0.4.0 → openscript-0.6.0}/tests/test_charges.py +0 -0
  135. {openscript-0.4.0 → openscript-0.6.0}/tests/test_colour.py +0 -0
  136. {openscript-0.4.0 → openscript-0.6.0}/tests/test_cost_models.py +0 -0
  137. {openscript-0.4.0 → openscript-0.6.0}/tests/test_frame_fold.py +0 -0
  138. {openscript-0.4.0 → openscript-0.6.0}/tests/test_frame_instant.py +0 -0
  139. {openscript-0.4.0 → openscript-0.6.0}/tests/test_harness.py +0 -0
  140. {openscript-0.4.0 → openscript-0.6.0}/tests/test_ledger_orders.py +0 -0
  141. {openscript-0.4.0 → openscript-0.6.0}/tests/test_library_vectors.py +0 -0
  142. {openscript-0.4.0 → openscript-0.6.0}/tests/test_logbook.py +0 -0
  143. {openscript-0.4.0 → openscript-0.6.0}/tests/test_machine.py +0 -0
  144. {openscript-0.4.0 → openscript-0.6.0}/tests/test_maths_rules.py +0 -0
  145. {openscript-0.4.0 → openscript-0.6.0}/tests/test_memory.py +0 -0
  146. {openscript-0.4.0 → openscript-0.6.0}/tests/test_number_text.py +0 -0
  147. {openscript-0.4.0 → openscript-0.6.0}/tests/test_opcodes.py +0 -0
  148. {openscript-0.4.0 → openscript-0.6.0}/tests/test_order_page.py +0 -0
  149. {openscript-0.4.0 → openscript-0.6.0}/tests/test_order_refusals.py +0 -0
  150. {openscript-0.4.0 → openscript-0.6.0}/tests/test_series_rules.py +0 -0
  151. {openscript-0.4.0 → openscript-0.6.0}/tests/test_session_window.py +0 -0
  152. {openscript-0.4.0 → openscript-0.6.0}/tests/test_sizing_refusal.py +0 -0
  153. {openscript-0.4.0 → openscript-0.6.0}/tests/test_stateful_vectors.py +0 -0
  154. {openscript-0.4.0 → openscript-0.6.0}/tests/test_strategy_cases.py +0 -0
  155. {openscript-0.4.0 → openscript-0.6.0}/tests/test_strategy_profile.py +0 -0
  156. {openscript-0.4.0 → openscript-0.6.0}/tests/test_string_ceiling.py +0 -0
  157. {openscript-0.4.0 → openscript-0.6.0}/tests/test_strings.py +0 -0
  158. {openscript-0.4.0 → openscript-0.6.0}/tests/test_supplied_schedule.py +0 -0
  159. {openscript-0.4.0 → openscript-0.6.0}/tests/test_surface.py +0 -0
  160. {openscript-0.4.0 → openscript-0.6.0}/tests/test_surface_bands.py +0 -0
  161. {openscript-0.4.0 → openscript-0.6.0}/tests/test_surface_example.py +0 -0
  162. {openscript-0.4.0 → openscript-0.6.0}/tests/test_time_programs.py +0 -0
  163. {openscript-0.4.0 → openscript-0.6.0}/tests/test_trades_and_summary.py +0 -0
  164. {openscript-0.4.0 → openscript-0.6.0}/tests/test_version.py +0 -0
@@ -1,82 +1,82 @@
1
- Metadata-Version: 2.4
2
- Name: openscript
3
- Version: 0.4.0
4
- Summary: An open trading language: the engine that runs a compiled program.
5
- License-Expression: Apache-2.0
6
- Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
7
- Project-URL: Source, https://github.com/marketcalls/openscript
8
- Keywords: trading,language,interpreter,indicators,backtesting
9
- Classifier: Programming Language :: Python :: 3.12
10
- Classifier: Programming Language :: Python :: 3.13
11
- Classifier: Topic :: Office/Business :: Financial :: Investment
12
- Requires-Python: >=3.12
13
- Description-Content-Type: text/markdown
14
-
15
- # openscript
16
-
17
- The engine that runs a compiled OpenScript program, in Python.
18
-
19
- OpenScript is an open trading language. A script is compiled to a **compiled
20
- program**: plain data, an instruction list, never generated code. This package
21
- runs one.
22
-
23
- It exists so that a platform can run a strategy where a JavaScript runtime is
24
- not available, which for a production trading server is the ordinary case. The
25
- compiler and the first engine are the `openalgo-script` package on npm; this is
26
- the second engine, and the two are held to each other by a conformance suite
27
- where any disagreement is a release blocker.
28
-
29
- ## What it is
30
-
31
- - **No compiler.** This package is handed a compiled program and never a script.
32
- The program arrives as canonical text, which is what a run's hash is taken
33
- over, so an engine cannot quietly run something other than what was recorded.
34
- - **Nothing builds code out of text.** No string evaluator, no statement
35
- executor, no import driven by hand, no object graph loaded out of bytes. That
36
- is what lets a platform run many people's scripts in one process, and it is
37
- enforced by a check rather than intended.
38
- - **Zero dependencies.** The standard library only, and not the parts of it that
39
- stop a run being reproducible. Measured on every build against the module
40
- names the running interpreter says are its own.
41
-
42
- ## Installing
43
-
44
- ```
45
- pip install openscript
46
- ```
47
-
48
- Python 3.12 or newer. Nothing else.
49
-
50
- ## Using it
51
-
52
- ```python
53
- from openscript.run import load_text
54
-
55
- loaded = load_text(program_text, settings, library)
56
- if loaded.ok:
57
- result = loaded.run.execute_bar(0, bar)
58
- ```
59
-
60
- A host loads a program once and pushes bars at it one at a time, keeping a
61
- checkpoint so a bar that is still moving can be executed again and rolled back.
62
- The conformance adapter is the other way in:
63
-
64
- ```
65
- python -m openscript --describe
66
- python -m openscript <case-directory>
67
- ```
68
-
69
- ## Where the documentation is
70
-
71
- The specification and the guides live in the repository:
72
-
73
- - `docs/integrating/the-python-engine.md` for what is in this package and what a
74
- host needs.
75
- - `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
76
- the bar cycle, the rollback a moving bar rests on, and the order boundary.
77
- - `spec/` for the language, the compiled program format, the standard library
78
- and the conformance suite.
79
-
80
- ## Licence
81
-
82
- Apache-2.0.
1
+ Metadata-Version: 2.4
2
+ Name: openscript
3
+ Version: 0.6.0
4
+ Summary: An open trading language: the engine that runs a compiled program.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
7
+ Project-URL: Source, https://github.com/marketcalls/openscript
8
+ Keywords: trading,language,interpreter,indicators,backtesting
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # openscript
16
+
17
+ The engine that runs a compiled OpenScript program, in Python.
18
+
19
+ OpenScript is an open trading language. A script is compiled to a **compiled
20
+ program**: plain data, an instruction list, never generated code. This package
21
+ runs one.
22
+
23
+ It exists so that a platform can run a strategy where a JavaScript runtime is
24
+ not available, which for a production trading server is the ordinary case. The
25
+ compiler and the first engine are the `openalgo-script` package on npm; this is
26
+ the second engine, and the two are held to each other by a conformance suite
27
+ where any disagreement is a release blocker.
28
+
29
+ ## What it is
30
+
31
+ - **No compiler.** This package is handed a compiled program and never a script.
32
+ The program arrives as canonical text, which is what a run's hash is taken
33
+ over, so an engine cannot quietly run something other than what was recorded.
34
+ - **Nothing builds code out of text.** No string evaluator, no statement
35
+ executor, no import driven by hand, no object graph loaded out of bytes. That
36
+ is what lets a platform run many people's scripts in one process, and it is
37
+ enforced by a check rather than intended.
38
+ - **Zero dependencies.** The standard library only, and not the parts of it that
39
+ stop a run being reproducible. Measured on every build against the module
40
+ names the running interpreter says are its own.
41
+
42
+ ## Installing
43
+
44
+ ```
45
+ pip install openscript
46
+ ```
47
+
48
+ Python 3.12 or newer. Nothing else.
49
+
50
+ ## Using it
51
+
52
+ ```python
53
+ from openscript.run import load_text
54
+
55
+ loaded = load_text(program_text, settings, library)
56
+ if loaded.ok:
57
+ result = loaded.run.execute_bar(0, bar)
58
+ ```
59
+
60
+ A host loads a program once and pushes bars at it one at a time, keeping a
61
+ checkpoint so a bar that is still moving can be executed again and rolled back.
62
+ The conformance adapter is the other way in:
63
+
64
+ ```
65
+ python -m openscript --describe
66
+ python -m openscript <case-directory>
67
+ ```
68
+
69
+ ## Where the documentation is
70
+
71
+ The specification and the guides live in the repository:
72
+
73
+ - `docs/integrating/the-python-engine.md` for what is in this package and what a
74
+ host needs.
75
+ - `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
76
+ the bar cycle, the rollback a moving bar rests on, and the order boundary.
77
+ - `spec/` for the language, the compiled program format, the standard library
78
+ and the conformance suite.
79
+
80
+ ## Licence
81
+
82
+ Apache-2.0.
@@ -16,7 +16,9 @@ What is where, for somebody reading this engine rather than running it:
16
16
  memory regions and their lifetimes, the values, the bars,
17
17
  the inputs, the verification done before the first bar,
18
18
  the execution budget, the canonical encoding, the
19
- diagnostics and the two version numbers.
19
+ diagnostics and the two version numbers, and the reads
20
+ of section 2.16: a timeframe's buckets, a read's plan
21
+ and body, and the fold that puts its value on the chart.
20
22
  - ``library/`` the functions of ``spec/stdlib.md``, each accumulating in
21
23
  the order that page fixes, in the two halves that page
22
24
  divides them into: the calls that remember nothing and
@@ -30,11 +30,12 @@ have it say. The stage's report records that as a defect of the page rather than
30
30
  leaving it to be discovered.
31
31
  """
32
32
 
33
+ import email.parser
33
34
  import json
34
35
  import sys
35
36
  import tomllib
36
37
  from pathlib import Path
37
- from typing import Any, Dict, Optional
38
+ from typing import Any, Callable, Dict, Mapping, Optional
38
39
 
39
40
  from ..version import FORMAT, LANGUAGE_VERSIONS
40
41
  from .expectations import expected_channels
@@ -47,6 +48,11 @@ from .spellings import Malformed
47
48
  #: The distribution file beside the package, which states the version once.
48
49
  _PROJECT = "pyproject.toml"
49
50
 
51
+ #: The distribution this engine is, as the build and the installer name it. It is
52
+ #: named for its top package, so the name is read from where this module sits
53
+ #: rather than written out a second time beside the one in pyproject.toml.
54
+ _NAME = __name__.split(".", 1)[0]
55
+
50
56
  #: Section 8: what this implementation claims. The profile that covers the cases
51
57
  #: it runs, which is the one a case it would report ``unsupported`` is not in.
52
58
  #:
@@ -65,24 +71,89 @@ PROFILE = PROFILES[2]
65
71
 
66
72
 
67
73
  def _distribution() -> Dict[str, Any]:
68
- """``pyproject.toml``, read from beside the package, or a refusal saying so.
69
-
70
- The version is a fact of the release and is written in the distribution
71
- file; ``scripts/check-python.mjs`` already holds that file and the package
72
- manifest equal, so reading it here adds no third copy. An installed package
73
- that does not carry the file cannot prove its version, and an identity with
74
- an invented version in it is worse than an adapter that says which file it
75
- could not find.
74
+ """The engine's name and version, from wherever this copy of it was built.
75
+
76
+ The version is a fact of the release, stated once in the distribution file,
77
+ and ``scripts/check-python.mjs`` holds that file and the package manifest
78
+ equal, so nothing read here is a third copy.
79
+ """
80
+ return _identity(Path(__file__).resolve().parent.parent.parent / _PROJECT, _installed)
81
+
82
+
83
+ def _installed(name: str, site: Optional[Path] = None) -> Optional[Mapping[str, str]]:
84
+ """The metadata an installer wrote beside this package, or ``None`` if none did.
85
+
86
+ Every installer records a distribution in ``<name>-<version>.dist-info``
87
+ beside the package it installed, and ``METADATA`` there holds the name and
88
+ version the build wrote. It is read directly, with the standard library's
89
+ own header parser, rather than through the import system's metadata finder:
90
+ this project refuses anything that loads modules by a name computed at run
91
+ time, and reading two headers out of one file does not need it.
92
+
93
+ Two records beside one package is a broken upgrade, and there is no telling
94
+ which of them describes the files actually installed, so that is refused
95
+ by name rather than resolved by picking one.
96
+ """
97
+ if site is None:
98
+ site = Path(__file__).resolve().parent.parent.parent
99
+ records = sorted(site.glob(f"{name}-*.dist-info/METADATA"))
100
+ if not records:
101
+ return None
102
+ if len(records) > 1:
103
+ raise Malformed(
104
+ f"{len(records)} installed records for {name!r} sit beside this package "
105
+ f"({', '.join(record.parent.name for record in records)}), so its version cannot "
106
+ "be told. Reinstall it so that one remains."
107
+ )
108
+ with records[0].open(encoding="utf-8") as file:
109
+ return email.parser.Parser().parse(file, headersonly=True)
110
+
111
+
112
+ def _identity(
113
+ project_file: Path, installed: Callable[[str], Optional[Mapping[str, str]]]
114
+ ) -> Dict[str, Any]:
115
+ """Read the identity from ``project_file``, else from the installed distribution.
116
+
117
+ **Two places, because a copy of this engine lives in one of two.** A checkout
118
+ has ``pyproject.toml`` beside the package, which is what the conformance suite
119
+ runs. An installed copy does not: the build reads that file and records what
120
+ it says in the distribution's own metadata, and the file itself is never
121
+ installed. This used to read only the first, so ``python -m openscript
122
+ --describe`` failed on every installed copy, 0.5.0 from the index included,
123
+ and no host could run the suite against the engine it had actually installed.
124
+
125
+ The installed metadata is not an invented version. It is the one the wheel
126
+ was built with, written by the same build from the same file, and it is what
127
+ the installer, the index and ``pip show`` all report for this copy.
128
+
129
+ **A file that names some other project is not this one's.** Beside an
130
+ installed package, the directory above is ``site-packages``, where a stray
131
+ ``pyproject.toml`` from anything else would otherwise be read as this
132
+ engine's identity. The name is checked, and a mismatch falls through.
133
+
134
+ Neither available is still a refusal that names both, because an identity
135
+ with a guessed version in it is worse than an adapter saying what it could
136
+ not find.
76
137
  """
77
- path = Path(__file__).resolve().parent.parent.parent / _PROJECT
138
+ reasons = []
78
139
  try:
79
- with path.open("rb") as file:
80
- return tomllib.load(file)["project"]
140
+ with project_file.open("rb") as file:
141
+ project = tomllib.load(file)["project"]
142
+ if project.get("name") == _NAME:
143
+ return project
144
+ reasons.append(f"{project_file} names {project.get('name')!r}, not {_NAME!r}")
81
145
  except (OSError, KeyError, ValueError) as reason:
82
- raise Malformed(
83
- f"{path} could not be read, and section 9 has an adapter answer with the engine's own "
84
- f"name and version: {reason}"
85
- ) from None
146
+ reasons.append(f"{project_file} could not be read: {reason}")
147
+
148
+ recorded = installed(_NAME)
149
+ if recorded is not None and recorded.get("Name") and recorded.get("Version"):
150
+ return {"name": recorded["Name"], "version": recorded["Version"]}
151
+ reasons.append(f"no installed distribution named {_NAME!r} was found")
152
+
153
+ raise Malformed(
154
+ "section 9 has an adapter answer with the engine's own name and version, and "
155
+ + "; ".join(reasons)
156
+ )
86
157
 
87
158
 
88
159
  def describe() -> Dict[str, Any]:
@@ -14,18 +14,20 @@ for every entry but one: ``chart.symbol`` is the empty string, because 3.4 says
14
14
  so. A default anywhere else would make "the smallest increment is five paise"
15
15
  and "nobody said" the same reading.
16
16
 
17
- **The two derived facts are not here.** ``chart.intervalMinutes`` and
18
- ``chart.isIntraday`` are computed from the interval string by section 15.2's
19
- canonical form, which this engine does not read yet, so a program calling one is
20
- refused at load by name (OS6004) rather than answered from a guess at what the
21
- string meant. The planned entries of both tables are absent for the same reason:
22
- a name the language does not serve yet is a refusal naming it.
17
+ **The two derived facts are computed from the interval, never read.**
18
+ ``chart.intervalMinutes`` and ``chart.isIntraday`` are derived from the one
19
+ interval string the host stated, so neither can disagree with it, by the rule the
20
+ first engine applies: a count, then an optional unit of seconds, minutes, hours,
21
+ days or weeks, where a count alone is minutes, and a month has no answer because
22
+ it is no fixed number of minutes. The planned entries of both tables are absent:
23
+ a name the language does not serve yet is a refusal naming it (OS6004).
23
24
  """
24
25
 
25
- from typing import Any, Dict, Optional, Protocol, Tuple
26
+ import re
27
+ from typing import Any, Dict, Mapping, Optional, Protocol, Tuple
26
28
 
27
29
  from ..values import ABSENT
28
- from .sessions import SESSION_FACTS
30
+ from .sessions import SESSION_FACT_OF, SESSION_FACTS
29
31
 
30
32
  #: Section 3.4 against ``host-interface.md`` 4.1: the call, and the fact of the
31
33
  #: instrument record it reads. Every one is arity 0, holds no state and has no
@@ -115,13 +117,47 @@ def position_value(name: str, book: Book) -> Any:
115
117
  #: 0, hold no state and have no effect, which is what section 2.5's check holds a
116
118
  #: program's own library table to. The session's own entry is beside them because
117
119
  #: it is read the same way and answered from the bar rather than from a record.
118
- FACT_NAMES: Tuple[str, ...] = (*CHART_FACTS, CHART_CLOCK, *POSITION_FACTS, *SESSION_FACTS)
120
+ DERIVED_FACTS: Tuple[str, ...] = ("chart.intervalMinutes", "chart.isIntraday")
121
+
122
+ FACT_NAMES: Tuple[str, ...] = (
123
+ *CHART_FACTS,
124
+ CHART_CLOCK,
125
+ *DERIVED_FACTS,
126
+ *POSITION_FACTS,
127
+ *SESSION_FACTS,
128
+ )
129
+
130
+ #: A count, then an optional unit. A count alone is minutes.
131
+ _INTERVAL = re.compile(r"^(\d+)([smhDWM]?)$")
132
+
133
+ #: Minutes in one of each unit. A month is absent from it on purpose.
134
+ _MINUTES = {"s": 1 / 60, "": 1, "m": 1, "h": 60, "D": 60 * 24, "W": 60 * 24 * 7}
135
+
136
+
137
+ def interval_minutes(interval: Any) -> Any:
138
+ """Minutes in an interval code, or absent for one that has no fixed length."""
139
+ if not isinstance(interval, str):
140
+ return ABSENT
141
+ found = _INTERVAL.match(interval)
142
+ if found is None or found.group(2) not in _MINUTES:
143
+ return ABSENT
144
+ return float(int(found.group(1)) * _MINUTES[found.group(2)])
145
+
146
+
147
+ def derived_value(name: str, instrument: Any) -> Any:
148
+ """``chart.intervalMinutes`` or ``chart.isIntraday``, from the stated interval."""
149
+ minutes = interval_minutes(instrument.get("interval", ABSENT))
150
+ if name == "chart.intervalMinutes" or minutes is ABSENT:
151
+ return minutes
152
+ return minutes < 60 * 24
119
153
 
120
154
 
121
- def fact_value(name: str, instrument: Any, now: Any, book: Book, bar: Any) -> Any:
155
+ def fact_value(name: str, instrument: Any, now: Any, book: Book, bar: Mapping[str, Any]) -> Any:
122
156
  """Whichever of the three namespaces this name belongs to, read once."""
123
157
  if name in POSITION_FACTS:
124
158
  return position_value(name, book)
125
159
  if name in SESSION_FACTS:
126
- return bar
160
+ return bar.get(SESSION_FACT_OF[name], ABSENT)
161
+ if name in DERIVED_FACTS:
162
+ return derived_value(name, instrument)
127
163
  return chart_value(name, instrument, now)
@@ -25,7 +25,10 @@ case can assert them.
25
25
  **The bars are confirmed and are not live.** A case is a run over a dataset, so
26
26
  every execution is a new, confirmed, non-realtime bar with one update, and the
27
27
  count supplied is the whole file: that is what makes ``bar.isLast`` true on the
28
- last row and false everywhere else. An engine that only backtests is handed
28
+ last row and false everywhere else. The whole file is also handed to the fold
29
+ before bar 0, as a history is, which a ``"lookahead"`` read is the one reading
30
+ to notice; a read of another instrument is answered from the case's own file
31
+ (``secondary.py``). An engine that only backtests is handed
29
32
  ``isRealtime`` false throughout, which is why no alert is raised here, and
30
33
  ``run.py`` reaches the same answer from the other side.
31
34
 
@@ -42,25 +45,32 @@ is here is the order the three are asked in.
42
45
  from typing import Any, Dict, List, Optional, Sequence, Tuple
43
46
 
44
47
  from ..accounting import report_of
45
- from ..contracts import Bar as EngineBar, BarState
48
+ from ..buckets import is_intraday
49
+ from ..contracts import BarState
50
+ from ..dates import BAR_TIME, NAMES as DATE_NAMES
46
51
  from ..inputs import utc_time
52
+ from ..logbook import Logbook
47
53
  from ..run import load_text
48
54
  from ..strategy import IntentBar
49
55
  from ..values import ABSENT
50
56
  from ..verify import capabilities
51
57
  from .channels import orders_channel, performance_channel, trade_row
52
58
  from .ordering import ORDER_ENTRIES, Desk, options_for, unfoldable
53
- from .reading import Bar, Case
59
+ from .reading import Case
54
60
  from .reporting import contract_for, marks_for, schedule_for, settings_problem
61
+ from .secondary import engine_bar, provider_for
55
62
  from .serving import Serving, is_reference
56
63
  from .sessions import (
57
64
  READABLE_ZONE,
58
65
  SESSION_FACTS,
59
66
  SESSION_FIRST,
67
+ SESSION_LAST,
60
68
  first_bars,
69
+ last_bars,
61
70
  session_from,
62
71
  )
63
72
  from .spellings import Malformed, as_reported
73
+ from .surface import DRAWINGS, SURFACE_TAGS, TABLE, drawings_channel, table_channel
64
74
 
65
75
  #: The codes a load raises when the program needs something this engine does not
66
76
  #: have, each with the field naming what it was.
@@ -75,7 +85,7 @@ _UNSUPPORTED_CODES = {
75
85
  #: assert is named ``unsupported`` on the case rather than answered emptily,
76
86
  #: because an empty channel compares equal to an empty expectation and would be a
77
87
  #: pass nobody earned.
78
- ANSWERED = ("diagnostics", "values", "orders", "trades", "performance")
88
+ ANSWERED = ("diagnostics", "values", "orders", "trades", "performance", "log", DRAWINGS, TABLE)
79
89
 
80
90
  #: ``compiled-program.md`` 2.2's tag for a program that places orders, and the
81
91
  #: word the meta uses for a program that is one. This engine serves the tag
@@ -143,19 +153,6 @@ def _unsupported_from(diagnostic: Any) -> Optional[str]:
143
153
  return sentence.format(value=value)
144
154
 
145
155
 
146
- def _engine_bar(bar: Bar) -> EngineBar:
147
- """One row of the file as the engine's own bar. An absent field stays absent."""
148
- return EngineBar(
149
- time=float(bar.time),
150
- open=bar.open,
151
- high=bar.high,
152
- low=bar.low,
153
- close=bar.close,
154
- volume=bar.volume,
155
- oi=ABSENT,
156
- )
157
-
158
-
159
156
  def _plot_channels(program: Dict[str, Any]) -> Dict[str, int]:
160
157
  """Every plot's legend title and its stable key, against its channel.
161
158
 
@@ -203,21 +200,30 @@ def _values_channel(
203
200
  return out
204
201
 
205
202
 
206
- def _unreadable_zone(program: Dict[str, Any], instrument: Dict[str, Any]) -> Optional[str]:
203
+ def _unreadable_zone(run: Any, instrument: Dict[str, Any]) -> Optional[str]:
207
204
  """What this program reads in a calendar this engine cannot read, or nothing.
208
205
 
209
206
  ``stdlib.md`` section 12.1 reads a calendar field in the chart's timezone,
210
207
  which the host states; this engine carries a reader for one zone and a host
211
208
  with any other supplies its own. A case in another zone whose script takes a
212
- written time, or asks where a session begins, is therefore a case this engine
213
- would answer under the wrong calendar, and it says so instead.
209
+ written time, asks where a session begins or folds a read by the calendar
210
+ (``buckets.py``) is therefore a case this engine would answer under the wrong
211
+ calendar, and it says so instead.
214
212
  """
215
213
  if instrument.get("timezone") == READABLE_ZONE:
216
214
  return None
215
+ program = run.program.raw
216
+ # A day, week or month read is dated in the zone as well. With no zone at
217
+ # all it is the read neither engine can date, which both answer alike.
218
+ zoned = instrument.get("timezone") is not None
219
+ if zoned and any(not is_intraday(one.timeframe) for one in run.plans):
220
+ return "a day, week or month read"
217
221
  if any(one["kind"] == "time" for one in program["inputs"]):
218
222
  return "a time input"
219
223
  if any(one["name"] in SESSION_FACTS for one in program["lib"]["functions"]):
220
224
  return "a session boundary"
225
+ if any(one["name"] in DATE_NAMES for one in program["lib"]["functions"]):
226
+ return "a calendar call"
221
227
  return None
222
228
 
223
229
 
@@ -238,8 +244,6 @@ def run_case(case: Case, program_text: str) -> Answer:
238
244
  "how a tick row becomes the newest bar's four prices is not written down anywhere, so "
239
245
  "a replay here would be this adapter's invention"
240
246
  )
241
- for name in case.secondary:
242
- answer.cannot(f"{name} (section 3): this engine is handed one series and reads no other")
243
247
  if case.frames is not None and case.bars is not None:
244
248
  beyond = unfoldable(case.frames, len(case.bars))
245
249
  if beyond:
@@ -264,8 +268,10 @@ def run_case(case: Case, program_text: str) -> Answer:
264
268
  program_text,
265
269
  dict(case.settings),
266
270
  serving,
267
- capabilities=capabilities(ORDERS),
271
+ capabilities=capabilities(ORDERS, *SURFACE_TAGS),
268
272
  read_time=utc_time,
273
+ instrument=case.instrument,
274
+ provider=provider_for(case),
269
275
  )
270
276
  if loaded.diagnostic is not None:
271
277
  feature = _unsupported_from(loaded.diagnostic)
@@ -280,7 +286,7 @@ def run_case(case: Case, program_text: str) -> Answer:
280
286
 
281
287
  run = loaded.run
282
288
  raw = run.program.raw
283
- unreadable = _unreadable_zone(raw, case.instrument)
289
+ unreadable = _unreadable_zone(run, case.instrument)
284
290
  if unreadable is not None:
285
291
  answer.cannot(
286
292
  f"{unreadable} under the timezone {case.instrument.get('timezone')}: this engine reads "
@@ -324,7 +330,8 @@ def run_case(case: Case, program_text: str) -> Answer:
324
330
  answer.channels = _with_empty(case, {"diagnostics": [_refusal_row(refused)]})
325
331
  return answer
326
332
 
327
- rows, diagnostics = _every_bar(run, serving, desk, case)
333
+ logbook = Logbook()
334
+ rows, diagnostics = _every_bar(run, serving, desk, case, logbook)
328
335
  answered: Dict[str, Any] = {"diagnostics": diagnostics}
329
336
  if "values" in case.asserts:
330
337
  if not case.expected_columns:
@@ -335,6 +342,14 @@ def run_case(case: Case, program_text: str) -> Answer:
335
342
  answered["values"] = _values_channel(case.expected_columns, raw, rows, answer)
336
343
  if "orders" in case.asserts:
337
344
  answered["orders"] = orders_channel(desk.rows(), desk.intents)
345
+ if "log" in case.asserts:
346
+ # conformance.md section 4: the bar, its time, and the value spelled as a
347
+ # cell of the values channel is, absence included.
348
+ answered["log"] = [dict(one, value=as_reported(one["value"])) for one in logbook.rows()]
349
+ if DRAWINGS in case.asserts:
350
+ answered[DRAWINGS] = drawings_channel(run.objects)
351
+ if TABLE in case.asserts:
352
+ answered[TABLE] = table_channel(run.objects)
338
353
  if "trades" in case.asserts or "performance" in case.asserts:
339
354
  report = report_of(
340
355
  desk.fills.settled(),
@@ -394,7 +409,7 @@ def _declaration(run: Any) -> Dict[str, Any]:
394
409
 
395
410
 
396
411
  def _every_bar(
397
- run: Any, serving: Serving, desk: Desk, case: Case
412
+ run: Any, serving: Serving, desk: Desk, case: Case, logbook: Logbook
398
413
  ) -> Tuple[List[List[Any]], List[Dict[str, Any]]]:
399
414
  """Every bar of the file, in order, stopping at the first that fails.
400
415
 
@@ -414,8 +429,12 @@ def _every_bar(
414
429
  bars = case.bars or []
415
430
  supplied = len(bars)
416
431
  when = case.declared.get("now", ABSENT)
417
- opens = first_bars([bar.time for bar in bars], session_from(case.instrument))
432
+ times = [bar.time for bar in bars]
433
+ session = session_from(case.instrument)
434
+ opens = first_bars(times, session)
435
+ closes = last_bars(times, session, case.instrument.get("interval"))
418
436
  rows: List[List[Any]] = []
437
+ run.history([engine_bar(one) for one in bars])
419
438
  for index, bar in enumerate(bars):
420
439
  desk.fold(index, float(bar.time))
421
440
  previous = bars[index - 1].close if index > 0 else ABSENT
@@ -427,12 +446,14 @@ def _every_bar(
427
446
  "previousClose": previous,
428
447
  "volume": bar.volume,
429
448
  SESSION_FIRST: opens[index],
449
+ SESSION_LAST: closes[index],
450
+ BAR_TIME: bar.time,
430
451
  },
431
452
  index == 0,
432
453
  )
433
454
  result = run.execute_bar(
434
455
  index,
435
- _engine_bar(bar),
456
+ engine_bar(bar),
436
457
  BarState(is_new=True, is_confirmed=True, is_realtime=False, updates=1.0),
437
458
  supplied=supplied,
438
459
  instrument=case.instrument,
@@ -441,7 +462,11 @@ def _every_bar(
441
462
  if result.diagnostic is not None:
442
463
  found = result.diagnostic
443
464
  return rows, [_diagnostic_row(found.code, found.line, found.column, index)]
444
- sent = desk.apply(result.applied, IntentBar(index=index, time=float(bar.time)))
465
+ # Step 9's records, each to the side of the run that owns it: an order
466
+ # to the desk, a log line to the book. Nothing else carries an effect.
467
+ logbook.write(result.applied, index, bar.time)
468
+ orders = [one for one in result.applied if one.effect == "order"]
469
+ sent = desk.apply(orders, IntentBar(index=index, time=float(bar.time)))
445
470
  if sent is not None:
446
471
  return rows, [_diagnostic_row(sent.code, sent.line, sent.column, index)]
447
472
  rows.append(list(result.columns))