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.
- openscript/__init__.py +40 -0
- openscript/__main__.py +62 -0
- openscript/accounting/__init__.py +74 -0
- openscript/accounting/analysis.py +174 -0
- openscript/accounting/charges.py +397 -0
- openscript/accounting/equity.py +234 -0
- openscript/accounting/report.py +82 -0
- openscript/accounting/shapes.py +74 -0
- openscript/accounting/statistics.py +300 -0
- openscript/accounting/trades.py +294 -0
- openscript/adapter/__init__.py +32 -0
- openscript/adapter/answers.py +215 -0
- openscript/adapter/channels.py +137 -0
- openscript/adapter/expectations.py +67 -0
- openscript/adapter/facts.py +127 -0
- openscript/adapter/matching.py +257 -0
- openscript/adapter/ordering.py +187 -0
- openscript/adapter/page.py +130 -0
- openscript/adapter/reading.py +357 -0
- openscript/adapter/reporting.py +244 -0
- openscript/adapter/running.py +449 -0
- openscript/adapter/serving.py +229 -0
- openscript/adapter/sessions.py +168 -0
- openscript/adapter/spellings.py +184 -0
- openscript/bars.py +157 -0
- openscript/budget.py +342 -0
- openscript/canonical.py +192 -0
- openscript/civil.py +196 -0
- openscript/contracts.py +165 -0
- openscript/dates.py +302 -0
- openscript/diagnostics.py +104 -0
- openscript/hours.py +165 -0
- openscript/inputs.py +239 -0
- openscript/intervals.py +60 -0
- openscript/library/__init__.py +76 -0
- openscript/library/arithmetic.py +128 -0
- openscript/library/averages.py +133 -0
- openscript/library/bars.py +60 -0
- openscript/library/bookkeeping.py +166 -0
- openscript/library/code_points.py +85 -0
- openscript/library/colour.py +202 -0
- openscript/library/composites.py +208 -0
- openscript/library/counting.py +218 -0
- openscript/library/deviation.py +155 -0
- openscript/library/elementary.py +206 -0
- openscript/library/extremes.py +122 -0
- openscript/library/flows.py +220 -0
- openscript/library/momentum.py +203 -0
- openscript/library/number_text.py +223 -0
- openscript/library/prices.py +36 -0
- openscript/library/ranges.py +105 -0
- openscript/library/rounding.py +123 -0
- openscript/library/series.py +213 -0
- openscript/library/stateful.py +442 -0
- openscript/library/stateless.py +261 -0
- openscript/library/strength.py +180 -0
- openscript/library/strings.py +228 -0
- openscript/library/trend.py +260 -0
- openscript/library/values.py +91 -0
- openscript/logbook.py +119 -0
- openscript/machine.py +499 -0
- openscript/memory.py +204 -0
- openscript/opcodes.py +166 -0
- openscript/program.py +146 -0
- openscript/run.py +368 -0
- openscript/strategy/__init__.py +78 -0
- openscript/strategy/calls.py +201 -0
- openscript/strategy/closable.py +182 -0
- openscript/strategy/fills.py +131 -0
- openscript/strategy/holdings.py +277 -0
- openscript/strategy/intents.py +162 -0
- openscript/strategy/ledger.py +270 -0
- openscript/strategy/placing.py +206 -0
- openscript/strategy/positions.py +124 -0
- openscript/strategy/refusals.py +293 -0
- openscript/strategy/rows.py +219 -0
- openscript/strategy/sizing.py +229 -0
- openscript/strategy/statuses.py +65 -0
- openscript/surface/__init__.py +115 -0
- openscript/surface/bands.py +103 -0
- openscript/surface/levels.py +44 -0
- openscript/surface/marks.py +52 -0
- openscript/surface/paints.py +58 -0
- openscript/surface/plots.py +44 -0
- openscript/surface/published.py +119 -0
- openscript/values.py +210 -0
- openscript/verify.py +301 -0
- openscript/verify_code.py +290 -0
- openscript/verify_requests.py +271 -0
- openscript/verify_shape.py +162 -0
- openscript/verify_tables.py +256 -0
- openscript/version.py +39 -0
- openscript/zones.py +118 -0
- openscript-0.4.0.dist-info/METADATA +82 -0
- openscript-0.4.0.dist-info/RECORD +97 -0
- openscript-0.4.0.dist-info/WHEEL +5 -0
- 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)
|