openscript 0.4.0__py3-none-any.whl

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 (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,220 @@
1
+ """The volume readings of `stdlib.md` section 20.6.
2
+
3
+ **The running totals here are anchored accumulations rather than windows**, so
4
+ each carries its total forward and adds one term per bar. That is what the
5
+ quantity is, not a cheaper way to compute a window sum, and the refusal of a
6
+ carried total in section 20.2.1 does not reach them: there is no window to sum.
7
+
8
+ **Every total starts at zero** before any bar has contributed to it, and an
9
+ absent bar produces an absent bar out and leaves the total where it was. It is
10
+ neither reset nor fed a zero in place of the missing term, so a gap costs the
11
+ reading the bars it covers and nothing after them.
12
+
13
+ Every function here is absent on every bar when the host supplies no volume for
14
+ the instrument, which is one case of the same rule: an absent volume is not a
15
+ zero volume, and a study that wants to branch on it tests the chart fact rather
16
+ than inspecting the result.
17
+ """
18
+
19
+ from typing import Optional
20
+
21
+ from . import averages, prices, strength
22
+ from .series import Region, back, contributed, region, running_total, total, window
23
+ from .values import ABSENT, Value, number, result
24
+
25
+
26
+ def session_average(state: Region, ctx, value: Value) -> Value:
27
+ """``vwap(src)``: the volume weighted average since the session opened."""
28
+ return _anchored(state, ctx, value, ctx.bar("isSessionFirst"))
29
+
30
+
31
+ def anchored_average(state: Region, ctx, value: Value, anchor: Value) -> Value:
32
+ """``vwapAnchor(src, resetWhen)``: the same average, restarted on a condition.
33
+
34
+ One calculation and two entries. The session reading resets where the host
35
+ says a session began, this one where the script says so, and the reset
36
+ happens **before** the bar's own term is added, so an anchor bar is the first
37
+ bar of the new average rather than the last bar of the old one.
38
+ """
39
+ return _anchored(state, ctx, value, anchor)
40
+
41
+
42
+ def _anchored(state: Region, ctx, value: Value, anchor: Value) -> Value:
43
+ """The two totals, the reset, and the one division both averages take."""
44
+ held = region(state, "vwap")
45
+ price = number(value)
46
+ traded = number(ctx.bar("volume"))
47
+ if anchor is not True and anchor is not False:
48
+ return ABSENT
49
+ if anchor:
50
+ held["flow"] = 0.0
51
+ held["traded"] = 0.0
52
+ if price is None or traded is None:
53
+ return ABSENT
54
+ flow = held.get("flow", 0.0) + price * traded
55
+ volume = held.get("traded", 0.0) + traded
56
+ held["flow"] = flow
57
+ held["traded"] = volume
58
+ if volume == 0:
59
+ return ABSENT
60
+ return result(flow / volume)
61
+
62
+
63
+ def balance(state: Region, ctx) -> Value:
64
+ """``obv()``: the whole of the bar's volume, signed by the close's direction.
65
+
66
+ The first bar has no close before it to compare against, so it contributes
67
+ nothing and the reading there is the 0 the total started at. An unchanged
68
+ close contributes nothing either.
69
+ """
70
+ close = number(ctx.bar("close"))
71
+ traded = number(ctx.bar("volume"))
72
+ before = back(contributed(state, "close", close, 2), 1)
73
+ if close is None or traded is None:
74
+ return ABSENT
75
+ term = 0.0
76
+ if isinstance(before, float):
77
+ if close > before:
78
+ term = traded
79
+ elif close < before:
80
+ term = -traded
81
+ return running_total(state, "run", term)
82
+
83
+
84
+ def accumulation(state: Region, ctx) -> Value:
85
+ """``ad()``: the running total of the term ``cmf`` sums over a window."""
86
+ return running_total(state, "run", position_flow(ctx))
87
+
88
+
89
+ def position_flow(ctx) -> Value:
90
+ """The per-bar term ``ad`` and ``cmf`` share, formed as section 20.6 writes it.
91
+
92
+ The two bracketed differences are formed first and subtracted, then divided by
93
+ the span, then multiplied by the volume. **A bar whose span is not above zero
94
+ contributes an exact 0** rather than ending the total.
95
+ """
96
+ high = number(ctx.bar("high"))
97
+ low = number(ctx.bar("low"))
98
+ close = number(ctx.bar("close"))
99
+ traded = number(ctx.bar("volume"))
100
+ if high is None or low is None or close is None or traded is None:
101
+ return ABSENT
102
+ span = high - low
103
+ if span <= 0:
104
+ return 0.0
105
+ return result((((close - low) - (high - close)) / span) * traded)
106
+
107
+
108
+ def flow_fraction(state: Region, ctx, length: Optional[int]) -> Value:
109
+ """``cmf(len)``: the window sum of that term over the window sum of the volume."""
110
+ terms = contributed(state, "term", position_flow(ctx), length)
111
+ volumes = contributed(state, "size", number(ctx.bar("volume")), length)
112
+ held = window(terms, length)
113
+ traded = window(volumes, length)
114
+ if held is None or traded is None:
115
+ return ABSENT
116
+ divisor = total(traded)
117
+ if divisor == 0:
118
+ return ABSENT
119
+ return result(total(held) / divisor)
120
+
121
+
122
+ def accumulation_gap(
123
+ state: Region, ctx, fast_length: Optional[int], slow_length: Optional[int]
124
+ ) -> Value:
125
+ """``adOsc(fast, slow)``: the fast exponential mean of the running total less the slow.
126
+
127
+ The averages run over the running total, not over the per-bar term, which is
128
+ what dates the turns of the total rather than the turns of one bar's flow.
129
+ """
130
+ running = accumulation(region(state, "total"), ctx)
131
+ fast = averages.exponential(region(state, "fast"), running, fast_length)
132
+ slow = averages.exponential(region(state, "slow"), running, slow_length)
133
+ if not isinstance(fast, float) or not isinstance(slow, float):
134
+ return ABSENT
135
+ return result(fast - slow)
136
+
137
+
138
+ def price_trend(state: Region, ctx) -> Value:
139
+ """``pvt()``: the running total of a proportion of the volume.
140
+
141
+ The proportion is formed and rounded before it meets the volume. The first
142
+ bar has no change behind it and is absent, and the second already carries its
143
+ own term on top of the zero the total started at.
144
+ """
145
+ close = number(ctx.bar("close"))
146
+ traded = number(ctx.bar("volume"))
147
+ before = back(contributed(state, "close", close, 2), 1)
148
+ if close is None or traded is None or not isinstance(before, float) or before == 0:
149
+ return ABSENT
150
+ return running_total(state, "run", ((close - before) / before) * traded)
151
+
152
+
153
+ def money_flow(state: Region, ctx, length: Optional[int]) -> Value:
154
+ """``mfi(len)``: the strength reading computed on money flow, with window sums.
155
+
156
+ The arrangement of the last line is section 20.4's, and the result is 100
157
+ where the falling side is zero, for the reason given there. A bar whose
158
+ typical price is unchanged contributes an exact zero to both sides.
159
+ """
160
+ price = prices.typical(ctx)
161
+ traded = number(ctx.bar("volume"))
162
+ values = contributed(state, "typical", price, 2)
163
+ before = back(values, 1)
164
+ rise = ABSENT
165
+ fall = ABSENT
166
+ if isinstance(price, float) and traded is not None and isinstance(before, float):
167
+ flow = price * traded
168
+ rise = flow if price > before else 0.0
169
+ fall = flow if price < before else 0.0
170
+ rises = window(contributed(state, "rise", rise, length), length)
171
+ falls = window(contributed(state, "fall", fall, length), length)
172
+ if rises is None or falls is None:
173
+ return ABSENT
174
+ return strength.line_of(total(rises), total(falls))
175
+
176
+
177
+ def movement(state: Region, ctx, length: Optional[int]) -> Value:
178
+ """``eom(len)``: how far price moved per unit of volume, then a simple mean.
179
+
180
+ The product is formed before the division. **There is no scaling constant**:
181
+ implementations of this reading usually multiply by a large divisor whose only
182
+ job is to bring the number into a readable range and they do not agree on it,
183
+ section 7 declares no such argument, and this is gap 3 of section 20.11. That
184
+ leaves the reading very small on a liquid instrument.
185
+ """
186
+ midpoint = prices.midpoint(ctx)
187
+ values = contributed(state, "midpoint", midpoint, 2)
188
+ before = back(values, 1)
189
+ high = number(ctx.bar("high"))
190
+ low = number(ctx.bar("low"))
191
+ traded = number(ctx.bar("volume"))
192
+ term = ABSENT
193
+ if isinstance(midpoint, float) and isinstance(before, float):
194
+ if high is not None and low is not None and traded is not None and traded != 0:
195
+ term = result(((midpoint - before) * (high - low)) / traded)
196
+ return averages.simple(region(state, "mean"), term, length)
197
+
198
+
199
+ def force(state: Region, ctx, length: Optional[int]) -> Value:
200
+ """``forceIndex(len)``: the one bar close change times the volume, smoothed.
201
+
202
+ The product is formed per bar and fed to the average, so a bar with no volume
203
+ freezes the average rather than feeding it a change with nothing behind it.
204
+ """
205
+ close = number(ctx.bar("close"))
206
+ traded = number(ctx.bar("volume"))
207
+ before = back(contributed(state, "close", close, 2), 1)
208
+ term = ABSENT
209
+ if close is not None and traded is not None and isinstance(before, float):
210
+ term = result((close - before) * traded)
211
+ return averages.exponential(region(state, "mean"), term, length)
212
+
213
+
214
+ def relative_volume(state: Region, ctx, length: Optional[int]) -> Value:
215
+ """``relativeVolume(len)``: the bar's volume over the simple mean of the volume."""
216
+ traded = number(ctx.bar("volume"))
217
+ average = averages.simple(region(state, "mean"), traded, length)
218
+ if traded is None or not isinstance(average, float) or average == 0:
219
+ return ABSENT
220
+ return result(traded / average)
@@ -0,0 +1,203 @@
1
+ """The momentum readings of `stdlib.md` section 20.4, and the two gaps between means.
2
+
3
+ **These scale by 100 before dividing**, which is the association their own
4
+ readings have always carried and the opposite of the directional index of section
5
+ 20.3. Writing one of them the other way round moves every value it reports.
6
+
7
+ Two of them are named in section 20.4 only so that the absence of a recipe is not
8
+ read as an oversight: the momentum reading is a single subtraction, and the
9
+ awesome oscillator is the fast simple mean of the bar midpoint less the slow one,
10
+ in that order.
11
+ """
12
+
13
+ from typing import Optional
14
+
15
+ from . import averages, bookkeeping, composites, prices
16
+ from .series import Region, back, contributed, region, total, window
17
+ from .values import ABSENT, Value, number, result
18
+
19
+
20
+ def gap(
21
+ state: Region,
22
+ value: Value,
23
+ fast_length: Optional[int],
24
+ slow_length: Optional[int],
25
+ signal_length: Optional[int],
26
+ percent: bool,
27
+ ) -> list:
28
+ """``macd(...)`` and ``ppo(...)``, which differ only in the first line.
29
+
30
+ The signal average is fed the line absences and all, so it seeds on the first
31
+ ``signal`` values the line produced, which is where its warmup comes from. The
32
+ histogram is the difference of the two values as reported, not recomputed
33
+ from the averages.
34
+ """
35
+ fast = averages.exponential(region(state, "fast"), value, fast_length)
36
+ slow = averages.exponential(region(state, "slow"), value, slow_length)
37
+ line = ABSENT
38
+ if isinstance(fast, float) and isinstance(slow, float):
39
+ if not percent:
40
+ line = result(fast - slow)
41
+ elif slow != 0:
42
+ line = result((100 * (fast - slow)) / slow)
43
+ trigger = averages.exponential(region(state, "signal"), line, signal_length)
44
+ histogram = ABSENT
45
+ if isinstance(line, float) and isinstance(trigger, float):
46
+ histogram = result(line - trigger)
47
+ return [line, trigger, histogram]
48
+
49
+
50
+ def distance(state: Region, value: Value, length: Optional[int]) -> Value:
51
+ """``mom(src, len)``: ``src - src[len]``, which is the change of section 9."""
52
+ return bookkeeping.change(state, value, length)
53
+
54
+
55
+ def rate(state: Region, value: Value, length: Optional[int]) -> Value:
56
+ """``roc(src, len)``: the same change as a percentage of the older value."""
57
+ keep = None if length is None else length + 1
58
+ values = contributed(state, "src", number(value), keep)
59
+ now = values[len(values) - 1]
60
+ before = back(values, length)
61
+ if not isinstance(now, float) or not isinstance(before, float) or before == 0:
62
+ return ABSENT
63
+ return result((100 * (now - before)) / before)
64
+
65
+
66
+ def midpoint_gap(
67
+ state: Region, ctx, fast_length: Optional[int], slow_length: Optional[int]
68
+ ) -> Value:
69
+ """``awesomeOsc(fast, slow)``: the fast simple mean of the midpoint less the slow one."""
70
+ value = prices.midpoint(ctx)
71
+ fast = averages.simple(region(state, "fast"), value, fast_length)
72
+ slow = averages.simple(region(state, "slow"), value, slow_length)
73
+ if not isinstance(fast, float) or not isinstance(slow, float):
74
+ return ABSENT
75
+ return result(fast - slow)
76
+
77
+
78
+ def smoothed_rate(state: Region, value: Value, length: Optional[int]) -> Value:
79
+ """``trix(src, len)``: the one bar percentage change of a triple exponential mean.
80
+
81
+ The other reading in circulation takes the change of the logarithm of the
82
+ average instead. It is a different number and it is not what section 5
83
+ describes.
84
+ """
85
+ smoothed = composites.smoothed_three(region(state, "chain"), value, length)
86
+ values = contributed(state, "smoothed", smoothed, 2)
87
+ now = values[len(values) - 1]
88
+ before = back(values, 1)
89
+ if not isinstance(now, float) or not isinstance(before, float) or before == 0:
90
+ return ABSENT
91
+ return result((100 * (now - before)) / before)
92
+
93
+
94
+ def double_smoothed(
95
+ state: Region, value: Value, long_length: Optional[int], short_length: Optional[int]
96
+ ) -> Value:
97
+ """``tsi(src, longLen, shortLen)``: the one bar change smoothed twice, over its own size.
98
+
99
+ The long length comes first and the short one second, and the absolute size
100
+ of the change is smoothed the same way in its own pair of state regions.
101
+ """
102
+ values = contributed(state, "src", number(value), 2)
103
+ now = values[len(values) - 1]
104
+ before = back(values, 1)
105
+ delta = ABSENT
106
+ size = ABSENT
107
+ if isinstance(now, float) and isinstance(before, float):
108
+ delta = now - before
109
+ size = abs(delta)
110
+ smoothed_change = _twice(region(state, "change"), delta, long_length, short_length)
111
+ smoothed_size = _twice(region(state, "size"), size, long_length, short_length)
112
+ if not isinstance(smoothed_change, float) or not isinstance(smoothed_size, float):
113
+ return ABSENT
114
+ if smoothed_size == 0:
115
+ return ABSENT
116
+ return result((100 * smoothed_change) / smoothed_size)
117
+
118
+
119
+ def _twice(
120
+ state: Region, value: Value, long_length: Optional[int], short_length: Optional[int]
121
+ ) -> Value:
122
+ """One quantity smoothed with the long length and then with the short one."""
123
+ once = averages.exponential(region(state, "long"), value, long_length)
124
+ return averages.exponential(region(state, "short"), once, short_length)
125
+
126
+
127
+ def detrended(state: Region, value: Value, length: Optional[int]) -> Value:
128
+ """``dpo(src, len)``: the simple mean removed as it stood, displaced back.
129
+
130
+ The displacement is ``floor(len / 2) + 1`` bars, which is what the declared
131
+ warmup of bar ``len + floor(len / 2)`` requires.
132
+ """
133
+ price = number(value)
134
+ average = averages.simple(region(state, "mean"), price, length)
135
+ shift = None if length is None else length // 2 + 1
136
+ keep = None if shift is None else shift + 1
137
+ held = contributed(state, "mean-values", average, keep)
138
+ older = back(held, shift)
139
+ if price is None or not isinstance(older, float):
140
+ return ABSENT
141
+ return result(price - older)
142
+
143
+
144
+ def blended(
145
+ state: Region,
146
+ ctx,
147
+ first_length: Optional[int],
148
+ second_length: Optional[int],
149
+ third_length: Optional[int],
150
+ ) -> Value:
151
+ """``ultimateOsc(len1, len2, len3)``: pressure over range, blended over three windows.
152
+
153
+ Both per-bar terms are formed against the previous close, which is the close
154
+ of the bar before this one as this call site saw it and not a fact the host
155
+ states: this reading counts changes, so the oldest bar has no term at all and
156
+ the exception section 6 grants the plain true range has nothing to do with
157
+ it. The three weighted terms are added left to right, shortest window first,
158
+ and the division by 7 is applied once at the end.
159
+ """
160
+ high = number(ctx.bar("high"))
161
+ low = number(ctx.bar("low"))
162
+ close = number(ctx.bar("close"))
163
+ before = back(contributed(state, "close", number(ctx.bar("close")), 2), 1)
164
+ pressure = ABSENT
165
+ span = ABSENT
166
+ if high is not None and low is not None and close is not None:
167
+ if isinstance(before, float):
168
+ floor_of = low if low < before else before
169
+ ceiling_of = high if high > before else before
170
+ pressure = close - floor_of
171
+ span = ceiling_of - floor_of
172
+ deepest = _deepest(first_length, second_length, third_length)
173
+ pressures = contributed(state, "pressure", pressure, deepest)
174
+ spans = contributed(state, "range", span, deepest)
175
+ fast = _ratio(pressures, spans, first_length)
176
+ middle = _ratio(pressures, spans, second_length)
177
+ slow = _ratio(pressures, spans, third_length)
178
+ if not isinstance(fast, float) or not isinstance(middle, float):
179
+ return ABSENT
180
+ if not isinstance(slow, float):
181
+ return ABSENT
182
+ blend = 4 * fast
183
+ blend = blend + 2 * middle
184
+ blend = blend + slow
185
+ return result((100 * blend) / 7)
186
+
187
+
188
+ def _deepest(*lengths) -> Optional[int]:
189
+ """The widest of the three windows, which is what both buffers keep."""
190
+ known = [one for one in lengths if one is not None]
191
+ return max(known) if known else None
192
+
193
+
194
+ def _ratio(pressures, spans, length: Optional[int]) -> Value:
195
+ """One window's pressure over its range, each the fresh sum of section 20.2.1."""
196
+ top = window(pressures, length)
197
+ bottom = window(spans, length)
198
+ if top is None or bottom is None:
199
+ return ABSENT
200
+ divisor = total(bottom)
201
+ if divisor == 0:
202
+ return ABSENT
203
+ return total(top) / divisor
@@ -0,0 +1,223 @@
1
+ """How a number becomes text, and how text becomes a number.
2
+
3
+ `language.md` section 5.5 is one rule for every place a number is written: the
4
+ ``text`` call, a table cell, an alert's message, a constant in a compiled
5
+ program, a case file and an expected column. Two engines compare numbers as bits
6
+ and text as text, so a number that two rules could spell two ways is a
7
+ disagreement with no number wrong anywhere.
8
+
9
+ **The digits** are the shortest string of decimal digits that reads back as the
10
+ same binary64 value. That is the conversion the host already performs natively
11
+ and two hosts have been measured to agree on it, so it is taken rather than
12
+ rebuilt. **The layout is where hosts part company**: where the point goes, when
13
+ an exponent starts, and what is never written. The host's own writing turns to an
14
+ exponent at a different magnitude, spells the exponent with a sign and a leading
15
+ zero, and writes a point and a zero after a whole number, and all four of those
16
+ are wrong here.
17
+
18
+ **That layout is written out once, in ``canonical.py``, and this file calls it.**
19
+ It was written out twice for one stage, here and there, with the two thresholds
20
+ typed into each; both passed the vectors, so the copy cost nothing until the day
21
+ one of them moved. What is this file's is what the layout is used for: the value
22
+ a script asks for as text, the fixed decimal conversion built on the same digits,
23
+ and the reading back of text as a number.
24
+
25
+ `spec/vectors/number-text.json` holds the boundary cases as bit patterns and as
26
+ text, in both directions, so an engine in any language can be held to the rule
27
+ without reading either engine's source. ``tests/test_number_text.py`` is this
28
+ engine held to it.
29
+
30
+ **``text(x, decimals)`` is the one call whose layout is always positional**, at
31
+ every magnitude, and its digits are this rule's digits for the rounded and scaled
32
+ whole number, zero filled. A conversion that changed shape above a threshold
33
+ would be a label that read correctly until the day a cumulative volume crossed
34
+ it.
35
+ """
36
+
37
+ import math
38
+
39
+ from ..canonical import canonical_number, spread
40
+ from .code_points import trimmed
41
+ from .colour import Colour, hex_byte
42
+ from .rounding import round_half_away, scale_of
43
+ from .values import ABSENT, Value, is_number, number, result, whole
44
+
45
+
46
+ def _carry(digits: str) -> str:
47
+ """One added to a string of digits, which grows it when every digit is nine."""
48
+ out = list(digits)
49
+ for at in range(len(out) - 1, -1, -1):
50
+ if out[at] != "9":
51
+ out[at] = chr(ord(out[at]) + 1)
52
+ return "".join(out)
53
+ out[at] = "0"
54
+ return "1" + "".join(out)
55
+
56
+
57
+ def _written(digits: str, point: int) -> str:
58
+ """A spread written out, rounded half up where the point falls inside it.
59
+
60
+ Half up on a magnitude is half away from zero, because the sign is carried
61
+ separately by every caller.
62
+ """
63
+ if point >= len(digits):
64
+ return digits + "0" * (point - len(digits))
65
+ if point < 0:
66
+ return "0"
67
+ kept = digits[:point]
68
+ if digits[point] < "5":
69
+ return kept if kept != "" else "0"
70
+ return _carry(kept)
71
+
72
+
73
+ def fixed(x: float, decimals: int) -> str:
74
+ """``text(x, decimals)``: a sign, at least one digit, and exactly ``decimals``.
75
+
76
+ The rounding is done on the number before it is written rather than left to a
77
+ formatting routine, because a routine's tie rule is the host's and the two
78
+ disagree at exactly the values a price lands on: this is halves away from
79
+ zero over the scaled value, and the host's is halves to even over the exact
80
+ binary expansion.
81
+
82
+ **The scale is ``round(x, decimals)``'s**, so the display conversion and the
83
+ rounding call multiply by one value (`stdlib.md` 20.7) and cannot part by an
84
+ ulp at the one count where a floating point power does.
85
+
86
+ Where the scaling leaves binary64 altogether there is nothing left to round:
87
+ a binary64 that large is a whole number already and a decimal place that far
88
+ from its leading digit is past every digit it carries, so the digits are the
89
+ unscaled value's and the point is moved instead.
90
+ """
91
+ scaled = round_half_away(x * scale_of(decimals))
92
+ usable = math.isfinite(scaled)
93
+ negative = scaled < 0 if usable else x < 0
94
+ sign = "-" if negative else ""
95
+ if usable:
96
+ digits, point = spread(canonical_number(abs(scaled)))
97
+ else:
98
+ digits, point = spread(canonical_number(abs(x)), decimals)
99
+ written = _written(digits, point).rjust(decimals + 1, "0")
100
+ if decimals == 0:
101
+ return sign + written
102
+ cut = len(written) - decimals
103
+ return sign + written[:cut] + "." + written[cut:]
104
+
105
+
106
+ def fixed_length(x: float, decimals: int) -> int:
107
+ """How long ``text(x, decimals)`` will be, before a character is built.
108
+
109
+ A floor rather than the exact count: a carry off the front adds one digit and
110
+ a negative adds the sign, and both are caught by the ceiling the built string
111
+ is checked against. What this is for is the decimal count a script computed,
112
+ which can ask for a string no engine can hold, and building it to find that
113
+ out is how an engine runs out of memory instead of reporting that it would
114
+ have. The ceiling itself is the interpreter's: nothing in this package raises.
115
+ """
116
+ _, point = spread(canonical_number(abs(x)))
117
+ return max(1, point) + decimals + (1 if decimals > 0 else 0)
118
+
119
+
120
+ def text_of(x: Value, decimals: Value) -> Value:
121
+ """``text(x, decimals)`` as the library entry calls it."""
122
+ value = number(x)
123
+ count = whole(decimals)
124
+ if value is None or count is None:
125
+ return ABSENT
126
+ return fixed(value, count)
127
+
128
+
129
+ def text_length(x: Value, decimals: Value) -> int | None:
130
+ """``text(x, decimals)``'s length, as the interpreter asks for it.
131
+
132
+ Absence for arguments the call itself answers absence for, because a call
133
+ that builds no string cannot pass a ceiling on strings.
134
+ """
135
+ value = number(x)
136
+ count = whole(decimals)
137
+ if value is None or count is None:
138
+ return None
139
+ return fixed_length(value, count)
140
+
141
+
142
+ _DIGITS = "0123456789"
143
+
144
+
145
+ def to_number(text: Value) -> Value:
146
+ """``toNumber(s)``: a string to a number, absent for what does not parse.
147
+
148
+ The grammar is walked here rather than handed to the host's own reader, which
149
+ accepts an infinity, a not-a-number and digit group separators, and which of
150
+ those a host accepts differs between hosts and moves between their releases.
151
+ Absence rather than zero for text that is not a number, so a
152
+ script can tell text that is not a number apart from the number zero. Only
153
+ these ten digits count: the host's own digit test accepts the decimal digits
154
+ of every script Unicode has, which would read a number no writer of the
155
+ script could have typed and no second engine would agree with.
156
+
157
+ The whitespace ignored at either end is ``str.trim``'s set and no other.
158
+ """
159
+ if not isinstance(text, str):
160
+ return ABSENT
161
+ bare = trimmed(text)
162
+ at = 0
163
+ end = len(bare)
164
+ if at < end and bare[at] in "+-":
165
+ at += 1
166
+ before = 0
167
+ while at < end and bare[at] in _DIGITS:
168
+ at += 1
169
+ before += 1
170
+ after = 0
171
+ if at < end and bare[at] == ".":
172
+ at += 1
173
+ while at < end and bare[at] in _DIGITS:
174
+ at += 1
175
+ after += 1
176
+ if before == 0 and after == 0:
177
+ return ABSENT
178
+ if at < end and bare[at] in "eE":
179
+ at += 1
180
+ if at < end and bare[at] in "+-":
181
+ at += 1
182
+ power = 0
183
+ while at < end and bare[at] in _DIGITS:
184
+ at += 1
185
+ power += 1
186
+ if power == 0:
187
+ return ABSENT
188
+ if at != end:
189
+ return ABSENT
190
+ return result(float(bare))
191
+
192
+
193
+ def spell(value: Value, of_reference=None) -> str:
194
+ """A value as ``text(x)`` spells it.
195
+
196
+ Absence is the word ``none``, a bool is its own word, a string is itself, and
197
+ a number is the rule at the top of this file. A colour is the ``#rrggbbaa``
198
+ spelling the conformance suite compares, where the alpha byte is
199
+ ``round(alpha * 255)`` with the language's own rounding: that conversion is
200
+ one way and is not a round trip, and nothing in the language observes the
201
+ difference, because a script reads an alpha with ``alpha()`` from the machine
202
+ value and never from the wire form.
203
+
204
+ A reference is the interpreter's: the heap and the object in it are not this
205
+ package's, so ``of_reference`` is asked for the kind of one and the word
206
+ ``none`` stands where no interpreter answered.
207
+ """
208
+ if value is None:
209
+ return "none"
210
+ if value is True:
211
+ return "true"
212
+ if value is False:
213
+ return "false"
214
+ if is_number(value):
215
+ return canonical_number(float(value))
216
+ if isinstance(value, str):
217
+ return value
218
+ if isinstance(value, Colour):
219
+ alpha = round_half_away(value.a * 255)
220
+ return "#" + hex_byte(value.r) + hex_byte(value.g) + hex_byte(value.b) + hex_byte(alpha)
221
+ if of_reference is None:
222
+ return "none"
223
+ return of_reference(value)
@@ -0,0 +1,36 @@
1
+ """The two derived prices the stateful half forms for itself, from facts it read.
2
+
3
+ A study that calls ``hlc3`` is handed the bar register `compiled-program.md`
4
+ section 2.10 defines, and never reaches this file. What reaches this file is the
5
+ library's own readings: ``cci`` and ``mfi`` are written against the typical
6
+ price, and ``eom``, ``awesomeOsc`` and the trailing band against the midpoint,
7
+ and none of those takes a source argument, so the value has to be formed from the
8
+ three bar facts the call read.
9
+
10
+ **The order of operations is section 2.10's and is part of the contract**, which
11
+ is why these are two named functions rather than an expression written out at
12
+ five call sites: a different association gives a different last bit, and a study
13
+ that matched a reference implementation on one engine and not on another is
14
+ exactly the failure this project exists to prevent.
15
+ """
16
+
17
+ from .values import ABSENT, Value, number, result
18
+
19
+
20
+ def midpoint(ctx) -> Value:
21
+ """``(high + low) / 2``, the bar's midpoint."""
22
+ high = number(ctx.bar("high"))
23
+ low = number(ctx.bar("low"))
24
+ if high is None or low is None:
25
+ return ABSENT
26
+ return result((high + low) / 2)
27
+
28
+
29
+ def typical(ctx) -> Value:
30
+ """``(high + low + close) / 3``: high to low, close to that, then the division."""
31
+ high = number(ctx.bar("high"))
32
+ low = number(ctx.bar("low"))
33
+ close = number(ctx.bar("close"))
34
+ if high is None or low is None or close is None:
35
+ return ABSENT
36
+ return result((high + low + close) / 3)