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,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}
|
openscript/intervals.py
ADDED
|
@@ -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
|
+
]
|