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