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,128 @@
|
|
|
1
|
+
"""The bare arithmetic of `stdlib.md` section 8.1.
|
|
2
|
+
|
|
3
|
+
None of these has a warmup: each reads this bar's values only, so it produces a
|
|
4
|
+
value on bar 0 whenever its arguments do. Every one is absent when any argument
|
|
5
|
+
is absent, and every numeric result goes through ``result``, so a value with no
|
|
6
|
+
finite real answer is absence rather than an infinity.
|
|
7
|
+
|
|
8
|
+
`stdlib.md` section 20.10 names most of this file as arithmetic with no
|
|
9
|
+
accumulation order to fix: ``abs`` and ``sign`` are one operation each, and
|
|
10
|
+
``min``, ``max`` and ``clamp`` select a value and compute nothing. ``mod`` is the
|
|
11
|
+
exception and its order is written out in section 8.1 and confirmed in 20.7: the
|
|
12
|
+
division, then the floor, then the multiplication, then the subtraction.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .values import ABSENT, Value, floor_of, number, result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def abs_of(x: Value) -> Value:
|
|
19
|
+
"""``abs(x)``: magnitude without sign."""
|
|
20
|
+
value = number(x)
|
|
21
|
+
return ABSENT if value is None else result(abs(value))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sign(x: Value) -> Value:
|
|
25
|
+
"""``sign(x)``: -1, 0 or 1.
|
|
26
|
+
|
|
27
|
+
Three comparisons rather than a division: the sign of a zero is not
|
|
28
|
+
observable in this language (`compiled-program.md` section 3.1), so a zero
|
|
29
|
+
of either sign answers 0 here.
|
|
30
|
+
"""
|
|
31
|
+
value = number(x)
|
|
32
|
+
if value is None:
|
|
33
|
+
return ABSENT
|
|
34
|
+
if value > 0:
|
|
35
|
+
return 1.0
|
|
36
|
+
if value < 0:
|
|
37
|
+
return -1.0
|
|
38
|
+
return 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def minimum(a: Value, b: Value) -> Value:
|
|
42
|
+
"""``min(a, b)``: the smaller of two."""
|
|
43
|
+
left = number(a)
|
|
44
|
+
right = number(b)
|
|
45
|
+
if left is None or right is None:
|
|
46
|
+
return ABSENT
|
|
47
|
+
return result(left if left < right else right)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def maximum(a: Value, b: Value) -> Value:
|
|
51
|
+
"""``max(a, b)``: the larger of two."""
|
|
52
|
+
left = number(a)
|
|
53
|
+
right = number(b)
|
|
54
|
+
if left is None or right is None:
|
|
55
|
+
return ABSENT
|
|
56
|
+
return result(left if left > right else right)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def clamp(x: Value, low: Value, high: Value) -> Value:
|
|
60
|
+
"""``clamp(x, lo, hi)``: ``x`` held inside a range.
|
|
61
|
+
|
|
62
|
+
Written as two comparisons in this order rather than as ``min(max(...))``,
|
|
63
|
+
because the two disagree when the bounds are crossed and this one answers
|
|
64
|
+
with the lower bound there, which is what a reader of the name expects.
|
|
65
|
+
"""
|
|
66
|
+
value = number(x)
|
|
67
|
+
lower = number(low)
|
|
68
|
+
upper = number(high)
|
|
69
|
+
if value is None or lower is None or upper is None:
|
|
70
|
+
return ABSENT
|
|
71
|
+
if value < lower:
|
|
72
|
+
return result(lower)
|
|
73
|
+
if value > upper:
|
|
74
|
+
return result(upper)
|
|
75
|
+
return result(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mod(a: Value, b: Value) -> Value:
|
|
79
|
+
"""``mod(a, b)``: ``a - b * floor(a / b)``, the floored remainder.
|
|
80
|
+
|
|
81
|
+
Its sign follows ``b``. The formula is written out rather than handed to the
|
|
82
|
+
interpreter's own remainder because "the modulo" names two different
|
|
83
|
+
functions in common use: this is the floored one and ``%`` is the truncated
|
|
84
|
+
one, so ``mod(-7, 3)`` is 2 where ``-7 % 3`` is -1 in this language. The two
|
|
85
|
+
agree for every positive ``b``, which is every use that wraps an index, a bar
|
|
86
|
+
count or a session offset.
|
|
87
|
+
|
|
88
|
+
**The host language's own floored remainder is not this function either.**
|
|
89
|
+
It agrees at every argument a script is likely to write and parts from it
|
|
90
|
+
where the division overflows, because it is exact rather than a division
|
|
91
|
+
followed by a floor: at ``mod(0.1, 1e-310)`` the quotient is an infinity and
|
|
92
|
+
this function is absent there, where the host's remainder answers
|
|
93
|
+
3.135287412051e-311.
|
|
94
|
+
|
|
95
|
+
``mod(a, 0)`` is absent, on the rule of `stdlib.md` section 2.4 that a result
|
|
96
|
+
with no finite real value is absent.
|
|
97
|
+
"""
|
|
98
|
+
left = number(a)
|
|
99
|
+
right = number(b)
|
|
100
|
+
if left is None or right is None or right == 0:
|
|
101
|
+
return ABSENT
|
|
102
|
+
return result(left - right * floor_of(left / right))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_none(x: Value) -> bool:
|
|
106
|
+
"""``isNone(x)``: true when the value is absent."""
|
|
107
|
+
return x is None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def or_else(x: Value, fallback: Value) -> Value:
|
|
111
|
+
"""``orElse(x, fallback)``: ``x`` when present, ``fallback`` when absent.
|
|
112
|
+
|
|
113
|
+
The fallback is returned as it stands, absence included, so
|
|
114
|
+
``orElse(x, none)`` is ``x``.
|
|
115
|
+
"""
|
|
116
|
+
return fallback if x is None else x
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def to_bool(x: Value) -> bool:
|
|
120
|
+
"""``toBool(x)``: absence to false, a bool to itself.
|
|
121
|
+
|
|
122
|
+
A number is a type error the checker refuses, so an engine that reaches one
|
|
123
|
+
is looking at a program its verifier should not have accepted, and false is
|
|
124
|
+
the safe reading. Identity against ``True`` rather than truthiness, because
|
|
125
|
+
every non-zero number is truthy in this interpreter and one reaching here
|
|
126
|
+
would come back as a bool the language never made.
|
|
127
|
+
"""
|
|
128
|
+
return x is True
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""The six means of `stdlib.md` section 20.3 that are not built from another mean.
|
|
2
|
+
|
|
3
|
+
Each is one of the two shapes of section 20.2 and nothing more: a fresh window
|
|
4
|
+
sum, or a window mean that seeds a recurrence. What makes them six functions
|
|
5
|
+
rather than one is the step and the weighting, and what makes each of them one
|
|
6
|
+
function rather than a family is the arrangement, which is why every entry here
|
|
7
|
+
names the arrangement it is not.
|
|
8
|
+
|
|
9
|
+
A value arrives already read off this bar's arguments, and every one of these
|
|
10
|
+
pushes it into a buffer of its own before it looks at the window, because a call
|
|
11
|
+
that skipped a bar's contribution would have a window a bar out of step with the
|
|
12
|
+
bars it is supposed to cover.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .series import Region, contributed, mean, seeded, total, window
|
|
16
|
+
from .values import ABSENT, Value, number, result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def simple(state: Region, value: Value, length: int | None) -> Value:
|
|
20
|
+
"""``sma(src, len)``: the window sum of 20.2.1 divided by ``len``."""
|
|
21
|
+
values = contributed(state, "src", number(value), length)
|
|
22
|
+
held = window(values, length)
|
|
23
|
+
if held is None or length is None:
|
|
24
|
+
return ABSENT
|
|
25
|
+
return result(mean(held, length))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def exponential(state: Region, value: Value, length: int | None) -> Value:
|
|
29
|
+
"""``ema(src, len)``: the seeded recurrence with the weight of section 4.
|
|
30
|
+
|
|
31
|
+
**The step is the two products added**, and it is not
|
|
32
|
+
``running + (value - running) * weight``. That third arrangement of the same
|
|
33
|
+
algebra is the one an engine reaches for, because it is one multiplication
|
|
34
|
+
rather than two, and over the fixture the release gate compares bit for bit
|
|
35
|
+
it differs on most of the values at every length.
|
|
36
|
+
"""
|
|
37
|
+
values = contributed(state, "src", number(value), length)
|
|
38
|
+
if length is None:
|
|
39
|
+
return ABSENT
|
|
40
|
+
weight = 2 / (length + 1)
|
|
41
|
+
rest = 1 - weight
|
|
42
|
+
|
|
43
|
+
def step(running: float, value: float) -> float:
|
|
44
|
+
return value * weight + running * rest
|
|
45
|
+
|
|
46
|
+
return seeded(state, "run", values, length, step)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def smoothed(state: Region, value: Value, length: int | None) -> Value:
|
|
50
|
+
"""``rma(src, len)``: the recurrence the classic oscillators are built on.
|
|
51
|
+
|
|
52
|
+
**This is not the shape of ``ema`` with a weight of ``1 / len``.** The two
|
|
53
|
+
are one function in exact arithmetic and two numbers in binary64, and the
|
|
54
|
+
difference propagates into the strength reading, the average true range, the
|
|
55
|
+
trailing band and the directional index, which are the readings a chart is
|
|
56
|
+
most often asked to reproduce. Neither is
|
|
57
|
+
``running + (value - running) / len``, which is a third arrangement again.
|
|
58
|
+
"""
|
|
59
|
+
values = contributed(state, "src", number(value), length)
|
|
60
|
+
if length is None:
|
|
61
|
+
return ABSENT
|
|
62
|
+
span = float(length)
|
|
63
|
+
|
|
64
|
+
def step(running: float, value: float) -> float:
|
|
65
|
+
return (running * (span - 1) + value) / span
|
|
66
|
+
|
|
67
|
+
return seeded(state, "run", values, length, step)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def linear(state: Region, value: Value, length: int | None) -> Value:
|
|
71
|
+
"""``wma(src, len)``: weight 1 added first, weight ``len`` last, one division.
|
|
72
|
+
|
|
73
|
+
The weight on ``w[k]`` is ``len - k``, formed as a multiplication of the
|
|
74
|
+
value by the whole number weight and not by a precomputed fraction. Dividing
|
|
75
|
+
each term by the divisor as it is added is a different number.
|
|
76
|
+
|
|
77
|
+
Nothing is fixed about how the divisor itself is formed: its factors are a
|
|
78
|
+
whole number product scaled by a power of two, which is exact however it is
|
|
79
|
+
grouped.
|
|
80
|
+
"""
|
|
81
|
+
values = contributed(state, "src", number(value), length)
|
|
82
|
+
held = window(values, length)
|
|
83
|
+
if held is None or length is None:
|
|
84
|
+
return ABSENT
|
|
85
|
+
span = float(length)
|
|
86
|
+
divisor = (span * (span + 1)) / 2
|
|
87
|
+
running = 0.0
|
|
88
|
+
for position in range(length):
|
|
89
|
+
running = running + held[length - 1 - position] * float(position + 1)
|
|
90
|
+
return result(running / divisor)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def symmetric(state: Region, value: Value) -> Value:
|
|
94
|
+
"""``swma(src)``: the fixed four bar mean, added left to right, divided once.
|
|
95
|
+
|
|
96
|
+
Over twenty thousand four bar windows of ordinary prices, regrouping the four
|
|
97
|
+
terms in the middle differs on 5281 of them, adding them right to left on
|
|
98
|
+
7230, and dividing each term by 6 as it is added on 9564. How the two middle
|
|
99
|
+
terms are doubled is not fixed, because an exact scaling by two is one value
|
|
100
|
+
however it is written.
|
|
101
|
+
"""
|
|
102
|
+
values = contributed(state, "src", number(value), 4)
|
|
103
|
+
held = window(values, 4)
|
|
104
|
+
if held is None:
|
|
105
|
+
return ABSENT
|
|
106
|
+
running = held[3]
|
|
107
|
+
running = running + 2 * held[2]
|
|
108
|
+
running = running + 2 * held[1]
|
|
109
|
+
running = running + held[0]
|
|
110
|
+
return result(running / 6)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def volume_weighted(state: Region, value: Value, size: Value, length: int | None) -> Value:
|
|
114
|
+
"""``vwma(src, len)``: this bar's product formed first, then two window sums.
|
|
115
|
+
|
|
116
|
+
Dividing each sum by ``len`` first and then dividing one by the other is the
|
|
117
|
+
same quantity with two extra roundings in it, and it is not this
|
|
118
|
+
arrangement. The result is absent where the denominator is zero, which is a
|
|
119
|
+
window nothing traded in rather than an error.
|
|
120
|
+
"""
|
|
121
|
+
price = number(value)
|
|
122
|
+
traded = number(size)
|
|
123
|
+
product = ABSENT if price is None or traded is None else price * traded
|
|
124
|
+
products = contributed(state, "flow", product, length)
|
|
125
|
+
volumes = contributed(state, "size", traded, length)
|
|
126
|
+
top = window(products, length)
|
|
127
|
+
bottom = window(volumes, length)
|
|
128
|
+
if top is None or bottom is None:
|
|
129
|
+
return ABSENT
|
|
130
|
+
divisor = total(bottom)
|
|
131
|
+
if divisor == 0:
|
|
132
|
+
return ABSENT
|
|
133
|
+
return result(total(top) / divisor)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""The readings taken from one bar and nothing before it.
|
|
2
|
+
|
|
3
|
+
`stdlib.md` section 20.5 fixes the one that is not a single operation:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
result = max(high - low, abs(high - previousClose), abs(low - previousClose))
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**The oldest bar of the dataset is the library's one deliberate exception to
|
|
10
|
+
absence propagation**, and `stdlib.md` section 6 puts it there rather than
|
|
11
|
+
leaving it to an engine: ``trueRange()`` is ``high - low`` on that bar. The other
|
|
12
|
+
two terms need a close that does not exist there, and propagating absence would
|
|
13
|
+
start every average of this reading one bar later than the declared warmup while
|
|
14
|
+
adding nothing, because the bar's own range is a true statement about that bar.
|
|
15
|
+
|
|
16
|
+
The exception is granted to ``trueRange`` and therefore to the averages of it. It
|
|
17
|
+
is not granted to every function that happens to use a range: where a function's
|
|
18
|
+
declared warmup shows it counts changes rather than levels, section 20.5's gap
|
|
19
|
+
aware form is what it takes, and that one is absent on the oldest bar like any
|
|
20
|
+
other change. Two functions use it and both declare a warmup one bar later than
|
|
21
|
+
the plain reading would give, which is how the page says which of the two they
|
|
22
|
+
take. That form is not a call a script can make, and an engine reaches it through
|
|
23
|
+
the stateful half of this library rather than through here.
|
|
24
|
+
|
|
25
|
+
**Where the previous close comes from is a host fact, not an arithmetic one.** An
|
|
26
|
+
engine is handed it with the bar and that is the one it must use, because a call
|
|
27
|
+
inside a branch does not see every bar and the close of the bar it last ran on is
|
|
28
|
+
a different number. So it is an argument here, and this function holds nothing
|
|
29
|
+
across bars.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .values import ABSENT, Value, number, result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def true_range(high: Value, low: Value, previous_close: Value, first_bar: bool) -> Value:
|
|
36
|
+
"""``trueRange()``: the bar's range, including any gap from the last close.
|
|
37
|
+
|
|
38
|
+
``first_bar`` is the caller's answer to "is this the oldest bar of the
|
|
39
|
+
dataset", which is the question section 6's exception turns on and not one
|
|
40
|
+
the three numbers can answer. An absent previous close in the middle of a run
|
|
41
|
+
is a hole, and a hole propagates.
|
|
42
|
+
"""
|
|
43
|
+
top = number(high)
|
|
44
|
+
bottom = number(low)
|
|
45
|
+
if first_bar:
|
|
46
|
+
if top is None or bottom is None:
|
|
47
|
+
return ABSENT
|
|
48
|
+
return result(top - bottom)
|
|
49
|
+
close_before = number(previous_close)
|
|
50
|
+
if top is None or bottom is None or close_before is None:
|
|
51
|
+
return ABSENT
|
|
52
|
+
within = top - bottom
|
|
53
|
+
up_gap = abs(top - close_before)
|
|
54
|
+
down_gap = abs(bottom - close_before)
|
|
55
|
+
widest = within
|
|
56
|
+
if up_gap > widest:
|
|
57
|
+
widest = up_gap
|
|
58
|
+
if down_gap > widest:
|
|
59
|
+
widest = down_gap
|
|
60
|
+
return result(widest)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""The comparisons and counters of `stdlib.md` section 9, named in 20.10.
|
|
2
|
+
|
|
3
|
+
None of these accumulates. They compare this bar's value with an older one, or
|
|
4
|
+
count bars, so there is one arrangement and no choice to record. What there is
|
|
5
|
+
instead is a set of edge rules, and each of them is a decision somebody has to
|
|
6
|
+
make the same way twice for two engines to agree:
|
|
7
|
+
|
|
8
|
+
- **A crossing keeps the two series apart rather than subtracting them.** The
|
|
9
|
+
test turns on whether one was at or below the other, and a rounded zero would
|
|
10
|
+
change the answer on exactly the bars a crossing matters.
|
|
11
|
+
- **A crossing is "at or below, then above"**, not "strictly below, then above",
|
|
12
|
+
so two series that touch and separate report one cross rather than none. The
|
|
13
|
+
alternative loses the case where the values are briefly equal, which is common
|
|
14
|
+
on instruments with a coarse tick.
|
|
15
|
+
- **A run test needs its own changes to be present.** ``rising(src, len)`` reads
|
|
16
|
+
``len`` changes and therefore ``len + 1`` values, which is where its warmup of
|
|
17
|
+
bar ``len`` comes from and why it is a bar later than a window of ``len``.
|
|
18
|
+
- **The two that wait for a condition are absent, not zero, before it has ever
|
|
19
|
+
been true.** Zero would read as "it happened on this bar".
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from .series import Region, back, contributed, raw_window, region
|
|
25
|
+
from .values import ABSENT, Value, number, result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def change(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
29
|
+
"""``change(src)`` and ``change(src, len)``: ``src - src[len]``, one subtraction."""
|
|
30
|
+
keep = None if length is None else length + 1
|
|
31
|
+
values = contributed(state, "src", number(value), keep)
|
|
32
|
+
older = back(values, length)
|
|
33
|
+
newer = values[len(values) - 1]
|
|
34
|
+
if not isinstance(newer, float) or not isinstance(older, float):
|
|
35
|
+
return ABSENT
|
|
36
|
+
return result(newer - older)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def rising(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
40
|
+
"""``rising(src, len)``: true when each of the last ``len`` changes was positive."""
|
|
41
|
+
return _run(state, value, length, True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def falling(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
45
|
+
"""``falling(src, len)``: true when each of the last ``len`` changes was negative."""
|
|
46
|
+
return _run(state, value, length, False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run(state: Region, value: Value, length: Optional[int], up: bool) -> Value:
|
|
50
|
+
"""The run test over the ``len + 1`` values the ``len`` changes are taken from.
|
|
51
|
+
|
|
52
|
+
**The changes are read oldest first and the answer is the first one that
|
|
53
|
+
settles it.** A change that is not in the direction asked for makes the run
|
|
54
|
+
false there and then, and a change that cannot be taken because one of its
|
|
55
|
+
two bars is absent makes the answer absent there and then. So a window with a
|
|
56
|
+
hole in it is false rather than absent whenever an older change had already
|
|
57
|
+
failed, which is not the blanket propagation of section 2.4 and is the order
|
|
58
|
+
this reading is taken in.
|
|
59
|
+
"""
|
|
60
|
+
keep = None if length is None else length + 1
|
|
61
|
+
held = raw_window(contributed(state, "src", number(value), keep), keep)
|
|
62
|
+
if held is None or length is None:
|
|
63
|
+
return ABSENT
|
|
64
|
+
for age in range(length - 1, -1, -1):
|
|
65
|
+
newer = held[age]
|
|
66
|
+
older = held[age + 1]
|
|
67
|
+
if not isinstance(newer, float) or not isinstance(older, float):
|
|
68
|
+
return ABSENT
|
|
69
|
+
if up and not newer > older:
|
|
70
|
+
return False
|
|
71
|
+
if not up and not newer < older:
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def crossed_up(state: Region, first: Value, second: Value) -> Value:
|
|
77
|
+
"""``crossUp(a, b)``: ``a`` was at or below ``b`` and is now above."""
|
|
78
|
+
pair = _pair(state, first, second)
|
|
79
|
+
if pair is None:
|
|
80
|
+
return ABSENT
|
|
81
|
+
(now_first, before_first), (now_second, before_second) = pair
|
|
82
|
+
return before_first <= before_second and now_first > now_second
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def crossed_down(state: Region, first: Value, second: Value) -> Value:
|
|
86
|
+
"""``crossDown(a, b)``: ``a`` was at or above ``b`` and is now below."""
|
|
87
|
+
pair = _pair(state, first, second)
|
|
88
|
+
if pair is None:
|
|
89
|
+
return ABSENT
|
|
90
|
+
(now_first, before_first), (now_second, before_second) = pair
|
|
91
|
+
return before_first >= before_second and now_first < now_second
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def crossed(state: Region, first: Value, second: Value) -> Value:
|
|
95
|
+
"""``cross(a, b)``: either direction, read from the one pair of readings."""
|
|
96
|
+
pair = _pair(state, first, second)
|
|
97
|
+
if pair is None:
|
|
98
|
+
return ABSENT
|
|
99
|
+
(now_first, before_first), (now_second, before_second) = pair
|
|
100
|
+
if before_first <= before_second and now_first > now_second:
|
|
101
|
+
return True
|
|
102
|
+
return before_first >= before_second and now_first < now_second
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _pair(state: Region, first: Value, second: Value):
|
|
106
|
+
"""This bar's and the previous bar's readings of both series, or absence.
|
|
107
|
+
|
|
108
|
+
Both are contributed on every bar whatever the other one did, so a hole in
|
|
109
|
+
one does not shift the other's idea of which bar was previous.
|
|
110
|
+
"""
|
|
111
|
+
left = contributed(state, "a", number(first), 2)
|
|
112
|
+
right = contributed(state, "b", number(second), 2)
|
|
113
|
+
now_left, before_left = left[len(left) - 1], back(left, 1)
|
|
114
|
+
now_right, before_right = right[len(right) - 1], back(right, 1)
|
|
115
|
+
for reading in (now_left, before_left, now_right, before_right):
|
|
116
|
+
if not isinstance(reading, float):
|
|
117
|
+
return None
|
|
118
|
+
return (now_left, before_left), (now_right, before_right)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def bars_since(state: Region, condition: Value) -> Value:
|
|
122
|
+
"""``barsSince(cond)``: bars since the condition last held, 0 on the bar itself.
|
|
123
|
+
|
|
124
|
+
Absent until the condition has been true once. A bar the condition is absent
|
|
125
|
+
on is a bar it did not hold: the count goes on rather than stopping, because
|
|
126
|
+
the question is how long ago the last true bar was and an unknown bar in
|
|
127
|
+
between does not change the answer.
|
|
128
|
+
"""
|
|
129
|
+
held = region(state, "since")
|
|
130
|
+
if condition is True:
|
|
131
|
+
held["bars"] = 0.0
|
|
132
|
+
return 0.0
|
|
133
|
+
since = held.get("bars")
|
|
134
|
+
if since is None:
|
|
135
|
+
return ABSENT
|
|
136
|
+
since = since + 1
|
|
137
|
+
held["bars"] = since
|
|
138
|
+
return since
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def value_when(
|
|
142
|
+
state: Region, condition: Value, value: Value, occurrence: Optional[int]
|
|
143
|
+
) -> Value:
|
|
144
|
+
"""``valueWhen(cond, src, occurrence)``: ``src`` as it stood on a true bar.
|
|
145
|
+
|
|
146
|
+
Occurrence 0 is the most recent true bar, 1 the one before it, and the answer
|
|
147
|
+
is absent until that many true bars have happened. The value recorded is
|
|
148
|
+
whatever ``src`` was on that bar, absence included: the condition decides
|
|
149
|
+
which bar is read and the source decides what was there.
|
|
150
|
+
"""
|
|
151
|
+
wanted = 0 if occurrence is None else occurrence
|
|
152
|
+
held = region(state, "seen")
|
|
153
|
+
values = held.get("values")
|
|
154
|
+
if values is None:
|
|
155
|
+
values = []
|
|
156
|
+
held["values"] = values
|
|
157
|
+
if condition is True:
|
|
158
|
+
values.append(value)
|
|
159
|
+
depth = max(wanted + 1, held.get("depth", 1))
|
|
160
|
+
held["depth"] = depth
|
|
161
|
+
extra = len(values) - depth
|
|
162
|
+
if extra > 0:
|
|
163
|
+
del values[:extra]
|
|
164
|
+
if wanted < 0 or len(values) <= wanted:
|
|
165
|
+
return ABSENT
|
|
166
|
+
return values[len(values) - 1 - wanted]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The two string rules two engines have to share, written from the page.
|
|
2
|
+
|
|
3
|
+
**A string is a sequence of code points** (`compiled-program.md` section 3.1),
|
|
4
|
+
so length, indexing and comparison count code points and not the storage unit of
|
|
5
|
+
whatever language an engine is written in. This host stores a string as code
|
|
6
|
+
points already, which makes the rule free here and does not make it optional: an
|
|
7
|
+
engine whose strings are sixteen bit units has to count and index past a
|
|
8
|
+
surrogate pair as one element, or the two disagree about the length of a string
|
|
9
|
+
holding a symbol outside the basic plane and about every substring taken after
|
|
10
|
+
one.
|
|
11
|
+
|
|
12
|
+
**Ordering.** Two strings are ordered by comparing code points from the front,
|
|
13
|
+
the first difference deciding, and a string that runs out first ordering first
|
|
14
|
+
(`language.md` section 9.3, `stdlib.md` section 10). It is the one order in the
|
|
15
|
+
language: the four comparison operators and a sort over an array of strings
|
|
16
|
+
agree. This host's own comparison is already by code point, so the function below
|
|
17
|
+
is the rule written down rather than a correction of the host, and a test holds
|
|
18
|
+
it to a pair the unit based order would get wrong.
|
|
19
|
+
|
|
20
|
+
**Whitespace.** ``str.trim`` removes, and ``toNumber`` ignores at either end,
|
|
21
|
+
exactly the code points `stdlib.md` section 10 lists, which are the ones with the
|
|
22
|
+
Unicode White_Space property, and no other. The host's own strip removes a
|
|
23
|
+
different set: it takes the four information separators U+001C to U+001F as well,
|
|
24
|
+
and another host takes the byte order mark U+FEFF and leaves those, so neither
|
|
25
|
+
host's default is the set and the set is written out here. ``tests/test_strings``
|
|
26
|
+
walks every code point of the basic plane against the table read out of the page,
|
|
27
|
+
which is what makes the page the input rather than this list.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# The code points `stdlib.md` section 10 lists, in the order it lists them.
|
|
31
|
+
_WHITESPACE: frozenset[int] = frozenset(
|
|
32
|
+
{
|
|
33
|
+
0x0009, # character tabulation
|
|
34
|
+
0x000A, # line feed
|
|
35
|
+
0x000B, # line tabulation
|
|
36
|
+
0x000C, # form feed
|
|
37
|
+
0x000D, # carriage return
|
|
38
|
+
0x0020, # space
|
|
39
|
+
0x0085, # next line
|
|
40
|
+
0x00A0, # no-break space
|
|
41
|
+
0x1680, # ogham space mark
|
|
42
|
+
0x2000, # en quad, through the hair space
|
|
43
|
+
0x2001,
|
|
44
|
+
0x2002,
|
|
45
|
+
0x2003,
|
|
46
|
+
0x2004,
|
|
47
|
+
0x2005,
|
|
48
|
+
0x2006,
|
|
49
|
+
0x2007,
|
|
50
|
+
0x2008,
|
|
51
|
+
0x2009,
|
|
52
|
+
0x200A,
|
|
53
|
+
0x2028, # line separator
|
|
54
|
+
0x2029, # paragraph separator
|
|
55
|
+
0x202F, # narrow no-break space
|
|
56
|
+
0x205F, # medium mathematical space
|
|
57
|
+
0x3000, # ideographic space
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_whitespace(point: int) -> bool:
|
|
63
|
+
"""Whether one code point is in the trimmed set."""
|
|
64
|
+
return point in _WHITESPACE
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def trimmed(text: str) -> str:
|
|
68
|
+
"""``text`` with the whitespace of the written set taken off both ends."""
|
|
69
|
+
start = 0
|
|
70
|
+
end = len(text)
|
|
71
|
+
while start < end and is_whitespace(ord(text[start])):
|
|
72
|
+
start += 1
|
|
73
|
+
while end > start and is_whitespace(ord(text[end - 1])):
|
|
74
|
+
end -= 1
|
|
75
|
+
return text[start:end]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compare(a: str, b: str) -> int:
|
|
79
|
+
"""The order of two strings: negative when ``a`` comes first, by code point."""
|
|
80
|
+
for one, other in zip(a, b):
|
|
81
|
+
if one != other:
|
|
82
|
+
return -1 if ord(one) < ord(other) else 1
|
|
83
|
+
if len(a) == len(b):
|
|
84
|
+
return 0
|
|
85
|
+
return -1 if len(a) < len(b) else 1
|