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,104 @@
1
+ """How this engine says something went wrong, and what it refuses to say twice.
2
+
3
+ Every failure carries a catalogue code, a position and the values the
4
+ catalogue's message has placeholders for. Nothing here holds the message text.
5
+ That is deliberate and it is the repository's own rule read in this language:
6
+ `spec/errors.json` is the catalogue and the authority, the wording is generated
7
+ into the first engine rather than retyped in it, and a second engine that typed
8
+ the sentences out again would be a second place for them to be wrong. A host
9
+ that wants the prose has the catalogue; what the conformance suite compares is
10
+ the code, the line, the column and the severity, and those are all here.
11
+
12
+ The placeholder names are held to the catalogue by a test rather than by
13
+ attention: a name this engine invents is a message with a hole in it, and the
14
+ hole would only ever be seen by the user it was written for.
15
+
16
+ **Severity is derived and not copied.** An engine's failure stops something: a
17
+ bar, or a load. There is no reading of a compiled program that produces a
18
+ warning, because nothing downstream of the engine could act on one, so every
19
+ diagnostic raised from here is an error and the constant below says so once. The
20
+ same test measures that against the catalogue.
21
+
22
+ **A failure is thrown rather than returned.** An error in the middle of an
23
+ expression has to abandon the rest of the bar, and there is no value a division
24
+ by zero could return that every instruction above it would not have to check
25
+ again. The throw crosses the interpreter and stops at the one boundary that
26
+ knows what to do with it, and nothing this package exposes lets it out: a host
27
+ draws many studies in one loop, and one script failing has to be a diagnostic
28
+ about that script and nothing else.
29
+ """
30
+
31
+ from dataclasses import dataclass, field
32
+ from typing import Any, Mapping, NamedTuple
33
+
34
+
35
+ class Position(NamedTuple):
36
+ """Where a diagnostic points, as ``debug.pos`` holds it: a line and a column.
37
+
38
+ A compiled program carries a position per instruction and not an offset into
39
+ text, because the engine is handed a program and not the source it came
40
+ from. Line and column still say where, which is what a message needs.
41
+ """
42
+
43
+ line: int
44
+ column: int
45
+
46
+
47
+ #: What a load-time failure carries. The defect is in the program rather than in
48
+ #: the source, so there is no line to name: ``compiled-program.md`` section 10.
49
+ NO_POSITION = Position(0, 0)
50
+
51
+ #: The one severity an engine raises. See the note at the head of this module.
52
+ ERROR = "error"
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class Diagnostic:
57
+ """One failure: a code, where it happened, and the message's own values."""
58
+
59
+ code: str
60
+ position: Position
61
+ values: Mapping[str, Any] = field(default_factory=dict)
62
+ severity: str = ERROR
63
+
64
+ @property
65
+ def line(self) -> int:
66
+ return self.position.line
67
+
68
+ @property
69
+ def column(self) -> int:
70
+ return self.position.column
71
+
72
+
73
+ class ScriptError(Exception):
74
+ """A diagnostic on its way out of the machine."""
75
+
76
+ def __init__(self, diagnostic: Diagnostic) -> None:
77
+ super().__init__(diagnostic.code)
78
+ self.diagnostic = diagnostic
79
+
80
+
81
+ def failure(code: str, position: Position = NO_POSITION, **values: Any) -> Diagnostic:
82
+ """The diagnostic for a code, with the values its message names."""
83
+ return Diagnostic(code=code, position=position, values=dict(values))
84
+
85
+
86
+ def raise_at(code: str, position: Position = NO_POSITION, **values: Any) -> Any:
87
+ """Throw that diagnostic. Returns nothing: the call never comes back."""
88
+ raise ScriptError(failure(code, position, **values))
89
+
90
+
91
+ def malformed(location: str, reason: str) -> Diagnostic:
92
+ """A verification failure, ``compiled-program.md`` section 3.5.
93
+
94
+ One code covers a malformed instruction list, an unreadable encoding and a
95
+ reference naming an input that was never declared, because they are not
96
+ three fixes: all three are defects of the compiler that wrote the program
97
+ and none of them is repairable by hand.
98
+ """
99
+ return failure("OS6018", NO_POSITION, location=location, reason=reason)
100
+
101
+
102
+ def at_instruction(listing: str, index: int) -> str:
103
+ """The location half of OS6018 for an instruction, spelled as 3.5 spells it."""
104
+ return f"instruction {index}" if listing == "code" else f"{listing} instruction {index}"
openscript/hours.py ADDED
@@ -0,0 +1,165 @@
1
+ """A range of wall clock hours, and where one reading sits inside it.
2
+
3
+ Two different things in this repository are a range of hours: the instrument's
4
+ own trading session, which a host states in its instrument record
5
+ (`host-interface.md` 4.3), and the range a script writes for ``session.isIn``
6
+ (`stdlib.md` 12.5). They are spelled differently and they mean different things,
7
+ and the arithmetic under them is one arithmetic, so it is here rather than in
8
+ each of them. A second copy would be a fact stated twice, and the two copies
9
+ would not drift over anything a reader could see: they would drift over midnight,
10
+ or over which day a list of days is read against, on the one dataset nobody
11
+ tests.
12
+
13
+ **The open is inclusive and the close is exclusive**, so two ranges written back
14
+ to back cover every minute once and no minute twice. A range whose start and end
15
+ are the same minute holds nothing, which is what writing a zero length range
16
+ says.
17
+
18
+ **A range whose end is before its start crosses midnight and is read that way**,
19
+ which is what an overnight session needs (4.3, and 12.5 for the script's own
20
+ spelling). The day list then names the day the range opened on rather than the
21
+ day the reading falls on, because a session that opens on Friday evening and
22
+ closes on Saturday morning is a Friday session: reading the list against the
23
+ reading's own day would drop half of every overnight session and keep the wrong
24
+ half.
25
+
26
+ **A session is named by the day it opened on**, which is what tells two readings
27
+ in one session from two readings in two. Naming it by an instant would be wrong
28
+ across a seasonal clock change, where a wall clock hour inside a session is not
29
+ an hour of elapsed time.
30
+ """
31
+
32
+ import re
33
+ from dataclasses import dataclass
34
+ from typing import Optional, Sequence, Tuple
35
+
36
+ from .civil import Civil, DAY_MINUTES, day_number, minutes_of, weekday_of_day
37
+
38
+ #: 4.3: midnight at the end of the day, which is the one spelling past 23:59.
39
+ END_OF_DAY = DAY_MINUTES
40
+
41
+ #: ``"HH:MM"``, the one spelling 4.3 fixes for a bound of the record's session.
42
+ _CLOCK = re.compile(r"^([0-9]{2}):([0-9]{2})$")
43
+
44
+ #: 12.5: ``"HHMM-HHMM"`` with an optional colon and a run of day digits.
45
+ _WINDOW = re.compile(r"^([0-9]{2})([0-9]{2})-([0-9]{2})([0-9]{2})(?::([1-7]{1,7}))?$")
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Hours:
50
+ """Hours as minutes from midnight, and the days they open on."""
51
+
52
+ start: int
53
+ #: Minutes from midnight at the close, which may be at 24:00.
54
+ end: int
55
+ #: The days it opens on, Monday as 1, or nothing for every day.
56
+ days: Optional[Tuple[int, ...]] = None
57
+
58
+ @property
59
+ def crosses_midnight(self) -> bool:
60
+ """4.3: "an ``end`` earlier than its ``start`` crosses midnight"."""
61
+ return self.end < self.start
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class Standing:
66
+ """Where one reading sits in a range of hours."""
67
+
68
+ inside: bool
69
+ #: Minutes since the hours opened, zero on the minute they opened.
70
+ elapsed: float = 0.0
71
+ #: Minutes until they are scheduled to close.
72
+ remaining: float = 0.0
73
+ #: The day they opened on, which is the name of this one session.
74
+ opening_day: int = 0
75
+
76
+
77
+ OUTSIDE = Standing(False)
78
+
79
+
80
+ def clock_minutes(text: object) -> Optional[int]:
81
+ """``"HH:MM"`` as minutes from midnight, or nothing for another spelling.
82
+
83
+ ``"9:00"`` is the spelling a host writes first and it is not a time here, so
84
+ it has no reading rather than a generous one: a record stating a window in a
85
+ spelling the page does not have is a record whose author believes they stated
86
+ a session, and 4.3 refuses it instead of reading it.
87
+ """
88
+ if not isinstance(text, str):
89
+ return None
90
+ found = _CLOCK.match(text)
91
+ if found is None:
92
+ return None
93
+ hour, minute = int(found.group(1)), int(found.group(2))
94
+ if hour > 24 or minute > 59 or (hour == 24 and minute != 0):
95
+ return None
96
+ return hour * 60 + minute
97
+
98
+
99
+ def window_of(spec: object) -> Optional[Hours]:
100
+ """The hours a ``session.isIn`` spec names, or nothing where it is malformed.
101
+
102
+ 12.5's grammar, and the bounds it allows: ``"2400"`` is midnight at the end
103
+ of the day and is the one hour past 23 a spec may write. A malformed spec is
104
+ OS3008 at compile time when it is a literal, and absence here when it is not:
105
+ the alternative would stop a chart on a string the script built on one bar
106
+ out of forty thousand, and absence is a value a script can test.
107
+ """
108
+ if not isinstance(spec, str):
109
+ return None
110
+ found = _WINDOW.match(spec)
111
+ if found is None:
112
+ return None
113
+ from_hour, from_minute = int(found.group(1)), int(found.group(2))
114
+ to_hour, to_minute = int(found.group(3)), int(found.group(4))
115
+ if from_hour > 23 or from_minute > 59 or to_minute > 59:
116
+ return None
117
+ if to_hour > 24 or (to_hour == 24 and to_minute != 0):
118
+ return None
119
+ listed = found.group(5)
120
+ days = None if listed is None else tuple(int(one) for one in listed)
121
+ return Hours(from_hour * 60 + from_minute, to_hour * 60 + to_minute, days)
122
+
123
+
124
+ def standing_in(hours: Hours, at: Civil) -> Standing:
125
+ """Where a wall clock reading sits in a range of hours, or outside every one."""
126
+ minutes = minutes_of(at)
127
+ today = day_number(at.year, at.month, at.day)
128
+
129
+ if hours.end > hours.start:
130
+ if minutes < hours.start or minutes >= hours.end:
131
+ return OUTSIDE
132
+ return _held(hours, today, minutes - hours.start, hours.end - minutes)
133
+ if hours.end == hours.start:
134
+ return OUTSIDE
135
+
136
+ # Crossing midnight: the evening part belongs to today's session and the
137
+ # morning part to yesterday's, which is the day the list is read against.
138
+ if minutes >= hours.start:
139
+ return _held(hours, today, minutes - hours.start, DAY_MINUTES - minutes + hours.end)
140
+ if minutes < hours.end:
141
+ return _held(
142
+ hours, today - 1, DAY_MINUTES - hours.start + minutes, hours.end - minutes
143
+ )
144
+ return OUTSIDE
145
+
146
+
147
+ def _held(hours: Hours, opening_day: int, elapsed: float, remaining: float) -> Standing:
148
+ """One reading inside the hours, unless the day it opened on is not one of theirs."""
149
+ if hours.days is not None and weekday_of_day(opening_day) not in hours.days:
150
+ return OUTSIDE
151
+ return Standing(True, elapsed, remaining, opening_day)
152
+
153
+
154
+ def days_numbered(days: Sequence[object]) -> bool:
155
+ """Whether a stated day list is the numbering ``date.dayOfWeek`` uses.
156
+
157
+ Monday is 1 through Sunday is 7, and an empty list is not a list of days: a
158
+ host numbering Sunday as 0 states a week the instrument never opens in, which
159
+ 4.3 refuses rather than reads.
160
+ """
161
+ if not days:
162
+ return False
163
+ return all(
164
+ isinstance(day, int) and not isinstance(day, bool) and 1 <= day <= 7 for day in days
165
+ )
openscript/inputs.py ADDED
@@ -0,0 +1,239 @@
1
+ """Inputs, resolved once at load, sections 2.6 and 5.1.
2
+
3
+ Input resolution and the substitution of every ``{ "input": ... }`` reference
4
+ happen **once, before step 1 of bar 0**, so the declarations in ``outputs`` and
5
+ anything built from them exist before the first bar runs. Step 5 writes already
6
+ resolved values into slots and resolves nothing.
7
+
8
+ **A value that fails validation is a refusal, not a fallback.** The host's value
9
+ is used when it passes and the declared default when the host supplies none, and
10
+ a supplied value that fails is OS6019 and the program does not run. Falling back
11
+ to the default would be a settings dialog that silently ignores what a user
12
+ typed, which is worse than one that says the value is out of range.
13
+
14
+ **An input value is never absent.** ``input()`` cannot declare ``none`` as a
15
+ default, because the default's type is what fixes the input's type.
16
+ """
17
+
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timezone
20
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
21
+
22
+ from .bars import is_bar_field
23
+ from .diagnostics import Diagnostic, failure
24
+ from .program import constant_value
25
+ from .values import ABSENT, Colour, is_number
26
+
27
+ #: The eight built-in series a ``"source"`` input may select, section 2.6.
28
+ SOURCES: Tuple[str, ...] = (
29
+ "open",
30
+ "high",
31
+ "low",
32
+ "close",
33
+ "hl2",
34
+ "hlc3",
35
+ "ohlc4",
36
+ "volume",
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ResolvedInput:
42
+ key: str
43
+ slot: int
44
+ #: The bar field a ``"source"`` input selected, which step 5 reads per bar.
45
+ field: Optional[str]
46
+ #: The effective value, for every other kind.
47
+ value: Any
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class InputResult:
52
+ inputs: Sequence[ResolvedInput]
53
+ diagnostic: Optional[Diagnostic] = None
54
+
55
+ @property
56
+ def ok(self) -> bool:
57
+ return self.diagnostic is None
58
+
59
+
60
+ TimeReader = Callable[[str], Optional[float]]
61
+
62
+
63
+ def resolve_inputs(
64
+ raw: Mapping[str, Any],
65
+ settings: Mapping[str, Any],
66
+ read_time: Optional[TimeReader] = None,
67
+ ) -> InputResult:
68
+ found: List[ResolvedInput] = []
69
+ for declared in raw["inputs"]:
70
+ one, refusal = _resolve_one(declared, settings, read_time)
71
+ if refusal is not None:
72
+ return InputResult((), refusal)
73
+ found.append(one)
74
+ return InputResult(tuple(found))
75
+
76
+
77
+ def _resolve_one(
78
+ declared: Mapping[str, Any],
79
+ settings: Mapping[str, Any],
80
+ read_time: Optional[TimeReader],
81
+ ) -> Tuple[Optional[ResolvedInput], Optional[Diagnostic]]:
82
+ supplied = settings[declared["key"]] if declared["key"] in settings else None
83
+ fallback = constant_value(declared["default"])
84
+
85
+ # A source and a time are checked even where the host stored nothing,
86
+ # because each declares its value as text this engine still has to read.
87
+ # Every other kind takes its declared default unexamined: a default is
88
+ # written in the source, and validation here is about a value that arrived
89
+ # from outside it.
90
+ ask_anyway = declared["kind"] in ("source", "time")
91
+ if declared["key"] not in settings and not ask_anyway:
92
+ return ResolvedInput(declared["key"], declared["slot"], None, fallback), None
93
+
94
+ held = fallback if declared["key"] not in settings else supplied
95
+ value, field, refusal = check_setting(declared, held, read_time)
96
+ if refusal is not None:
97
+ return None, failure(
98
+ "OS6019", value=_describe(held), key=declared["key"], validation=refusal
99
+ )
100
+ return ResolvedInput(declared["key"], declared["slot"], field, value), None
101
+
102
+
103
+ def check_setting(
104
+ declared: Mapping[str, Any],
105
+ supplied: Any,
106
+ read_time: Optional[TimeReader] = None,
107
+ ) -> Tuple[Any, Optional[str], Optional[str]]:
108
+ """What the engine will run with for one stored value, or the rule it breaks.
109
+
110
+ Answered outside the engine as well: a chart builds a settings dialog before
111
+ anything is loaded, so it has to know what a stored value is worth before the
112
+ run that would refuse it exists. A second copy of these rules in an adapter
113
+ would be the same fact in two files, answering differently the first time
114
+ either was edited.
115
+ """
116
+ kind = declared["kind"]
117
+ if kind == "source":
118
+ if not isinstance(supplied, str) or supplied not in SOURCES:
119
+ return None, None, f"a source names one of {', '.join(SOURCES)}"
120
+ if not is_bar_field(supplied):
121
+ return None, None, "this engine has no bar field of that name"
122
+ return ABSENT, supplied, None
123
+
124
+ if kind == "time":
125
+ unreadable = "a time is a timestamp or a date and time the host can read"
126
+ if is_number(supplied):
127
+ return float(supplied), None, None
128
+ if not isinstance(supplied, str):
129
+ return None, None, unreadable
130
+ if read_time is None:
131
+ return supplied, None, None
132
+ when = read_time(supplied)
133
+ if when is None:
134
+ return None, None, unreadable
135
+ return float(when), None, None
136
+
137
+ broken = _validate(declared, supplied)
138
+ if broken is not None:
139
+ return None, None, broken
140
+ return _as_value(supplied), None, None
141
+
142
+
143
+ def _as_value(supplied: Any) -> Any:
144
+ """A stored setting as a machine value: a number is a float, a colour a colour."""
145
+ if isinstance(supplied, bool) or isinstance(supplied, str) or supplied is None:
146
+ return supplied if supplied is not None else ABSENT
147
+ if is_number(supplied):
148
+ return float(supplied)
149
+ if isinstance(supplied, Colour):
150
+ return supplied
151
+ if isinstance(supplied, (list, tuple)) and len(supplied) == 4:
152
+ red, green, blue, alpha = supplied
153
+ return Colour(float(red), float(green), float(blue), float(alpha))
154
+ return supplied
155
+
156
+
157
+ def _validate(declared: Mapping[str, Any], supplied: Any) -> Optional[str]:
158
+ """What rule the host's value broke, or nothing when it passes."""
159
+ kind = declared["kind"]
160
+ if kind == "number":
161
+ if not is_number(supplied):
162
+ return "this input takes a number"
163
+ if declared.get("min") is not None and supplied < declared["min"]:
164
+ return f"the minimum is {declared['min']}"
165
+ if declared.get("max") is not None and supplied > declared["max"]:
166
+ return f"the maximum is {declared['max']}"
167
+ return None
168
+ if kind == "bool":
169
+ return None if isinstance(supplied, bool) else "this input takes true or false"
170
+ if kind == "color":
171
+ return None if _is_colour(supplied) else "this input takes a colour"
172
+ if kind == "select":
173
+ if not isinstance(supplied, str):
174
+ return "this input takes one of its listed values"
175
+ allowed = [constant_value(one) for one in (declared.get("options") or [])]
176
+ if supplied not in allowed:
177
+ return f"the choices are {', '.join(str(one) for one in allowed)}"
178
+ return None
179
+ return None if isinstance(supplied, str) else "this input takes a string"
180
+
181
+
182
+ def _is_colour(value: Any) -> bool:
183
+ if isinstance(value, Colour):
184
+ return True
185
+ return isinstance(value, (list, tuple)) and len(value) == 4 and all(
186
+ is_number(one) for one in value
187
+ )
188
+
189
+
190
+ def field_value(field: Any, inputs: Sequence[ResolvedInput]) -> Any:
191
+ """A declaration field with its ``{ "input": key }`` reference substituted.
192
+
193
+ Every field of ``meta``, of ``meta.strategy`` and of every declaration in
194
+ ``outputs`` may hold the reference, and it is gone before bar 0.
195
+ """
196
+ if field is None:
197
+ return ABSENT
198
+ if isinstance(field, dict):
199
+ key = field.get("input")
200
+ for one in inputs:
201
+ if one.key == key:
202
+ return one.value
203
+ return ABSENT
204
+ if isinstance(field, (list, tuple)) and len(field) == 4 and all(is_number(one) for one in field):
205
+ red, green, blue, alpha = field
206
+ return Colour(float(red), float(green), float(blue), float(alpha))
207
+ return field
208
+
209
+
210
+ #: A date and time read as if it were UTC.
211
+ #:
212
+ #: **A stand-in.** ``stdlib.md`` section 12.1 reads every calendar field in the
213
+ #: chart's timezone, which is an IANA name the host states, and an engine that
214
+ #: has not been given one cannot apply it. A host with a zone supplies its own
215
+ #: reader; this is what a test and a host with no zone get, and it is
216
+ #: deterministic, which is the property that matters most here.
217
+ def utc_time(text: str) -> Optional[float]:
218
+ for pattern in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M",
219
+ "%Y-%m-%dT%H:%M", "%Y-%m-%d"):
220
+ try:
221
+ when = datetime.strptime(text.strip(), pattern)
222
+ except ValueError:
223
+ continue
224
+ return when.replace(tzinfo=timezone.utc).timestamp() * 1000
225
+ return None
226
+
227
+
228
+ def _describe(value: Any) -> str:
229
+ if value is None:
230
+ return "none"
231
+ if isinstance(value, str):
232
+ return f'"{value}"'
233
+ if isinstance(value, bool) or is_number(value):
234
+ return str(value)
235
+ return "a value of another type"
236
+
237
+
238
+ def settings_slots(inputs: Sequence[ResolvedInput]) -> Dict[int, ResolvedInput]:
239
+ return {one.slot: one for one in inputs}
@@ -0,0 +1,60 @@
1
+ """Timeframe strings, and the one thing the session facts need from them.
2
+
3
+ `stdlib.md` section 15.2 fixes the spelling: a count and a unit, with the unit
4
+ letters case sensitive so ``"1M"`` is one month and ``"1m"`` one minute, and a
5
+ bare number read as minutes because that is the form an interval input supplies.
6
+ `host-interface.md` 4.1 states the instrument's own interval in that spelling and
7
+ calls two facts derived from it, which is why an engine reads the string rather
8
+ than being told the number: two sources for one fact can disagree and no rule
9
+ would say which of them wins.
10
+
11
+ **What is here is the length of one bar and nothing else.** Folding a coarser
12
+ timeframe onto a chart's bars is section 15's work and another stage's; the
13
+ session facts need one number, the minutes a bar covers, and they need it for one
14
+ reason, which is the fact `host-interface.md` 4.3 says earns the session its
15
+ place: ``session.isLastBar`` is true on the last bar of the schedule even when
16
+ trading stopped early, and the bar that reaches the scheduled close is a bar slot
17
+ measured against it. Without the interval there is no slot, and an engine that
18
+ answered from the next bar's time would be making a reading no live engine can
19
+ make: on the bar in front of you there is no next bar.
20
+
21
+ **A month is counted at its nominal length**, thirty days, and that is not a
22
+ claim about a calendar. Any bar of a day or more spans a whole session whatever
23
+ the exact figure, so every number past the length of a session gives the session
24
+ facts the same answer. Where a calendar bucket actually begins is a different
25
+ question, and a stage that folds one answers it from the calendar rather than
26
+ from this.
27
+
28
+ **A string this grammar does not hold leaves the answer absent.** A host that
29
+ states an interval in a spelling 15.2 does not have has stated nothing this
30
+ engine can read, and the session facts that depend on it are absent rather than
31
+ answered from a guess at what the string meant.
32
+ """
33
+
34
+ import re
35
+ from typing import Optional
36
+
37
+ #: 15.2's grammar. A bare number is minutes, which is the interval form a host
38
+ #: supplies, and the letters are case sensitive.
39
+ _WRITTEN = re.compile(r"^([0-9]+)(m|h|D|W|M)?$")
40
+
41
+ #: The nominal length of one unit in minutes, which is what orders two
42
+ #: timeframes and what sizes a bar slot.
43
+ _NOMINAL = {"m": 1, "h": 60, "D": 1440, "W": 10080, "M": 43200}
44
+
45
+
46
+ def bar_minutes_of(interval: object) -> Optional[float]:
47
+ """Minutes one bar of this interval covers, or nothing for a string 15.2 has not.
48
+
49
+ A count of zero is not an interval: a bar of no length would make every bar
50
+ the last of its session, which is the one reading worse than absence.
51
+ """
52
+ if not isinstance(interval, str):
53
+ return None
54
+ found = _WRITTEN.match(interval.strip())
55
+ if found is None:
56
+ return None
57
+ count = int(found.group(1))
58
+ if count < 1:
59
+ return None
60
+ return float(count * _NOMINAL[found.group(2) or "m"])
@@ -0,0 +1,76 @@
1
+ """The library of `spec/stdlib.md`, in the accumulation order section 20 fixes.
2
+
3
+ This door exports **both halves**, as two tables rather than one. The stateless
4
+ half is every function whose result depends on this bar's arguments and this
5
+ bar's facts and on nothing it remembers; the stateful half is the averages and
6
+ the windowed studies that carry a state region from bar to bar. They are two
7
+ tables because a call of one takes two things and a call of the other takes
8
+ three: the region is the difference, and a single table would hand a caller two
9
+ shapes to tell apart at the call site rather than at the manifest.
10
+
11
+ What is here, by the page it is written from:
12
+
13
+ - ``values`` absence, and the two rules every number returned has passed
14
+ - ``arithmetic`` section 8.1, the bare calls that compute one value
15
+ - ``rounding`` section 8.1 and 20.7, one rule for halves and one scale table
16
+ - ``elementary`` section 8.2, the square root and the gap 1 family
17
+ - ``bars`` section 20.5's one reading that remembers nothing
18
+ - ``code_points`` section 10's whitespace set and the one string order
19
+ - ``number_text`` `language.md` 5.5, how a number becomes text and back
20
+ - ``strings`` section 10, the string calls
21
+ - ``colour`` section 11, the nineteen names and the calls that compute one
22
+ - ``stateless`` the manifest rows of the first half
23
+ - ``series`` section 20.2's two shapes, and the region they are kept in
24
+ - ``prices`` the two derived prices the second half forms for itself
25
+ - ``averages`` section 20.3, the six means not built from another mean
26
+ - ``composites`` section 20.3, the means built from those, and the fit
27
+ - ``trend`` section 20.3, the studies that carry a decision from bar to bar
28
+ - ``strength`` section 20.4, the position and strength readings
29
+ - ``momentum`` section 20.4, the momentum readings and the two mean gaps
30
+ - ``ranges`` section 20.5, the readings taken from the bar's own range
31
+ - ``deviation`` section 20.5, the spread readings and the bands built on them
32
+ - ``flows`` section 20.6, the volume readings
33
+ - ``extremes`` sections 9 and 20.10, the window scans and the pivots
34
+ - ``counting`` section 20.8, the totals, the ranks and the pair statistics
35
+ - ``bookkeeping`` section 9, the comparisons and the two that wait on a condition
36
+ - ``stateful`` the manifest rows of the second half
37
+
38
+ **One thing this package will not do.** It raises nothing: a wrong argument is a
39
+ diagnostic with a code and a span, produced by the checker or the interpreter
40
+ before a call arrives, and every backstop here answers with absence instead
41
+ (`stdlib.md` section 2.4). The memory ceilings are the interpreter's for the same
42
+ reason, and ``MEASURED`` below is what it asks this package before it spends one:
43
+ the two calls that can be asked for a string no engine could hold, and how long
44
+ each of them will be before a character of it exists.
45
+
46
+ **What a stateful call is handed that a stateless one is not** is the call site's
47
+ own region, a plain mapping the engine created and owns, because
48
+ `compiled-program.md` section 2.11 requires a region to be snapshottable by a
49
+ mechanical copy without the engine knowing which function it belongs to. A
50
+ stateless call needs none, which is what lets an engine call one of those from
51
+ anywhere without a checkpoint or a rollback.
52
+
53
+ **One family is not held to the vectors.** ``exp``, ``log``, ``log10``,
54
+ ``math.log2``, ``pow``, ``math.hypot`` and the trigonometric namespace reach gap 1
55
+ of `stdlib.md` section 20.11, and so do ``alma``, ``hv`` and ``chop``, which are
56
+ built on the first three: no portable reference algorithm is written down
57
+ anywhere, so they call the host's maths module and carry no cross-engine
58
+ guarantee. ``elementary`` says it again where an implementer will be standing.
59
+ """
60
+
61
+ from .stateful import ENTRIES as STATEFUL_ENTRIES
62
+ from .stateful import Entry as StatefulEntry
63
+ from .stateful import table as stateful_table
64
+ from .stateless import BUILDS_A_STRING, ENTRIES, MEASURED, Context, Entry, table
65
+
66
+ __all__ = [
67
+ "ENTRIES",
68
+ "Context",
69
+ "Entry",
70
+ "BUILDS_A_STRING",
71
+ "MEASURED",
72
+ "STATEFUL_ENTRIES",
73
+ "StatefulEntry",
74
+ "stateful_table",
75
+ "table",
76
+ ]