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,220 @@
|
|
|
1
|
+
"""The volume readings of `stdlib.md` section 20.6.
|
|
2
|
+
|
|
3
|
+
**The running totals here are anchored accumulations rather than windows**, so
|
|
4
|
+
each carries its total forward and adds one term per bar. That is what the
|
|
5
|
+
quantity is, not a cheaper way to compute a window sum, and the refusal of a
|
|
6
|
+
carried total in section 20.2.1 does not reach them: there is no window to sum.
|
|
7
|
+
|
|
8
|
+
**Every total starts at zero** before any bar has contributed to it, and an
|
|
9
|
+
absent bar produces an absent bar out and leaves the total where it was. It is
|
|
10
|
+
neither reset nor fed a zero in place of the missing term, so a gap costs the
|
|
11
|
+
reading the bars it covers and nothing after them.
|
|
12
|
+
|
|
13
|
+
Every function here is absent on every bar when the host supplies no volume for
|
|
14
|
+
the instrument, which is one case of the same rule: an absent volume is not a
|
|
15
|
+
zero volume, and a study that wants to branch on it tests the chart fact rather
|
|
16
|
+
than inspecting the result.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from . import averages, prices, strength
|
|
22
|
+
from .series import Region, back, contributed, region, running_total, total, window
|
|
23
|
+
from .values import ABSENT, Value, number, result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def session_average(state: Region, ctx, value: Value) -> Value:
|
|
27
|
+
"""``vwap(src)``: the volume weighted average since the session opened."""
|
|
28
|
+
return _anchored(state, ctx, value, ctx.bar("isSessionFirst"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def anchored_average(state: Region, ctx, value: Value, anchor: Value) -> Value:
|
|
32
|
+
"""``vwapAnchor(src, resetWhen)``: the same average, restarted on a condition.
|
|
33
|
+
|
|
34
|
+
One calculation and two entries. The session reading resets where the host
|
|
35
|
+
says a session began, this one where the script says so, and the reset
|
|
36
|
+
happens **before** the bar's own term is added, so an anchor bar is the first
|
|
37
|
+
bar of the new average rather than the last bar of the old one.
|
|
38
|
+
"""
|
|
39
|
+
return _anchored(state, ctx, value, anchor)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _anchored(state: Region, ctx, value: Value, anchor: Value) -> Value:
|
|
43
|
+
"""The two totals, the reset, and the one division both averages take."""
|
|
44
|
+
held = region(state, "vwap")
|
|
45
|
+
price = number(value)
|
|
46
|
+
traded = number(ctx.bar("volume"))
|
|
47
|
+
if anchor is not True and anchor is not False:
|
|
48
|
+
return ABSENT
|
|
49
|
+
if anchor:
|
|
50
|
+
held["flow"] = 0.0
|
|
51
|
+
held["traded"] = 0.0
|
|
52
|
+
if price is None or traded is None:
|
|
53
|
+
return ABSENT
|
|
54
|
+
flow = held.get("flow", 0.0) + price * traded
|
|
55
|
+
volume = held.get("traded", 0.0) + traded
|
|
56
|
+
held["flow"] = flow
|
|
57
|
+
held["traded"] = volume
|
|
58
|
+
if volume == 0:
|
|
59
|
+
return ABSENT
|
|
60
|
+
return result(flow / volume)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def balance(state: Region, ctx) -> Value:
|
|
64
|
+
"""``obv()``: the whole of the bar's volume, signed by the close's direction.
|
|
65
|
+
|
|
66
|
+
The first bar has no close before it to compare against, so it contributes
|
|
67
|
+
nothing and the reading there is the 0 the total started at. An unchanged
|
|
68
|
+
close contributes nothing either.
|
|
69
|
+
"""
|
|
70
|
+
close = number(ctx.bar("close"))
|
|
71
|
+
traded = number(ctx.bar("volume"))
|
|
72
|
+
before = back(contributed(state, "close", close, 2), 1)
|
|
73
|
+
if close is None or traded is None:
|
|
74
|
+
return ABSENT
|
|
75
|
+
term = 0.0
|
|
76
|
+
if isinstance(before, float):
|
|
77
|
+
if close > before:
|
|
78
|
+
term = traded
|
|
79
|
+
elif close < before:
|
|
80
|
+
term = -traded
|
|
81
|
+
return running_total(state, "run", term)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def accumulation(state: Region, ctx) -> Value:
|
|
85
|
+
"""``ad()``: the running total of the term ``cmf`` sums over a window."""
|
|
86
|
+
return running_total(state, "run", position_flow(ctx))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def position_flow(ctx) -> Value:
|
|
90
|
+
"""The per-bar term ``ad`` and ``cmf`` share, formed as section 20.6 writes it.
|
|
91
|
+
|
|
92
|
+
The two bracketed differences are formed first and subtracted, then divided by
|
|
93
|
+
the span, then multiplied by the volume. **A bar whose span is not above zero
|
|
94
|
+
contributes an exact 0** rather than ending the total.
|
|
95
|
+
"""
|
|
96
|
+
high = number(ctx.bar("high"))
|
|
97
|
+
low = number(ctx.bar("low"))
|
|
98
|
+
close = number(ctx.bar("close"))
|
|
99
|
+
traded = number(ctx.bar("volume"))
|
|
100
|
+
if high is None or low is None or close is None or traded is None:
|
|
101
|
+
return ABSENT
|
|
102
|
+
span = high - low
|
|
103
|
+
if span <= 0:
|
|
104
|
+
return 0.0
|
|
105
|
+
return result((((close - low) - (high - close)) / span) * traded)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def flow_fraction(state: Region, ctx, length: Optional[int]) -> Value:
|
|
109
|
+
"""``cmf(len)``: the window sum of that term over the window sum of the volume."""
|
|
110
|
+
terms = contributed(state, "term", position_flow(ctx), length)
|
|
111
|
+
volumes = contributed(state, "size", number(ctx.bar("volume")), length)
|
|
112
|
+
held = window(terms, length)
|
|
113
|
+
traded = window(volumes, length)
|
|
114
|
+
if held is None or traded is None:
|
|
115
|
+
return ABSENT
|
|
116
|
+
divisor = total(traded)
|
|
117
|
+
if divisor == 0:
|
|
118
|
+
return ABSENT
|
|
119
|
+
return result(total(held) / divisor)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def accumulation_gap(
|
|
123
|
+
state: Region, ctx, fast_length: Optional[int], slow_length: Optional[int]
|
|
124
|
+
) -> Value:
|
|
125
|
+
"""``adOsc(fast, slow)``: the fast exponential mean of the running total less the slow.
|
|
126
|
+
|
|
127
|
+
The averages run over the running total, not over the per-bar term, which is
|
|
128
|
+
what dates the turns of the total rather than the turns of one bar's flow.
|
|
129
|
+
"""
|
|
130
|
+
running = accumulation(region(state, "total"), ctx)
|
|
131
|
+
fast = averages.exponential(region(state, "fast"), running, fast_length)
|
|
132
|
+
slow = averages.exponential(region(state, "slow"), running, slow_length)
|
|
133
|
+
if not isinstance(fast, float) or not isinstance(slow, float):
|
|
134
|
+
return ABSENT
|
|
135
|
+
return result(fast - slow)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def price_trend(state: Region, ctx) -> Value:
|
|
139
|
+
"""``pvt()``: the running total of a proportion of the volume.
|
|
140
|
+
|
|
141
|
+
The proportion is formed and rounded before it meets the volume. The first
|
|
142
|
+
bar has no change behind it and is absent, and the second already carries its
|
|
143
|
+
own term on top of the zero the total started at.
|
|
144
|
+
"""
|
|
145
|
+
close = number(ctx.bar("close"))
|
|
146
|
+
traded = number(ctx.bar("volume"))
|
|
147
|
+
before = back(contributed(state, "close", close, 2), 1)
|
|
148
|
+
if close is None or traded is None or not isinstance(before, float) or before == 0:
|
|
149
|
+
return ABSENT
|
|
150
|
+
return running_total(state, "run", ((close - before) / before) * traded)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def money_flow(state: Region, ctx, length: Optional[int]) -> Value:
|
|
154
|
+
"""``mfi(len)``: the strength reading computed on money flow, with window sums.
|
|
155
|
+
|
|
156
|
+
The arrangement of the last line is section 20.4's, and the result is 100
|
|
157
|
+
where the falling side is zero, for the reason given there. A bar whose
|
|
158
|
+
typical price is unchanged contributes an exact zero to both sides.
|
|
159
|
+
"""
|
|
160
|
+
price = prices.typical(ctx)
|
|
161
|
+
traded = number(ctx.bar("volume"))
|
|
162
|
+
values = contributed(state, "typical", price, 2)
|
|
163
|
+
before = back(values, 1)
|
|
164
|
+
rise = ABSENT
|
|
165
|
+
fall = ABSENT
|
|
166
|
+
if isinstance(price, float) and traded is not None and isinstance(before, float):
|
|
167
|
+
flow = price * traded
|
|
168
|
+
rise = flow if price > before else 0.0
|
|
169
|
+
fall = flow if price < before else 0.0
|
|
170
|
+
rises = window(contributed(state, "rise", rise, length), length)
|
|
171
|
+
falls = window(contributed(state, "fall", fall, length), length)
|
|
172
|
+
if rises is None or falls is None:
|
|
173
|
+
return ABSENT
|
|
174
|
+
return strength.line_of(total(rises), total(falls))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def movement(state: Region, ctx, length: Optional[int]) -> Value:
|
|
178
|
+
"""``eom(len)``: how far price moved per unit of volume, then a simple mean.
|
|
179
|
+
|
|
180
|
+
The product is formed before the division. **There is no scaling constant**:
|
|
181
|
+
implementations of this reading usually multiply by a large divisor whose only
|
|
182
|
+
job is to bring the number into a readable range and they do not agree on it,
|
|
183
|
+
section 7 declares no such argument, and this is gap 3 of section 20.11. That
|
|
184
|
+
leaves the reading very small on a liquid instrument.
|
|
185
|
+
"""
|
|
186
|
+
midpoint = prices.midpoint(ctx)
|
|
187
|
+
values = contributed(state, "midpoint", midpoint, 2)
|
|
188
|
+
before = back(values, 1)
|
|
189
|
+
high = number(ctx.bar("high"))
|
|
190
|
+
low = number(ctx.bar("low"))
|
|
191
|
+
traded = number(ctx.bar("volume"))
|
|
192
|
+
term = ABSENT
|
|
193
|
+
if isinstance(midpoint, float) and isinstance(before, float):
|
|
194
|
+
if high is not None and low is not None and traded is not None and traded != 0:
|
|
195
|
+
term = result(((midpoint - before) * (high - low)) / traded)
|
|
196
|
+
return averages.simple(region(state, "mean"), term, length)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def force(state: Region, ctx, length: Optional[int]) -> Value:
|
|
200
|
+
"""``forceIndex(len)``: the one bar close change times the volume, smoothed.
|
|
201
|
+
|
|
202
|
+
The product is formed per bar and fed to the average, so a bar with no volume
|
|
203
|
+
freezes the average rather than feeding it a change with nothing behind it.
|
|
204
|
+
"""
|
|
205
|
+
close = number(ctx.bar("close"))
|
|
206
|
+
traded = number(ctx.bar("volume"))
|
|
207
|
+
before = back(contributed(state, "close", close, 2), 1)
|
|
208
|
+
term = ABSENT
|
|
209
|
+
if close is not None and traded is not None and isinstance(before, float):
|
|
210
|
+
term = result((close - before) * traded)
|
|
211
|
+
return averages.exponential(region(state, "mean"), term, length)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def relative_volume(state: Region, ctx, length: Optional[int]) -> Value:
|
|
215
|
+
"""``relativeVolume(len)``: the bar's volume over the simple mean of the volume."""
|
|
216
|
+
traded = number(ctx.bar("volume"))
|
|
217
|
+
average = averages.simple(region(state, "mean"), traded, length)
|
|
218
|
+
if traded is None or not isinstance(average, float) or average == 0:
|
|
219
|
+
return ABSENT
|
|
220
|
+
return result(traded / average)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""The momentum readings of `stdlib.md` section 20.4, and the two gaps between means.
|
|
2
|
+
|
|
3
|
+
**These scale by 100 before dividing**, which is the association their own
|
|
4
|
+
readings have always carried and the opposite of the directional index of section
|
|
5
|
+
20.3. Writing one of them the other way round moves every value it reports.
|
|
6
|
+
|
|
7
|
+
Two of them are named in section 20.4 only so that the absence of a recipe is not
|
|
8
|
+
read as an oversight: the momentum reading is a single subtraction, and the
|
|
9
|
+
awesome oscillator is the fast simple mean of the bar midpoint less the slow one,
|
|
10
|
+
in that order.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from . import averages, bookkeeping, composites, prices
|
|
16
|
+
from .series import Region, back, contributed, region, total, window
|
|
17
|
+
from .values import ABSENT, Value, number, result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gap(
|
|
21
|
+
state: Region,
|
|
22
|
+
value: Value,
|
|
23
|
+
fast_length: Optional[int],
|
|
24
|
+
slow_length: Optional[int],
|
|
25
|
+
signal_length: Optional[int],
|
|
26
|
+
percent: bool,
|
|
27
|
+
) -> list:
|
|
28
|
+
"""``macd(...)`` and ``ppo(...)``, which differ only in the first line.
|
|
29
|
+
|
|
30
|
+
The signal average is fed the line absences and all, so it seeds on the first
|
|
31
|
+
``signal`` values the line produced, which is where its warmup comes from. The
|
|
32
|
+
histogram is the difference of the two values as reported, not recomputed
|
|
33
|
+
from the averages.
|
|
34
|
+
"""
|
|
35
|
+
fast = averages.exponential(region(state, "fast"), value, fast_length)
|
|
36
|
+
slow = averages.exponential(region(state, "slow"), value, slow_length)
|
|
37
|
+
line = ABSENT
|
|
38
|
+
if isinstance(fast, float) and isinstance(slow, float):
|
|
39
|
+
if not percent:
|
|
40
|
+
line = result(fast - slow)
|
|
41
|
+
elif slow != 0:
|
|
42
|
+
line = result((100 * (fast - slow)) / slow)
|
|
43
|
+
trigger = averages.exponential(region(state, "signal"), line, signal_length)
|
|
44
|
+
histogram = ABSENT
|
|
45
|
+
if isinstance(line, float) and isinstance(trigger, float):
|
|
46
|
+
histogram = result(line - trigger)
|
|
47
|
+
return [line, trigger, histogram]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def distance(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
51
|
+
"""``mom(src, len)``: ``src - src[len]``, which is the change of section 9."""
|
|
52
|
+
return bookkeeping.change(state, value, length)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rate(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
56
|
+
"""``roc(src, len)``: the same change as a percentage of the older value."""
|
|
57
|
+
keep = None if length is None else length + 1
|
|
58
|
+
values = contributed(state, "src", number(value), keep)
|
|
59
|
+
now = values[len(values) - 1]
|
|
60
|
+
before = back(values, length)
|
|
61
|
+
if not isinstance(now, float) or not isinstance(before, float) or before == 0:
|
|
62
|
+
return ABSENT
|
|
63
|
+
return result((100 * (now - before)) / before)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def midpoint_gap(
|
|
67
|
+
state: Region, ctx, fast_length: Optional[int], slow_length: Optional[int]
|
|
68
|
+
) -> Value:
|
|
69
|
+
"""``awesomeOsc(fast, slow)``: the fast simple mean of the midpoint less the slow one."""
|
|
70
|
+
value = prices.midpoint(ctx)
|
|
71
|
+
fast = averages.simple(region(state, "fast"), value, fast_length)
|
|
72
|
+
slow = averages.simple(region(state, "slow"), value, slow_length)
|
|
73
|
+
if not isinstance(fast, float) or not isinstance(slow, float):
|
|
74
|
+
return ABSENT
|
|
75
|
+
return result(fast - slow)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def smoothed_rate(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
79
|
+
"""``trix(src, len)``: the one bar percentage change of a triple exponential mean.
|
|
80
|
+
|
|
81
|
+
The other reading in circulation takes the change of the logarithm of the
|
|
82
|
+
average instead. It is a different number and it is not what section 5
|
|
83
|
+
describes.
|
|
84
|
+
"""
|
|
85
|
+
smoothed = composites.smoothed_three(region(state, "chain"), value, length)
|
|
86
|
+
values = contributed(state, "smoothed", smoothed, 2)
|
|
87
|
+
now = values[len(values) - 1]
|
|
88
|
+
before = back(values, 1)
|
|
89
|
+
if not isinstance(now, float) or not isinstance(before, float) or before == 0:
|
|
90
|
+
return ABSENT
|
|
91
|
+
return result((100 * (now - before)) / before)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def double_smoothed(
|
|
95
|
+
state: Region, value: Value, long_length: Optional[int], short_length: Optional[int]
|
|
96
|
+
) -> Value:
|
|
97
|
+
"""``tsi(src, longLen, shortLen)``: the one bar change smoothed twice, over its own size.
|
|
98
|
+
|
|
99
|
+
The long length comes first and the short one second, and the absolute size
|
|
100
|
+
of the change is smoothed the same way in its own pair of state regions.
|
|
101
|
+
"""
|
|
102
|
+
values = contributed(state, "src", number(value), 2)
|
|
103
|
+
now = values[len(values) - 1]
|
|
104
|
+
before = back(values, 1)
|
|
105
|
+
delta = ABSENT
|
|
106
|
+
size = ABSENT
|
|
107
|
+
if isinstance(now, float) and isinstance(before, float):
|
|
108
|
+
delta = now - before
|
|
109
|
+
size = abs(delta)
|
|
110
|
+
smoothed_change = _twice(region(state, "change"), delta, long_length, short_length)
|
|
111
|
+
smoothed_size = _twice(region(state, "size"), size, long_length, short_length)
|
|
112
|
+
if not isinstance(smoothed_change, float) or not isinstance(smoothed_size, float):
|
|
113
|
+
return ABSENT
|
|
114
|
+
if smoothed_size == 0:
|
|
115
|
+
return ABSENT
|
|
116
|
+
return result((100 * smoothed_change) / smoothed_size)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _twice(
|
|
120
|
+
state: Region, value: Value, long_length: Optional[int], short_length: Optional[int]
|
|
121
|
+
) -> Value:
|
|
122
|
+
"""One quantity smoothed with the long length and then with the short one."""
|
|
123
|
+
once = averages.exponential(region(state, "long"), value, long_length)
|
|
124
|
+
return averages.exponential(region(state, "short"), once, short_length)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def detrended(state: Region, value: Value, length: Optional[int]) -> Value:
|
|
128
|
+
"""``dpo(src, len)``: the simple mean removed as it stood, displaced back.
|
|
129
|
+
|
|
130
|
+
The displacement is ``floor(len / 2) + 1`` bars, which is what the declared
|
|
131
|
+
warmup of bar ``len + floor(len / 2)`` requires.
|
|
132
|
+
"""
|
|
133
|
+
price = number(value)
|
|
134
|
+
average = averages.simple(region(state, "mean"), price, length)
|
|
135
|
+
shift = None if length is None else length // 2 + 1
|
|
136
|
+
keep = None if shift is None else shift + 1
|
|
137
|
+
held = contributed(state, "mean-values", average, keep)
|
|
138
|
+
older = back(held, shift)
|
|
139
|
+
if price is None or not isinstance(older, float):
|
|
140
|
+
return ABSENT
|
|
141
|
+
return result(price - older)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def blended(
|
|
145
|
+
state: Region,
|
|
146
|
+
ctx,
|
|
147
|
+
first_length: Optional[int],
|
|
148
|
+
second_length: Optional[int],
|
|
149
|
+
third_length: Optional[int],
|
|
150
|
+
) -> Value:
|
|
151
|
+
"""``ultimateOsc(len1, len2, len3)``: pressure over range, blended over three windows.
|
|
152
|
+
|
|
153
|
+
Both per-bar terms are formed against the previous close, which is the close
|
|
154
|
+
of the bar before this one as this call site saw it and not a fact the host
|
|
155
|
+
states: this reading counts changes, so the oldest bar has no term at all and
|
|
156
|
+
the exception section 6 grants the plain true range has nothing to do with
|
|
157
|
+
it. The three weighted terms are added left to right, shortest window first,
|
|
158
|
+
and the division by 7 is applied once at the end.
|
|
159
|
+
"""
|
|
160
|
+
high = number(ctx.bar("high"))
|
|
161
|
+
low = number(ctx.bar("low"))
|
|
162
|
+
close = number(ctx.bar("close"))
|
|
163
|
+
before = back(contributed(state, "close", number(ctx.bar("close")), 2), 1)
|
|
164
|
+
pressure = ABSENT
|
|
165
|
+
span = ABSENT
|
|
166
|
+
if high is not None and low is not None and close is not None:
|
|
167
|
+
if isinstance(before, float):
|
|
168
|
+
floor_of = low if low < before else before
|
|
169
|
+
ceiling_of = high if high > before else before
|
|
170
|
+
pressure = close - floor_of
|
|
171
|
+
span = ceiling_of - floor_of
|
|
172
|
+
deepest = _deepest(first_length, second_length, third_length)
|
|
173
|
+
pressures = contributed(state, "pressure", pressure, deepest)
|
|
174
|
+
spans = contributed(state, "range", span, deepest)
|
|
175
|
+
fast = _ratio(pressures, spans, first_length)
|
|
176
|
+
middle = _ratio(pressures, spans, second_length)
|
|
177
|
+
slow = _ratio(pressures, spans, third_length)
|
|
178
|
+
if not isinstance(fast, float) or not isinstance(middle, float):
|
|
179
|
+
return ABSENT
|
|
180
|
+
if not isinstance(slow, float):
|
|
181
|
+
return ABSENT
|
|
182
|
+
blend = 4 * fast
|
|
183
|
+
blend = blend + 2 * middle
|
|
184
|
+
blend = blend + slow
|
|
185
|
+
return result((100 * blend) / 7)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _deepest(*lengths) -> Optional[int]:
|
|
189
|
+
"""The widest of the three windows, which is what both buffers keep."""
|
|
190
|
+
known = [one for one in lengths if one is not None]
|
|
191
|
+
return max(known) if known else None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _ratio(pressures, spans, length: Optional[int]) -> Value:
|
|
195
|
+
"""One window's pressure over its range, each the fresh sum of section 20.2.1."""
|
|
196
|
+
top = window(pressures, length)
|
|
197
|
+
bottom = window(spans, length)
|
|
198
|
+
if top is None or bottom is None:
|
|
199
|
+
return ABSENT
|
|
200
|
+
divisor = total(bottom)
|
|
201
|
+
if divisor == 0:
|
|
202
|
+
return ABSENT
|
|
203
|
+
return total(top) / divisor
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""How a number becomes text, and how text becomes a number.
|
|
2
|
+
|
|
3
|
+
`language.md` section 5.5 is one rule for every place a number is written: the
|
|
4
|
+
``text`` call, a table cell, an alert's message, a constant in a compiled
|
|
5
|
+
program, a case file and an expected column. Two engines compare numbers as bits
|
|
6
|
+
and text as text, so a number that two rules could spell two ways is a
|
|
7
|
+
disagreement with no number wrong anywhere.
|
|
8
|
+
|
|
9
|
+
**The digits** are the shortest string of decimal digits that reads back as the
|
|
10
|
+
same binary64 value. That is the conversion the host already performs natively
|
|
11
|
+
and two hosts have been measured to agree on it, so it is taken rather than
|
|
12
|
+
rebuilt. **The layout is where hosts part company**: where the point goes, when
|
|
13
|
+
an exponent starts, and what is never written. The host's own writing turns to an
|
|
14
|
+
exponent at a different magnitude, spells the exponent with a sign and a leading
|
|
15
|
+
zero, and writes a point and a zero after a whole number, and all four of those
|
|
16
|
+
are wrong here.
|
|
17
|
+
|
|
18
|
+
**That layout is written out once, in ``canonical.py``, and this file calls it.**
|
|
19
|
+
It was written out twice for one stage, here and there, with the two thresholds
|
|
20
|
+
typed into each; both passed the vectors, so the copy cost nothing until the day
|
|
21
|
+
one of them moved. What is this file's is what the layout is used for: the value
|
|
22
|
+
a script asks for as text, the fixed decimal conversion built on the same digits,
|
|
23
|
+
and the reading back of text as a number.
|
|
24
|
+
|
|
25
|
+
`spec/vectors/number-text.json` holds the boundary cases as bit patterns and as
|
|
26
|
+
text, in both directions, so an engine in any language can be held to the rule
|
|
27
|
+
without reading either engine's source. ``tests/test_number_text.py`` is this
|
|
28
|
+
engine held to it.
|
|
29
|
+
|
|
30
|
+
**``text(x, decimals)`` is the one call whose layout is always positional**, at
|
|
31
|
+
every magnitude, and its digits are this rule's digits for the rounded and scaled
|
|
32
|
+
whole number, zero filled. A conversion that changed shape above a threshold
|
|
33
|
+
would be a label that read correctly until the day a cumulative volume crossed
|
|
34
|
+
it.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import math
|
|
38
|
+
|
|
39
|
+
from ..canonical import canonical_number, spread
|
|
40
|
+
from .code_points import trimmed
|
|
41
|
+
from .colour import Colour, hex_byte
|
|
42
|
+
from .rounding import round_half_away, scale_of
|
|
43
|
+
from .values import ABSENT, Value, is_number, number, result, whole
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _carry(digits: str) -> str:
|
|
47
|
+
"""One added to a string of digits, which grows it when every digit is nine."""
|
|
48
|
+
out = list(digits)
|
|
49
|
+
for at in range(len(out) - 1, -1, -1):
|
|
50
|
+
if out[at] != "9":
|
|
51
|
+
out[at] = chr(ord(out[at]) + 1)
|
|
52
|
+
return "".join(out)
|
|
53
|
+
out[at] = "0"
|
|
54
|
+
return "1" + "".join(out)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _written(digits: str, point: int) -> str:
|
|
58
|
+
"""A spread written out, rounded half up where the point falls inside it.
|
|
59
|
+
|
|
60
|
+
Half up on a magnitude is half away from zero, because the sign is carried
|
|
61
|
+
separately by every caller.
|
|
62
|
+
"""
|
|
63
|
+
if point >= len(digits):
|
|
64
|
+
return digits + "0" * (point - len(digits))
|
|
65
|
+
if point < 0:
|
|
66
|
+
return "0"
|
|
67
|
+
kept = digits[:point]
|
|
68
|
+
if digits[point] < "5":
|
|
69
|
+
return kept if kept != "" else "0"
|
|
70
|
+
return _carry(kept)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fixed(x: float, decimals: int) -> str:
|
|
74
|
+
"""``text(x, decimals)``: a sign, at least one digit, and exactly ``decimals``.
|
|
75
|
+
|
|
76
|
+
The rounding is done on the number before it is written rather than left to a
|
|
77
|
+
formatting routine, because a routine's tie rule is the host's and the two
|
|
78
|
+
disagree at exactly the values a price lands on: this is halves away from
|
|
79
|
+
zero over the scaled value, and the host's is halves to even over the exact
|
|
80
|
+
binary expansion.
|
|
81
|
+
|
|
82
|
+
**The scale is ``round(x, decimals)``'s**, so the display conversion and the
|
|
83
|
+
rounding call multiply by one value (`stdlib.md` 20.7) and cannot part by an
|
|
84
|
+
ulp at the one count where a floating point power does.
|
|
85
|
+
|
|
86
|
+
Where the scaling leaves binary64 altogether there is nothing left to round:
|
|
87
|
+
a binary64 that large is a whole number already and a decimal place that far
|
|
88
|
+
from its leading digit is past every digit it carries, so the digits are the
|
|
89
|
+
unscaled value's and the point is moved instead.
|
|
90
|
+
"""
|
|
91
|
+
scaled = round_half_away(x * scale_of(decimals))
|
|
92
|
+
usable = math.isfinite(scaled)
|
|
93
|
+
negative = scaled < 0 if usable else x < 0
|
|
94
|
+
sign = "-" if negative else ""
|
|
95
|
+
if usable:
|
|
96
|
+
digits, point = spread(canonical_number(abs(scaled)))
|
|
97
|
+
else:
|
|
98
|
+
digits, point = spread(canonical_number(abs(x)), decimals)
|
|
99
|
+
written = _written(digits, point).rjust(decimals + 1, "0")
|
|
100
|
+
if decimals == 0:
|
|
101
|
+
return sign + written
|
|
102
|
+
cut = len(written) - decimals
|
|
103
|
+
return sign + written[:cut] + "." + written[cut:]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def fixed_length(x: float, decimals: int) -> int:
|
|
107
|
+
"""How long ``text(x, decimals)`` will be, before a character is built.
|
|
108
|
+
|
|
109
|
+
A floor rather than the exact count: a carry off the front adds one digit and
|
|
110
|
+
a negative adds the sign, and both are caught by the ceiling the built string
|
|
111
|
+
is checked against. What this is for is the decimal count a script computed,
|
|
112
|
+
which can ask for a string no engine can hold, and building it to find that
|
|
113
|
+
out is how an engine runs out of memory instead of reporting that it would
|
|
114
|
+
have. The ceiling itself is the interpreter's: nothing in this package raises.
|
|
115
|
+
"""
|
|
116
|
+
_, point = spread(canonical_number(abs(x)))
|
|
117
|
+
return max(1, point) + decimals + (1 if decimals > 0 else 0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def text_of(x: Value, decimals: Value) -> Value:
|
|
121
|
+
"""``text(x, decimals)`` as the library entry calls it."""
|
|
122
|
+
value = number(x)
|
|
123
|
+
count = whole(decimals)
|
|
124
|
+
if value is None or count is None:
|
|
125
|
+
return ABSENT
|
|
126
|
+
return fixed(value, count)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def text_length(x: Value, decimals: Value) -> int | None:
|
|
130
|
+
"""``text(x, decimals)``'s length, as the interpreter asks for it.
|
|
131
|
+
|
|
132
|
+
Absence for arguments the call itself answers absence for, because a call
|
|
133
|
+
that builds no string cannot pass a ceiling on strings.
|
|
134
|
+
"""
|
|
135
|
+
value = number(x)
|
|
136
|
+
count = whole(decimals)
|
|
137
|
+
if value is None or count is None:
|
|
138
|
+
return None
|
|
139
|
+
return fixed_length(value, count)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
_DIGITS = "0123456789"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def to_number(text: Value) -> Value:
|
|
146
|
+
"""``toNumber(s)``: a string to a number, absent for what does not parse.
|
|
147
|
+
|
|
148
|
+
The grammar is walked here rather than handed to the host's own reader, which
|
|
149
|
+
accepts an infinity, a not-a-number and digit group separators, and which of
|
|
150
|
+
those a host accepts differs between hosts and moves between their releases.
|
|
151
|
+
Absence rather than zero for text that is not a number, so a
|
|
152
|
+
script can tell text that is not a number apart from the number zero. Only
|
|
153
|
+
these ten digits count: the host's own digit test accepts the decimal digits
|
|
154
|
+
of every script Unicode has, which would read a number no writer of the
|
|
155
|
+
script could have typed and no second engine would agree with.
|
|
156
|
+
|
|
157
|
+
The whitespace ignored at either end is ``str.trim``'s set and no other.
|
|
158
|
+
"""
|
|
159
|
+
if not isinstance(text, str):
|
|
160
|
+
return ABSENT
|
|
161
|
+
bare = trimmed(text)
|
|
162
|
+
at = 0
|
|
163
|
+
end = len(bare)
|
|
164
|
+
if at < end and bare[at] in "+-":
|
|
165
|
+
at += 1
|
|
166
|
+
before = 0
|
|
167
|
+
while at < end and bare[at] in _DIGITS:
|
|
168
|
+
at += 1
|
|
169
|
+
before += 1
|
|
170
|
+
after = 0
|
|
171
|
+
if at < end and bare[at] == ".":
|
|
172
|
+
at += 1
|
|
173
|
+
while at < end and bare[at] in _DIGITS:
|
|
174
|
+
at += 1
|
|
175
|
+
after += 1
|
|
176
|
+
if before == 0 and after == 0:
|
|
177
|
+
return ABSENT
|
|
178
|
+
if at < end and bare[at] in "eE":
|
|
179
|
+
at += 1
|
|
180
|
+
if at < end and bare[at] in "+-":
|
|
181
|
+
at += 1
|
|
182
|
+
power = 0
|
|
183
|
+
while at < end and bare[at] in _DIGITS:
|
|
184
|
+
at += 1
|
|
185
|
+
power += 1
|
|
186
|
+
if power == 0:
|
|
187
|
+
return ABSENT
|
|
188
|
+
if at != end:
|
|
189
|
+
return ABSENT
|
|
190
|
+
return result(float(bare))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def spell(value: Value, of_reference=None) -> str:
|
|
194
|
+
"""A value as ``text(x)`` spells it.
|
|
195
|
+
|
|
196
|
+
Absence is the word ``none``, a bool is its own word, a string is itself, and
|
|
197
|
+
a number is the rule at the top of this file. A colour is the ``#rrggbbaa``
|
|
198
|
+
spelling the conformance suite compares, where the alpha byte is
|
|
199
|
+
``round(alpha * 255)`` with the language's own rounding: that conversion is
|
|
200
|
+
one way and is not a round trip, and nothing in the language observes the
|
|
201
|
+
difference, because a script reads an alpha with ``alpha()`` from the machine
|
|
202
|
+
value and never from the wire form.
|
|
203
|
+
|
|
204
|
+
A reference is the interpreter's: the heap and the object in it are not this
|
|
205
|
+
package's, so ``of_reference`` is asked for the kind of one and the word
|
|
206
|
+
``none`` stands where no interpreter answered.
|
|
207
|
+
"""
|
|
208
|
+
if value is None:
|
|
209
|
+
return "none"
|
|
210
|
+
if value is True:
|
|
211
|
+
return "true"
|
|
212
|
+
if value is False:
|
|
213
|
+
return "false"
|
|
214
|
+
if is_number(value):
|
|
215
|
+
return canonical_number(float(value))
|
|
216
|
+
if isinstance(value, str):
|
|
217
|
+
return value
|
|
218
|
+
if isinstance(value, Colour):
|
|
219
|
+
alpha = round_half_away(value.a * 255)
|
|
220
|
+
return "#" + hex_byte(value.r) + hex_byte(value.g) + hex_byte(value.b) + hex_byte(alpha)
|
|
221
|
+
if of_reference is None:
|
|
222
|
+
return "none"
|
|
223
|
+
return of_reference(value)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The two derived prices the stateful half forms for itself, from facts it read.
|
|
2
|
+
|
|
3
|
+
A study that calls ``hlc3`` is handed the bar register `compiled-program.md`
|
|
4
|
+
section 2.10 defines, and never reaches this file. What reaches this file is the
|
|
5
|
+
library's own readings: ``cci`` and ``mfi`` are written against the typical
|
|
6
|
+
price, and ``eom``, ``awesomeOsc`` and the trailing band against the midpoint,
|
|
7
|
+
and none of those takes a source argument, so the value has to be formed from the
|
|
8
|
+
three bar facts the call read.
|
|
9
|
+
|
|
10
|
+
**The order of operations is section 2.10's and is part of the contract**, which
|
|
11
|
+
is why these are two named functions rather than an expression written out at
|
|
12
|
+
five call sites: a different association gives a different last bit, and a study
|
|
13
|
+
that matched a reference implementation on one engine and not on another is
|
|
14
|
+
exactly the failure this project exists to prevent.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .values import ABSENT, Value, number, result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def midpoint(ctx) -> Value:
|
|
21
|
+
"""``(high + low) / 2``, the bar's midpoint."""
|
|
22
|
+
high = number(ctx.bar("high"))
|
|
23
|
+
low = number(ctx.bar("low"))
|
|
24
|
+
if high is None or low is None:
|
|
25
|
+
return ABSENT
|
|
26
|
+
return result((high + low) / 2)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def typical(ctx) -> Value:
|
|
30
|
+
"""``(high + low + close) / 3``: high to low, close to that, then the division."""
|
|
31
|
+
high = number(ctx.bar("high"))
|
|
32
|
+
low = number(ctx.bar("low"))
|
|
33
|
+
close = number(ctx.bar("close"))
|
|
34
|
+
if high is None or low is None or close is None:
|
|
35
|
+
return ABSENT
|
|
36
|
+
return result((high + low + close) / 3)
|