openscript 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,180 @@
1
+ """The position and strength readings of `stdlib.md` section 20.4.
2
+
3
+ Each of these asks the same question, where does this bar sit against its recent
4
+ past, and each answers it on a different scale. What they share is an
5
+ arrangement, and the arrangement is the thing two engines part company over:
6
+
7
+ - **The strength line is ``100 - 100 / (1 + up / down)``**, in that order: the
8
+ ratio, then one added to it, then 100 divided by that, then subtracted from
9
+ 100. The other reading in circulation, ``100 * up / (up + down)``, is
10
+ mathematically equal and lands one unit in the last place away on ordinary
11
+ data. Over the eighty bar fixture the release gate compares bit for bit it
12
+ differs on about half the values at every length.
13
+ - **The position readings multiply by 100 first**, which is the opposite of the
14
+ directional index of section 20.3 and is deliberate: each is the association
15
+ its own reading has always carried, and matching one to the other would move
16
+ every value of one of them.
17
+ - **The deviation reading divides by the mean absolute deviation**, not by the
18
+ standard deviation, and the constant is applied to the deviation rather than to
19
+ the numerator. The 0.015 is calibrated against the mean absolute deviation, and
20
+ substituting a standard deviation changes every reading while still producing a
21
+ plausible line.
22
+
23
+ Naming a value first is not a second arrangement: writing the ratio into a name
24
+ and reading it back is bit identical to writing it out, because every operation
25
+ already rounds its result to binary64 (section 20.1's first rule).
26
+ """
27
+
28
+ from typing import Optional
29
+
30
+ from . import averages, prices
31
+ from .extremes import window_high, window_low
32
+ from .series import Region, back, contributed, mean, region, total, window
33
+ from .values import ABSENT, Value, number, result
34
+
35
+
36
+ def line_of(up: Value, down: Value) -> Value:
37
+ """The strength line both this section's reading and the money flow one take.
38
+
39
+ **The result is 100 when the down side is zero**, which also covers a window
40
+ that never moved and in which both sides are zero. The ratio has no value
41
+ there, and every reference reading is 100.
42
+ """
43
+ if not isinstance(up, float) or not isinstance(down, float):
44
+ return ABSENT
45
+ if down == 0:
46
+ return 100.0
47
+ return result(100 - 100 / (1 + up / down))
48
+
49
+
50
+ def rises_and_falls(values) -> tuple:
51
+ """This bar's change split into the two sides, each an exact zero where flat.
52
+
53
+ The falling side is formed by negating the change and taking the larger of
54
+ that and zero, so a rising bar contributes an exact zero to it rather than a
55
+ negative zero that a later division could carry a sign out of.
56
+ """
57
+ now = values[len(values) - 1]
58
+ before = back(values, 1)
59
+ if not isinstance(now, float) or not isinstance(before, float):
60
+ return ABSENT, ABSENT
61
+ delta = now - before
62
+ up = delta if delta > 0 else 0.0
63
+ down = -delta if -delta > 0 else 0.0
64
+ return up, down
65
+
66
+
67
+ def strength(state: Region, value: Value, length: Optional[int]) -> Value:
68
+ """``rsi(src, len)``: the one bar change split, each side smoothed with ``rma``."""
69
+ values = contributed(state, "src", number(value), 2)
70
+ up, down = rises_and_falls(values)
71
+ average_up = averages.smoothed(region(state, "up"), up, length)
72
+ average_down = averages.smoothed(region(state, "down"), down, length)
73
+ return line_of(average_up, average_down)
74
+
75
+
76
+ def change_ratio(state: Region, value: Value, length: Optional[int]) -> Value:
77
+ """``cmo(src, len)``: the two sides summed over the window outright, no smoothing."""
78
+ values = contributed(state, "src", number(value), 2)
79
+ up, down = rises_and_falls(values)
80
+ rises = window(contributed(state, "up", up, length), length)
81
+ falls = window(contributed(state, "down", down, length), length)
82
+ if rises is None or falls is None:
83
+ return ABSENT
84
+ rise = total(rises)
85
+ fall = total(falls)
86
+ divisor = rise + fall
87
+ if divisor == 0:
88
+ return ABSENT
89
+ return result((100 * (rise - fall)) / divisor)
90
+
91
+
92
+ def position(state: Region, ctx, length: Optional[int], smooth_k, smooth_d) -> list:
93
+ """``stoch(len, smoothK, smoothD)``: the close inside the window's outright range.
94
+
95
+ The range is the bars' own highs and lows and not the close's extremes, and
96
+ the span is formed once: the result is absent where it is zero, which is a
97
+ window that did not move at all.
98
+ """
99
+ highs = contributed(state, "high", number(ctx.bar("high")), length)
100
+ lows = contributed(state, "low", number(ctx.bar("low")), length)
101
+ raw = _placed(
102
+ number(ctx.bar("close")), window_high(highs, length), window_low(lows, length)
103
+ )
104
+ return _smoothed_pair(state, raw, smooth_k, smooth_d)
105
+
106
+
107
+ def strength_position(
108
+ state: Region,
109
+ value: Value,
110
+ strength_length: Optional[int],
111
+ window_length: Optional[int],
112
+ smooth_k,
113
+ smooth_d,
114
+ ) -> list:
115
+ """``stochRsi(...)``: the same three lines with the strength reading in place of the close.
116
+
117
+ The window high and the window low are taken from the strength reading itself
118
+ rather than from the bars, which is the whole difference between this and the
119
+ reading above.
120
+ """
121
+ reading = strength(region(state, "rsi"), value, strength_length)
122
+ values = contributed(state, "src", reading, window_length)
123
+ raw = _placed(
124
+ reading if isinstance(reading, float) else None,
125
+ window_high(values, window_length),
126
+ window_low(values, window_length),
127
+ )
128
+ return _smoothed_pair(state, raw, smooth_k, smooth_d)
129
+
130
+
131
+ def range_position(state: Region, ctx, length: Optional[int]) -> Value:
132
+ """``williamsR(len)``: the same position on the other scale.
133
+
134
+ It is formed from the distance below the window high rather than as the
135
+ stochastic reading less one hundred, which is a different last bit.
136
+ """
137
+ highs = contributed(state, "high", number(ctx.bar("high")), length)
138
+ lows = contributed(state, "low", number(ctx.bar("low")), length)
139
+ close = number(ctx.bar("close"))
140
+ top = window_high(highs, length)
141
+ bottom = window_low(lows, length)
142
+ if close is None or not isinstance(top, float) or not isinstance(bottom, float):
143
+ return ABSENT
144
+ span = top - bottom
145
+ if span == 0:
146
+ return ABSENT
147
+ return result((-100 * (top - close)) / span)
148
+
149
+
150
+ def deviation_reading(state: Region, ctx, length: Optional[int]) -> Value:
151
+ """``cci(len)``: how far the typical price sits from its mean, in deviation units."""
152
+ price = prices.typical(ctx)
153
+ held = window(contributed(state, "src", price, length), length)
154
+ if held is None or length is None or not isinstance(price, float):
155
+ return ABSENT
156
+ middle = mean(held, length)
157
+ spread = 0.0
158
+ for at in range(length - 1, -1, -1):
159
+ spread = spread + abs(held[at] - middle)
160
+ spread = spread / length
161
+ if spread == 0:
162
+ return ABSENT
163
+ return result((price - middle) / (0.015 * spread))
164
+
165
+
166
+ def _placed(value: Optional[float], top: Value, bottom: Value) -> Value:
167
+ """The position of a value inside a span, scaled by 100 before the division."""
168
+ if value is None or not isinstance(top, float) or not isinstance(bottom, float):
169
+ return ABSENT
170
+ span = top - bottom
171
+ if span == 0:
172
+ return ABSENT
173
+ return result((100 * (value - bottom)) / span)
174
+
175
+
176
+ def _smoothed_pair(state: Region, raw: Value, smooth_k, smooth_d) -> list:
177
+ """The two smoothed lines both position readings report, in that order."""
178
+ fast = averages.simple(region(state, "k"), raw, smooth_k)
179
+ slow = averages.simple(region(state, "d"), fast, smooth_d)
180
+ return [fast, slow]
@@ -0,0 +1,228 @@
1
+ """Strings, `stdlib.md` section 10.
2
+
3
+ None of these has a warmup: a string operation on a present string produces a
4
+ value on bar 0. Every one is absent when an argument it needs is absent, and none
5
+ of them raises: the string and array ceilings of `compiled-program.md` section
6
+ 2.4 are the interpreter's to enforce, and the two measuring functions here exist
7
+ so that it can enforce them before a string is built rather than after.
8
+
9
+ **Every index and every length counts code points**, through ``code_points``. On
10
+ this host that is what the string type already is, which makes the rule free and
11
+ does not make it optional.
12
+
13
+ **Case conversion is invariant rather than locale aware.** A script whose output
14
+ changed because the machine running it was configured for another locale would
15
+ break the determinism rule of `language.md` section 7.6, and the cases where a
16
+ locale aware conversion differs are not cases a trading script needs. What the
17
+ page does not say is which case conversion, and that is a gap worth naming:
18
+ "upper case, invariant" is the host's default mapping here and the host's default
19
+ mapping there, and the two agree on every letter a symbol or a label is likely to
20
+ hold and are not guaranteed to agree on every code point. An engine is held to
21
+ the conformance suite's strings, not to this sentence.
22
+ """
23
+
24
+ from .code_points import trimmed
25
+ from .number_text import spell
26
+ from .values import ABSENT, Value, whole
27
+
28
+
29
+ def _string(value: Value) -> str | None:
30
+ """A string argument, or absence for anything that is not one."""
31
+ return value if isinstance(value, str) else None
32
+
33
+
34
+ def length(s: Value) -> Value:
35
+ """``str.length(s)``: count of Unicode code points."""
36
+ text = _string(s)
37
+ return ABSENT if text is None else float(len(text))
38
+
39
+
40
+ def upper(s: Value) -> Value:
41
+ """``str.upper(s)``: upper case, invariant, not locale dependent."""
42
+ text = _string(s)
43
+ return ABSENT if text is None else text.upper()
44
+
45
+
46
+ def lower(s: Value) -> Value:
47
+ """``str.lower(s)``: lower case, on the same terms."""
48
+ text = _string(s)
49
+ return ABSENT if text is None else text.lower()
50
+
51
+
52
+ def trim(s: Value) -> Value:
53
+ """``str.trim(s)``: the whitespace of section 10's table, off both ends."""
54
+ text = _string(s)
55
+ return ABSENT if text is None else trimmed(text)
56
+
57
+
58
+ def contains(s: Value, part: Value) -> Value:
59
+ """``str.contains(s, part)``: whether one string appears in another."""
60
+ text = _string(s)
61
+ piece = _string(part)
62
+ if text is None or piece is None:
63
+ return ABSENT
64
+ return piece in text
65
+
66
+
67
+ def starts_with(s: Value, part: Value) -> Value:
68
+ """``str.startsWith(s, part)``: prefix test."""
69
+ text = _string(s)
70
+ piece = _string(part)
71
+ if text is None or piece is None:
72
+ return ABSENT
73
+ return text.startswith(piece)
74
+
75
+
76
+ def ends_with(s: Value, part: Value) -> Value:
77
+ """``str.endsWith(s, part)``: suffix test."""
78
+ text = _string(s)
79
+ piece = _string(part)
80
+ if text is None or piece is None:
81
+ return ABSENT
82
+ return text.endswith(piece)
83
+
84
+
85
+ def index_of(s: Value, part: Value) -> Value:
86
+ """``str.indexOf(s, part)``: first position of ``part``, or -1.
87
+
88
+ The position is in code points, which is what this host's own search already
89
+ reports; an engine whose strings are sixteen bit units converts, or it hands
90
+ a script an index that its own substring call then reads as a different one.
91
+ """
92
+ text = _string(s)
93
+ piece = _string(part)
94
+ if text is None or piece is None:
95
+ return ABSENT
96
+ return float(text.find(piece))
97
+
98
+
99
+ def substring(s: Value, start: Value, stop: Value) -> Value:
100
+ """``str.substring(s, from, to = none)``: from inclusive, to exclusive.
101
+
102
+ An absent ``to`` runs to the end, which is the default the page states, and
103
+ is why absence is not propagated from that argument alone.
104
+ """
105
+ text = _string(s)
106
+ first = whole(start)
107
+ if text is None or first is None:
108
+ return ABSENT
109
+ if stop is None:
110
+ return text[first:]
111
+ last = whole(stop)
112
+ if last is None:
113
+ return ABSENT
114
+ return text[first:last]
115
+
116
+
117
+ def replace(s: Value, find: Value, into: Value) -> Value:
118
+ """``str.replace(s, find, with)``: the first occurrence only."""
119
+ text = _string(s)
120
+ needle = _string(find)
121
+ other = _string(into)
122
+ if text is None or needle is None or other is None:
123
+ return ABSENT
124
+ return text.replace(needle, other, 1)
125
+
126
+
127
+ def replace_all(s: Value, find: Value, into: Value) -> Value:
128
+ """``str.replaceAll(s, find, with)``: every occurrence."""
129
+ text = _string(s)
130
+ needle = _string(find)
131
+ other = _string(into)
132
+ if text is None or needle is None or other is None:
133
+ return ABSENT
134
+ return text.replace(needle, other)
135
+
136
+
137
+ def split(s: Value, separator: Value) -> list[str] | None:
138
+ """``str.split(s, separator)``: the parts, for the interpreter to make an array.
139
+
140
+ The array is the interpreter's to allocate, because the heap is, so this
141
+ returns the parts and absence stands for a call that produces nothing.
142
+
143
+ **An empty separator splits into code points**, which is
144
+ `compiled-program.md` section 3.1's rule and not every host's: a host whose
145
+ strings are sixteen bit units splits a symbol outside the basic plane into
146
+ two halves of a surrogate pair, neither of which is a string the language has
147
+ a value for.
148
+ """
149
+ text = _string(s)
150
+ mark = _string(separator)
151
+ if text is None or mark is None:
152
+ return None
153
+ if mark == "":
154
+ return list(text)
155
+ return text.split(mark)
156
+
157
+
158
+ def join(parts: list[Value], separator: Value) -> Value:
159
+ """``str.join(parts, separator)``: an array back into one string.
160
+
161
+ An element that is not a string is spelled as ``text`` spells it, which is
162
+ what makes joining an array of numbers produce the same characters as writing
163
+ each of them.
164
+ """
165
+ mark = _string(separator)
166
+ if parts is None or mark is None:
167
+ return ABSENT
168
+ return mark.join(spell(item) for item in parts)
169
+
170
+
171
+ def _pad(text: str, width: int, fill: str, left: bool) -> str:
172
+ if len(text) >= width:
173
+ return text
174
+ needed = width - len(text)
175
+ padding = "".join(fill[at % len(fill)] for at in range(needed))
176
+ return padding + text if left else text + padding
177
+
178
+
179
+ def pad_left(s: Value, width: Value, fill: Value) -> Value:
180
+ """``str.padLeft(s, width, fill = " ")``: pad to a width, for a column.
181
+
182
+ The padding is the fill repeated and cut to length, code point by code point,
183
+ rather than the fill repeated a whole number of times: a two character fill
184
+ padding an odd gap has to stop halfway, and where it stops is what a table
185
+ column lines up on.
186
+ """
187
+ return _padded(s, width, fill, True)
188
+
189
+
190
+ def pad_right(s: Value, width: Value, fill: Value) -> Value:
191
+ """``str.padRight(s, width, fill = " ")``: the same from the other side."""
192
+ return _padded(s, width, fill, False)
193
+
194
+
195
+ def _padded(s: Value, width: Value, fill: Value, left: bool) -> Value:
196
+ text = _string(s)
197
+ if text is None:
198
+ return ABSENT
199
+ size = whole(width)
200
+ filler = _string(fill)
201
+ if filler is None:
202
+ filler = " "
203
+ if size is None or filler == "":
204
+ return text
205
+ return _pad(text, size, filler, left)
206
+
207
+
208
+ def repeat(s: Value, count: Value) -> Value:
209
+ """``str.repeat(s, n)``: ``n`` copies, for a bar drawn out of characters."""
210
+ text = _string(s)
211
+ times = whole(count)
212
+ if text is None or times is None:
213
+ return ABSENT
214
+ return text * times
215
+
216
+
217
+ def repeat_length(s: Value, count: Value) -> int | None:
218
+ """How long ``str.repeat`` will be, before a character of it is built.
219
+
220
+ A length times a count is where the string ceiling is actually reached, and
221
+ building the string to find that out is how an engine runs out of memory
222
+ instead of reporting that it would have.
223
+ """
224
+ text = _string(s)
225
+ times = whole(count)
226
+ if text is None or times is None:
227
+ return None
228
+ return len(text) * times
@@ -0,0 +1,260 @@
1
+ """The trend studies of `stdlib.md` section 20.3 that carry a decision from bar to bar.
2
+
3
+ These five are the ones where a seeding rule is not a detail. A band that starts
4
+ on the wrong side, a stop clamped into the previous two bars' range, or a
5
+ directional reading smoothed against the wrong range is a **different study**
6
+ rather than a different last bit: it flips on different bars and stays apart
7
+ until a later flip happens to put the two back together. So each entry below
8
+ says what it is not, and the reason is in section 20.3 rather than here.
9
+
10
+ Both bands and both stops keep their state as plain numbers and a side, because
11
+ section 2.11 requires a region an engine can copy without knowing whose it is.
12
+ """
13
+
14
+ from typing import Optional
15
+
16
+ from . import averages, ranges
17
+ from .extremes import high_at, low_at
18
+ from .ranges import gap_range
19
+ from .series import Region, contributed, raw_window, region, window
20
+ from .values import ABSENT, Value, number, result
21
+
22
+ #: The two directions, as section 4 reports them: the sign of the side the band
23
+ #: is protecting against, so that ``direction != direction[1]`` reads as the flip.
24
+ LONG = -1.0
25
+ SHORT = 1.0
26
+
27
+
28
+ def trailing_band(state: Region, ctx, factor: Value, width_length: Optional[int]) -> list:
29
+ """``supertrend(factor, atrLen)``: a trailing band from the average true range.
30
+
31
+ On the first bar the width is available on, both bands are the raw bands,
32
+ **the line starts on the upper band**, and the bar reports absence: the carry
33
+ forward and the flip test both read the previous bar's close and the previous
34
+ bar's bands, and on that bar there is neither. The band it leaves the state
35
+ on is the one the bar after it inherits.
36
+
37
+ Starting on the lower band is a different study. Measured over four hundred
38
+ bars at three parameter sets, a line seeded there differed on between fifteen
39
+ and forty of the bars it reported, by as much as sixteen price units, with the
40
+ direction inverted on exactly those bars.
41
+ """
42
+ width = ranges.average_range(region(state, "width"), ctx, width_length)
43
+ high = number(ctx.bar("high"))
44
+ low = number(ctx.bar("low"))
45
+ close = number(ctx.bar("close"))
46
+ before = number(ctx.bar("previousClose"))
47
+ multiple = number(factor)
48
+ held = region(state, "band")
49
+ if not isinstance(width, float) or high is None or low is None or close is None:
50
+ return [ABSENT, ABSENT]
51
+ if multiple is None:
52
+ return [ABSENT, ABSENT]
53
+ midpoint = (high + low) / 2
54
+ raw_upper = midpoint + multiple * width
55
+ raw_lower = midpoint - multiple * width
56
+ upper = held.get("upper")
57
+ lower = held.get("lower")
58
+ if upper is None or lower is None or before is None:
59
+ upper, lower = raw_upper, raw_lower
60
+ held["upper"] = upper
61
+ held["lower"] = lower
62
+ held["upside"] = not close > upper
63
+ return [ABSENT, ABSENT]
64
+ if raw_upper < upper or before > upper:
65
+ upper = raw_upper
66
+ if raw_lower > lower or before < lower:
67
+ lower = raw_lower
68
+ upside = held.get("upside", True)
69
+ upside = close <= upper if upside else not close >= lower
70
+ held["upper"] = upper
71
+ held["lower"] = lower
72
+ held["upside"] = upside
73
+ return [result(upper if upside else lower), SHORT if upside else LONG]
74
+
75
+
76
+ def accelerating_stop(
77
+ state: Region, ctx, start: Value, step: Value, most: Value
78
+ ) -> list:
79
+ """``psar(start, step, max)``: a stop that accelerates toward price.
80
+
81
+ The stop moves first, then the flip test, then the extreme test, in that
82
+ order. **The second test is reached on a flip bar as well**, where it cannot
83
+ fire, because the extreme has just been set to this bar's own low or high and
84
+ both comparisons are strict, so an engine that writes them as one chain and
85
+ an engine that writes them as two agree on every bar.
86
+
87
+ **The stop is reported as the recurrence produced it, and there is no clamp.**
88
+ Holding it out of the previous two bars' range swallows flips this recurrence
89
+ fires, because the next bar's flip test reads the stop this bar left behind.
90
+ The two are different functions rather than two arrangements of one.
91
+ """
92
+ high = number(ctx.bar("high"))
93
+ low = number(ctx.bar("low"))
94
+ close = number(ctx.bar("close"))
95
+ first = number(start)
96
+ rise = number(step)
97
+ ceiling = number(most)
98
+ held = region(state, "stop")
99
+ highs = contributed(state, "high", high, 2)
100
+ lows = contributed(state, "low", low, 2)
101
+ closes = contributed(state, "close", close, 2)
102
+ if high is None or low is None or close is None:
103
+ return [ABSENT, ABSENT]
104
+ if first is None or rise is None or ceiling is None:
105
+ return [ABSENT, ABSENT]
106
+ stop = held.get("stop")
107
+ if stop is None:
108
+ return _seed_stop(held, highs, lows, closes, first)
109
+ long = held.get("long", True)
110
+ speed = held.get("speed", first)
111
+ extreme = held.get("extreme", high)
112
+ stop = stop + speed * (extreme - stop)
113
+ if long and stop > low:
114
+ long = False
115
+ stop = extreme
116
+ extreme = low
117
+ speed = first
118
+ elif not long and stop < high:
119
+ long = True
120
+ stop = extreme
121
+ extreme = high
122
+ speed = first
123
+ if long and high > extreme:
124
+ extreme = high
125
+ speed = min(speed + rise, ceiling)
126
+ elif not long and low < extreme:
127
+ extreme = low
128
+ speed = min(speed + rise, ceiling)
129
+ held["stop"] = stop
130
+ held["long"] = long
131
+ held["speed"] = speed
132
+ held["extreme"] = extreme
133
+ return [result(stop), LONG if long else SHORT]
134
+
135
+
136
+ def _seed_stop(held: Region, highs, lows, closes, first: float) -> list:
137
+ """The seed of section 20.3: the first pair of complete bars, and that bar reports it."""
138
+ if len(closes) < 2 or not isinstance(closes[len(closes) - 2], float):
139
+ return [ABSENT, ABSENT]
140
+ before = closes[len(closes) - 2]
141
+ now = closes[len(closes) - 1]
142
+ high = highs[len(highs) - 1]
143
+ low = lows[len(lows) - 1]
144
+ long = now > before
145
+ stop = lows[len(lows) - 2] if long else highs[len(highs) - 2]
146
+ if not isinstance(stop, float):
147
+ return [ABSENT, ABSENT]
148
+ held["stop"] = stop
149
+ held["long"] = long
150
+ held["speed"] = first
151
+ held["extreme"] = high if long else low
152
+ return [result(stop), LONG if long else SHORT]
153
+
154
+
155
+ def directional(state: Region, ctx, di_length: Optional[int], adx_length: Optional[int]) -> list:
156
+ """``adx(diLen, adxLen)``: directional movement against the gap aware true range.
157
+
158
+ All three smoothed quantities cover the same bars, which is what the gap
159
+ aware form is for: the plain reading's exception on the oldest bar would put
160
+ the range one bar ahead of the two movements and the strength reading would
161
+ be built from three windows that do not line up.
162
+
163
+ **The division comes before the multiplication by 100 on all three lines.**
164
+ The other association is the one the readings of section 20.4 use and it is a
165
+ different number here.
166
+ """
167
+ high = number(ctx.bar("high"))
168
+ low = number(ctx.bar("low"))
169
+ highs = contributed(state, "high", high, 2)
170
+ lows = contributed(state, "low", low, 2)
171
+ before_high = highs[len(highs) - 2] if len(highs) > 1 else ABSENT
172
+ before_low = lows[len(lows) - 2] if len(lows) > 1 else ABSENT
173
+ up_move = ABSENT
174
+ down_move = ABSENT
175
+ if high is not None and low is not None:
176
+ if isinstance(before_high, float) and isinstance(before_low, float):
177
+ up = high - before_high
178
+ down = before_low - low
179
+ up_move = up if up > down and up > 0 else 0.0
180
+ down_move = down if down > up and down > 0 else 0.0
181
+ smoothed_up = averages.smoothed(region(state, "up"), up_move, di_length)
182
+ smoothed_down = averages.smoothed(region(state, "down"), down_move, di_length)
183
+ smoothed_range = averages.smoothed(region(state, "range"), gap_range(ctx), di_length)
184
+ plus = _reading(smoothed_up, smoothed_range)
185
+ minus = _reading(smoothed_down, smoothed_range)
186
+ spread = ABSENT
187
+ if isinstance(plus, float) and isinstance(minus, float):
188
+ divisor = plus + minus
189
+ spread = 0.0 if divisor == 0 else (abs(plus - minus) / divisor) * 100
190
+ strength = averages.smoothed(region(state, "adx"), spread, adx_length)
191
+ return [strength, plus, minus]
192
+
193
+
194
+ def _reading(side: Value, width: Value) -> Value:
195
+ """One directional reading: the division, then the multiplication by 100."""
196
+ if not isinstance(side, float) or not isinstance(width, float) or width == 0:
197
+ return ABSENT
198
+ return result((side / width) * 100)
199
+
200
+
201
+ def ages(state: Region, ctx, length: Optional[int]) -> list:
202
+ """``aroon(len)``: how recently the window's high and low were set, as a percentage.
203
+
204
+ The window is ``len + 1`` bars wide, so an extreme set ``len`` bars ago is
205
+ still inside it and a reading of 0 is reachable. The two ages are counted
206
+ under the tie rule of section 20.10: the most recent bar that set the extreme.
207
+ """
208
+ span = None if length is None else length + 1
209
+ highs = window(contributed(state, "high", number(ctx.bar("high")), span), span)
210
+ lows = window(contributed(state, "low", number(ctx.bar("low")), span), span)
211
+ if highs is None or lows is None or length is None:
212
+ return [ABSENT, ABSENT]
213
+ up = (100 * (length - high_at(highs))) / length
214
+ down = (100 * (length - low_at(lows))) / length
215
+ return [result(up), result(down)]
216
+
217
+
218
+ def cloud(
219
+ state: Region, ctx, conversion_length: Optional[int], base_length: Optional[int],
220
+ span_length: Optional[int],
221
+ ) -> list:
222
+ """``ichimoku(convLen, baseLen, spanLen)``: the five line frame, undisplaced.
223
+
224
+ Each of the first four lines is the midpoint of a window's outright extremes.
225
+ The third is the mean of the two midpoints already computed at the shorter two
226
+ lengths, and not a midpoint recomputed over a window of its own. The fifth is
227
+ this bar's close, reported at the bar it belongs to: a study draws the spans
228
+ and the lagging line forward or back with the plot's own offset, because a
229
+ shifted series cannot be compared with anything else in the script without
230
+ shifting it back.
231
+ """
232
+ deepest = _deepest(conversion_length, base_length, span_length)
233
+ highs = contributed(state, "high", number(ctx.bar("high")), deepest)
234
+ lows = contributed(state, "low", number(ctx.bar("low")), deepest)
235
+ conversion = _midpoint(highs, lows, conversion_length)
236
+ base = _midpoint(highs, lows, base_length)
237
+ leading = ABSENT
238
+ if isinstance(conversion, float) and isinstance(base, float):
239
+ leading = result((conversion + base) / 2)
240
+ second = _midpoint(highs, lows, span_length)
241
+ lagging = ABSENT
242
+ if raw_window(highs, base_length) is not None:
243
+ lagging = number(ctx.bar("close"))
244
+ lagging = ABSENT if lagging is None else result(lagging)
245
+ return [conversion, base, leading, second, lagging]
246
+
247
+
248
+ def _deepest(*lengths) -> Optional[int]:
249
+ """The widest window any of the lines asks for, which is what the buffer keeps."""
250
+ known = [one for one in lengths if one is not None]
251
+ return max(known) if known else None
252
+
253
+
254
+ def _midpoint(highs, lows, length: Optional[int]) -> Value:
255
+ """The midpoint of a window's outright extremes, which four of the five lines are."""
256
+ top = window(highs, length)
257
+ bottom = window(lows, length)
258
+ if top is None or bottom is None:
259
+ return ABSENT
260
+ return result((top[high_at(top)] + bottom[low_at(bottom)]) / 2)