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,162 @@
|
|
|
1
|
+
"""Check 1 of ``compiled-program.md`` section 3.5: is this a compiled program at all.
|
|
2
|
+
|
|
3
|
+
This is the half of verification that reads an untrusted object and answers that
|
|
4
|
+
question before anything else runs, because every later check indexes into a
|
|
5
|
+
table this one proves is a table.
|
|
6
|
+
|
|
7
|
+
Every failure here is OS6018. A malformed instruction list, an unreadable
|
|
8
|
+
encoding and a field of the wrong type are not three fixes: each is a defect of
|
|
9
|
+
the compiler that wrote the program and none of them is repairable by hand. What
|
|
10
|
+
varies is the location, and the location is a field path, so whoever wrote the
|
|
11
|
+
compiler is told ``outputs.plots[2].channel`` rather than "structure".
|
|
12
|
+
|
|
13
|
+
One difference from the first engine, and it is this interpreter's alone: its
|
|
14
|
+
own ``True`` is an instance of its integer type, so every numeric test below
|
|
15
|
+
refuses a boolean explicitly. Without that line a program carrying ``true``
|
|
16
|
+
where a channel index belongs would verify, and the failure would arrive later
|
|
17
|
+
as a wrong number.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Callable, Optional, Sequence, Set
|
|
21
|
+
|
|
22
|
+
from .diagnostics import Diagnostic, malformed
|
|
23
|
+
|
|
24
|
+
#: The tags a constant pool entry may carry, section 2.9.
|
|
25
|
+
CONSTANT_TAGS = ("z", "b", "n", "s", "c")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ShapeCheck:
|
|
29
|
+
"""Collects the first failure and stops: a malformed program has no second opinion."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._failure: Optional[Diagnostic] = None
|
|
33
|
+
|
|
34
|
+
def problem(self) -> Optional[Diagnostic]:
|
|
35
|
+
return self._failure
|
|
36
|
+
|
|
37
|
+
def fail(self, path: str, reason: str) -> bool:
|
|
38
|
+
if self._failure is None:
|
|
39
|
+
self._failure = malformed(path, reason)
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def object(self, value: Any, path: str) -> bool:
|
|
43
|
+
if not isinstance(value, dict):
|
|
44
|
+
return self.fail(path, "an object was required")
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def array(self, value: Any, path: str) -> bool:
|
|
48
|
+
if not isinstance(value, list):
|
|
49
|
+
return self.fail(path, "an array was required")
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def number(self, value: Any, path: str) -> bool:
|
|
53
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
54
|
+
return self.fail(path, "a finite number was required")
|
|
55
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
56
|
+
return self.fail(path, "a finite number was required")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def whole(self, value: Any, path: str) -> bool:
|
|
60
|
+
if not self.number(value, path):
|
|
61
|
+
return False
|
|
62
|
+
if float(value) != int(value):
|
|
63
|
+
return self.fail(path, "a whole number was required")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
def string(self, value: Any, path: str) -> bool:
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
return self.fail(path, "a string was required")
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def boolean(self, value: Any, path: str) -> bool:
|
|
72
|
+
if not isinstance(value, bool):
|
|
73
|
+
return self.fail(path, "a true or false was required")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def index(self, value: Any, path: str, size: int, table: str) -> bool:
|
|
77
|
+
"""A whole number that indexes a table, with the table's size in the message."""
|
|
78
|
+
if not self.whole(value, path):
|
|
79
|
+
return False
|
|
80
|
+
if value < 0 or value >= size:
|
|
81
|
+
return self.fail(path, f"{value} is outside {table}, which holds {size}")
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def one(self, value: Any, path: str, allowed: Sequence[str]) -> bool:
|
|
85
|
+
if not self.string(value, path):
|
|
86
|
+
return False
|
|
87
|
+
if value not in allowed:
|
|
88
|
+
return self.fail(path, f"{value} is not one of {', '.join(allowed)}")
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
def nullable(self, value: Any, path: str, check: Callable[[Any, str], bool]) -> bool:
|
|
92
|
+
"""A field that may be a value or null, which is a value a script could write."""
|
|
93
|
+
return True if value is None else check(value, path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_colour(shape: ShapeCheck, value: Any, path: str) -> bool:
|
|
97
|
+
if not shape.array(value, path):
|
|
98
|
+
return False
|
|
99
|
+
if len(value) != 4:
|
|
100
|
+
return shape.fail(path, "a colour is four numbers")
|
|
101
|
+
for at in range(3):
|
|
102
|
+
if not shape.whole(value[at], f"{path}[{at}]"):
|
|
103
|
+
return False
|
|
104
|
+
channel = value[at]
|
|
105
|
+
if channel < 0 or channel > 255:
|
|
106
|
+
return shape.fail(f"{path}[{at}]", f"a channel runs 0 to 255 and this is {channel}")
|
|
107
|
+
if not shape.number(value[3], f"{path}[3]"):
|
|
108
|
+
return False
|
|
109
|
+
alpha = value[3]
|
|
110
|
+
if alpha < 0 or alpha > 1:
|
|
111
|
+
return shape.fail(f"{path}[3]", f"an alpha runs 0 to 1 and this is {alpha}")
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_constant(shape: ShapeCheck, value: Any, path: str) -> bool:
|
|
116
|
+
"""One constant pool entry, section 2.9: a two element array of tag and value."""
|
|
117
|
+
if not shape.array(value, path):
|
|
118
|
+
return False
|
|
119
|
+
if len(value) != 2:
|
|
120
|
+
return shape.fail(path, "a pool entry is a tag and a value")
|
|
121
|
+
tag = value[0]
|
|
122
|
+
if not shape.one(tag, f"{path}[0]", CONSTANT_TAGS):
|
|
123
|
+
return False
|
|
124
|
+
held = value[1]
|
|
125
|
+
if tag == "z":
|
|
126
|
+
return True if held is None else shape.fail(f"{path}[1]", "the absent entry holds null")
|
|
127
|
+
if tag == "b":
|
|
128
|
+
return shape.boolean(held, f"{path}[1]")
|
|
129
|
+
if tag == "n":
|
|
130
|
+
return shape.number(held, f"{path}[1]")
|
|
131
|
+
if tag == "s":
|
|
132
|
+
return shape.string(held, f"{path}[1]")
|
|
133
|
+
return check_colour(shape, held, f"{path}[1]")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_field(shape: ShapeCheck, value: Any, path: str, keys: Set[str]) -> bool:
|
|
137
|
+
"""A declaration field, section 2.3: a value, or the input reference.
|
|
138
|
+
|
|
139
|
+
The key half of check 10 lives here, because this is the one walk that visits
|
|
140
|
+
every field that may hold a reference. The value half runs later, with the
|
|
141
|
+
host's settings in hand, and is OS6019 rather than OS6018: that value came
|
|
142
|
+
from a settings dialog and the user who typed it can correct it.
|
|
143
|
+
"""
|
|
144
|
+
if value is None or isinstance(value, (bool, str)):
|
|
145
|
+
return True
|
|
146
|
+
if isinstance(value, (int, float)):
|
|
147
|
+
return shape.number(value, path)
|
|
148
|
+
if isinstance(value, list):
|
|
149
|
+
for at, one in enumerate(value):
|
|
150
|
+
if not shape.number(one, f"{path}[{at}]"):
|
|
151
|
+
return False
|
|
152
|
+
return True
|
|
153
|
+
if not shape.object(value, path):
|
|
154
|
+
return False
|
|
155
|
+
if "input" not in value:
|
|
156
|
+
return shape.fail(path, "an object here is an input reference")
|
|
157
|
+
key = value["input"]
|
|
158
|
+
if not shape.string(key, f"{path}.input"):
|
|
159
|
+
return False
|
|
160
|
+
if key not in keys:
|
|
161
|
+
return shape.fail(path, f"it names the input {key}, which inputs[] does not declare")
|
|
162
|
+
return True
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Check 1 of section 3.5 over the program's own tables, and check 10's key half.
|
|
2
|
+
|
|
3
|
+
``verify_shape.py`` answers whether a field is a number; this walks the whole
|
|
4
|
+
program asking it, table by table, and checks that every index into a table is
|
|
5
|
+
in range: the constant pool, slots, cells, states, registers, channels, library
|
|
6
|
+
functions, call sites, loops and functions. It is separate from ``verify.py``
|
|
7
|
+
because it is one long traversal with no decisions in it, and the decisions
|
|
8
|
+
there, which version, which capability, which budget, are what a reader comes
|
|
9
|
+
looking for.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Sequence, Set
|
|
13
|
+
|
|
14
|
+
from .verify_requests import MachineTables, check_machine_tables, check_requests
|
|
15
|
+
from .verify_shape import ShapeCheck, check_constant, check_field
|
|
16
|
+
|
|
17
|
+
META_KINDS = ("study", "strategy")
|
|
18
|
+
CHANNEL_TYPES = ("number", "string", "color", "bool")
|
|
19
|
+
EFFECTS = ("none", "signal", "order", "draw", "log")
|
|
20
|
+
|
|
21
|
+
#: Every declaration option of ``meta``, section 2.3.
|
|
22
|
+
META_FIELDS = (
|
|
23
|
+
"title",
|
|
24
|
+
"short",
|
|
25
|
+
"overlay",
|
|
26
|
+
"precision",
|
|
27
|
+
"format",
|
|
28
|
+
"range",
|
|
29
|
+
"scale",
|
|
30
|
+
"group",
|
|
31
|
+
"onUnconfirmed",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
#: The tables a later minor of this format major added, with the minor that added each.
|
|
35
|
+
#:
|
|
36
|
+
#: Section 9.2's rule read from the other side, which 9.4 step 3 states and
|
|
37
|
+
#: ``spec/decisions.md`` minute 56 settled: a table a later minor added and an
|
|
38
|
+
#: earlier program lacks reads as empty, never as a refusal, because 9.5's first
|
|
39
|
+
#: line is a promise about that program. A program stamped at this minor or a
|
|
40
|
+
#: later one has no such excuse. Section 2 says an empty table is written as an
|
|
41
|
+
#: empty array and never omitted, so its absence there is the defect check 1
|
|
42
|
+
#: exists for, and the version is what tells the two apart.
|
|
43
|
+
#:
|
|
44
|
+
#: ``spec/format-history.json`` records the same additions, one entry per format
|
|
45
|
+
#: version, and this engine's tests read that file rather than a list of their
|
|
46
|
+
#: own, so a version the history gains is a case the tests gain.
|
|
47
|
+
ADDED_AT_MINOR = (("requests", 1),)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def minor_of(raw: Dict[str, Any]) -> int:
|
|
51
|
+
"""The minor of a ``major.minor`` the version step has already proved is one."""
|
|
52
|
+
parts = str(raw["openscript"]["format"]).split(".")
|
|
53
|
+
return int(parts[1]) if len(parts) > 1 else 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def supply_added_tables(raw: Dict[str, Any]) -> None:
|
|
57
|
+
"""Writes the empty table an earlier minor is owed into the program itself.
|
|
58
|
+
|
|
59
|
+
Into the object rather than into a local, because every later step reads the
|
|
60
|
+
program as its own shape and would find the table missing again: the
|
|
61
|
+
verifier hands the same object on, and the engine holds it.
|
|
62
|
+
"""
|
|
63
|
+
minor = minor_of(raw)
|
|
64
|
+
for table, added in ADDED_AT_MINOR:
|
|
65
|
+
if table in raw or minor >= added:
|
|
66
|
+
continue
|
|
67
|
+
raw[table] = []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
ARRAY_TABLES = (
|
|
71
|
+
"requires",
|
|
72
|
+
"inputs",
|
|
73
|
+
"channels",
|
|
74
|
+
"consts",
|
|
75
|
+
"series",
|
|
76
|
+
"cells",
|
|
77
|
+
"states",
|
|
78
|
+
"functions",
|
|
79
|
+
"callSites",
|
|
80
|
+
"loops",
|
|
81
|
+
"code",
|
|
82
|
+
"requests",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
OBJECT_TABLES = ("compiler", "source", "meta", "limits", "lib", "outputs", "frame", "debug")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_tables(shape: ShapeCheck, raw: Any) -> bool:
|
|
89
|
+
"""Check 1 over every table the machine indexes, and every declaration field."""
|
|
90
|
+
if not shape.object(raw, "the program"):
|
|
91
|
+
return False
|
|
92
|
+
supply_added_tables(raw)
|
|
93
|
+
for name in ARRAY_TABLES:
|
|
94
|
+
if not shape.array(raw.get(name), name):
|
|
95
|
+
return False
|
|
96
|
+
for name in OBJECT_TABLES:
|
|
97
|
+
if not shape.object(raw.get(name), name):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
limits = raw["limits"]
|
|
101
|
+
if not shape.whole(limits.get("loops"), "limits.loops"):
|
|
102
|
+
return False
|
|
103
|
+
if limits.get("history") is not None and not shape.whole(limits["history"], "limits.history"):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
frame = raw["frame"]
|
|
107
|
+
if not shape.whole(frame.get("slots"), "frame.slots"):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
lib = raw["lib"]
|
|
111
|
+
if not shape.whole(lib.get("manifest"), "lib.manifest"):
|
|
112
|
+
return False
|
|
113
|
+
if not shape.array(lib.get("functions"), "lib.functions"):
|
|
114
|
+
return False
|
|
115
|
+
for at, entry in enumerate(lib["functions"]):
|
|
116
|
+
path = f"lib.functions[{at}]"
|
|
117
|
+
if not shape.object(entry, path):
|
|
118
|
+
return False
|
|
119
|
+
if not shape.string(entry.get("name"), f"{path}.name"):
|
|
120
|
+
return False
|
|
121
|
+
if not shape.whole(entry.get("arity"), f"{path}.arity"):
|
|
122
|
+
return False
|
|
123
|
+
if not shape.boolean(entry.get("state"), f"{path}.state"):
|
|
124
|
+
return False
|
|
125
|
+
if not shape.one(entry.get("effect"), f"{path}.effect", EFFECTS):
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
for at, entry in enumerate(raw["consts"]):
|
|
129
|
+
if not check_constant(shape, entry, f"consts[{at}]"):
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
keys: Set[str] = set()
|
|
133
|
+
for at, one in enumerate(raw["inputs"]):
|
|
134
|
+
path = f"inputs[{at}]"
|
|
135
|
+
if not shape.object(one, path):
|
|
136
|
+
return False
|
|
137
|
+
if not shape.string(one.get("key"), f"{path}.key"):
|
|
138
|
+
return False
|
|
139
|
+
if not shape.string(one.get("kind"), f"{path}.kind"):
|
|
140
|
+
return False
|
|
141
|
+
if not shape.index(one.get("slot"), f"{path}.slot", frame["slots"], "the frame"):
|
|
142
|
+
return False
|
|
143
|
+
if not check_constant(shape, one.get("default"), f"{path}.default"):
|
|
144
|
+
return False
|
|
145
|
+
keys.add(one["key"])
|
|
146
|
+
|
|
147
|
+
for at, channel in enumerate(raw["channels"]):
|
|
148
|
+
path = f"channels[{at}]"
|
|
149
|
+
if not shape.object(channel, path):
|
|
150
|
+
return False
|
|
151
|
+
if channel.get("id") != at:
|
|
152
|
+
return shape.fail(f"{path}.id", "a channel id is its position")
|
|
153
|
+
if not shape.one(channel.get("type"), f"{path}.type", CHANNEL_TYPES):
|
|
154
|
+
return False
|
|
155
|
+
for name in ("defer", "once"):
|
|
156
|
+
if not shape.boolean(channel.get(name), f"{path}.{name}"):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
tables = MachineTables(
|
|
160
|
+
series=raw["series"],
|
|
161
|
+
cells=raw["cells"],
|
|
162
|
+
states=raw["states"],
|
|
163
|
+
functions=raw["functions"],
|
|
164
|
+
call_sites=raw["callSites"],
|
|
165
|
+
loops=raw["loops"],
|
|
166
|
+
slots=frame["slots"],
|
|
167
|
+
lib_functions=len(lib["functions"]),
|
|
168
|
+
)
|
|
169
|
+
if not check_machine_tables(shape, "", tables):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# 2.16: a read's body is walked on the same terms, because the same machine
|
|
173
|
+
# executes it over another instrument's bars.
|
|
174
|
+
if not check_requests(shape, "", raw["requests"], len(raw["series"]), len(lib["functions"]), keys):
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
meta = raw["meta"]
|
|
178
|
+
if not shape.one(meta.get("kind"), "meta.kind", META_KINDS):
|
|
179
|
+
return False
|
|
180
|
+
for name in META_FIELDS:
|
|
181
|
+
if not check_field(shape, meta.get(name), f"meta.{name}", keys):
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
debug = raw["debug"]
|
|
185
|
+
for name in ("pos", "fnPos"):
|
|
186
|
+
if not shape.array(debug.get(name), f"debug.{name}"):
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
return _check_outputs(shape, raw["outputs"], len(raw["channels"]), frame["slots"], keys)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#: Each declaration group, the fields of it that name a channel, and the fields
|
|
193
|
+
#: that hold a value or the input reference that resolves to one.
|
|
194
|
+
DECLARATIONS = (
|
|
195
|
+
("plots", ("channel",),
|
|
196
|
+
("title", "color", "width", "lineStyle", "offset", "overlay", "scale")),
|
|
197
|
+
("levels", ("channel",), ("title", "color", "lineStyle", "lineWidth")),
|
|
198
|
+
("markers", ("channel",), ("position", "shape", "color", "textColor")),
|
|
199
|
+
("alerts", ("condChannel",), ("title", "frequency")),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
OUTPUT_GROUPS = ("plots", "fills", "levels", "markers", "tables", "alerts")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _check_outputs(
|
|
206
|
+
shape: ShapeCheck, outputs: Any, channels: int, slots: int, keys: Set[str]
|
|
207
|
+
) -> bool:
|
|
208
|
+
"""Every declaration in ``outputs``, and every channel it points at."""
|
|
209
|
+
for group in OUTPUT_GROUPS:
|
|
210
|
+
if not shape.array(outputs.get(group), f"outputs.{group}"):
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def channel_field(holder: Any, path: str, name: str) -> bool:
|
|
214
|
+
return shape.index(holder.get(name), f"{path}.{name}", channels, "channels")
|
|
215
|
+
|
|
216
|
+
for group, channel_fields, value_fields in DECLARATIONS:
|
|
217
|
+
for at, one in enumerate(outputs[group]):
|
|
218
|
+
path = f"outputs.{group}[{at}]"
|
|
219
|
+
if not shape.object(one, path):
|
|
220
|
+
return False
|
|
221
|
+
for name in channel_fields:
|
|
222
|
+
if not channel_field(one, path, name):
|
|
223
|
+
return False
|
|
224
|
+
for name in value_fields:
|
|
225
|
+
if not check_field(shape, one.get(name), f"{path}.{name}", keys):
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
for at, grid in enumerate(outputs["tables"]):
|
|
229
|
+
path = f"outputs.tables[{at}]"
|
|
230
|
+
if not shape.object(grid, path):
|
|
231
|
+
return False
|
|
232
|
+
if not shape.index(grid.get("slot"), f"{path}.slot", slots, "the frame"):
|
|
233
|
+
return False
|
|
234
|
+
for name in ("title", "position", "rows", "cols"):
|
|
235
|
+
if not check_field(shape, grid.get(name), f"{path}.{name}", keys):
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
for paint in ("barColor", "background"):
|
|
239
|
+
value = outputs.get(paint)
|
|
240
|
+
if value is None:
|
|
241
|
+
continue
|
|
242
|
+
if not shape.object(value, f"outputs.{paint}"):
|
|
243
|
+
return False
|
|
244
|
+
if not channel_field(value, f"outputs.{paint}", "channel"):
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def input_keys(raw: Any) -> List[str]:
|
|
251
|
+
"""The declared settings keys, in the order of section 2.6."""
|
|
252
|
+
return [one["key"] for one in raw["inputs"]]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def channel_flags(raw: Any, name: str) -> Sequence[bool]:
|
|
256
|
+
return [bool(one[name]) for one in raw["channels"]]
|
openscript/version.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""The two version numbers this engine declares, and why they are typed here.
|
|
2
|
+
|
|
3
|
+
``openscript.format`` versions the compiled program's structure and
|
|
4
|
+
``openscript.language`` versions meaning, and section 9.1 says they move at
|
|
5
|
+
different speeds for different reasons. An engine declares which majors it can
|
|
6
|
+
load and which language versions it has library semantics for, and section 9.4
|
|
7
|
+
refuses on both.
|
|
8
|
+
|
|
9
|
+
**These numbers exist elsewhere and this is the second copy, held by a check.**
|
|
10
|
+
The first engine has them generated from the page and from the package manifest,
|
|
11
|
+
because a number typed twice is a number that will disagree the day somebody
|
|
12
|
+
edits one copy. That generator writes one language's file. A package a host
|
|
13
|
+
installs cannot read the specification, which is not shipped with it, so the
|
|
14
|
+
numbers have to be in the package, and the honest arrangement is the one the
|
|
15
|
+
distribution file next door already uses for the package version: write it, and
|
|
16
|
+
have a check fail the build the day it drifts. ``tests/test_version.py`` reads
|
|
17
|
+
the sentence out of ``spec/compiled-program.md`` by the same pattern the
|
|
18
|
+
generator uses, so the page stays the source and this stays a copy that cannot
|
|
19
|
+
go stale quietly.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
#: The compiled format this engine implements: the major it loads, and the
|
|
23
|
+
#: highest minor of that major it was written against.
|
|
24
|
+
FORMAT = "1.1"
|
|
25
|
+
|
|
26
|
+
#: The language versions this engine has library semantics for.
|
|
27
|
+
#:
|
|
28
|
+
#: An engine selects semantics by the program's ``language`` and not by the
|
|
29
|
+
#: newest it implements, so that a saved script never changes its numbers.
|
|
30
|
+
LANGUAGE_VERSIONS = (1,)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_major() -> int:
|
|
34
|
+
return int(FORMAT.split(".")[0])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_minor() -> int:
|
|
38
|
+
parts = FORMAT.split(".")
|
|
39
|
+
return int(parts[1]) if len(parts) > 1 else 0
|
openscript/zones.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Reading an instant in a named zone, and the one zone this engine can read.
|
|
2
|
+
|
|
3
|
+
`stdlib.md` section 12.1 fixes what a zone is: an IANA name, never a fixed
|
|
4
|
+
offset, because an offset held constant is silently wrong for half the year
|
|
5
|
+
anywhere that observes a seasonal clock change and nothing about the wrong half
|
|
6
|
+
looks wrong. Section 12.2 adds that the name is resolved against the host
|
|
7
|
+
runtime's own timezone database, that two engines agree as far as their databases
|
|
8
|
+
do, and that a name a database does not hold is OS6005 and never a guessed
|
|
9
|
+
offset.
|
|
10
|
+
|
|
11
|
+
**This engine holds one zone and declines the rest, and the reason is the
|
|
12
|
+
dependency rule rather than the calendar.** What the standard library can answer
|
|
13
|
+
is set out here rather than discovered by whoever meets it:
|
|
14
|
+
|
|
15
|
+
- The civil arithmetic needs nothing installed. ``civil.py`` is the proleptic
|
|
16
|
+
Gregorian formula, so ``UTC`` is exact on every machine and for every instant,
|
|
17
|
+
including the ones an interpreter's own date type refuses.
|
|
18
|
+
- The offsets do not come with the interpreter. The module that reads named
|
|
19
|
+
zones is in the standard library, but the table it reads is not: it looks for a
|
|
20
|
+
database the operating system ships, and where there is none it falls back to a
|
|
21
|
+
separately installed package of the same data. On the machine this stage was
|
|
22
|
+
written on, that search path is empty and the fallback package is what answers,
|
|
23
|
+
so an engine built on it would have taken a dependency the host never accepted,
|
|
24
|
+
and ``scripts/check-python.mjs`` could not have caught it: the check reads
|
|
25
|
+
import names against the interpreter's own list, and the import that resolves
|
|
26
|
+
a zone is one of the interpreter's own. It is the data behind it that is not.
|
|
27
|
+
- The consequence is worse than the dependency. `conformance.md` section 5
|
|
28
|
+
requires a case to produce the same result on every machine and to read nothing
|
|
29
|
+
outside the case directory, environment variables included, and that module
|
|
30
|
+
consults an environment variable for its search path. A run that answered a
|
|
31
|
+
session in one zone on a build machine and declined it on a laptop would be
|
|
32
|
+
reproducible on neither.
|
|
33
|
+
|
|
34
|
+
So a zone that is not ``UTC`` is not answered here under a guessed offset and not
|
|
35
|
+
answered from a table this repository copied, which would be the same staleness a
|
|
36
|
+
fixed offset has. It is declined, and the caller reports the case
|
|
37
|
+
``unsupported`` naming the feature, which `conformance.md` section 8 defines and
|
|
38
|
+
counts separately from a pass. A host whose instruments trade in another zone
|
|
39
|
+
supplies the reader, which is the same footing `stdlib.md` section 12.2 puts every
|
|
40
|
+
engine on: the database is the host runtime's.
|
|
41
|
+
|
|
42
|
+
**The shape of the name is checked before any database is.** Section 12.2
|
|
43
|
+
requires it: "an engine applies this rule before it consults one: a script
|
|
44
|
+
refused on one engine has to be refused on every engine, and an abbreviation is
|
|
45
|
+
ambiguous in any case". So a name that is neither an area and a location nor
|
|
46
|
+
``UTC`` is malformed on every engine, whatever any database would have said about
|
|
47
|
+
it, and that is a different answer from a well formed name this engine cannot
|
|
48
|
+
read. The two are told apart below because only one of them is this engine's
|
|
49
|
+
limit.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
import re
|
|
53
|
+
from typing import Optional
|
|
54
|
+
|
|
55
|
+
from .civil import MAX_INSTANT, Civil, fields_at, instant_at, whole_instant
|
|
56
|
+
|
|
57
|
+
#: The one name with no area, which names one offset everywhere and is the whole
|
|
58
|
+
#: of what this engine's own calendar is right for.
|
|
59
|
+
READABLE = "UTC"
|
|
60
|
+
|
|
61
|
+
#: An area and a location, section 12.2's rule, applied before any database is.
|
|
62
|
+
_AREA_AND_LOCATION = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*/[A-Za-z][A-Za-z0-9+_/-]*$")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def named(zone: object) -> bool:
|
|
66
|
+
"""Whether this is a zone name at all, as against an abbreviation or an offset.
|
|
67
|
+
|
|
68
|
+
``"UTC"`` or an area and a location. A name that fails this is one every
|
|
69
|
+
engine refuses, and refusing it is OS6005's own sentence rather than a
|
|
70
|
+
limit of this engine's calendar.
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(zone, str):
|
|
73
|
+
return False
|
|
74
|
+
return zone == READABLE or _AREA_AND_LOCATION.match(zone) is not None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def readable(zone: object) -> bool:
|
|
78
|
+
"""Whether this engine can read a calendar in this zone.
|
|
79
|
+
|
|
80
|
+
True of one name. A well formed name this answers false to is not a bad
|
|
81
|
+
name: it is a zone whose offsets this engine does not hold, which is the
|
|
82
|
+
caller's ``unsupported`` and never an answer under another zone's clock.
|
|
83
|
+
"""
|
|
84
|
+
return zone == READABLE
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fields_in(instant: float, zone: object) -> Optional[Civil]:
|
|
88
|
+
"""The civil fields an instant has in a zone, or nothing where there are none.
|
|
89
|
+
|
|
90
|
+
Three ways to have none, and they are one answer here because the calls above
|
|
91
|
+
make one of them: a zone this engine does not read, a timestamp that is not a
|
|
92
|
+
number, and a magnitude past the largest instant a calendar reads.
|
|
93
|
+
"""
|
|
94
|
+
if not readable(zone):
|
|
95
|
+
return None
|
|
96
|
+
if not isinstance(instant, (int, float)) or isinstance(instant, bool):
|
|
97
|
+
return None
|
|
98
|
+
whole = whole_instant(float(instant))
|
|
99
|
+
return None if whole is None else fields_at(whole)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def instant_of(fields: Civil, zone: object) -> Optional[int]:
|
|
103
|
+
"""The instant a wall clock reading names in a zone, or nothing for no reading.
|
|
104
|
+
|
|
105
|
+
In ``UTC`` a reading names exactly one instant, so the two readings section
|
|
106
|
+
12.2 settles for a zone that changes its clock do not arise: there is no hour
|
|
107
|
+
this zone skips and none it repeats. A zone that has them is one this engine
|
|
108
|
+
declines above, which is why that rule is stated there and the arithmetic is
|
|
109
|
+
not written here to be exercised by nothing.
|
|
110
|
+
"""
|
|
111
|
+
if not readable(zone):
|
|
112
|
+
return None
|
|
113
|
+
found = instant_at(fields)
|
|
114
|
+
# Compared as it is rather than as a binary64: a year a script computed can
|
|
115
|
+
# be an integer with three hundred digits in it, and converting one of those
|
|
116
|
+
# to a float raises where the first engine answers absence. Nothing in this
|
|
117
|
+
# package raises, so the magnitude is compared and the answer is absence.
|
|
118
|
+
return None if abs(found) > MAX_INSTANT else found
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openscript
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: An open trading language: the engine that runs a compiled program.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
|
|
7
|
+
Project-URL: Source, https://github.com/marketcalls/openscript
|
|
8
|
+
Keywords: trading,language,interpreter,indicators,backtesting
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# openscript
|
|
16
|
+
|
|
17
|
+
The engine that runs a compiled OpenScript program, in Python.
|
|
18
|
+
|
|
19
|
+
OpenScript is an open trading language. A script is compiled to a **compiled
|
|
20
|
+
program**: plain data, an instruction list, never generated code. This package
|
|
21
|
+
runs one.
|
|
22
|
+
|
|
23
|
+
It exists so that a platform can run a strategy where a JavaScript runtime is
|
|
24
|
+
not available, which for a production trading server is the ordinary case. The
|
|
25
|
+
compiler and the first engine are the `openalgo-script` package on npm; this is
|
|
26
|
+
the second engine, and the two are held to each other by a conformance suite
|
|
27
|
+
where any disagreement is a release blocker.
|
|
28
|
+
|
|
29
|
+
## What it is
|
|
30
|
+
|
|
31
|
+
- **No compiler.** This package is handed a compiled program and never a script.
|
|
32
|
+
The program arrives as canonical text, which is what a run's hash is taken
|
|
33
|
+
over, so an engine cannot quietly run something other than what was recorded.
|
|
34
|
+
- **Nothing builds code out of text.** No string evaluator, no statement
|
|
35
|
+
executor, no import driven by hand, no object graph loaded out of bytes. That
|
|
36
|
+
is what lets a platform run many people's scripts in one process, and it is
|
|
37
|
+
enforced by a check rather than intended.
|
|
38
|
+
- **Zero dependencies.** The standard library only, and not the parts of it that
|
|
39
|
+
stop a run being reproducible. Measured on every build against the module
|
|
40
|
+
names the running interpreter says are its own.
|
|
41
|
+
|
|
42
|
+
## Installing
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install openscript
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.12 or newer. Nothing else.
|
|
49
|
+
|
|
50
|
+
## Using it
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from openscript.run import load_text
|
|
54
|
+
|
|
55
|
+
loaded = load_text(program_text, settings, library)
|
|
56
|
+
if loaded.ok:
|
|
57
|
+
result = loaded.run.execute_bar(0, bar)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A host loads a program once and pushes bars at it one at a time, keeping a
|
|
61
|
+
checkpoint so a bar that is still moving can be executed again and rolled back.
|
|
62
|
+
The conformance adapter is the other way in:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
python -m openscript --describe
|
|
66
|
+
python -m openscript <case-directory>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Where the documentation is
|
|
70
|
+
|
|
71
|
+
The specification and the guides live in the repository:
|
|
72
|
+
|
|
73
|
+
- `docs/integrating/the-python-engine.md` for what is in this package and what a
|
|
74
|
+
host needs.
|
|
75
|
+
- `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
|
|
76
|
+
the bar cycle, the rollback a moving bar rests on, and the order boundary.
|
|
77
|
+
- `spec/` for the language, the compiled program format, the standard library
|
|
78
|
+
and the conformance suite.
|
|
79
|
+
|
|
80
|
+
## Licence
|
|
81
|
+
|
|
82
|
+
Apache-2.0.
|