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,202 @@
|
|
|
1
|
+
"""Colour: the nineteen names, and the calls that compute one.
|
|
2
|
+
|
|
3
|
+
**A colour's channels are whole numbers and its alpha is not**
|
|
4
|
+
(`compiled-program.md` section 3.1). Red, green and blue are whole numbers from
|
|
5
|
+
0 to 255 in every colour the machine holds, not only in a literal, because every
|
|
6
|
+
call that computes one rounds the three channels before it returns, with the
|
|
7
|
+
language's own rounding, halves away from zero (`stdlib.md` section 11.2). Alpha
|
|
8
|
+
stays a binary64 number from 0 to 1 and becomes a byte only at the contract
|
|
9
|
+
boundary, where the conversion is one way and is not a round trip.
|
|
10
|
+
|
|
11
|
+
The two arrangements `stdlib.md` section 20.9 fixes are the whole of the
|
|
12
|
+
arithmetic here, and both were chosen against a mathematically equal alternative
|
|
13
|
+
that differs in the last bit, so both are written out rather than described:
|
|
14
|
+
|
|
15
|
+
- ``mix`` interpolates as ``from + (to - from) * weight``, channel by channel and
|
|
16
|
+
the alpha included. It is not ``from * (1 - weight) + to * weight``.
|
|
17
|
+
- ``fade`` sets the alpha to ``(100 - percent) / 100``, the subtraction before
|
|
18
|
+
the division. It is not ``1 - percent / 100``, which is a different number at
|
|
19
|
+
40 of the 101 whole percentages, and it sets the alpha rather than scaling the
|
|
20
|
+
one the colour already carried, which is what makes a nested fade the inner
|
|
21
|
+
call's fade (`stdlib.md` section 11.2).
|
|
22
|
+
|
|
23
|
+
**The channel table is a copy of a fact this repository holds once.**
|
|
24
|
+
`spec/colours.json` is the authority and part of the conformance suite;
|
|
25
|
+
`stdlib.md` section 11.1 fixes the values there and gap 4 of section 20.11
|
|
26
|
+
records that they are not written out in the specification prose. A package a
|
|
27
|
+
host installs cannot read that file at run time, so the table is here, and
|
|
28
|
+
``tests/test_colour.py`` holds it to the authority character for character rather
|
|
29
|
+
than to the other engine's copy of it. Two copies that agree with each other and
|
|
30
|
+
with nothing published are still a contract nobody can implement against.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from typing import NamedTuple
|
|
35
|
+
|
|
36
|
+
from .rounding import round_half_away
|
|
37
|
+
from .values import ABSENT, Value, number
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Colour(NamedTuple):
|
|
41
|
+
"""Red, green and blue as whole numbers 0 to 255, alpha as 0 to 1."""
|
|
42
|
+
|
|
43
|
+
r: float
|
|
44
|
+
g: float
|
|
45
|
+
b: float
|
|
46
|
+
a: float
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_HEX_DIGITS = "0123456789abcdef"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def hex_byte(channel: float) -> str:
|
|
53
|
+
"""A whole number from 0 to 255 as two hex digits, from a table.
|
|
54
|
+
|
|
55
|
+
Not the decimal rule and no host conversion: each nibble indexes a string of
|
|
56
|
+
sixteen characters, so the spelling cannot pick up a locale, a sign or a
|
|
57
|
+
point on the way.
|
|
58
|
+
"""
|
|
59
|
+
value = int(channel)
|
|
60
|
+
if value < 0:
|
|
61
|
+
value = 0
|
|
62
|
+
if value > 255:
|
|
63
|
+
value = 255
|
|
64
|
+
return _HEX_DIGITS[value >> 4] + _HEX_DIGITS[value & 15]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _channel(x: float) -> float:
|
|
68
|
+
"""One computed channel: rounded to a whole number, then held in range.
|
|
69
|
+
|
|
70
|
+
The rounding is the requirement of section 11.2 and the clamp is the
|
|
71
|
+
backstop under a diagnostic: a channel argument outside 0 to 255 is refused
|
|
72
|
+
by the checker, because a colour computed from data and landing at 300 is a
|
|
73
|
+
bug in the computation rather than a value to fix silently. A value that
|
|
74
|
+
reached here anyway is held inside the range the value model promises, since
|
|
75
|
+
the alternative is a colour the machine says cannot exist.
|
|
76
|
+
|
|
77
|
+
**A channel with no finite value is 0, and no page says so.** It is reachable:
|
|
78
|
+
a blend at an enormous weight overflows the interpolation before the rounding
|
|
79
|
+
ever runs. Neither 11.2 nor 20.9 states an answer, and the first engine's is
|
|
80
|
+
0, so that is what is here, and the silence is recorded where an engine author
|
|
81
|
+
will meet it rather than left for two engines to differ over.
|
|
82
|
+
"""
|
|
83
|
+
if not math.isfinite(x):
|
|
84
|
+
return 0.0
|
|
85
|
+
rounded = round_half_away(x)
|
|
86
|
+
if rounded < 0:
|
|
87
|
+
return 0.0
|
|
88
|
+
if rounded > 255:
|
|
89
|
+
return 255.0
|
|
90
|
+
return rounded
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def make(r: float, g: float, b: float, a: float) -> Colour:
|
|
94
|
+
"""A colour with the value model's invariant already true of it."""
|
|
95
|
+
alpha = 0.0 if a < 0 else (1.0 if a > 1 else (0.0 if a == 0 else a))
|
|
96
|
+
return Colour(_channel(r), _channel(g), _channel(b), alpha)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# The channel values of the named colours, which `spec/colours.json` is the
|
|
100
|
+
# authority for. Each is at full opacity, as section 11.1 states.
|
|
101
|
+
_CHANNELS: dict[str, tuple[int, int, int]] = {
|
|
102
|
+
"aqua": (0, 255, 255),
|
|
103
|
+
"black": (0, 0, 0),
|
|
104
|
+
"blue": (0, 0, 255),
|
|
105
|
+
"brown": (165, 42, 42),
|
|
106
|
+
"fuchsia": (255, 0, 255),
|
|
107
|
+
"gray": (128, 128, 128),
|
|
108
|
+
"green": (0, 128, 0),
|
|
109
|
+
"lime": (0, 255, 0),
|
|
110
|
+
"maroon": (128, 0, 0),
|
|
111
|
+
"navy": (0, 0, 128),
|
|
112
|
+
"olive": (128, 128, 0),
|
|
113
|
+
"orange": (255, 165, 0),
|
|
114
|
+
"pink": (255, 192, 203),
|
|
115
|
+
"purple": (128, 0, 128),
|
|
116
|
+
"red": (255, 0, 0),
|
|
117
|
+
"silver": (192, 192, 192),
|
|
118
|
+
"teal": (0, 128, 128),
|
|
119
|
+
"white": (255, 255, 255),
|
|
120
|
+
"yellow": (255, 255, 0),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# The nineteen names, for a test that has to compare two tables.
|
|
124
|
+
NAMES: tuple[str, ...] = tuple(_CHANNELS)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def named(name: str) -> Value:
|
|
128
|
+
"""The colour one of the nineteen bare names holds, or absence."""
|
|
129
|
+
channels = _CHANNELS.get(name)
|
|
130
|
+
if channels is None:
|
|
131
|
+
return ABSENT
|
|
132
|
+
return make(float(channels[0]), float(channels[1]), float(channels[2]), 1.0)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _colour_of(value: Value) -> Colour | None:
|
|
136
|
+
return value if isinstance(value, Colour) else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def rgb(r: Value, g: Value, b: Value) -> Value:
|
|
140
|
+
"""``rgb(r, g, b)``: channels 0 to 255, fully opaque."""
|
|
141
|
+
red = number(r)
|
|
142
|
+
green = number(g)
|
|
143
|
+
blue = number(b)
|
|
144
|
+
if red is None or green is None or blue is None:
|
|
145
|
+
return ABSENT
|
|
146
|
+
return make(red, green, blue, 1.0)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def rgba(r: Value, g: Value, b: Value, a: Value) -> Value:
|
|
150
|
+
"""``rgba(r, g, b, a)``: the same with alpha 0 to 1, where 1 is opaque."""
|
|
151
|
+
red = number(r)
|
|
152
|
+
green = number(g)
|
|
153
|
+
blue = number(b)
|
|
154
|
+
alpha = number(a)
|
|
155
|
+
if red is None or green is None or blue is None or alpha is None:
|
|
156
|
+
return ABSENT
|
|
157
|
+
return make(red, green, blue, alpha)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def fade(base: Value, percent: Value) -> Value:
|
|
161
|
+
"""``fade(color, percent)``: the same colour at ``percent`` transparency.
|
|
162
|
+
|
|
163
|
+
Transparency, not opacity, and a percentage: 100 is invisible. The two
|
|
164
|
+
conventions are opposites and a script that guesses wrong draws something
|
|
165
|
+
nobody can see, so the arithmetic is written out rather than left to be
|
|
166
|
+
inferred from the name. ``withAlpha`` is the call for the other convention.
|
|
167
|
+
"""
|
|
168
|
+
colour = _colour_of(base)
|
|
169
|
+
amount = number(percent)
|
|
170
|
+
if colour is None or amount is None:
|
|
171
|
+
return ABSENT
|
|
172
|
+
return make(colour.r, colour.g, colour.b, (100 - amount) / 100)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def mix(a: Value, b: Value, weight: Value) -> Value:
|
|
176
|
+
"""``mix(a, b, weight)``: a blend, 0 giving the first and 1 the second."""
|
|
177
|
+
first = _colour_of(a)
|
|
178
|
+
second = _colour_of(b)
|
|
179
|
+
amount = number(weight)
|
|
180
|
+
if first is None or second is None or amount is None:
|
|
181
|
+
return ABSENT
|
|
182
|
+
return make(
|
|
183
|
+
first.r + (second.r - first.r) * amount,
|
|
184
|
+
first.g + (second.g - first.g) * amount,
|
|
185
|
+
first.b + (second.b - first.b) * amount,
|
|
186
|
+
first.a + (second.a - first.a) * amount,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def alpha(base: Value) -> Value:
|
|
191
|
+
"""``alpha(color)``: read a colour's alpha, 0 to 1."""
|
|
192
|
+
colour = _colour_of(base)
|
|
193
|
+
return ABSENT if colour is None else colour.a
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def with_alpha(base: Value, a: Value) -> Value:
|
|
197
|
+
"""``withAlpha(color, a)``: the same colour at a stated alpha."""
|
|
198
|
+
colour = _colour_of(base)
|
|
199
|
+
amount = number(a)
|
|
200
|
+
if colour is None or amount is None:
|
|
201
|
+
return ABSENT
|
|
202
|
+
return make(colour.r, colour.g, colour.b, amount)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""The means of `stdlib.md` section 20.3 that are built out of other means, and the fit.
|
|
2
|
+
|
|
3
|
+
Four of these feed one average's output into another, absences and all, which is
|
|
4
|
+
where their warmups come from: the outer average seeds on the first values the
|
|
5
|
+
inner one produced, not on the first bars of the source. That composition is the
|
|
6
|
+
whole reason `stdlib.md` section 1 states warmups in bars of the call's own
|
|
7
|
+
source rather than in bars of the chart.
|
|
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
|
|
11
|
+
squares fit, whose constants are functions of the length alone.
|
|
12
|
+
|
|
13
|
+
``named`` is here rather than beside the six means because it has to be able to
|
|
14
|
+
select any of them, this file's included.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
|
|
19
|
+
from . import averages, elementary
|
|
20
|
+
from .rounding import round_half_away
|
|
21
|
+
from .series import Region, contributed, region, window
|
|
22
|
+
from .values import ABSENT, Value, number, result, whole
|
|
23
|
+
|
|
24
|
+
#: The six averages ``ma`` and the channel studies select by name, section 4.
|
|
25
|
+
NAMED = ("sma", "ema", "wma", "rma", "hma", "vwma")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def named(state: Region, ctx, value: Value, length: int | None, kind: Value) -> Value:
|
|
29
|
+
"""``ma(src, len, type)``: exactly the named average's arithmetic.
|
|
30
|
+
|
|
31
|
+
Each type has a state region of its own, so a run that switched type mid
|
|
32
|
+
history starts the new average from its own seed rather than from what the
|
|
33
|
+
old one left behind. The volume weighted type reads the bar, which is why
|
|
34
|
+
this one takes the context that the five others have no use for.
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(kind, str) or kind not in NAMED:
|
|
37
|
+
return ABSENT
|
|
38
|
+
held = region(state, kind)
|
|
39
|
+
if kind == "sma":
|
|
40
|
+
return averages.simple(held, value, length)
|
|
41
|
+
if kind == "ema":
|
|
42
|
+
return averages.exponential(held, value, length)
|
|
43
|
+
if kind == "wma":
|
|
44
|
+
return averages.linear(held, value, length)
|
|
45
|
+
if kind == "rma":
|
|
46
|
+
return averages.smoothed(held, value, length)
|
|
47
|
+
if kind == "hma":
|
|
48
|
+
return hull(held, value, length)
|
|
49
|
+
return averages.volume_weighted(held, value, ctx.bar("volume"), length)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def half_length(length: int | None) -> int | None:
|
|
53
|
+
"""``floor(len / 2)``, held at a minimum of 1: the inner length of ``hma``.
|
|
54
|
+
|
|
55
|
+
Section 4 fixes the outer length and says nothing about this one, so it is
|
|
56
|
+
gap 2 of section 20.11: an implementation choice recorded rather than a
|
|
57
|
+
reading of the page. Selecting the average by name reaches the gap as surely
|
|
58
|
+
as calling it.
|
|
59
|
+
"""
|
|
60
|
+
if length is None:
|
|
61
|
+
return None
|
|
62
|
+
half = length // 2
|
|
63
|
+
return 1 if half < 1 else half
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def root_length(length: int | None) -> int | None:
|
|
67
|
+
"""``round(sqrt(len))``, held at a minimum of 1: the outer length of ``hma``.
|
|
68
|
+
|
|
69
|
+
The rounding is section 8.1's, halves away from zero, and the square root is
|
|
70
|
+
the one call section 20.10 exempts from gap 1: IEEE-754 has it correctly
|
|
71
|
+
rounded, so it is bit identical on every conforming platform without a
|
|
72
|
+
portable implementation of its own.
|
|
73
|
+
"""
|
|
74
|
+
if length is None:
|
|
75
|
+
return None
|
|
76
|
+
outer = whole(round_half_away(math.sqrt(float(length))))
|
|
77
|
+
return 1 if outer is None or outer < 1 else outer
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def hull(state: Region, value: Value, length: int | None) -> Value:
|
|
81
|
+
"""``hma(src, len)``: three linearly weighted means, the raw one formed left to right.
|
|
82
|
+
|
|
83
|
+
The raw series is fed to the outer mean absences and all, so the outer mean
|
|
84
|
+
fills on the first ``outer`` values it produced, which is where the declared
|
|
85
|
+
warmup of ``len + round(sqrt(len)) - 2`` comes from.
|
|
86
|
+
"""
|
|
87
|
+
fast = averages.linear(region(state, "half"), value, half_length(length))
|
|
88
|
+
slow = averages.linear(region(state, "full"), value, length)
|
|
89
|
+
raw = ABSENT
|
|
90
|
+
if isinstance(fast, float) and isinstance(slow, float):
|
|
91
|
+
raw = result(2 * fast - slow)
|
|
92
|
+
return averages.linear(region(state, "outer"), raw, root_length(length))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def doubled(state: Region, value: Value, length: int | None) -> Value:
|
|
96
|
+
"""``dema(src, len)``: ``2 * e1 - e2``, formed left to right."""
|
|
97
|
+
first, second, _third = _chain(state, value, length, 2)
|
|
98
|
+
if not isinstance(first, float) or not isinstance(second, float):
|
|
99
|
+
return ABSENT
|
|
100
|
+
return result(2 * first - second)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tripled(state: Region, value: Value, length: int | None) -> Value:
|
|
104
|
+
"""``tema(src, len)``: three terms added left to right, not ``3 * (e1 - e2) + e3``."""
|
|
105
|
+
first, second, third = _chain(state, value, length, 3)
|
|
106
|
+
if not isinstance(first, float) or not isinstance(second, float):
|
|
107
|
+
return ABSENT
|
|
108
|
+
if not isinstance(third, float):
|
|
109
|
+
return ABSENT
|
|
110
|
+
return result(3 * first - 3 * second + third)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _chain(state: Region, value: Value, length: int | None, depth: int):
|
|
114
|
+
"""The exponential means of a chain, each fed the previous one's output.
|
|
115
|
+
|
|
116
|
+
Absences and all: an average fed absence freezes rather than skipping, which
|
|
117
|
+
is what makes the third mean's warmup three times the length less three
|
|
118
|
+
rather than an accident of how much history happened to be present.
|
|
119
|
+
"""
|
|
120
|
+
first = averages.exponential(region(state, "one"), value, length)
|
|
121
|
+
second = averages.exponential(region(state, "two"), first, length)
|
|
122
|
+
if depth < 3:
|
|
123
|
+
return first, second, ABSENT
|
|
124
|
+
third = averages.exponential(region(state, "three"), second, length)
|
|
125
|
+
return first, second, third
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def smoothed_three(state: Region, value: Value, length: int | None) -> Value:
|
|
129
|
+
"""The third mean of the same chain, which is what ``trix`` takes the change of."""
|
|
130
|
+
_first, _second, third = _chain(state, value, length, 3)
|
|
131
|
+
return third
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def gaussian(
|
|
135
|
+
state: Region, value: Value, length: int | None, offset: Value, sigma: Value
|
|
136
|
+
) -> Value:
|
|
137
|
+
"""``alma(src, len, offset, sigma)``: the kernel built from the position, two passes.
|
|
138
|
+
|
|
139
|
+
Position 0 is the oldest bar of the window and both passes run over that
|
|
140
|
+
order. The denominator of the exponent is **one product, divided once**: the
|
|
141
|
+
chain of divisions in circulation differs on about one exponent in four.
|
|
142
|
+
|
|
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.
|
|
147
|
+
"""
|
|
148
|
+
values = contributed(state, "src", number(value), length)
|
|
149
|
+
held = window(values, length)
|
|
150
|
+
peak_at = number(offset)
|
|
151
|
+
width = number(sigma)
|
|
152
|
+
if held is None or length is None or peak_at is None or width is None:
|
|
153
|
+
return ABSENT
|
|
154
|
+
if width == 0:
|
|
155
|
+
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
|
|
167
|
+
running = 0.0
|
|
168
|
+
for position in range(length):
|
|
169
|
+
running = running + held[length - 1 - position] * weights[position]
|
|
170
|
+
if norm == 0:
|
|
171
|
+
return ABSENT
|
|
172
|
+
return result(running / norm)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def fitted(state: Region, value: Value, length: int | None, offset: Value) -> Value:
|
|
176
|
+
"""``linreg(src, len, offset)``: the least squares line, read ``offset`` bars back.
|
|
177
|
+
|
|
178
|
+
``x`` runs 0 at the oldest bar of the window to ``len - 1`` at this one, and
|
|
179
|
+
the sums over ``x`` are constants of the length. **The sum of squares is one
|
|
180
|
+
product divided once**: splitting the 6 into the two factors it is made of
|
|
181
|
+
holds every intermediate below the whole product and parts company with this
|
|
182
|
+
at 3716 of the first hundred thousand lengths, the first at a length of 15.
|
|
183
|
+
|
|
184
|
+
The two accumulations each add their own terms oldest first and neither reads
|
|
185
|
+
the other, so whether an engine runs them in one pass or two is not fixed.
|
|
186
|
+
The result is absent where the divisor is zero, which is every window of
|
|
187
|
+
length 1: a line fitted to one point is not a fit.
|
|
188
|
+
"""
|
|
189
|
+
values = contributed(state, "src", number(value), length)
|
|
190
|
+
held = window(values, length)
|
|
191
|
+
back = number(offset)
|
|
192
|
+
if held is None or length is None or back is None:
|
|
193
|
+
return ABSENT
|
|
194
|
+
span = float(length)
|
|
195
|
+
sum_x = ((span - 1) * span) / 2
|
|
196
|
+
sum_x_squared = ((span - 1) * span * (2 * span - 1)) / 6
|
|
197
|
+
divisor = span * sum_x_squared - sum_x * sum_x
|
|
198
|
+
if divisor == 0:
|
|
199
|
+
return ABSENT
|
|
200
|
+
sum_y = 0.0
|
|
201
|
+
sum_xy = 0.0
|
|
202
|
+
for position in range(length):
|
|
203
|
+
y = held[length - 1 - position]
|
|
204
|
+
sum_y = sum_y + y
|
|
205
|
+
sum_xy = sum_xy + y * float(position)
|
|
206
|
+
slope = (span * sum_xy - sum_x * sum_y) / divisor
|
|
207
|
+
intercept = (sum_y - slope * sum_x) / span
|
|
208
|
+
return result(intercept + slope * (span - 1 - back))
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""The totals, the ranks and the two pair statistics of `stdlib.md` section 20.8.
|
|
2
|
+
|
|
3
|
+
Every window here is the fresh oldest first sum of section 20.2.1 or a scan over
|
|
4
|
+
the same window, and the three that pass over an absent bar say so in their
|
|
5
|
+
names, which is section 2.4's rule and the reason they are three functions rather
|
|
6
|
+
than a flag on the others.
|
|
7
|
+
|
|
8
|
+
**The two pair statistics are two passes and are population forms.** The means
|
|
9
|
+
are finished before any deviation is taken. The single pass arrangement, summing
|
|
10
|
+
squares and cross products and subtracting at the end, is mathematically equal,
|
|
11
|
+
loses most of its significant digits on a price series where the values are large
|
|
12
|
+
and their spread is small, and can return a negative variance that then has to be
|
|
13
|
+
floored at zero. An implementation that needs a floor to stay real is computing a
|
|
14
|
+
different quantity.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Optional, Sequence
|
|
18
|
+
|
|
19
|
+
from . import elementary
|
|
20
|
+
from .series import Region, back, contributed, raw_window, running_total, total, window
|
|
21
|
+
from .values import ABSENT, Value, floor_of, number, result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def windowed_sum(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
25
|
+
"""``sum(src, len)``: the window sum of 20.2.1."""
|
|
26
|
+
held = window(contributed(state, "src", number(value), length), length)
|
|
27
|
+
return ABSENT if held is None else result(total(held))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def counted(state: Region, condition: Value, length: Optional[int]) -> Value:
|
|
31
|
+
"""``count(cond, len)``: the same sum over a window of ones and zeros.
|
|
32
|
+
|
|
33
|
+
A count is an addition of whole numbers and is therefore exact, which is why
|
|
34
|
+
it can be the same sum rather than a scan of its own.
|
|
35
|
+
|
|
36
|
+
**An absent condition is a bar the condition did not hold**, and not a hole
|
|
37
|
+
that makes the count absent. The window still has to be full of bars, so the
|
|
38
|
+
warmup is the window's, but a bar nobody could evaluate is counted as false:
|
|
39
|
+
the reading is how many of the last ``len`` bars the condition held on, and
|
|
40
|
+
an unknown bar is not one of them.
|
|
41
|
+
"""
|
|
42
|
+
term = 1.0 if condition is True else 0.0
|
|
43
|
+
held = window(contributed(state, "cond", term, length), length)
|
|
44
|
+
return ABSENT if held is None else result(total(held))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def accumulated(state: Region, value: Value) -> Value:
|
|
48
|
+
"""``cum(src)``: the running total from the first bar, on section 20.6's terms."""
|
|
49
|
+
return running_total(state, "run", number(value))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def skipping_sum(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
53
|
+
"""``sumSkip(src, len)``: the same oldest first sum with the absent bars passed over.
|
|
54
|
+
|
|
55
|
+
A window with nothing present in it is the empty accumulation: the sum is the
|
|
56
|
+
0 it started at and is reported as 0. The window still has to be full of
|
|
57
|
+
bars, which is why the warmup is the same as the window sum's.
|
|
58
|
+
"""
|
|
59
|
+
held = raw_window(contributed(state, "src", number(value), length), length)
|
|
60
|
+
if held is None:
|
|
61
|
+
return ABSENT
|
|
62
|
+
return result(total(_present(held)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def skipping_mean(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
66
|
+
"""``avgSkip(src, len)``: that sum over the number of bars that had a value.
|
|
67
|
+
|
|
68
|
+
Dividing by ``len`` would be a different quantity. A window with nothing
|
|
69
|
+
present in it has a divisor of zero and is absent, which is the one place
|
|
70
|
+
this and ``sumSkip`` part company.
|
|
71
|
+
"""
|
|
72
|
+
held = raw_window(contributed(state, "src", number(value), length), length)
|
|
73
|
+
if held is None:
|
|
74
|
+
return ABSENT
|
|
75
|
+
present = _present(held)
|
|
76
|
+
if not present:
|
|
77
|
+
return ABSENT
|
|
78
|
+
return result(total(present) / len(present))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def present_count(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
82
|
+
"""``countPresent(src, len)``: how many bars of the window had a value."""
|
|
83
|
+
held = raw_window(contributed(state, "src", number(value), length), length)
|
|
84
|
+
return ABSENT if held is None else float(len(_present(held)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ranked(state: Region, value: Value, length: Optional[int], percent: Value) -> Value:
|
|
88
|
+
"""``percentile(src, len, p)``: linear interpolation between the two ranks either side.
|
|
89
|
+
|
|
90
|
+
The nearest rank method, which returns an actual member of the window, is the
|
|
91
|
+
other common choice and is not this one: an even length window is the mean of
|
|
92
|
+
its two middles rather than one of them chosen by a rule nobody remembers.
|
|
93
|
+
"""
|
|
94
|
+
held = window(contributed(state, "src", number(value), length), length)
|
|
95
|
+
return _interpolated(held, length, number(percent))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def middle(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
99
|
+
"""``median(src, len)``: the same at ``p`` of 50 and nothing else."""
|
|
100
|
+
held = window(contributed(state, "src", number(value), length), length)
|
|
101
|
+
return _interpolated(held, length, 50.0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rank_of(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
105
|
+
"""``percentRank(src, len)``: this bar's value counted among the window.
|
|
106
|
+
|
|
107
|
+
The bar counts itself, so the reading runs from ``100 / len`` to 100 rather
|
|
108
|
+
than from 0.
|
|
109
|
+
"""
|
|
110
|
+
held = window(contributed(state, "src", number(value), length), length)
|
|
111
|
+
if held is None or length is None:
|
|
112
|
+
return ABSENT
|
|
113
|
+
counted_below = 0
|
|
114
|
+
for other in held:
|
|
115
|
+
if other <= held[0]:
|
|
116
|
+
counted_below = counted_below + 1
|
|
117
|
+
return result((float(counted_below) * 100) / length)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def covariance(
|
|
121
|
+
state: Region, first: Value, second: Value, length: Optional[int]
|
|
122
|
+
) -> Value:
|
|
123
|
+
"""``covariance(a, b, len)``: the population form, two passes over both windows."""
|
|
124
|
+
pair = _pair(state, first, second, length)
|
|
125
|
+
if pair is None or length is None:
|
|
126
|
+
return ABSENT
|
|
127
|
+
left, right = pair
|
|
128
|
+
cross, _squares_left, _squares_right = _deviations(left, right, length)
|
|
129
|
+
return result(cross / length)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def correlation(
|
|
133
|
+
state: Region, first: Value, second: Value, length: Optional[int]
|
|
134
|
+
) -> Value:
|
|
135
|
+
"""``correlation(a, b, len)``: that covariance over a product of two square roots.
|
|
136
|
+
|
|
137
|
+
**It is not the square root of a product**: the two are mathematically equal,
|
|
138
|
+
differ in the last bit, and this is the one the page states.
|
|
139
|
+
"""
|
|
140
|
+
pair = _pair(state, first, second, length)
|
|
141
|
+
if pair is None or length is None:
|
|
142
|
+
return ABSENT
|
|
143
|
+
left, right = pair
|
|
144
|
+
cross, squares_left, squares_right = _deviations(left, right, length)
|
|
145
|
+
spread_left = elementary.sqrt(squares_left / length)
|
|
146
|
+
spread_right = elementary.sqrt(squares_right / length)
|
|
147
|
+
if not isinstance(spread_left, float) or not isinstance(spread_right, float):
|
|
148
|
+
return ABSENT
|
|
149
|
+
divisor = spread_left * spread_right
|
|
150
|
+
if divisor == 0:
|
|
151
|
+
return ABSENT
|
|
152
|
+
return result((cross / length) / divisor)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _pair(state: Region, first: Value, second: Value, length: Optional[int]):
|
|
156
|
+
"""Both windows, or absence where either is short or holed.
|
|
157
|
+
|
|
158
|
+
Both series are contributed on every bar whatever the other one did, because
|
|
159
|
+
a bar one of them was absent on is still a bar the pair covered.
|
|
160
|
+
"""
|
|
161
|
+
left = window(contributed(state, "a", number(first), length), length)
|
|
162
|
+
right = window(contributed(state, "b", number(second), length), length)
|
|
163
|
+
if left is None or right is None:
|
|
164
|
+
return None
|
|
165
|
+
return left, right
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _deviations(left: Sequence[float], right: Sequence[float], length: int):
|
|
169
|
+
"""The three second pass accumulations, each oldest first over its own terms.
|
|
170
|
+
|
|
171
|
+
None of the three reads another, so neither the order they are written in nor
|
|
172
|
+
whether an engine runs them in one pass or in three is fixed: section 20.1's
|
|
173
|
+
fourth rule. What the two passes do fix is that both means are finished
|
|
174
|
+
before any deviation is taken.
|
|
175
|
+
"""
|
|
176
|
+
mean_left = total(left) / length
|
|
177
|
+
mean_right = total(right) / length
|
|
178
|
+
cross = 0.0
|
|
179
|
+
squares_left = 0.0
|
|
180
|
+
squares_right = 0.0
|
|
181
|
+
for at in range(length - 1, -1, -1):
|
|
182
|
+
cross = cross + (left[at] - mean_left) * (right[at] - mean_right)
|
|
183
|
+
squares_left = squares_left + (left[at] - mean_left) * (left[at] - mean_left)
|
|
184
|
+
squares_right = squares_right + (right[at] - mean_right) * (right[at] - mean_right)
|
|
185
|
+
return cross, squares_left, squares_right
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _present(held: Sequence[Value]) -> list:
|
|
189
|
+
"""The values of a raw window that are there, newest first as they arrived."""
|
|
190
|
+
return [value for value in held if isinstance(value, float)]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _interpolated(
|
|
194
|
+
held: Optional[Sequence[float]], length: Optional[int], percent: Optional[float]
|
|
195
|
+
) -> Value:
|
|
196
|
+
"""The rank read off the sorted window, interpolated between the two either side."""
|
|
197
|
+
if held is None or length is None or percent is None:
|
|
198
|
+
return ABSENT
|
|
199
|
+
ascending = sorted(held)
|
|
200
|
+
rank = (percent / 100) * (length - 1)
|
|
201
|
+
below = floor_of(rank)
|
|
202
|
+
if below < 0 or below >= length:
|
|
203
|
+
return ABSENT
|
|
204
|
+
at = int(below)
|
|
205
|
+
if at >= length - 1:
|
|
206
|
+
return result(ascending[at])
|
|
207
|
+
return result(ascending[at] + (rank - below) * (ascending[at + 1] - ascending[at]))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def history_at(state: Region, value: Value, offset: Optional[int]) -> Value:
|
|
211
|
+
"""``history(src, n)``: ``src`` as it stood ``n`` bars ago, the explicit ``src[n]``.
|
|
212
|
+
|
|
213
|
+
One operation and no accumulation, section 20.10. It is here rather than
|
|
214
|
+
beside the bookkeeping because what it reads is the same buffer every window
|
|
215
|
+
in this file is read from.
|
|
216
|
+
"""
|
|
217
|
+
keep = None if offset is None else offset + 1
|
|
218
|
+
return back(contributed(state, "src", value, keep), offset)
|