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,155 @@
1
+ """The spread readings of `stdlib.md` section 20.5 and the bands built on them.
2
+
3
+ **The variance is two passes and the one pass arrangement is not permitted.**
4
+ Subtracting the square of the mean from the mean of the squares is mathematically
5
+ equal, loses most of its significant digits on a price series where the values
6
+ are large and their spread is small, and can return a negative variance that then
7
+ has to be floored at zero. An implementation that needs a floor to stay real is
8
+ computing a different quantity, and the floor is how it hides.
9
+
10
+ **The two readings taken from the bands are computed from the bands as
11
+ reported.** A study that plots all three lines and a study that plots one reading
12
+ then agree to the last bit, which they would not if the reading were rebuilt from
13
+ the basis and the width.
14
+ """
15
+
16
+ from typing import Optional
17
+
18
+ from . import averages, composites, elementary, ranges
19
+ from .series import Region, contributed, mean, region, window
20
+ from .values import ABSENT, Value, number, result
21
+
22
+
23
+ def variance(
24
+ state: Region, value: Value, length: Optional[int], sample: Value
25
+ ) -> Value:
26
+ """``variance(src, len, sample)``: the mean first, the deviations from it second.
27
+
28
+ Each deviation is squared as a product of the deviation with itself. The
29
+ divisor is ``len`` for the population form and ``len - 1`` for the sample
30
+ form, which is what the argument names rather than a correction a script has
31
+ to write by hand.
32
+ """
33
+ held = window(contributed(state, "src", number(value), length), length)
34
+ if held is None or length is None:
35
+ return ABSENT
36
+ squares = spread_of(held, length)
37
+ divisor = (length - 1) if sample is True else length
38
+ if divisor == 0:
39
+ return ABSENT
40
+ return result(squares / divisor)
41
+
42
+
43
+ def spread_of(held, length: int) -> float:
44
+ """The second pass: the squared deviations from the finished mean, oldest first."""
45
+ middle = mean(held, length)
46
+ squares = 0.0
47
+ for at in range(length - 1, -1, -1):
48
+ squares = squares + (held[at] - middle) * (held[at] - middle)
49
+ return squares
50
+
51
+
52
+ def deviation(
53
+ state: Region, value: Value, length: Optional[int], sample: Value
54
+ ) -> Value:
55
+ """``stdev(src, len, sample)``: the square root of that variance, taken once."""
56
+ return elementary.sqrt(variance(state, value, length, sample))
57
+
58
+
59
+ def bands(state: Region, value: Value, length: Optional[int], multiplier: Value) -> list:
60
+ """``bollinger(src, len, mult)``: a mean with deviation bands, formed as written.
61
+
62
+ The multiplier is applied to the width and the product added to the basis,
63
+ which is one rounding of the product and one of the sum.
64
+ """
65
+ basis = averages.simple(region(state, "basis"), value, length)
66
+ width = deviation(region(state, "width"), value, length, False)
67
+ return _banded(basis, width, number(multiplier))
68
+
69
+
70
+ def band_width(
71
+ state: Region, value: Value, length: Optional[int], multiplier: Value
72
+ ) -> Value:
73
+ """``bbWidth(src, len, mult)``: the band width over the basis, a squeeze reads low."""
74
+ basis, upper, lower = bands(state, value, length, multiplier)
75
+ if not isinstance(upper, float) or not isinstance(lower, float):
76
+ return ABSENT
77
+ if not isinstance(basis, float) or basis == 0:
78
+ return ABSENT
79
+ return result((upper - lower) / basis)
80
+
81
+
82
+ def band_position(
83
+ state: Region, value: Value, length: Optional[int], multiplier: Value
84
+ ) -> Value:
85
+ """``bbPercent(src, len, mult)``: where the source sits between the two bands."""
86
+ _basis, upper, lower = bands(state, value, length, multiplier)
87
+ price = number(value)
88
+ if not isinstance(upper, float) or not isinstance(lower, float) or price is None:
89
+ return ABSENT
90
+ span = upper - lower
91
+ if span == 0:
92
+ return ABSENT
93
+ return result((price - lower) / span)
94
+
95
+
96
+ def range_bands(
97
+ state: Region,
98
+ ctx,
99
+ length: Optional[int],
100
+ multiplier: Value,
101
+ width_length: Optional[int],
102
+ kind: Value,
103
+ ) -> list:
104
+ """``keltner(len, mult, atrLen, maType)``: the same picture built from the range.
105
+
106
+ **The basis is the close**, averaged at ``len`` by the type the argument
107
+ names, and not the typical price. Section 6 writes the call without a source
108
+ argument and the close is the source it means: the two agree only on a bar
109
+ whose close is its own typical price, so this decides which function the call
110
+ is rather than the order its arithmetic runs in.
111
+
112
+ The width is the average true range exactly as section 20.5 defines it, the
113
+ oldest bar's exception included, and not the gap aware form the directional
114
+ index takes.
115
+ """
116
+ basis = composites.named(region(state, "basis"), ctx, ctx.bar("close"), length, kind)
117
+ width = ranges.average_range(region(state, "width"), ctx, width_length)
118
+ return _banded(basis, width, number(multiplier))
119
+
120
+
121
+ def historical(
122
+ state: Region, value: Value, length: Optional[int], periods: Value
123
+ ) -> Value:
124
+ """``hv(src, len, periodsPerYear)``: the spread of the log return, annualised once.
125
+
126
+ The ratio is formed before the logarithm is taken. The result is a proportion
127
+ and is not scaled by a hundred: section 6 says annualised standard deviation
128
+ of log returns and says nothing about a percentage, so a study that wants a
129
+ percentage axis multiplies at the plot, where a reader can see it happen.
130
+
131
+ **This reading depends on ``log``**, so it reaches gap 1 of section 20.11 and
132
+ carries no cross-engine guarantee.
133
+ """
134
+ values = contributed(state, "src", number(value), 2)
135
+ now = values[len(values) - 1]
136
+ before = values[len(values) - 2] if len(values) > 1 else ABSENT
137
+ ratio = ABSENT
138
+ if isinstance(now, float) and isinstance(before, float) and before != 0:
139
+ ratio = now / before
140
+ step = elementary.log(ratio)
141
+ spread = deviation(region(state, "spread"), step, length, False)
142
+ count = number(periods)
143
+ scale = elementary.sqrt(count)
144
+ if not isinstance(spread, float) or not isinstance(scale, float):
145
+ return ABSENT
146
+ return result(spread * scale)
147
+
148
+
149
+ def _banded(basis: Value, width: Value, multiplier: Optional[float]) -> list:
150
+ """The three lines of a band study: the basis, and the width either side of it."""
151
+ if not isinstance(basis, float) or not isinstance(width, float):
152
+ return [basis if isinstance(basis, float) else ABSENT, ABSENT, ABSENT]
153
+ if multiplier is None:
154
+ return [basis, ABSENT, ABSENT]
155
+ return [basis, result(basis + multiplier * width), result(basis - multiplier * width)]
@@ -0,0 +1,206 @@
1
+ """Square root, the logarithms, the powers and the trigonometry.
2
+
3
+ **Everything here except ``sqrt`` reaches gap 1 of `stdlib.md` section 20.11,
4
+ and is not held to the vectors.** That section states the gap and
5
+ `compiled-program.md` section 8.3 states the requirement it cannot meet: a
6
+ transcendental function is to be computed by a portable reference algorithm
7
+ rather than by the platform's own maths library, because a platform's is correct
8
+ to within about an ulp and differs between platforms in the last bit. **No such
9
+ algorithm is written down anywhere**, so there is nothing to implement against,
10
+ and inventing one here would be a third answer rather than a second engine.
11
+
12
+ So these call the host's maths module and say so. `conformance.md` section 8
13
+ scopes the difference out: no case may assert a value that reaches this row, not
14
+ even with a tolerance, because an engine that is correct can fail such a case and
15
+ the release gate would then stop a release over a hole in the specification. The
16
+ calls still work and still compute what they always did; what they do not carry
17
+ is a cross-engine guarantee.
18
+
19
+ Measured against `spec/vectors/library/`, which is the first engine's arithmetic,
20
+ this file's answers differ on a minority of cells and never by more than one ulp.
21
+ ``exp``, ``log``, ``log10``, ``pow``, ``sin``, ``cos``, ``tan``, ``hypot`` and
22
+ ``atan2`` each have some; ``log2``, ``asin``, ``acos`` and ``atan`` had none on
23
+ the interpreter this was written under, which is a fact about two maths libraries
24
+ on one machine and not a guarantee about a third.
25
+
26
+ **``sqrt`` is the exception and is held to the vectors.** IEEE-754 requires it to
27
+ be correctly rounded, so every conforming platform returns the same bits, and
28
+ section 20.10 says so rather than leaving it to be assumed.
29
+
30
+ The domain rules are `stdlib.md` section 8.1's, and are checked before the call
31
+ rather than read off the answer: the host raises where this language is absent,
32
+ and an engine that let the raise out would turn "no finite real value" into a
33
+ stopped run.
34
+ """
35
+
36
+ import math
37
+
38
+ from .values import ABSENT, Value, number, result
39
+
40
+ # The circle constant and the base of the natural logarithm, as `stdlib.md`
41
+ # section 8.2 names them. Both are the nearest binary64 to the constant and are
42
+ # the same bits on every platform, so neither reaches the gap above.
43
+ PI: float = math.pi
44
+ E: float = math.e
45
+
46
+
47
+ def _guarded(x: float, of) -> Value:
48
+ """A host maths call whose refusals become absence.
49
+
50
+ The host raises where the argument is outside the function's domain and
51
+ where the answer overflows. `stdlib.md` section 2.4 makes both of those
52
+ absence: a function whose result has no finite real value returns ``none``
53
+ rather than raising, and it is a function whose *arguments* are wrong that
54
+ raises, which is a diagnostic the checker or the interpreter produces long
55
+ before the call gets here.
56
+ """
57
+ try:
58
+ return result(of(x))
59
+ except (ValueError, OverflowError, ZeroDivisionError):
60
+ return ABSENT
61
+
62
+
63
+ def sqrt(x: Value) -> Value:
64
+ """``sqrt(x)``: square root, absent below zero."""
65
+ value = number(x)
66
+ if value is None or value < 0:
67
+ return ABSENT
68
+ return result(math.sqrt(value))
69
+
70
+
71
+ def exp(x: Value) -> Value:
72
+ """``exp(x)``: e to the power x. Reaches gap 1."""
73
+ value = number(x)
74
+ return ABSENT if value is None else _guarded(value, math.exp)
75
+
76
+
77
+ def log(x: Value) -> Value:
78
+ """``log(x)``: natural logarithm, absent at or below zero. Reaches gap 1."""
79
+ value = number(x)
80
+ if value is None or value <= 0:
81
+ return ABSENT
82
+ return _guarded(value, math.log)
83
+
84
+
85
+ def log10(x: Value) -> Value:
86
+ """``log10(x)``: base ten logarithm, same absence rule. Reaches gap 1."""
87
+ value = number(x)
88
+ if value is None or value <= 0:
89
+ return ABSENT
90
+ return _guarded(value, math.log10)
91
+
92
+
93
+ def log2(x: Value) -> Value:
94
+ """``math.log2(x)``: base two logarithm, same absence rule. Reaches gap 1."""
95
+ value = number(x)
96
+ if value is None or value <= 0:
97
+ return ABSENT
98
+ return _guarded(value, math.log2)
99
+
100
+
101
+ def power(x: Value, y: Value) -> Value:
102
+ """``pow(x, y)``: absent where the result is not a finite real.
103
+
104
+ Reaches gap 1. A negative base raised to a fractional exponent has no real
105
+ answer and a zero base raised to a negative one has no finite answer, and
106
+ both are absence here rather than the refusal the host makes of them.
107
+ """
108
+ base = number(x)
109
+ exponent = number(y)
110
+ if base is None or exponent is None:
111
+ return ABSENT
112
+ try:
113
+ return result(math.pow(base, exponent))
114
+ except (ValueError, OverflowError, ZeroDivisionError):
115
+ return ABSENT
116
+
117
+
118
+ def hypot(x: Value, y: Value) -> Value:
119
+ """``math.hypot(x, y)``: the diagonal without intermediate overflow.
120
+
121
+ Reaches gap 1, and it is the reading of this file most likely to differ:
122
+ avoiding the overflow is what makes it more than a square root of a sum of
123
+ squares, and no two libraries scale the same way.
124
+ """
125
+ left = number(x)
126
+ right = number(y)
127
+ if left is None or right is None:
128
+ return ABSENT
129
+ try:
130
+ return result(math.hypot(left, right))
131
+ except (ValueError, OverflowError):
132
+ return ABSENT
133
+
134
+
135
+ def to_degrees(x: Value) -> Value:
136
+ """``math.toDegrees(x)``: ``(x * 180) / pi``, multiply first.
137
+
138
+ Not a gap: it is two operations over binary64 and `stdlib.md` section 20.7
139
+ fixes their order. Folding the constant into one factor is a different number
140
+ at about a quarter of the arguments, so the association is written out rather
141
+ than handed to a conversion the host provides.
142
+ """
143
+ value = number(x)
144
+ return ABSENT if value is None else result((value * 180) / math.pi)
145
+
146
+
147
+ def to_radians(x: Value) -> Value:
148
+ """``math.toRadians(x)``: ``(x * pi) / 180``, multiply first. Not a gap."""
149
+ value = number(x)
150
+ return ABSENT if value is None else result((value * math.pi) / 180)
151
+
152
+
153
+ def sin(x: Value) -> Value:
154
+ """``math.sin(x)``: sine of an angle in radians. Reaches gap 1."""
155
+ value = number(x)
156
+ return ABSENT if value is None else _guarded(value, math.sin)
157
+
158
+
159
+ def cos(x: Value) -> Value:
160
+ """``math.cos(x)``: cosine. Reaches gap 1."""
161
+ value = number(x)
162
+ return ABSENT if value is None else _guarded(value, math.cos)
163
+
164
+
165
+ def tan(x: Value) -> Value:
166
+ """``math.tan(x)``: tangent. Reaches gap 1."""
167
+ value = number(x)
168
+ return ABSENT if value is None else _guarded(value, math.tan)
169
+
170
+
171
+ def asin(x: Value) -> Value:
172
+ """``math.asin(x)``: inverse sine, absent outside -1 to 1. Reaches gap 1."""
173
+ value = number(x)
174
+ if value is None or value < -1 or value > 1:
175
+ return ABSENT
176
+ return _guarded(value, math.asin)
177
+
178
+
179
+ def acos(x: Value) -> Value:
180
+ """``math.acos(x)``: inverse cosine, same range rule. Reaches gap 1."""
181
+ value = number(x)
182
+ if value is None or value < -1 or value > 1:
183
+ return ABSENT
184
+ return _guarded(value, math.acos)
185
+
186
+
187
+ def atan(x: Value) -> Value:
188
+ """``math.atan(x)``: inverse tangent. Reaches gap 1."""
189
+ value = number(x)
190
+ return ABSENT if value is None else _guarded(value, math.atan)
191
+
192
+
193
+ def atan2(y: Value, x: Value) -> Value:
194
+ """``math.atan2(y, x)``: angle of a vector, in all four quadrants.
195
+
196
+ Reaches gap 1. The argument order is the page's, which is the one every
197
+ library states: the vertical component first.
198
+ """
199
+ vertical = number(y)
200
+ horizontal = number(x)
201
+ if vertical is None or horizontal is None:
202
+ return ABSENT
203
+ try:
204
+ return result(math.atan2(vertical, horizontal))
205
+ except (ValueError, OverflowError):
206
+ return ABSENT
@@ -0,0 +1,122 @@
1
+ """The window scans of `stdlib.md` sections 9 and 20.10, and the pivots.
2
+
3
+ Section 20.10 names these as the functions with no accumulation order to fix:
4
+ they select a value and compute nothing, so an implementer can write the scan the
5
+ obvious way and be bit identical. **The one thing a scan can differ on is a
6
+ tie**, and the tie rules are section 9's, so they are what this file is careful
7
+ about.
8
+
9
+ - An extreme goes to **the most recent bar that set it**, so an equal value later
10
+ in the window replaces the earlier one. That only shows in the two calls that
11
+ report an age, and it shows there on every flat stretch of a series.
12
+ - A pivot is **strict on both sides**, so a run of equal values holds no pivot at
13
+ all. A test written with "at or above" would report a pivot at each end of a
14
+ flat top, which is two pivots where the chart shows none.
15
+
16
+ A window arrives newest first, so an index into it is an age in bars and
17
+ ``held[0]`` is what this bar contributed.
18
+ """
19
+
20
+ from typing import Optional, Sequence
21
+
22
+ from .series import Region, contributed, window
23
+ from .values import ABSENT, Value, number, result
24
+
25
+
26
+ def high_at(held: Sequence[float]) -> int:
27
+ """The age in bars of the window's high, the most recent bar that set it.
28
+
29
+ The scan runs oldest first and replaces on an equal value, which is how the
30
+ tie reaches the newest of the bars that hold it rather than the oldest.
31
+ """
32
+ at = len(held) - 1
33
+ for age in range(len(held) - 2, -1, -1):
34
+ if held[age] >= held[at]:
35
+ at = age
36
+ return at
37
+
38
+
39
+ def low_at(held: Sequence[float]) -> int:
40
+ """The age in bars of the window's low, on the same terms."""
41
+ at = len(held) - 1
42
+ for age in range(len(held) - 2, -1, -1):
43
+ if held[age] <= held[at]:
44
+ at = age
45
+ return at
46
+
47
+
48
+ def window_high(values: Sequence[Value], length: Optional[int]) -> Value:
49
+ """The outright high of the last ``length`` contributions, or absence."""
50
+ held = window(values, length)
51
+ return ABSENT if held is None else result(held[high_at(held)])
52
+
53
+
54
+ def window_low(values: Sequence[Value], length: Optional[int]) -> Value:
55
+ """The outright low of the last ``length`` contributions, or absence."""
56
+ held = window(values, length)
57
+ return ABSENT if held is None else result(held[low_at(held)])
58
+
59
+
60
+ def highest(state: Region, value: Value, length: Optional[int]) -> Value:
61
+ """``highest(src, len)``: the largest value in the last ``len`` bars."""
62
+ return window_high(contributed(state, "src", number(value), length), length)
63
+
64
+
65
+ def lowest(state: Region, value: Value, length: Optional[int]) -> Value:
66
+ """``lowest(src, len)``: the smallest."""
67
+ return window_low(contributed(state, "src", number(value), length), length)
68
+
69
+
70
+ def highest_bars(state: Region, value: Value, length: Optional[int]) -> Value:
71
+ """``highestBars(src, len)``: how many bars back the window's high was set."""
72
+ held = window(contributed(state, "src", number(value), length), length)
73
+ return ABSENT if held is None else float(high_at(held))
74
+
75
+
76
+ def lowest_bars(state: Region, value: Value, length: Optional[int]) -> Value:
77
+ """``lowestBars(src, len)``: how many bars back the window's low was set."""
78
+ held = window(contributed(state, "src", number(value), length), length)
79
+ return ABSENT if held is None else float(low_at(held))
80
+
81
+
82
+ def pivot_high(state: Region, value: Value, left: Optional[int], right: Optional[int]) -> Value:
83
+ """``pivotHigh(src, left, right)``: a local high, reported ``right`` bars after it.
84
+
85
+ Reporting it at the pivot bar would be a lookahead: the value would appear on
86
+ history at a bar where no script could have had it. So the window is
87
+ ``left + right + 1`` wide, the candidate sits ``right`` bars back inside it,
88
+ and the answer is absent on every bar that is not one.
89
+ """
90
+ values = contributed(state, "src", number(value), _span(left, right))
91
+ return _pivot(values, left, right, True)
92
+
93
+
94
+ def pivot_low(state: Region, value: Value, left: Optional[int], right: Optional[int]) -> Value:
95
+ """``pivotLow(src, left, right)``: a local low, on the same terms."""
96
+ values = contributed(state, "src", number(value), _span(left, right))
97
+ return _pivot(values, left, right, False)
98
+
99
+
100
+ def _span(left: Optional[int], right: Optional[int]) -> Optional[int]:
101
+ """The window a pivot test needs: the bars either side and the candidate."""
102
+ if left is None or right is None:
103
+ return None
104
+ return left + right + 1
105
+
106
+
107
+ def _pivot(
108
+ values: Sequence[Value], left: Optional[int], right: Optional[int], above: bool
109
+ ) -> Value:
110
+ """The candidate's value where it beats both sides strictly, absence otherwise."""
111
+ held = window(values, _span(left, right))
112
+ if held is None or right is None:
113
+ return ABSENT
114
+ candidate = held[right]
115
+ for age in range(len(held)):
116
+ if age == right:
117
+ continue
118
+ if above and held[age] >= candidate:
119
+ return ABSENT
120
+ if not above and held[age] <= candidate:
121
+ return ABSENT
122
+ return result(candidate)