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,105 @@
|
|
|
1
|
+
"""The readings of `stdlib.md` section 20.5 that are taken from the bar's own range.
|
|
2
|
+
|
|
3
|
+
**There are two true ranges and they are different quantities.** The plain one is
|
|
4
|
+
the call a script can make, and `stdlib.md` section 6 grants the oldest bar of
|
|
5
|
+
the dataset the library's one exception to absence propagation there: the reading
|
|
6
|
+
is ``high - low``, because the other two terms need a close that does not exist
|
|
7
|
+
and the bar's own range is a true statement about that bar. The gap aware form is
|
|
8
|
+
the same three terms with no exception, it is absent on that bar like any other
|
|
9
|
+
change, and it is what the choppiness reading and the directional index take.
|
|
10
|
+
Section 6 says which of the two a function takes by declaring its warmup one bar
|
|
11
|
+
later than the plain reading would give.
|
|
12
|
+
|
|
13
|
+
Neither form is written out again here. ``bars.py`` holds the three terms and the
|
|
14
|
+
exception, and this file asks it for the reading it wants by saying whether the
|
|
15
|
+
bar it is on is the oldest one.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from . import averages, elementary
|
|
21
|
+
from .bars import true_range
|
|
22
|
+
from .extremes import window_high, window_low
|
|
23
|
+
from .series import Region, contributed, total, window
|
|
24
|
+
from .values import ABSENT, Value, number, result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def plain_range(ctx) -> Value:
|
|
28
|
+
"""``trueRange()`` as a stateful caller reads it, oldest bar exception and all."""
|
|
29
|
+
return true_range(
|
|
30
|
+
ctx.bar("high"), ctx.bar("low"), ctx.bar("previousClose"), ctx.first_bar()
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gap_range(ctx) -> Value:
|
|
35
|
+
"""The gap aware form: the same three terms with no exception on the oldest bar.
|
|
36
|
+
|
|
37
|
+
It is not a call a script can make. An engine reaches it through this half of
|
|
38
|
+
the library, which is where the two functions that count changes rather than
|
|
39
|
+
levels ask for it.
|
|
40
|
+
"""
|
|
41
|
+
return true_range(ctx.bar("high"), ctx.bar("low"), ctx.bar("previousClose"), False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def average_range(state: Region, ctx, length: Optional[int]) -> Value:
|
|
45
|
+
"""``atr(len)``: ``rma`` over ``trueRange``, that first bar included."""
|
|
46
|
+
return averages.smoothed(state, plain_range(ctx), length)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalised_range(state: Region, ctx, length: Optional[int]) -> Value:
|
|
50
|
+
"""``natr(len)``: ``(100 * atr) / close``, the percentage form."""
|
|
51
|
+
width = average_range(state, ctx, length)
|
|
52
|
+
close = number(ctx.bar("close"))
|
|
53
|
+
if not isinstance(width, float) or close is None:
|
|
54
|
+
return ABSENT
|
|
55
|
+
if close == 0:
|
|
56
|
+
return ABSENT
|
|
57
|
+
return result((100 * width) / close)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def channel(state: Region, ctx, length: Optional[int]) -> list:
|
|
61
|
+
"""``donchian(len)``: the window's outright high, its midpoint and its low.
|
|
62
|
+
|
|
63
|
+
The midpoint is the mean of the two extremes as reported, which is the one
|
|
64
|
+
arrangement section 20.3 states for a midpoint of a channel.
|
|
65
|
+
"""
|
|
66
|
+
highs = contributed(state, "high", number(ctx.bar("high")), length)
|
|
67
|
+
lows = contributed(state, "low", number(ctx.bar("low")), length)
|
|
68
|
+
upper = window_high(highs, length)
|
|
69
|
+
lower = window_low(lows, length)
|
|
70
|
+
if not isinstance(upper, float) or not isinstance(lower, float):
|
|
71
|
+
return [ABSENT, ABSENT, ABSENT]
|
|
72
|
+
return [upper, result((upper + lower) / 2), lower]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def choppiness(state: Region, ctx, length: Optional[int]) -> Value:
|
|
76
|
+
"""``chop(len)``: the distance travelled against the range covered.
|
|
77
|
+
|
|
78
|
+
The ratio is formed first, then its logarithm, then the multiplication by
|
|
79
|
+
100, then the division by the logarithm of the length. The result is absent
|
|
80
|
+
where the span or the distance is not above zero, and where the length is 1
|
|
81
|
+
and the scale is zero.
|
|
82
|
+
|
|
83
|
+
**This reading depends on ``log10``**, so it reaches gap 1 of section 20.11
|
|
84
|
+
and carries no cross-engine guarantee: there is no portable reference
|
|
85
|
+
algorithm for a logarithm anywhere in the specification.
|
|
86
|
+
"""
|
|
87
|
+
ranges = contributed(state, "range", gap_range(ctx), length)
|
|
88
|
+
highs = contributed(state, "high", number(ctx.bar("high")), length)
|
|
89
|
+
lows = contributed(state, "low", number(ctx.bar("low")), length)
|
|
90
|
+
held = window(ranges, length)
|
|
91
|
+
upper = window_high(highs, length)
|
|
92
|
+
lower = window_low(lows, length)
|
|
93
|
+
if held is None or length is None:
|
|
94
|
+
return ABSENT
|
|
95
|
+
if not isinstance(upper, float) or not isinstance(lower, float):
|
|
96
|
+
return ABSENT
|
|
97
|
+
distance = total(held)
|
|
98
|
+
span = upper - lower
|
|
99
|
+
if span <= 0 or distance <= 0:
|
|
100
|
+
return ABSENT
|
|
101
|
+
travelled = elementary.log10(distance / span)
|
|
102
|
+
scale = elementary.log10(float(length))
|
|
103
|
+
if not isinstance(travelled, float) or not isinstance(scale, float) or scale == 0:
|
|
104
|
+
return ABSENT
|
|
105
|
+
return result((100 * travelled) / scale)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Rounding, and the one rule that decides every case.
|
|
2
|
+
|
|
3
|
+
**Halves go away from zero, never to even.** `stdlib.md` section 8.1 fixes it
|
|
4
|
+
and gives the reason: a price rounded for display should agree with what a
|
|
5
|
+
trader would write down, and round half to even surprises people at exactly the
|
|
6
|
+
values that matter. Fixing it also means two engines cannot differ by one tick.
|
|
7
|
+
|
|
8
|
+
**Adding a half and taking the floor is not this function.** It is the usual
|
|
9
|
+
shortcut and it is wrong for the value just below a half, where adding 0.5 rounds
|
|
10
|
+
up to the next representable number before the floor ever runs and the answer
|
|
11
|
+
comes out one too high. `stdlib.md` section 20.7 writes the comparison of the
|
|
12
|
+
fractional part out instead, which has no such case, and that is what is below.
|
|
13
|
+
|
|
14
|
+
**The scale of a fixed decimal rounding is not a floating point power.** 20.7
|
|
15
|
+
again: it is the binary64 nearest to ten to that count, the value the literal
|
|
16
|
+
``1e23`` reads as. On one host the two differ by an ulp at a count of 23 out of
|
|
17
|
+
the 309 a binary64 can hold, and over a price walk that one count moved 814
|
|
18
|
+
results. The table here is built once from exact whole number arithmetic, which
|
|
19
|
+
is the "exact integer power converted once" 20.7 names, and it is the one table:
|
|
20
|
+
the display conversion of ``text(x, decimals)`` scales by it as well.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
|
|
25
|
+
from .values import ABSENT, Value, floor_of, number, result, whole
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def round_half_away(x: float) -> float:
|
|
29
|
+
"""The nearest whole number, halves away from zero.
|
|
30
|
+
|
|
31
|
+
A value with no whole number near it, an infinity produced by a scaling that
|
|
32
|
+
overflowed, is returned unchanged for ``result`` to turn into absence.
|
|
33
|
+
"""
|
|
34
|
+
if not math.isfinite(x):
|
|
35
|
+
return x
|
|
36
|
+
below = floor_of(x)
|
|
37
|
+
fraction = x - below
|
|
38
|
+
if fraction > 0.5:
|
|
39
|
+
return below + 1.0
|
|
40
|
+
if fraction < 0.5:
|
|
41
|
+
return below
|
|
42
|
+
# Exactly a half. Away from zero: upward above zero, and `below` already is
|
|
43
|
+
# the downward answer for a negative, since the floor of -2.5 is -3.
|
|
44
|
+
return below + 1.0 if x > 0 else below
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Ten to each whole power a binary64 can hold, as the binary64 nearest to it.
|
|
48
|
+
# Built from exact whole number arithmetic rather than from a floating point
|
|
49
|
+
# power, for the reason at the top of this file.
|
|
50
|
+
_POWERS_OF_TEN: tuple[float, ...] = tuple(float(10**count) for count in range(309))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def scale_of(decimals: int) -> float:
|
|
54
|
+
"""The scale for a digit count: the nearest binary64 to ten to that power.
|
|
55
|
+
|
|
56
|
+
Past the last finite power it is the infinity the power would have been, so
|
|
57
|
+
that a count a script computed scales to absence rather than to a wrong
|
|
58
|
+
number.
|
|
59
|
+
"""
|
|
60
|
+
if 0 <= decimals < len(_POWERS_OF_TEN):
|
|
61
|
+
return _POWERS_OF_TEN[decimals]
|
|
62
|
+
return math.inf
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def floor(x: Value) -> Value:
|
|
66
|
+
"""``floor(x)``: toward negative infinity."""
|
|
67
|
+
value = number(x)
|
|
68
|
+
return ABSENT if value is None else result(floor_of(value))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ceil(x: Value) -> Value:
|
|
72
|
+
"""``ceil(x)``: toward positive infinity."""
|
|
73
|
+
value = number(x)
|
|
74
|
+
if value is None:
|
|
75
|
+
return ABSENT
|
|
76
|
+
return result(value if not math.isfinite(value) else float(math.ceil(value)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def trunc(x: Value) -> Value:
|
|
80
|
+
"""``trunc(x)``: toward zero."""
|
|
81
|
+
value = number(x)
|
|
82
|
+
if value is None:
|
|
83
|
+
return ABSENT
|
|
84
|
+
return result(value if not math.isfinite(value) else float(math.trunc(value)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def round_to_whole(x: Value) -> Value:
|
|
88
|
+
"""``round(x)``: to the nearest whole number, halves away from zero."""
|
|
89
|
+
value = number(x)
|
|
90
|
+
return ABSENT if value is None else result(round_half_away(value))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def round_to(x: Value, decimals: Value) -> Value:
|
|
94
|
+
"""``round(x, decimals)``: scale, round once, scale back."""
|
|
95
|
+
value = number(x)
|
|
96
|
+
count = whole(decimals)
|
|
97
|
+
if value is None or count is None:
|
|
98
|
+
return ABSENT
|
|
99
|
+
scale = scale_of(count)
|
|
100
|
+
return result(round_half_away(value * scale) / scale)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def round_to_step(x: Value, step: Value) -> Value:
|
|
104
|
+
"""``roundToStep(x, step)``: to the nearest multiple of ``step``."""
|
|
105
|
+
value = number(x)
|
|
106
|
+
size = number(step)
|
|
107
|
+
if value is None or size is None or size <= 0:
|
|
108
|
+
return ABSENT
|
|
109
|
+
return result(round_half_away(value / size) * size)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def round_to_tick(price: Value, tick_size: Value) -> Value:
|
|
113
|
+
"""``roundToTick(price)``: to the instrument's tick.
|
|
114
|
+
|
|
115
|
+
Absent when the host has stated no tick size, rather than the price
|
|
116
|
+
unrounded. Returning the input would produce an order price that looks
|
|
117
|
+
rounded and is not, and a venue refuses an order off the tick, so a script
|
|
118
|
+
that cannot round has to be able to see that it cannot.
|
|
119
|
+
"""
|
|
120
|
+
tick = number(tick_size)
|
|
121
|
+
if tick is None or tick <= 0:
|
|
122
|
+
return ABSENT
|
|
123
|
+
return round_to_step(price, tick)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""The two shapes of `stdlib.md` section 20.2, and the region they are kept in.
|
|
2
|
+
|
|
3
|
+
Almost every length taking function of the stateful half is one of these two or
|
|
4
|
+
is built out of them, so they are written once here and read from everywhere
|
|
5
|
+
else. Getting them right is most of the work, and getting them wrong is a whole
|
|
6
|
+
library that is plausible and never bit identical.
|
|
7
|
+
|
|
8
|
+
**A region is plain data and nothing else.** `compiled-program.md` section 2.11
|
|
9
|
+
requires a state region to be snapshottable by a mechanical copy, by an engine
|
|
10
|
+
that does not know which function owns it, so what a function keeps here is
|
|
11
|
+
dictionaries, lists and numbers. An object with behaviour in it would copy in a
|
|
12
|
+
way the engine cannot promise, and the rollback of section 6 would carry a
|
|
13
|
+
reference where it meant to carry a value.
|
|
14
|
+
|
|
15
|
+
**A buffer holds what the bars contributed, not what the bars were.** A call
|
|
16
|
+
inside a branch does not run on every bar, and the window of section 20.1 is the
|
|
17
|
+
values this call site contributed, oldest still reachable at the end. The depth
|
|
18
|
+
kept is the largest length the call has asked for, because `stdlib.md` section
|
|
19
|
+
2.5 measures a series length's warmup against the largest value it has taken and
|
|
20
|
+
a buffer trimmed to today's length could not answer tomorrow's.
|
|
21
|
+
|
|
22
|
+
**Absence is not arithmetic.** A window with a hole in it is absent, which is
|
|
23
|
+
section 2.4's rule for every windowed function, and the three that pass over a
|
|
24
|
+
hole say so in their names and ask for the raw window instead.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
28
|
+
|
|
29
|
+
from .values import ABSENT, Value, result
|
|
30
|
+
|
|
31
|
+
#: One call site's state region: what the engine created, copies and rolls back.
|
|
32
|
+
Region = Dict[str, Any]
|
|
33
|
+
|
|
34
|
+
#: The step of a seeded recurrence: what this bar's value does to the running one.
|
|
35
|
+
Step = Callable[[float, float], float]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def region(state: Region, key: str) -> Region:
|
|
39
|
+
"""A named region inside a region, for a function built out of others.
|
|
40
|
+
|
|
41
|
+
``dema`` is two exponential means and ``macd`` is three, and each of them
|
|
42
|
+
has to keep its own running value. Nesting rather than prefixing every key
|
|
43
|
+
means a function composes without its parts having to know they were
|
|
44
|
+
composed.
|
|
45
|
+
"""
|
|
46
|
+
held = state.get(key)
|
|
47
|
+
if held is None:
|
|
48
|
+
held = {}
|
|
49
|
+
state[key] = held
|
|
50
|
+
return held
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def contributed(state: Region, key: str, value: Value, keep: Optional[int]) -> List[Value]:
|
|
54
|
+
"""This bar's contribution pushed into a named buffer, and the buffer back.
|
|
55
|
+
|
|
56
|
+
``keep`` is how many of the most recent contributions this bar's call needs.
|
|
57
|
+
The buffer keeps the largest depth it has ever been asked for, so a length
|
|
58
|
+
that grows mid-run still has the history behind it, and one bar is kept even
|
|
59
|
+
where the caller asked for nothing, because a call whose length is absent on
|
|
60
|
+
this bar still happened on this bar.
|
|
61
|
+
"""
|
|
62
|
+
held = state.get(key)
|
|
63
|
+
if held is None:
|
|
64
|
+
held = {"values": [], "depth": 1}
|
|
65
|
+
state[key] = held
|
|
66
|
+
if keep is not None and keep > held["depth"]:
|
|
67
|
+
held["depth"] = keep
|
|
68
|
+
values: List[Value] = held["values"]
|
|
69
|
+
values.append(value)
|
|
70
|
+
extra = len(values) - held["depth"]
|
|
71
|
+
if extra > 0:
|
|
72
|
+
del values[:extra]
|
|
73
|
+
return values
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def raw_window(values: Sequence[Value], length: Optional[int]) -> Optional[List[Value]]:
|
|
77
|
+
"""The last ``length`` contributions, newest first, holes and all.
|
|
78
|
+
|
|
79
|
+
Newest first is section 20.1's indexing: ``w[0]`` is what this bar
|
|
80
|
+
contributed and ``w[len - 1]`` is the oldest value still in the window. The
|
|
81
|
+
answer is absent only where the window is not full yet, which is the warmup
|
|
82
|
+
every entry in sections 4 to 9 declares.
|
|
83
|
+
"""
|
|
84
|
+
if length is None or length < 1 or len(values) < length:
|
|
85
|
+
return None
|
|
86
|
+
held = list(values[len(values) - length :])
|
|
87
|
+
held.reverse()
|
|
88
|
+
return held
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def window(values: Sequence[Value], length: Optional[int]) -> Optional[List[float]]:
|
|
92
|
+
"""The same window, absent where any bar in it is absent.
|
|
93
|
+
|
|
94
|
+
Section 2.4: every windowed function propagates absence. A window that
|
|
95
|
+
dropped its holes would be a mean of however many bars happened to have a
|
|
96
|
+
value, which is a different quantity and one whose divisor nobody could
|
|
97
|
+
state.
|
|
98
|
+
"""
|
|
99
|
+
held = raw_window(values, length)
|
|
100
|
+
if held is None:
|
|
101
|
+
return None
|
|
102
|
+
for value in held:
|
|
103
|
+
if not isinstance(value, float):
|
|
104
|
+
return None
|
|
105
|
+
return held # type: ignore[return-value]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def back(values: Sequence[Value], offset: Optional[int]) -> Value:
|
|
109
|
+
"""What the buffer held ``offset`` contributions ago, or absence.
|
|
110
|
+
|
|
111
|
+
Offset 0 is this bar. Absence here is "the run is not that old yet", which
|
|
112
|
+
is why it is the same answer as a hole: neither is a value the caller can
|
|
113
|
+
compute with.
|
|
114
|
+
"""
|
|
115
|
+
if offset is None or offset < 0 or len(values) <= offset:
|
|
116
|
+
return ABSENT
|
|
117
|
+
return values[len(values) - 1 - offset]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def total(held: Sequence[float]) -> float:
|
|
121
|
+
"""Section 20.2.1: the window sum, taken fresh, oldest first.
|
|
122
|
+
|
|
123
|
+
The window arrives newest first, so the index runs from ``len - 1`` down to
|
|
124
|
+
0 and the oldest value is added to zero first. Carrying a total forward and
|
|
125
|
+
subtracting the value that leaves the window is the same quantity in exact
|
|
126
|
+
arithmetic, a different number in binary64, and refused outright by
|
|
127
|
+
`compiled-program.md` section 8.3.
|
|
128
|
+
"""
|
|
129
|
+
running = 0.0
|
|
130
|
+
for at in range(len(held) - 1, -1, -1):
|
|
131
|
+
running = running + held[at]
|
|
132
|
+
return running
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def mean(held: Sequence[float], length: int) -> float:
|
|
136
|
+
"""The window mean: one division applied to the finished sum.
|
|
137
|
+
|
|
138
|
+
Never a running mean, and never a sum divided term by term as it is built.
|
|
139
|
+
Section 20.2.1 states it in one sentence and it is the sentence most easily
|
|
140
|
+
lost, because a running mean is the cheaper thing to write.
|
|
141
|
+
"""
|
|
142
|
+
return total(held) / length
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def seeded(
|
|
146
|
+
state: Region,
|
|
147
|
+
key: str,
|
|
148
|
+
values: Sequence[Value],
|
|
149
|
+
length: Optional[int],
|
|
150
|
+
step: Step,
|
|
151
|
+
) -> Value:
|
|
152
|
+
"""Section 20.2.2: the seeded recurrence every seeded average in the library has.
|
|
153
|
+
|
|
154
|
+
Absent before the seed bar, the window mean on it, and ``step`` after it. The
|
|
155
|
+
seed bar is the first bar whose window holds ``length`` present values, which
|
|
156
|
+
is what makes the warmups of `stdlib.md` compose rather than having to be
|
|
157
|
+
asserted one call at a time.
|
|
158
|
+
|
|
159
|
+
**A hole after the seed freezes the recurrence.** The bar is absent and the
|
|
160
|
+
running value is left where it was, so the next present bar continues from
|
|
161
|
+
the last present one. Consuming absence as zero drags the average toward
|
|
162
|
+
nothing and re-seeding lets one missing bar restart a two hundred bar
|
|
163
|
+
average.
|
|
164
|
+
|
|
165
|
+
Seeding from bar 0 with the first value is the common and cheaper
|
|
166
|
+
alternative. It is not this one: it draws a line where there should be a gap
|
|
167
|
+
and stays materially wrong until the seed decays away.
|
|
168
|
+
"""
|
|
169
|
+
held = region(state, key)
|
|
170
|
+
running = held.get("running")
|
|
171
|
+
if running is None:
|
|
172
|
+
full = window(values, length)
|
|
173
|
+
if full is None or length is None:
|
|
174
|
+
return ABSENT
|
|
175
|
+
running = mean(full, length)
|
|
176
|
+
held["running"] = running
|
|
177
|
+
return _reported(running)
|
|
178
|
+
value = values[len(values) - 1] if values else ABSENT
|
|
179
|
+
if not isinstance(value, float):
|
|
180
|
+
return ABSENT
|
|
181
|
+
running = step(running, value)
|
|
182
|
+
held["running"] = running
|
|
183
|
+
return _reported(running)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def running_total(state: Region, key: str, term: Value) -> Value:
|
|
187
|
+
"""Section 20.6's anchored accumulation: one term per bar, from zero.
|
|
188
|
+
|
|
189
|
+
Every total here starts at zero before any bar has contributed to it, and an
|
|
190
|
+
absent bar produces an absent bar out and leaves the total where it was. It
|
|
191
|
+
is neither reset nor fed a zero in place of the missing term, so a gap costs
|
|
192
|
+
the reading the bars it covers and nothing after them.
|
|
193
|
+
|
|
194
|
+
This is not the carried total section 20.2.1 refuses. There is no window to
|
|
195
|
+
sum here, so it is a different quantity rather than a cheaper way to compute
|
|
196
|
+
the same one.
|
|
197
|
+
"""
|
|
198
|
+
held = region(state, key)
|
|
199
|
+
if not isinstance(term, float):
|
|
200
|
+
return ABSENT
|
|
201
|
+
running = held.get("running", 0.0) + term
|
|
202
|
+
held["running"] = running
|
|
203
|
+
return _reported(running)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _reported(running: float) -> Value:
|
|
207
|
+
"""A running value as it is reported, under `compiled-program.md` 3.1.
|
|
208
|
+
|
|
209
|
+
The state keeps what the arithmetic produced and the caller is handed what
|
|
210
|
+
the language can hold, so an overflow is absent on the chart without the
|
|
211
|
+
recurrence quietly re-seeding itself on the bar after it.
|
|
212
|
+
"""
|
|
213
|
+
return result(running)
|