openscript 0.6.0__tar.gz → 0.7.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 (188) hide show
  1. {openscript-0.6.0 → openscript-0.7.0}/PKG-INFO +11 -1
  2. {openscript-0.6.0 → openscript-0.7.0}/README.md +10 -0
  3. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/__init__.py +5 -7
  4. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/arithmetic.py +5 -4
  5. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/averages.py +7 -14
  6. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/bookkeeping.py +8 -12
  7. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/composites.py +10 -18
  8. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/deviation.py +4 -3
  9. openscript-0.7.0/openscript/library/elementary.py +146 -0
  10. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/flows.py +10 -8
  11. openscript-0.7.0/openscript/library/gaussian.py +51 -0
  12. openscript-0.7.0/openscript/library/history.py +72 -0
  13. openscript-0.7.0/openscript/library/hypotenuse.py +65 -0
  14. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/momentum.py +8 -13
  15. openscript-0.7.0/openscript/library/power.py +79 -0
  16. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/ranges.py +2 -4
  17. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/series.py +45 -30
  18. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/strength.py +16 -6
  19. openscript-0.7.0/openscript/library/transcendental.py +166 -0
  20. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/trend.py +26 -25
  21. openscript-0.7.0/openscript/library/trigonometric.py +200 -0
  22. {openscript-0.6.0 → openscript-0.7.0}/openscript/memory.py +10 -2
  23. {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/PKG-INFO +11 -1
  24. {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/SOURCES.txt +24 -1
  25. {openscript-0.6.0 → openscript-0.7.0}/pyproject.toml +1 -1
  26. openscript-0.7.0/tests/test_hypot.py +156 -0
  27. {openscript-0.6.0 → openscript-0.7.0}/tests/test_library_vectors.py +7 -68
  28. openscript-0.7.0/tests/test_mfi_numeric_edges.py +146 -0
  29. openscript-0.7.0/tests/test_momentum_overflow.py +186 -0
  30. openscript-0.7.0/tests/test_power.py +136 -0
  31. openscript-0.7.0/tests/test_range_numeric_edges.py +47 -0
  32. openscript-0.7.0/tests/test_recursive_overflow.py +83 -0
  33. openscript-0.7.0/tests/test_stateful_numeric_edges.py +82 -0
  34. {openscript-0.6.0 → openscript-0.7.0}/tests/test_stateful_vectors.py +7 -74
  35. openscript-0.7.0/tests/test_statistics_overflow.py +112 -0
  36. openscript-0.7.0/tests/test_transcendental.py +174 -0
  37. openscript-0.7.0/tests/test_trend_numeric_edges.py +116 -0
  38. openscript-0.7.0/tests/test_trigonometric.py +135 -0
  39. openscript-0.7.0/tests/test_varying_distance.py +60 -0
  40. openscript-0.7.0/tests/test_varying_lookback.py +158 -0
  41. openscript-0.7.0/tests/test_varying_occurrence.py +67 -0
  42. openscript-0.7.0/tests/test_varying_recursive.py +120 -0
  43. openscript-0.7.0/tests/test_volume_numeric_edges.py +121 -0
  44. openscript-0.7.0/tests/test_zero_boundaries.py +59 -0
  45. openscript-0.6.0/openscript/library/elementary.py +0 -206
  46. {openscript-0.6.0 → openscript-0.7.0}/openscript/__init__.py +0 -0
  47. {openscript-0.6.0 → openscript-0.7.0}/openscript/__main__.py +0 -0
  48. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/__init__.py +0 -0
  49. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/analysis.py +0 -0
  50. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/charges.py +0 -0
  51. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/equity.py +0 -0
  52. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/report.py +0 -0
  53. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/shapes.py +0 -0
  54. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/statistics.py +0 -0
  55. {openscript-0.6.0 → openscript-0.7.0}/openscript/accounting/trades.py +0 -0
  56. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/__init__.py +0 -0
  57. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/answers.py +0 -0
  58. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/channels.py +0 -0
  59. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/expectations.py +0 -0
  60. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/facts.py +0 -0
  61. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/matching.py +0 -0
  62. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/ordering.py +0 -0
  63. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/page.py +0 -0
  64. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/reading.py +0 -0
  65. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/reporting.py +0 -0
  66. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/running.py +0 -0
  67. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/secondary.py +0 -0
  68. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/serving.py +0 -0
  69. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/sessions.py +0 -0
  70. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/spellings.py +0 -0
  71. {openscript-0.6.0 → openscript-0.7.0}/openscript/adapter/surface.py +0 -0
  72. {openscript-0.6.0 → openscript-0.7.0}/openscript/arrays.py +0 -0
  73. {openscript-0.6.0 → openscript-0.7.0}/openscript/bars.py +0 -0
  74. {openscript-0.6.0 → openscript-0.7.0}/openscript/buckets.py +0 -0
  75. {openscript-0.6.0 → openscript-0.7.0}/openscript/budget.py +0 -0
  76. {openscript-0.6.0 → openscript-0.7.0}/openscript/canonical.py +0 -0
  77. {openscript-0.6.0 → openscript-0.7.0}/openscript/civil.py +0 -0
  78. {openscript-0.6.0 → openscript-0.7.0}/openscript/contracts.py +0 -0
  79. {openscript-0.6.0 → openscript-0.7.0}/openscript/dates.py +0 -0
  80. {openscript-0.6.0 → openscript-0.7.0}/openscript/diagnostics.py +0 -0
  81. {openscript-0.6.0 → openscript-0.7.0}/openscript/hours.py +0 -0
  82. {openscript-0.6.0 → openscript-0.7.0}/openscript/inputs.py +0 -0
  83. {openscript-0.6.0 → openscript-0.7.0}/openscript/intervals.py +0 -0
  84. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/bars.py +0 -0
  85. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/code_points.py +0 -0
  86. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/colour.py +0 -0
  87. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/counting.py +0 -0
  88. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/extremes.py +0 -0
  89. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/number_text.py +0 -0
  90. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/prices.py +0 -0
  91. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/rounding.py +0 -0
  92. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/stateful.py +0 -0
  93. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/stateless.py +0 -0
  94. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/strings.py +0 -0
  95. {openscript-0.6.0 → openscript-0.7.0}/openscript/library/values.py +0 -0
  96. {openscript-0.6.0 → openscript-0.7.0}/openscript/logbook.py +0 -0
  97. {openscript-0.6.0 → openscript-0.7.0}/openscript/machine.py +0 -0
  98. {openscript-0.6.0 → openscript-0.7.0}/openscript/objects.py +0 -0
  99. {openscript-0.6.0 → openscript-0.7.0}/openscript/opcodes.py +0 -0
  100. {openscript-0.6.0 → openscript-0.7.0}/openscript/program.py +0 -0
  101. {openscript-0.6.0 → openscript-0.7.0}/openscript/reasons.py +0 -0
  102. {openscript-0.6.0 → openscript-0.7.0}/openscript/request_body.py +0 -0
  103. {openscript-0.6.0 → openscript-0.7.0}/openscript/request_plan.py +0 -0
  104. {openscript-0.6.0 → openscript-0.7.0}/openscript/requests.py +0 -0
  105. {openscript-0.6.0 → openscript-0.7.0}/openscript/run.py +0 -0
  106. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/__init__.py +0 -0
  107. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/calls.py +0 -0
  108. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/closable.py +0 -0
  109. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/fills.py +0 -0
  110. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/holdings.py +0 -0
  111. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/intents.py +0 -0
  112. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/ledger.py +0 -0
  113. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/placing.py +0 -0
  114. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/positions.py +0 -0
  115. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/refusals.py +0 -0
  116. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/rows.py +0 -0
  117. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/sizing.py +0 -0
  118. {openscript-0.6.0 → openscript-0.7.0}/openscript/strategy/statuses.py +0 -0
  119. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/__init__.py +0 -0
  120. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/bands.py +0 -0
  121. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/levels.py +0 -0
  122. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/marks.py +0 -0
  123. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/paints.py +0 -0
  124. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/plots.py +0 -0
  125. {openscript-0.6.0 → openscript-0.7.0}/openscript/surface/published.py +0 -0
  126. {openscript-0.6.0 → openscript-0.7.0}/openscript/values.py +0 -0
  127. {openscript-0.6.0 → openscript-0.7.0}/openscript/verify.py +0 -0
  128. {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_code.py +0 -0
  129. {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_requests.py +0 -0
  130. {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_shape.py +0 -0
  131. {openscript-0.6.0 → openscript-0.7.0}/openscript/verify_tables.py +0 -0
  132. {openscript-0.6.0 → openscript-0.7.0}/openscript/version.py +0 -0
  133. {openscript-0.6.0 → openscript-0.7.0}/openscript/zones.py +0 -0
  134. {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/dependency_links.txt +0 -0
  135. {openscript-0.6.0 → openscript-0.7.0}/openscript.egg-info/top_level.txt +0 -0
  136. {openscript-0.6.0 → openscript-0.7.0}/setup.cfg +0 -0
  137. {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_cases.py +0 -0
  138. {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_identity.py +0 -0
  139. {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_matching.py +0 -0
  140. {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_page.py +0 -0
  141. {openscript-0.6.0 → openscript-0.7.0}/tests/test_adapter_spellings.py +0 -0
  142. {openscript-0.6.0 → openscript-0.7.0}/tests/test_analysis_and_runup.py +0 -0
  143. {openscript-0.6.0 → openscript-0.7.0}/tests/test_arrays.py +0 -0
  144. {openscript-0.6.0 → openscript-0.7.0}/tests/test_bar_cycle.py +0 -0
  145. {openscript-0.6.0 → openscript-0.7.0}/tests/test_bars.py +0 -0
  146. {openscript-0.6.0 → openscript-0.7.0}/tests/test_calendar.py +0 -0
  147. {openscript-0.6.0 → openscript-0.7.0}/tests/test_calendar_reads.py +0 -0
  148. {openscript-0.6.0 → openscript-0.7.0}/tests/test_calls.py +0 -0
  149. {openscript-0.6.0 → openscript-0.7.0}/tests/test_canonical.py +0 -0
  150. {openscript-0.6.0 → openscript-0.7.0}/tests/test_charges.py +0 -0
  151. {openscript-0.6.0 → openscript-0.7.0}/tests/test_colour.py +0 -0
  152. {openscript-0.6.0 → openscript-0.7.0}/tests/test_cost_models.py +0 -0
  153. {openscript-0.6.0 → openscript-0.7.0}/tests/test_diagnostics.py +0 -0
  154. {openscript-0.6.0 → openscript-0.7.0}/tests/test_engine_facts.py +0 -0
  155. {openscript-0.6.0 → openscript-0.7.0}/tests/test_frame_fold.py +0 -0
  156. {openscript-0.6.0 → openscript-0.7.0}/tests/test_frame_instant.py +0 -0
  157. {openscript-0.6.0 → openscript-0.7.0}/tests/test_hand_over.py +0 -0
  158. {openscript-0.6.0 → openscript-0.7.0}/tests/test_harness.py +0 -0
  159. {openscript-0.6.0 → openscript-0.7.0}/tests/test_host_surface.py +0 -0
  160. {openscript-0.6.0 → openscript-0.7.0}/tests/test_ledger_orders.py +0 -0
  161. {openscript-0.6.0 → openscript-0.7.0}/tests/test_logbook.py +0 -0
  162. {openscript-0.6.0 → openscript-0.7.0}/tests/test_machine.py +0 -0
  163. {openscript-0.6.0 → openscript-0.7.0}/tests/test_maths_rules.py +0 -0
  164. {openscript-0.6.0 → openscript-0.7.0}/tests/test_memory.py +0 -0
  165. {openscript-0.6.0 → openscript-0.7.0}/tests/test_number_text.py +0 -0
  166. {openscript-0.6.0 → openscript-0.7.0}/tests/test_objects.py +0 -0
  167. {openscript-0.6.0 → openscript-0.7.0}/tests/test_opcodes.py +0 -0
  168. {openscript-0.6.0 → openscript-0.7.0}/tests/test_order_page.py +0 -0
  169. {openscript-0.6.0 → openscript-0.7.0}/tests/test_order_refusals.py +0 -0
  170. {openscript-0.6.0 → openscript-0.7.0}/tests/test_programs.py +0 -0
  171. {openscript-0.6.0 → openscript-0.7.0}/tests/test_request_host.py +0 -0
  172. {openscript-0.6.0 → openscript-0.7.0}/tests/test_requests.py +0 -0
  173. {openscript-0.6.0 → openscript-0.7.0}/tests/test_series_rules.py +0 -0
  174. {openscript-0.6.0 → openscript-0.7.0}/tests/test_session_window.py +0 -0
  175. {openscript-0.6.0 → openscript-0.7.0}/tests/test_sizing_refusal.py +0 -0
  176. {openscript-0.6.0 → openscript-0.7.0}/tests/test_strategy_cases.py +0 -0
  177. {openscript-0.6.0 → openscript-0.7.0}/tests/test_strategy_profile.py +0 -0
  178. {openscript-0.6.0 → openscript-0.7.0}/tests/test_string_ceiling.py +0 -0
  179. {openscript-0.6.0 → openscript-0.7.0}/tests/test_strings.py +0 -0
  180. {openscript-0.6.0 → openscript-0.7.0}/tests/test_supplied_schedule.py +0 -0
  181. {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface.py +0 -0
  182. {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface_bands.py +0 -0
  183. {openscript-0.6.0 → openscript-0.7.0}/tests/test_surface_example.py +0 -0
  184. {openscript-0.6.0 → openscript-0.7.0}/tests/test_time_programs.py +0 -0
  185. {openscript-0.6.0 → openscript-0.7.0}/tests/test_trades_and_summary.py +0 -0
  186. {openscript-0.6.0 → openscript-0.7.0}/tests/test_values.py +0 -0
  187. {openscript-0.6.0 → openscript-0.7.0}/tests/test_verify.py +0 -0
  188. {openscript-0.6.0 → openscript-0.7.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscript
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: An open trading language: the engine that runs a compiled program.
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
@@ -39,6 +39,16 @@ where any disagreement is a release blocker.
39
39
  stop a run being reproducible. Measured on every build against the module
40
40
  names the running interpreter says are its own.
41
41
 
42
+ ## Numerical results in 0.7.0
43
+
44
+ The two engines use the same specified rounding, seeds and missing-value rules.
45
+ Portable elementary kernels remove host-dependent final-bit differences; the
46
+ release audit requires exact agreement for every numerical signature it covers,
47
+ including forming-bar replacement and checkpoint replay. Install the matching
48
+ 0.7.0 compiler package when comparing browser and server results. Source calls
49
+ and the compiled format are unchanged, but corrected results can differ from
50
+ previous releases.
51
+
42
52
  ## Installing
43
53
 
44
54
  ```
@@ -25,6 +25,16 @@ where any disagreement is a release blocker.
25
25
  stop a run being reproducible. Measured on every build against the module
26
26
  names the running interpreter says are its own.
27
27
 
28
+ ## Numerical results in 0.7.0
29
+
30
+ The two engines use the same specified rounding, seeds and missing-value rules.
31
+ Portable elementary kernels remove host-dependent final-bit differences; the
32
+ release audit requires exact agreement for every numerical signature it covers,
33
+ including forming-bar replacement and checkpoint replay. Install the matching
34
+ 0.7.0 compiler package when comparing browser and server results. Source calls
35
+ and the compiled format are unchanged, but corrected results can differ from
36
+ previous releases.
37
+
28
38
  ## Installing
29
39
 
30
40
  ```
@@ -13,7 +13,7 @@ What is here, by the page it is written from:
13
13
  - ``values`` absence, and the two rules every number returned has passed
14
14
  - ``arithmetic`` section 8.1, the bare calls that compute one value
15
15
  - ``rounding`` section 8.1 and 20.7, one rule for halves and one scale table
16
- - ``elementary`` section 8.2, the square root and the gap 1 family
16
+ - ``elementary`` section 8.2 and the portable arithmetic of section 20.10
17
17
  - ``bars`` section 20.5's one reading that remembers nothing
18
18
  - ``code_points`` section 10's whitespace set and the one string order
19
19
  - ``number_text`` `language.md` 5.5, how a number becomes text and back
@@ -50,12 +50,10 @@ mechanical copy without the engine knowing which function it belongs to. A
50
50
  stateless call needs none, which is what lets an engine call one of those from
51
51
  anywhere without a checkpoint or a rollback.
52
52
 
53
- **One family is not held to the vectors.** ``exp``, ``log``, ``log10``,
54
- ``math.log2``, ``pow``, ``math.hypot`` and the trigonometric namespace reach gap 1
55
- of `stdlib.md` section 20.11, and so do ``alma``, ``hv`` and ``chop``, which are
56
- built on the first three: no portable reference algorithm is written down
57
- anywhere, so they call the host's maths module and carry no cross-engine
58
- guarantee. ``elementary`` says it again where an implementer will be standing.
53
+ Power and the trigonometric namespace still reach gap 1 of `stdlib.md` section
54
+ 20.11. Hypotenuse follows section 20.10.1, while exponential and logarithmic
55
+ functions follow section 20.10.2. Those portable kernels and every stateful
56
+ reading, including ``alma``, ``hv`` and ``chop``, are held to exact vector bits.
59
57
  """
60
58
 
61
59
  from .stateful import ENTRIES as STATEFUL_ENTRIES
@@ -12,7 +12,7 @@ exception and its order is written out in section 8.1 and confirmed in 20.7: the
12
12
  division, then the floor, then the multiplication, then the subtraction.
13
13
  """
14
14
 
15
- from .values import ABSENT, Value, floor_of, number, result
15
+ from .values import ABSENT, Value, floor_of, is_number, number, result
16
16
 
17
17
 
18
18
  def abs_of(x: Value) -> Value:
@@ -110,10 +110,11 @@ def is_none(x: Value) -> bool:
110
110
  def or_else(x: Value, fallback: Value) -> Value:
111
111
  """``orElse(x, fallback)``: ``x`` when present, ``fallback`` when absent.
112
112
 
113
- The fallback is returned as it stands, absence included, so
114
- ``orElse(x, none)`` is ``x``.
113
+ Absence and every nonnumeric tag keep their identity; numeric zero follows
114
+ the same positive-zero rule as other library results.
115
115
  """
116
- return fallback if x is None else x
116
+ value = fallback if x is None else x
117
+ return 0.0 if is_number(value) and value == 0 else value
117
118
 
118
119
 
119
120
  def to_bool(x: Value) -> bool:
@@ -6,10 +6,9 @@ rather than one is the step and the weighting, and what makes each of them one
6
6
  function rather than a family is the arrangement, which is why every entry here
7
7
  names the arrangement it is not.
8
8
 
9
- A value arrives already read off this bar's arguments, and every one of these
10
- pushes it into a buffer of its own before it looks at the window, because a call
11
- that skipped a bar's contribution would have a window a bar out of step with the
12
- bars it is supposed to cover.
9
+ A value arrives already read off this bar's arguments. Every invocation counts
10
+ as a contribution, including one with an absent length. Finite windows retain
11
+ their contributions; a seeded recurrence releases its history after seeding.
13
12
  """
14
13
 
15
14
  from .series import Region, contributed, mean, seeded, total, window
@@ -34,16 +33,13 @@ def exponential(state: Region, value: Value, length: int | None) -> Value:
34
33
  rather than two, and over the fixture the release gate compares bit for bit
35
34
  it differs on most of the values at every length.
36
35
  """
37
- values = contributed(state, "src", number(value), length)
38
- if length is None:
39
- return ABSENT
40
- weight = 2 / (length + 1)
36
+ weight = 0.0 if length is None else 2 / (length + 1)
41
37
  rest = 1 - weight
42
38
 
43
39
  def step(running: float, value: float) -> float:
44
40
  return value * weight + running * rest
45
41
 
46
- return seeded(state, "run", values, length, step)
42
+ return seeded(state, "run", number(value), length, step)
47
43
 
48
44
 
49
45
  def smoothed(state: Region, value: Value, length: int | None) -> Value:
@@ -56,15 +52,12 @@ def smoothed(state: Region, value: Value, length: int | None) -> Value:
56
52
  most often asked to reproduce. Neither is
57
53
  ``running + (value - running) / len``, which is a third arrangement again.
58
54
  """
59
- values = contributed(state, "src", number(value), length)
60
- if length is None:
61
- return ABSENT
62
- span = float(length)
55
+ span = 1.0 if length is None else float(length)
63
56
 
64
57
  def step(running: float, value: float) -> float:
65
58
  return (running * (span - 1) + value) / span
66
59
 
67
- return seeded(state, "run", values, length, step)
60
+ return seeded(state, "run", number(value), length, step)
68
61
 
69
62
 
70
63
  def linear(state: Region, value: Value, length: int | None) -> Value:
@@ -21,6 +21,7 @@ make the same way twice for two engines to agree:
21
21
 
22
22
  from typing import Optional
23
23
 
24
+ from .history import ContributionHistory
24
25
  from .series import Region, back, contributed, raw_window, region
25
26
  from .values import ABSENT, Value, number, result
26
27
 
@@ -150,17 +151,12 @@ def value_when(
150
151
  """
151
152
  wanted = 0 if occurrence is None else occurrence
152
153
  held = region(state, "seen")
153
- values = held.get("values")
154
- if values is None:
155
- values = []
156
- held["values"] = values
154
+ history = held.get("history")
155
+ if not isinstance(history, ContributionHistory):
156
+ history = ContributionHistory()
157
157
  if condition is True:
158
- values.append(value)
159
- depth = max(wanted + 1, held.get("depth", 1))
160
- held["depth"] = depth
161
- extra = len(values) - depth
162
- if extra > 0:
163
- del values[:extra]
164
- if wanted < 0 or len(values) <= wanted:
158
+ history = history.append(value, None)
159
+ held["history"] = history
160
+ if wanted < 0 or history.count <= wanted:
165
161
  return ABSENT
166
- return values[len(values) - 1 - wanted]
162
+ return history.view(wanted + 1)[0]
@@ -6,8 +6,8 @@ inner one produced, not on the first bars of the source. That composition is the
6
6
  whole reason `stdlib.md` section 1 states warmups in bars of the call's own
7
7
  source rather than in bars of the chart.
8
8
 
9
- The other two compute over the window directly: the Gaussian kernel, whose one
10
- dependence on the exponential puts it in gap 1 of section 20.11, and the least
9
+ The other two compute over the window directly: the portable Gaussian kernel
10
+ of section 20.10.2, and the least
11
11
  squares fit, whose constants are functions of the length alone.
12
12
 
13
13
  ``named`` is here rather than beside the six means because it has to be able to
@@ -17,6 +17,7 @@ select any of them, this file's included.
17
17
  import math
18
18
 
19
19
  from . import averages, elementary
20
+ from .gaussian import gaussian_weights
20
21
  from .rounding import round_half_away
21
22
  from .series import Region, contributed, region, window
22
23
  from .values import ABSENT, Value, number, result, whole
@@ -140,10 +141,8 @@ def gaussian(
140
141
  order. The denominator of the exponent is **one product, divided once**: the
141
142
  chain of divisions in circulation differs on about one exponent in four.
142
143
 
143
- **This is the one average whose value depends on ``exp``**, so it reaches gap
144
- 1 of section 20.11 and carries no cross-engine guarantee. The kernel depends
145
- only on the position, so an engine may build it once per length, provided the
146
- values it builds are the ones these lines produce.
144
+ A bounded pure cache holds immutable weights for exact parameter values.
145
+ It holds no source observations and does not enter checkpoint state.
147
146
  """
148
147
  values = contributed(state, "src", number(value), length)
149
148
  held = window(values, length)
@@ -151,19 +150,12 @@ def gaussian(
151
150
  width = number(sigma)
152
151
  if held is None or length is None or peak_at is None or width is None:
153
152
  return ABSENT
154
- if width == 0:
153
+ if width <= 0:
155
154
  return ABSENT
156
- peak = peak_at * (length - 1)
157
- spread = length / width
158
- weights = []
159
- norm = 0.0
160
- for position in range(length):
161
- gap = position - peak
162
- weight = elementary.exp(-(gap * gap) / (2 * spread * spread))
163
- if not isinstance(weight, float):
164
- return ABSENT
165
- weights.append(weight)
166
- norm = norm + weight
155
+ kernel = gaussian_weights(length, peak_at, width)
156
+ if kernel is None:
157
+ return ABSENT
158
+ weights, norm = kernel
167
159
  running = 0.0
168
160
  for position in range(length):
169
161
  running = running + held[length - 1 - position] * weights[position]
@@ -128,18 +128,19 @@ def historical(
128
128
  of log returns and says nothing about a percentage, so a study that wants a
129
129
  percentage axis multiplies at the plot, where a reader can see it happen.
130
130
 
131
- **This reading depends on ``log``**, so it reaches gap 1 of section 20.11 and
132
- carries no cross-engine guarantee.
131
+ The logarithm follows the portable interval recipe of section 20.10.2.
133
132
  """
134
133
  values = contributed(state, "src", number(value), 2)
135
134
  now = values[len(values) - 1]
136
135
  before = values[len(values) - 2] if len(values) > 1 else ABSENT
137
136
  ratio = ABSENT
138
- if isinstance(now, float) and isinstance(before, float) and before != 0:
137
+ if isinstance(now, float) and isinstance(before, float) and now > 0 and before > 0:
139
138
  ratio = now / before
140
139
  step = elementary.log(ratio)
141
140
  spread = deviation(region(state, "spread"), step, length, False)
142
141
  count = number(periods)
142
+ if count is None or count <= 0:
143
+ return ABSENT
143
144
  scale = elementary.sqrt(count)
144
145
  if not isinstance(spread, float) or not isinstance(scale, float):
145
146
  return ABSENT
@@ -0,0 +1,146 @@
1
+ """Elementary numeric functions with reproducible rounding.
2
+
3
+ Hypotenuse and exp/log use the exact integer recipes in sections 20.10.1 and
4
+ 20.10.2; power and trigonometry use sections 20.10.3 and 20.10.4.
5
+ Square root uses its correctly rounded host operation.
6
+ """
7
+
8
+ import math
9
+
10
+ from .hypotenuse import hypotenuse
11
+ from .transcendental import transcendental
12
+ from .power import power as real_power
13
+ from .trigonometric import trigonometric
14
+ from .values import ABSENT, Value, number, result
15
+
16
+ # The circle constant and the base of the natural logarithm, as `stdlib.md`
17
+ # section 8.2 names them. Both are the nearest binary64 to the constant and are
18
+ # the same bits on every platform.
19
+ PI: float = math.pi
20
+ E: float = math.e
21
+
22
+
23
+ def sqrt(x: Value) -> Value:
24
+ """``sqrt(x)``: square root, absent below zero."""
25
+ value = number(x)
26
+ if value is None or value < 0:
27
+ return ABSENT
28
+ return result(math.sqrt(value))
29
+
30
+
31
+ def exp(x: Value) -> Value:
32
+ """``exp(x)``: e to the power x, using section 20.10.2."""
33
+ value = number(x)
34
+ return ABSENT if value is None else transcendental('exp', value)
35
+
36
+
37
+ def log(x: Value) -> Value:
38
+ """``log(x)``: portable natural logarithm, absent at or below zero."""
39
+ value = number(x)
40
+ if value is None or value <= 0:
41
+ return ABSENT
42
+ return transcendental('log', value)
43
+
44
+
45
+ def log10(x: Value) -> Value:
46
+ """``log10(x)``: portable base ten logarithm, same absence rule."""
47
+ value = number(x)
48
+ if value is None or value <= 0:
49
+ return ABSENT
50
+ return transcendental('log10', value)
51
+
52
+
53
+ def log2(x: Value) -> Value:
54
+ """``math.log2(x)``: portable base two logarithm, same absence rule."""
55
+ value = number(x)
56
+ if value is None or value <= 0:
57
+ return ABSENT
58
+ return transcendental('log2', value)
59
+
60
+
61
+ def power(x: Value, y: Value) -> Value:
62
+ """``pow(x, y)``: portable real power, using section 20.10.3."""
63
+ base = number(x)
64
+ exponent = number(y)
65
+ if base is None or exponent is None:
66
+ return ABSENT
67
+ return real_power(base, exponent)
68
+
69
+
70
+ def hypot(x: Value, y: Value) -> Value:
71
+ """``math.hypot(x, y)``: the correctly rounded exact diagonal, section 20.10.1."""
72
+ left = number(x)
73
+ right = number(y)
74
+ if left is None or right is None:
75
+ return ABSENT
76
+ return hypotenuse(left, right)
77
+
78
+
79
+ def to_degrees(x: Value) -> Value:
80
+ """``math.toDegrees(x)``: ``(x * 180) / pi``, multiply first.
81
+
82
+ Not a gap: it is two operations over binary64 and `stdlib.md` section 20.7
83
+ fixes their order. Folding the constant into one factor is a different number
84
+ at about a quarter of the arguments, so the association is written out rather
85
+ than handed to a conversion the host provides.
86
+ """
87
+ value = number(x)
88
+ return ABSENT if value is None else result((value * 180) / math.pi)
89
+
90
+
91
+ def to_radians(x: Value) -> Value:
92
+ """``math.toRadians(x)``: ``(x * pi) / 180``, multiply first. Not a gap."""
93
+ value = number(x)
94
+ return ABSENT if value is None else result((value * math.pi) / 180)
95
+
96
+
97
+ def sin(x: Value) -> Value:
98
+ """``math.sin(x)``: sine of an angle in radians, using section 20.10.4."""
99
+ value = number(x)
100
+ return ABSENT if value is None else trigonometric('sin', value)
101
+
102
+
103
+ def cos(x: Value) -> Value:
104
+ """``math.cos(x)``: cosine, using section 20.10.4."""
105
+ value = number(x)
106
+ return ABSENT if value is None else trigonometric('cos', value)
107
+
108
+
109
+ def tan(x: Value) -> Value:
110
+ """``math.tan(x)``: tangent, using section 20.10.4."""
111
+ value = number(x)
112
+ return ABSENT if value is None else trigonometric('tan', value)
113
+
114
+
115
+ def asin(x: Value) -> Value:
116
+ """``math.asin(x)``: inverse sine, absent outside -1 to 1."""
117
+ value = number(x)
118
+ if value is None or value < -1 or value > 1:
119
+ return ABSENT
120
+ return trigonometric('asin', value)
121
+
122
+
123
+ def acos(x: Value) -> Value:
124
+ """``math.acos(x)``: inverse cosine, same range rule."""
125
+ value = number(x)
126
+ if value is None or value < -1 or value > 1:
127
+ return ABSENT
128
+ return trigonometric('acos', value)
129
+
130
+
131
+ def atan(x: Value) -> Value:
132
+ """``math.atan(x)``: inverse tangent, using section 20.10.4."""
133
+ value = number(x)
134
+ return ABSENT if value is None else trigonometric('atan', value)
135
+
136
+
137
+ def atan2(y: Value, x: Value) -> Value:
138
+ """``math.atan2(y, x)``: angle of a vector, in all four quadrants.
139
+
140
+ The vertical component comes first, as section 20.10.4 specifies.
141
+ """
142
+ vertical = number(y)
143
+ horizontal = number(x)
144
+ if vertical is None or horizontal is None:
145
+ return ABSENT
146
+ return trigonometric('atan2', vertical, horizontal)
@@ -44,11 +44,12 @@ def _anchored(state: Region, ctx, value: Value, anchor: Value) -> Value:
44
44
  held = region(state, "vwap")
45
45
  price = number(value)
46
46
  traded = number(ctx.bar("volume"))
47
- if anchor is not True and anchor is not False:
48
- return ABSENT
49
- if anchor:
47
+ if anchor is True:
50
48
  held["flow"] = 0.0
51
49
  held["traded"] = 0.0
50
+ held["anchored"] = True
51
+ if held.get("anchored") is not True:
52
+ return ABSENT
52
53
  if price is None or traded is None:
53
54
  return ABSENT
54
55
  flow = held.get("flow", 0.0) + price * traded
@@ -113,10 +114,11 @@ def flow_fraction(state: Region, ctx, length: Optional[int]) -> Value:
113
114
  traded = window(volumes, length)
114
115
  if held is None or traded is None:
115
116
  return ABSENT
116
- divisor = total(traded)
117
- if divisor == 0:
117
+ flow = result(total(held))
118
+ divisor = result(total(traded))
119
+ if flow is None or divisor is None or divisor == 0:
118
120
  return ABSENT
119
- return result(total(held) / divisor)
121
+ return result(flow / divisor)
120
122
 
121
123
 
122
124
  def accumulation_gap(
@@ -164,14 +166,14 @@ def money_flow(state: Region, ctx, length: Optional[int]) -> Value:
164
166
  rise = ABSENT
165
167
  fall = ABSENT
166
168
  if isinstance(price, float) and traded is not None and isinstance(before, float):
167
- flow = price * traded
169
+ flow = result(price * traded)
168
170
  rise = flow if price > before else 0.0
169
171
  fall = flow if price < before else 0.0
170
172
  rises = window(contributed(state, "rise", rise, length), length)
171
173
  falls = window(contributed(state, "fall", fall, length), length)
172
174
  if rises is None or falls is None:
173
175
  return ABSENT
174
- return strength.line_of(total(rises), total(falls))
176
+ return strength.line_of(result(total(rises)), result(total(falls)))
175
177
 
176
178
 
177
179
  def movement(state: Region, ctx, length: Optional[int]) -> Value:
@@ -0,0 +1,51 @@
1
+ """Bounded immutable Gaussian coefficients, independent of checkpoint state."""
2
+ import math
3
+ import struct
4
+
5
+ from .elementary import exp
6
+
7
+
8
+ class GaussianCache:
9
+ """FIFO with eight parameter tuples and at most 4096 coefficients."""
10
+ def __init__(self):
11
+ self.entries = {}
12
+ self.coefficients = 0
13
+
14
+ def get(self, length, offset, sigma):
15
+ if not math.isfinite(offset) or not math.isfinite(sigma) or sigma <= 0:
16
+ return None
17
+ key = (length, struct.pack('>d', offset), struct.pack('>d', sigma))
18
+ if key in self.entries:
19
+ return self.entries[key]
20
+ peak, spread = offset * (length - 1), length / sigma
21
+ denominator = (2 * spread) * spread
22
+ if denominator == 0:
23
+ return None
24
+ weights = []
25
+ norm = 0.0
26
+ for position in range(length):
27
+ gap = position - peak
28
+ exponent = -(gap * gap) / denominator
29
+ weight = 0.0 if exponent == -math.inf else exp(exponent)
30
+ if weight is None:
31
+ return None
32
+ weights.append(weight)
33
+ norm += weight
34
+ if norm == 0:
35
+ return None
36
+ computed = (tuple(weights), norm)
37
+ if length <= 4096:
38
+ while len(self.entries) >= 8 or self.coefficients + length > 4096:
39
+ oldest = next(iter(self.entries))
40
+ self.coefficients -= len(self.entries[oldest][0])
41
+ del self.entries[oldest]
42
+ self.entries[key] = computed
43
+ self.coefficients += length
44
+ return computed
45
+
46
+
47
+ _CACHE = GaussianCache()
48
+
49
+
50
+ def gaussian_weights(length, offset, sigma):
51
+ return _CACHE.get(length, offset, sigma)
@@ -0,0 +1,72 @@
1
+ """Immutable contributions with bounded append copying and cheap checkpoints."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Sequence
5
+
6
+ from .values import Value
7
+
8
+ _WIDTH = 32
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class _Chunk:
13
+ values: tuple[Value, ...]
14
+ prior: Optional["_Chunk"]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ContributionHistory:
19
+ """A version shares its sealed prefix with previous versions and forks."""
20
+
21
+ head: Optional[_Chunk] = None
22
+ tail: tuple[Value, ...] = ()
23
+ count: int = 0
24
+ maximum: int = 0
25
+
26
+ def append(self, value: Value, length: Optional[int]) -> "ContributionHistory":
27
+ full = len(self.tail) == _WIDTH
28
+ head = _Chunk(self.tail, self.head) if full else self.head
29
+ tail = (value,) if full else self.tail + (value,)
30
+ maximum = max(self.maximum, length or 0)
31
+ return ContributionHistory(head, tail, self.count + 1, maximum)
32
+
33
+ def __deepcopy__(self, memo):
34
+ """All reachable state is immutable, so checkpoint copying shares it."""
35
+ return self
36
+
37
+ def view(self, length: int) -> "ContributionView":
38
+ size = min(length, self.count)
39
+ chunks = []
40
+ remaining = size - len(self.tail)
41
+ head = self.head
42
+ while remaining > 0 and head is not None:
43
+ chunks.append(head.values)
44
+ head = head.prior
45
+ remaining -= _WIDTH
46
+ return ContributionView(self, tuple(chunks), size)
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ContributionView(Sequence[Value]):
51
+ """A transient oldest-first sequence indexing only the requested chunks."""
52
+
53
+ history: ContributionHistory
54
+ chunks: tuple[tuple[Value, ...], ...]
55
+ size: int
56
+
57
+ def __len__(self):
58
+ return self.size
59
+
60
+ def __getitem__(self, index):
61
+ if isinstance(index, slice):
62
+ return [self[at] for at in range(*index.indices(self.size))]
63
+ if index < 0:
64
+ index += self.size
65
+ if index < 0 or index >= self.size:
66
+ raise IndexError(index)
67
+ back = self.size - 1 - index
68
+ tail = self.history.tail
69
+ if back < len(tail):
70
+ return tail[len(tail) - 1 - back]
71
+ before = back - len(tail)
72
+ return self.chunks[before // _WIDTH][_WIDTH - 1 - before % _WIDTH]
@@ -0,0 +1,65 @@
1
+ """Exact two-argument hypotenuse, `stdlib.md` section 20.10.1."""
2
+ import math
3
+ import struct
4
+
5
+ _LEADING = 1 << 52
6
+ _FRACTION = _LEADING - 1
7
+
8
+
9
+ def _parts(value: float) -> tuple[int, int]:
10
+ encoded = struct.unpack('>Q', struct.pack('>d', abs(value)))[0]
11
+ exponent = encoded >> 52
12
+ significand = (encoded & _FRACTION) | (_LEADING if exponent else 0)
13
+ return significand, exponent - 1075 if exponent else -1074
14
+
15
+
16
+ def _decode(encoded: int) -> float:
17
+ return struct.unpack('>d', struct.pack('>Q', encoded))[0]
18
+
19
+
20
+ def _integer_sqrt(value: int) -> int:
21
+ """Monotone Newton steps from a power-of-two upper bound reach the floor."""
22
+ if value < 2:
23
+ return value
24
+ before = 1 << ((value.bit_length() + 1) // 2)
25
+ while True:
26
+ following = (before + value // before) // 2
27
+ if following >= before:
28
+ return before
29
+ before = following
30
+
31
+
32
+ def hypotenuse(x: float, y: float) -> float | None:
33
+ """Round the exact squared sum by comparing with a squared midpoint.
34
+
35
+ Binary64 input widths bound every temporary integer to fewer than 4,200 bits.
36
+ Encoding only after that decision avoids a second rounding through a scale.
37
+ """
38
+ if not math.isfinite(x) or not math.isfinite(y):
39
+ return None
40
+ if x == 0:
41
+ return abs(y)
42
+ if y == 0:
43
+ return abs(x)
44
+ a, ea = _parts(x)
45
+ b, eb = _parts(y)
46
+ exponent = min(ea, eb)
47
+ square = (a * a << (2 * (ea - exponent))) + (b * b << (2 * (eb - exponent)))
48
+ spacing = max(-1074, exponent + (square.bit_length() - 1) // 2 - 52)
49
+ shift = exponent - spacing
50
+ numerator = square if shift < 0 else square << (2 * shift)
51
+ denominator = 1 << (-2 * shift) if shift < 0 else 1
52
+ significand = _integer_sqrt(numerator // denominator)
53
+ midpoint = 2 * significand + 1
54
+ comparison = 4 * numerator - denominator * midpoint * midpoint
55
+ if comparison > 0 or comparison == 0 and significand % 2:
56
+ significand += 1
57
+ if significand >= 2 * _LEADING:
58
+ significand >>= 1
59
+ spacing += 1
60
+ if significand < _LEADING:
61
+ return _decode(significand)
62
+ encoded_exponent = spacing + 1075
63
+ if encoded_exponent >= 2047:
64
+ return None
65
+ return _decode(encoded_exponent << 52 | (significand - _LEADING))