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,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)