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,449 @@
|
|
|
1
|
+
"""One case, run on this engine, and the channels that come out of it.
|
|
2
|
+
|
|
3
|
+
This is the half of the adapter that is about the engine rather than about the
|
|
4
|
+
command line. It takes a case directory already read and the compiled program
|
|
5
|
+
the invocation was handed, loads the program, executes every bar of ``bars.csv``
|
|
6
|
+
and projects what it computed into the channels the case asserts, in the
|
|
7
|
+
encoding ``conformance.md`` section 4 gives each of them. It compares nothing.
|
|
8
|
+
|
|
9
|
+
**What this engine cannot do is named rather than guessed at, and the engine is
|
|
10
|
+
what names it.** ``compiled-program.md`` section 9.4 has a load refuse a program
|
|
11
|
+
whose capability tag the engine does not serve (OS6006), whose library entry its
|
|
12
|
+
manifest does not hold (OS6004), and whose format or language version it does not
|
|
13
|
+
implement (OS6016, OS6017). Each of those refusals carries the thing it refused,
|
|
14
|
+
by name, so the ``unsupported`` outcome section 9 asks for is read out of the
|
|
15
|
+
engine's own answer instead of being decided by a list kept here. A list kept
|
|
16
|
+
here would say what somebody believed this engine cannot do; the refusal says
|
|
17
|
+
what it actually refused.
|
|
18
|
+
|
|
19
|
+
**Every other refusal is a diagnostic and not an absence of support.** A
|
|
20
|
+
malformed program, a stored setting the engine will not run with, a budget
|
|
21
|
+
overrun, a division the script wrote: those are things this engine did, they
|
|
22
|
+
carry a code and a position, and they go into the ``diagnostics`` channel where a
|
|
23
|
+
case can assert them.
|
|
24
|
+
|
|
25
|
+
**The bars are confirmed and are not live.** A case is a run over a dataset, so
|
|
26
|
+
every execution is a new, confirmed, non-realtime bar with one update, and the
|
|
27
|
+
count supplied is the whole file: that is what makes ``bar.isLast`` true on the
|
|
28
|
+
last row and false everywhere else. An engine that only backtests is handed
|
|
29
|
+
``isRealtime`` false throughout, which is why no alert is raised here, and
|
|
30
|
+
``run.py`` reaches the same answer from the other side.
|
|
31
|
+
|
|
32
|
+
**A strategy case is the same loop with a desk beside it.** The frames are the
|
|
33
|
+
case's, delivered after the bar ``frames.csv`` names and folded before the next
|
|
34
|
+
execution; the orders are the calls step 9 applied, in the order the bar made
|
|
35
|
+
them; and the trades and the summary are folded after the last bar from the
|
|
36
|
+
fills, under the three facts ``backtest.json`` states. Nothing in that sentence
|
|
37
|
+
is this module's decision: ``ordering`` holds the boundary, ``reporting`` holds
|
|
38
|
+
what the report was folded under, and ``accounting`` holds the arithmetic. What
|
|
39
|
+
is here is the order the three are asked in.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
43
|
+
|
|
44
|
+
from ..accounting import report_of
|
|
45
|
+
from ..contracts import Bar as EngineBar, BarState
|
|
46
|
+
from ..inputs import utc_time
|
|
47
|
+
from ..run import load_text
|
|
48
|
+
from ..strategy import IntentBar
|
|
49
|
+
from ..values import ABSENT
|
|
50
|
+
from ..verify import capabilities
|
|
51
|
+
from .channels import orders_channel, performance_channel, trade_row
|
|
52
|
+
from .ordering import ORDER_ENTRIES, Desk, options_for, unfoldable
|
|
53
|
+
from .reading import Bar, Case
|
|
54
|
+
from .reporting import contract_for, marks_for, schedule_for, settings_problem
|
|
55
|
+
from .serving import Serving, is_reference
|
|
56
|
+
from .sessions import (
|
|
57
|
+
READABLE_ZONE,
|
|
58
|
+
SESSION_FACTS,
|
|
59
|
+
SESSION_FIRST,
|
|
60
|
+
first_bars,
|
|
61
|
+
session_from,
|
|
62
|
+
)
|
|
63
|
+
from .spellings import Malformed, as_reported
|
|
64
|
+
|
|
65
|
+
#: The codes a load raises when the program needs something this engine does not
|
|
66
|
+
#: have, each with the field naming what it was.
|
|
67
|
+
_UNSUPPORTED_CODES = {
|
|
68
|
+
"OS6006": ("tag", "the capability {value}"),
|
|
69
|
+
"OS6004": ("name", "the library function {value}"),
|
|
70
|
+
"OS6016": ("found", "the compiled format {value}"),
|
|
71
|
+
"OS6017": ("found", "language version {value}"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#: Section 4: the channels this engine answers today. Everything else a case may
|
|
75
|
+
#: assert is named ``unsupported`` on the case rather than answered emptily,
|
|
76
|
+
#: because an empty channel compares equal to an empty expectation and would be a
|
|
77
|
+
#: pass nobody earned.
|
|
78
|
+
ANSWERED = ("diagnostics", "values", "orders", "trades", "performance")
|
|
79
|
+
|
|
80
|
+
#: ``compiled-program.md`` 2.2's tag for a program that places orders, and the
|
|
81
|
+
#: word the meta uses for a program that is one. This engine serves the tag
|
|
82
|
+
#: because it has a ledger: what it cannot serve is refused by name at load.
|
|
83
|
+
ORDERS = "orders"
|
|
84
|
+
STRATEGY = "strategy"
|
|
85
|
+
|
|
86
|
+
#: What this engine reads out of the declaration: the five the ledger sizes an
|
|
87
|
+
#: order from, and the three the money charges a fill under, plus the capital
|
|
88
|
+
#: every figure in the report is a fraction of.
|
|
89
|
+
DECLARED: Tuple[str, ...] = (
|
|
90
|
+
"qty",
|
|
91
|
+
"qtyType",
|
|
92
|
+
"product",
|
|
93
|
+
"pyramiding",
|
|
94
|
+
"capital",
|
|
95
|
+
"commission",
|
|
96
|
+
"commissionType",
|
|
97
|
+
"slippage",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Answer:
|
|
102
|
+
"""What one case run produced: channels, and what could not be answered.
|
|
103
|
+
|
|
104
|
+
``column_types`` is the declared type of the channel behind each column of
|
|
105
|
+
``expected.csv`` that this program resolved, which is what lets a cell of
|
|
106
|
+
that file be read as the value it is rather than as the shape of its text.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
self.channels: Dict[str, Any] = {}
|
|
111
|
+
self.unsupported: List[str] = []
|
|
112
|
+
self.column_types: Dict[str, str] = {}
|
|
113
|
+
|
|
114
|
+
def cannot(self, feature: str) -> None:
|
|
115
|
+
if feature not in self.unsupported:
|
|
116
|
+
self.unsupported.append(feature)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _diagnostic_row(code: str, line: int, column: int, bar: Optional[int]) -> Dict[str, Any]:
|
|
120
|
+
"""A diagnostic in the columns section 4 compares one on, and the bar it was on."""
|
|
121
|
+
return {"code": code, "line": line, "column": column, "severity": "error", "barIndex": bar}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _refusal_row(refused: Any) -> Dict[str, Any]:
|
|
125
|
+
"""A setting refusal, as a run refused before its first bar records it.
|
|
126
|
+
|
|
127
|
+
A setting the run cannot be carried out under is refused while nothing has
|
|
128
|
+
been computed, so it carries no position in the source: the defect is in what
|
|
129
|
+
the host stated rather than in a line of the script. The message's own values
|
|
130
|
+
are not in the row, because section 4 compares a diagnostic on its code, its
|
|
131
|
+
line, its column and its severity and deliberately not on its wording.
|
|
132
|
+
"""
|
|
133
|
+
return _diagnostic_row(refused.code, refused.line, refused.column, None)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _unsupported_from(diagnostic: Any) -> Optional[str]:
|
|
137
|
+
"""The feature a load refusal names, or nothing when it is a real diagnostic."""
|
|
138
|
+
held = _UNSUPPORTED_CODES.get(diagnostic.code)
|
|
139
|
+
if held is None:
|
|
140
|
+
return None
|
|
141
|
+
field, sentence = held
|
|
142
|
+
value = diagnostic.values.get(field)
|
|
143
|
+
return sentence.format(value=value)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _engine_bar(bar: Bar) -> EngineBar:
|
|
147
|
+
"""One row of the file as the engine's own bar. An absent field stays absent."""
|
|
148
|
+
return EngineBar(
|
|
149
|
+
time=float(bar.time),
|
|
150
|
+
open=bar.open,
|
|
151
|
+
high=bar.high,
|
|
152
|
+
low=bar.low,
|
|
153
|
+
close=bar.close,
|
|
154
|
+
volume=bar.volume,
|
|
155
|
+
oi=ABSENT,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _plot_channels(program: Dict[str, Any]) -> Dict[str, int]:
|
|
160
|
+
"""Every plot's legend title and its stable key, against its channel.
|
|
161
|
+
|
|
162
|
+
``compiled-program.md`` section 11 maps ``outputs.plots[]`` and their
|
|
163
|
+
channels to one column per plot, which is what a column of ``expected.csv``
|
|
164
|
+
is. The title is what a legend shows and what an expected file names; the key
|
|
165
|
+
is accepted beside it because it is the plot's stable identity and a case may
|
|
166
|
+
reasonably have been written against either.
|
|
167
|
+
"""
|
|
168
|
+
found: Dict[str, int] = {}
|
|
169
|
+
for plot in program["outputs"]["plots"]:
|
|
170
|
+
for name in (plot["title"], plot["key"]):
|
|
171
|
+
if isinstance(name, str) and name not in found:
|
|
172
|
+
found[name] = plot["channel"]
|
|
173
|
+
return found
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _values_channel(
|
|
177
|
+
columns: Sequence[str], program: Dict[str, Any], rows: Sequence[List[Any]], answer: Answer
|
|
178
|
+
) -> List[Dict[str, Any]]:
|
|
179
|
+
"""One object per bar, holding the asserted columns and nothing else.
|
|
180
|
+
|
|
181
|
+
A column names a plot, and a plot names a channel. A column this program has
|
|
182
|
+
no plot for is not answered as absence, because absence is a value a case can
|
|
183
|
+
assert and a column nobody can resolve is a case this engine cannot run.
|
|
184
|
+
"""
|
|
185
|
+
channels = _plot_channels(program)
|
|
186
|
+
resolved: List[Tuple[str, int]] = []
|
|
187
|
+
for name in columns:
|
|
188
|
+
if name not in channels:
|
|
189
|
+
answer.cannot(
|
|
190
|
+
f"the column {name}: this program declares no plot with that title or key, so "
|
|
191
|
+
"there is no channel for it to be read from"
|
|
192
|
+
)
|
|
193
|
+
continue
|
|
194
|
+
resolved.append((name, channels[name]))
|
|
195
|
+
answer.column_types[name] = program["channels"][channels[name]]["type"]
|
|
196
|
+
out: List[Dict[str, Any]] = []
|
|
197
|
+
for row in rows:
|
|
198
|
+
held: Dict[str, Any] = {}
|
|
199
|
+
for name, channel in resolved:
|
|
200
|
+
value = row[channel] if channel < len(row) else ABSENT
|
|
201
|
+
held[name] = None if is_reference(value) else as_reported(value)
|
|
202
|
+
out.append(held)
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unreadable_zone(program: Dict[str, Any], instrument: Dict[str, Any]) -> Optional[str]:
|
|
207
|
+
"""What this program reads in a calendar this engine cannot read, or nothing.
|
|
208
|
+
|
|
209
|
+
``stdlib.md`` section 12.1 reads a calendar field in the chart's timezone,
|
|
210
|
+
which the host states; this engine carries a reader for one zone and a host
|
|
211
|
+
with any other supplies its own. A case in another zone whose script takes a
|
|
212
|
+
written time, or asks where a session begins, is therefore a case this engine
|
|
213
|
+
would answer under the wrong calendar, and it says so instead.
|
|
214
|
+
"""
|
|
215
|
+
if instrument.get("timezone") == READABLE_ZONE:
|
|
216
|
+
return None
|
|
217
|
+
if any(one["kind"] == "time" for one in program["inputs"]):
|
|
218
|
+
return "a time input"
|
|
219
|
+
if any(one["name"] in SESSION_FACTS for one in program["lib"]["functions"]):
|
|
220
|
+
return "a session boundary"
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def run_case(case: Case, program_text: str) -> Answer:
|
|
225
|
+
"""Load the program, run every bar, and project the asserted channels."""
|
|
226
|
+
answer = Answer()
|
|
227
|
+
unanswered = [channel for channel in case.asserts if channel not in ANSWERED]
|
|
228
|
+
if unanswered:
|
|
229
|
+
answer.cannot(
|
|
230
|
+
f"the {', '.join(unanswered)} channel{'' if len(unanswered) == 1 else 's'} "
|
|
231
|
+
"of section 2: this engine answers "
|
|
232
|
+
f"{' and '.join(ANSWERED)}, and every other channel belongs to a stage that is not "
|
|
233
|
+
"wired into it yet"
|
|
234
|
+
)
|
|
235
|
+
if case.ticks:
|
|
236
|
+
answer.cannot(
|
|
237
|
+
"ticks.csv (section 3): the machine re-executes a bar and rolls its state back, and "
|
|
238
|
+
"how a tick row becomes the newest bar's four prices is not written down anywhere, so "
|
|
239
|
+
"a replay here would be this adapter's invention"
|
|
240
|
+
)
|
|
241
|
+
for name in case.secondary:
|
|
242
|
+
answer.cannot(f"{name} (section 3): this engine is handed one series and reads no other")
|
|
243
|
+
if case.frames is not None and case.bars is not None:
|
|
244
|
+
beyond = unfoldable(case.frames, len(case.bars))
|
|
245
|
+
if beyond:
|
|
246
|
+
answer.cannot(
|
|
247
|
+
f"frames.csv: {len(beyond)} frame{'' if len(beyond) == 1 else 's'} delivered after "
|
|
248
|
+
f"bar {beyond[0].after_bar} of {len(case.bars)} (section 3): a frame is delivered "
|
|
249
|
+
"after a bar and folded before the next execution, and the last bar has none, so "
|
|
250
|
+
"what becomes of it is not written down anywhere"
|
|
251
|
+
)
|
|
252
|
+
if "expectedExitCode" in case.declared:
|
|
253
|
+
answer.cannot(
|
|
254
|
+
"case.json's expectedExitCode: section 2 names the field and fixes no shape for it, so "
|
|
255
|
+
"a reading here would be one no other engine has to share"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if answer.unsupported:
|
|
259
|
+
return answer
|
|
260
|
+
|
|
261
|
+
desk = Desk(case.frames or ())
|
|
262
|
+
serving = Serving(desk)
|
|
263
|
+
loaded = load_text(
|
|
264
|
+
program_text,
|
|
265
|
+
dict(case.settings),
|
|
266
|
+
serving,
|
|
267
|
+
capabilities=capabilities(ORDERS),
|
|
268
|
+
read_time=utc_time,
|
|
269
|
+
)
|
|
270
|
+
if loaded.diagnostic is not None:
|
|
271
|
+
feature = _unsupported_from(loaded.diagnostic)
|
|
272
|
+
if feature is not None:
|
|
273
|
+
answer.cannot(feature)
|
|
274
|
+
return answer
|
|
275
|
+
found = loaded.diagnostic
|
|
276
|
+
answer.channels = _with_empty(
|
|
277
|
+
case, {"diagnostics": [_diagnostic_row(found.code, found.line, found.column, None)]}
|
|
278
|
+
)
|
|
279
|
+
return answer
|
|
280
|
+
|
|
281
|
+
run = loaded.run
|
|
282
|
+
raw = run.program.raw
|
|
283
|
+
unreadable = _unreadable_zone(raw, case.instrument)
|
|
284
|
+
if unreadable is not None:
|
|
285
|
+
answer.cannot(
|
|
286
|
+
f"{unreadable} under the timezone {case.instrument.get('timezone')}: this engine reads "
|
|
287
|
+
f"a calendar as {READABLE_ZONE} and a host with another zone supplies its own reader"
|
|
288
|
+
)
|
|
289
|
+
if answer.unsupported:
|
|
290
|
+
return answer
|
|
291
|
+
|
|
292
|
+
if case.bars is None:
|
|
293
|
+
needed = [one for one in case.asserts if one != "diagnostics"]
|
|
294
|
+
if needed:
|
|
295
|
+
raise Malformed(
|
|
296
|
+
f"the case asserts {', '.join(needed)} and holds no bars.csv, which section 2 "
|
|
297
|
+
"requires of an engine case"
|
|
298
|
+
)
|
|
299
|
+
answer.channels = {"diagnostics": []}
|
|
300
|
+
return answer
|
|
301
|
+
|
|
302
|
+
trading = raw["meta"]["kind"] == STRATEGY
|
|
303
|
+
if not trading and any((one["name"], one["arity"]) in ORDER_ENTRIES for one in raw["lib"]["functions"]):
|
|
304
|
+
# The ledger is sized by the declaration a strategy carries and a study
|
|
305
|
+
# has none, so an order sent from one would be an order sized by this
|
|
306
|
+
# adapter's defaults. The compiler does not emit such a program; a file
|
|
307
|
+
# that arrives as one is refused rather than run under numbers nobody
|
|
308
|
+
# wrote.
|
|
309
|
+
raise Malformed(
|
|
310
|
+
"the program declares itself a study and calls an order function, which is a program "
|
|
311
|
+
"with orders to place and no declaration to size them from"
|
|
312
|
+
)
|
|
313
|
+
declared = _declaration(run) if trading else {}
|
|
314
|
+
schedule = None
|
|
315
|
+
if trading:
|
|
316
|
+
# Every one of the three arrives before a bar runs, and each from the file
|
|
317
|
+
# section 3 puts it in: a run under a digit count nobody stated is a run
|
|
318
|
+
# that agrees with the engine next door by coincidence.
|
|
319
|
+
desk.begin(options_for(declared, case.instrument))
|
|
320
|
+
contract = contract_for(case)
|
|
321
|
+
schedule = schedule_for(case, declared)
|
|
322
|
+
refused = settings_problem(schedule, declared, contract)
|
|
323
|
+
if refused is not None:
|
|
324
|
+
answer.channels = _with_empty(case, {"diagnostics": [_refusal_row(refused)]})
|
|
325
|
+
return answer
|
|
326
|
+
|
|
327
|
+
rows, diagnostics = _every_bar(run, serving, desk, case)
|
|
328
|
+
answered: Dict[str, Any] = {"diagnostics": diagnostics}
|
|
329
|
+
if "values" in case.asserts:
|
|
330
|
+
if not case.expected_columns:
|
|
331
|
+
raise Malformed(
|
|
332
|
+
"the case asserts values and holds no expected.csv, which section 4 says is where "
|
|
333
|
+
"one column per asserted channel and one row per bar are written"
|
|
334
|
+
)
|
|
335
|
+
answered["values"] = _values_channel(case.expected_columns, raw, rows, answer)
|
|
336
|
+
if "orders" in case.asserts:
|
|
337
|
+
answered["orders"] = orders_channel(desk.rows(), desk.intents)
|
|
338
|
+
if "trades" in case.asserts or "performance" in case.asserts:
|
|
339
|
+
report = report_of(
|
|
340
|
+
desk.fills.settled(),
|
|
341
|
+
marks_for(case, case.bars),
|
|
342
|
+
schedule,
|
|
343
|
+
contract_for(case),
|
|
344
|
+
declared.get("capital", 0.0),
|
|
345
|
+
)
|
|
346
|
+
answered["trades"] = [trade_row(one) for one in report.trades]
|
|
347
|
+
answered["performance"] = performance_channel(report.summary)
|
|
348
|
+
answer.channels = _empty_but(case, answered)
|
|
349
|
+
return answer
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _empty_but(case: Case, answered: Dict[str, Any]) -> Dict[str, Any]:
|
|
353
|
+
"""The asserted channels this engine answers, and nothing it does not."""
|
|
354
|
+
return {channel: answered[channel] for channel in case.asserts if channel in answered}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _with_empty(case: Case, answered: Dict[str, Any]) -> Dict[str, Any]:
|
|
358
|
+
"""The asserted channels a run that did not happen has nothing for, as empty lists.
|
|
359
|
+
|
|
360
|
+
A refusal before the first bar leaves every channel with nothing in it, and
|
|
361
|
+
nothing is an empty list rather than a channel left out: a case comparing an
|
|
362
|
+
absent channel and one comparing an empty one are different reports, and the
|
|
363
|
+
second is the one that says how far the run got.
|
|
364
|
+
"""
|
|
365
|
+
return {channel: answered.get(channel, []) for channel in case.asserts}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _declaration(run: Any) -> Dict[str, Any]:
|
|
369
|
+
"""The strategy declaration, with every input reference behind it resolved.
|
|
370
|
+
|
|
371
|
+
Read field by field through the run's own reader rather than off the program,
|
|
372
|
+
because ``language.md`` 13 lets a declaration state a quantity, a commission
|
|
373
|
+
or a capital from an input, and a ledger handed the reference rather than the
|
|
374
|
+
value would size every order from a shape.
|
|
375
|
+
|
|
376
|
+
The eight names below are what this engine reads out of the declaration, and
|
|
377
|
+
a program of kind strategy that states none of them is refused here rather
|
|
378
|
+
than part way through a bar: a ledger sizing an order from a field that was
|
|
379
|
+
not there would be an order nobody wrote.
|
|
380
|
+
"""
|
|
381
|
+
block = run.program.raw["meta"].get(STRATEGY)
|
|
382
|
+
if not isinstance(block, dict):
|
|
383
|
+
raise Malformed(
|
|
384
|
+
"the program declares itself a strategy and its meta holds no strategy declaration, "
|
|
385
|
+
"which compiled-program.md 2.3 says every one of them carries"
|
|
386
|
+
)
|
|
387
|
+
missing = [name for name in DECLARED if name not in block]
|
|
388
|
+
if missing:
|
|
389
|
+
raise Malformed(
|
|
390
|
+
f"the strategy declaration states no {', '.join(missing)}, which this engine reads to "
|
|
391
|
+
"size an order and to charge a fill"
|
|
392
|
+
)
|
|
393
|
+
return {name: run.declaration(("meta", STRATEGY, name)) for name in block}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _every_bar(
|
|
397
|
+
run: Any, serving: Serving, desk: Desk, case: Case
|
|
398
|
+
) -> Tuple[List[List[Any]], List[Dict[str, Any]]]:
|
|
399
|
+
"""Every bar of the file, in order, stopping at the first that fails.
|
|
400
|
+
|
|
401
|
+
A study stops at the bar that failed and reports the diagnostic: the columns
|
|
402
|
+
of the bars before it stand, and there are no columns after it. That is the
|
|
403
|
+
engine's own behaviour and not a decision here, and it is what makes a length
|
|
404
|
+
difference in the values channel the first thing a reader of a failed case
|
|
405
|
+
sees.
|
|
406
|
+
|
|
407
|
+
**The fold is before the execution and the orders are after it**, which is
|
|
408
|
+
``host-interface.md`` 7.4: a driver that folded after the bar would let a
|
|
409
|
+
script react within the bar its own order was sent in, and one that folded
|
|
410
|
+
during it would give two executions of a moving bar two different positions
|
|
411
|
+
to read. An order call refused stops the run the same way a failed bar does,
|
|
412
|
+
and takes back every row that bar had appended.
|
|
413
|
+
"""
|
|
414
|
+
bars = case.bars or []
|
|
415
|
+
supplied = len(bars)
|
|
416
|
+
when = case.declared.get("now", ABSENT)
|
|
417
|
+
opens = first_bars([bar.time for bar in bars], session_from(case.instrument))
|
|
418
|
+
rows: List[List[Any]] = []
|
|
419
|
+
for index, bar in enumerate(bars):
|
|
420
|
+
desk.fold(index, float(bar.time))
|
|
421
|
+
previous = bars[index - 1].close if index > 0 else ABSENT
|
|
422
|
+
serving.at_bar(
|
|
423
|
+
{
|
|
424
|
+
"high": bar.high,
|
|
425
|
+
"low": bar.low,
|
|
426
|
+
"close": bar.close,
|
|
427
|
+
"previousClose": previous,
|
|
428
|
+
"volume": bar.volume,
|
|
429
|
+
SESSION_FIRST: opens[index],
|
|
430
|
+
},
|
|
431
|
+
index == 0,
|
|
432
|
+
)
|
|
433
|
+
result = run.execute_bar(
|
|
434
|
+
index,
|
|
435
|
+
_engine_bar(bar),
|
|
436
|
+
BarState(is_new=True, is_confirmed=True, is_realtime=False, updates=1.0),
|
|
437
|
+
supplied=supplied,
|
|
438
|
+
instrument=case.instrument,
|
|
439
|
+
now=when,
|
|
440
|
+
)
|
|
441
|
+
if result.diagnostic is not None:
|
|
442
|
+
found = result.diagnostic
|
|
443
|
+
return rows, [_diagnostic_row(found.code, found.line, found.column, index)]
|
|
444
|
+
sent = desk.apply(result.applied, IntentBar(index=index, time=float(bar.time)))
|
|
445
|
+
if sent is not None:
|
|
446
|
+
return rows, [_diagnostic_row(sent.code, sent.line, sent.column, index)]
|
|
447
|
+
rows.append(list(result.columns))
|
|
448
|
+
desk.deliver_after(index)
|
|
449
|
+
return rows, []
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""The seam between the library's entries and the machine's ``Library`` protocol.
|
|
2
|
+
|
|
3
|
+
``contracts.py`` says what the machine asks of a library: a manifest to disagree
|
|
4
|
+
with at load, a sentence describing what this engine holds instead, and a way to
|
|
5
|
+
call one. ``library/`` answers a different shape, and two of them: a stateless
|
|
6
|
+
table whose calls take a context and the arguments, and a stateful one whose
|
|
7
|
+
calls take the call site's region as well. Nothing in the package joins them, so
|
|
8
|
+
this adapter carries the join, and it carries it here rather than inside the
|
|
9
|
+
answer so that the day a module in the package does it, one import changes and
|
|
10
|
+
nothing else does.
|
|
11
|
+
|
|
12
|
+
**Four tables and not one.** Beside the two halves of the library are the two
|
|
13
|
+
namespaces that are not in it: the ``chart`` and ``pos`` facts of ``facts.py``,
|
|
14
|
+
whose answer is the host's record and the strategy's own fills, and the nine
|
|
15
|
+
order calls of ``ordering.py``, which carry an effect and are therefore never
|
|
16
|
+
called through here at all. A name in none of the four is a name this engine's
|
|
17
|
+
manifest does not hold, and a program calling it is refused at load (OS6004)
|
|
18
|
+
naming the function and what this engine holds instead.
|
|
19
|
+
|
|
20
|
+
**Two things the machine cannot pass through, and how they are served.**
|
|
21
|
+
|
|
22
|
+
``CallContext`` carries a bar index, the instrument facts and the fixed value of
|
|
23
|
+
the clock. It does not carry the bar. ``stdlib.md`` section 6's ``trueRange``
|
|
24
|
+
reads the bar's high, its low and the previous bar's close, and section 20.5
|
|
25
|
+
grants the oldest bar the library's one exception to absence propagation, so a
|
|
26
|
+
context of three fields cannot answer it. The caller driving the bars knows all
|
|
27
|
+
three, so it states them here before each execution, and a call that arrives
|
|
28
|
+
without them is answered with absence rather than with a value read from
|
|
29
|
+
somewhere else.
|
|
30
|
+
|
|
31
|
+
``roundToTick`` asks the host for the instrument's tick, which ``CallContext``
|
|
32
|
+
does carry, so it is read from there and from nowhere else.
|
|
33
|
+
|
|
34
|
+
**The heap is not a table.** An array on this machine is the object itself
|
|
35
|
+
(``values.ArrayValue``), so making one and reading one are the two lines below
|
|
36
|
+
rather than an allocation and an index.
|
|
37
|
+
|
|
38
|
+
**A colour has two spellings in this package, and this seam is where they meet.**
|
|
39
|
+
The machine builds one shape from the constant pool and the library returns
|
|
40
|
+
another, so a colour a script computes is not the type the machine's own tag
|
|
41
|
+
function knows. That is a defect in the package rather than in either half, and
|
|
42
|
+
it is written up in the stage's report; nothing here papers over it, because a
|
|
43
|
+
seam that converted quietly would hide the one place a test can see it.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
47
|
+
|
|
48
|
+
from ..contracts import CallContext, LibraryEntry
|
|
49
|
+
from ..library import BUILDS_A_STRING, MEASURED, stateful_table, table
|
|
50
|
+
from ..library.stateless import Entry
|
|
51
|
+
from ..values import ABSENT, ArrayValue, Reference, tag
|
|
52
|
+
from .facts import FACT_NAMES, POSITION_FACTS, Book, fact_value
|
|
53
|
+
from .ordering import ORDER_ENTRIES
|
|
54
|
+
from .sessions import SESSION_FIRST
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Serving:
|
|
58
|
+
"""The library and the two namespaces that are not in it, as a ``Library``.
|
|
59
|
+
|
|
60
|
+
One instance per run. ``at_bar`` is called by whatever drives the bars,
|
|
61
|
+
before each execution, with the three facts a bar-reading call needs and no
|
|
62
|
+
context carries.
|
|
63
|
+
|
|
64
|
+
``book`` is the strategy's position book, which is the ledger. A run without
|
|
65
|
+
one serves no ``pos`` entry and no order call, so a program that places an
|
|
66
|
+
order is refused at load naming the function (OS6004) rather than running as
|
|
67
|
+
a study that quietly trades nothing.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, book: Optional[Book] = None) -> None:
|
|
71
|
+
self._entries = table()
|
|
72
|
+
self._stateful = stateful_table()
|
|
73
|
+
self._book = book
|
|
74
|
+
self._bar: Dict[str, Any] = {}
|
|
75
|
+
self._first = False
|
|
76
|
+
self._context: Optional[CallContext] = None
|
|
77
|
+
|
|
78
|
+
# -- what the caller states before a bar --------------------------------
|
|
79
|
+
|
|
80
|
+
def at_bar(self, facts: Dict[str, Any], first: bool) -> None:
|
|
81
|
+
"""The bar facts a library call reads, for the execution about to happen.
|
|
82
|
+
|
|
83
|
+
Six of them, and each is read by name: ``high``, ``low``, ``close``,
|
|
84
|
+
``previousClose``, ``volume`` and ``isSessionFirst``. The names are the
|
|
85
|
+
library's, asked for through ``bar``, and a fact the caller does not state
|
|
86
|
+
is absent rather than a value read from somewhere else: a study that
|
|
87
|
+
answered absence for every bar would be a study with a silently empty
|
|
88
|
+
line through it, and this is the one place that could happen quietly.
|
|
89
|
+
"""
|
|
90
|
+
self._bar = dict(facts)
|
|
91
|
+
self._first = first
|
|
92
|
+
|
|
93
|
+
# -- the manifest, which is what a load is held to ----------------------
|
|
94
|
+
|
|
95
|
+
def _facts(self) -> Sequence[str]:
|
|
96
|
+
"""The fact names this run can answer, which the ``pos`` half depends on."""
|
|
97
|
+
if self._book is not None:
|
|
98
|
+
return FACT_NAMES
|
|
99
|
+
return tuple(name for name in FACT_NAMES if name not in POSITION_FACTS)
|
|
100
|
+
|
|
101
|
+
def entry(self, name: str, arity: int) -> Optional[LibraryEntry]:
|
|
102
|
+
"""One manifest row, from whichever of the four tables holds the name.
|
|
103
|
+
|
|
104
|
+
The order they are asked in decides nothing: a name is in one of them,
|
|
105
|
+
because the two halves of the library are keyed apart by their own module
|
|
106
|
+
and neither namespace is a function the library holds.
|
|
107
|
+
"""
|
|
108
|
+
held = self._entries.get((name, arity)) or self._stateful.get((name, arity))
|
|
109
|
+
if held is not None:
|
|
110
|
+
return LibraryEntry(held.name, held.arity, held.state, held.effect)
|
|
111
|
+
if arity == 0 and name in self._facts():
|
|
112
|
+
return LibraryEntry(name, 0, False, "none")
|
|
113
|
+
return ORDER_ENTRIES.get((name, arity))
|
|
114
|
+
|
|
115
|
+
def _arities(self, name: str) -> Sequence[int]:
|
|
116
|
+
"""Every argument count this engine holds the name under."""
|
|
117
|
+
found = {arity for (held, arity) in self._entries if held == name}
|
|
118
|
+
found |= {arity for (held, arity) in self._stateful if held == name}
|
|
119
|
+
found |= {arity for (held, arity) in ORDER_ENTRIES if held == name}
|
|
120
|
+
if name in self._facts():
|
|
121
|
+
found.add(0)
|
|
122
|
+
return sorted(found)
|
|
123
|
+
|
|
124
|
+
def length_of(self, name: str, arguments: Sequence[Any]) -> Optional[int]:
|
|
125
|
+
"""How long the string this call will build is, before it is built.
|
|
126
|
+
|
|
127
|
+
The library says which of its calls can answer that and how, and the
|
|
128
|
+
machine spends the ceiling: a string no engine could hold is reported
|
|
129
|
+
rather than allocated. Absence for every other name, which is the honest
|
|
130
|
+
answer and not a refusal to look: a padded or a replaced string's length
|
|
131
|
+
is not known until it is built, and building it costs what it costs.
|
|
132
|
+
|
|
133
|
+
Neither namespace is here. A ``chart`` fact is the instrument record the
|
|
134
|
+
host stated and a ``pos`` fact is a number, so neither is a string this
|
|
135
|
+
bar grew.
|
|
136
|
+
"""
|
|
137
|
+
measure = MEASURED.get((name, len(arguments)))
|
|
138
|
+
return None if measure is None else measure(*arguments)
|
|
139
|
+
|
|
140
|
+
def builds_a_string(self, name: str) -> bool:
|
|
141
|
+
"""Whether this call makes the string it answers, rather than passing one on.
|
|
142
|
+
|
|
143
|
+
The ceiling is on what a script grows. A call that hands back a string
|
|
144
|
+
it was given, or one the host stated, grew nothing, and holding it to the
|
|
145
|
+
ceiling would refuse a host its own long instrument name on a script that
|
|
146
|
+
only read it.
|
|
147
|
+
"""
|
|
148
|
+
return name in BUILDS_A_STRING
|
|
149
|
+
|
|
150
|
+
def describe(self, name: str) -> str:
|
|
151
|
+
"""What this engine's manifest holds for a name, in OS6004's own words."""
|
|
152
|
+
arities = self._arities(name)
|
|
153
|
+
if not arities:
|
|
154
|
+
return f"no function called {name}"
|
|
155
|
+
spelled = " or ".join(str(one) for one in arities)
|
|
156
|
+
return f"{name} with {spelled} arguments"
|
|
157
|
+
|
|
158
|
+
# -- the call -----------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def call(
|
|
161
|
+
self,
|
|
162
|
+
name: str,
|
|
163
|
+
arguments: Sequence[Any],
|
|
164
|
+
state: Optional[Dict[str, Any]],
|
|
165
|
+
context: CallContext,
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""One ``CALL_LIB``, dispatched by the name and the argument count.
|
|
168
|
+
|
|
169
|
+
``state`` is the call site's own region and is passed to the half of the
|
|
170
|
+
library that keeps one and to nothing else: a region handed to a
|
|
171
|
+
stateless call would be a region the load-time check should have refused,
|
|
172
|
+
and a stateful call reaching this engine without one is the same
|
|
173
|
+
disagreement from the other side, so it answers absence rather than
|
|
174
|
+
making a region of its own that no rollback would ever restore.
|
|
175
|
+
|
|
176
|
+
An order call never arrives here. Its manifest row carries an effect, so
|
|
177
|
+
the machine holds it until step 9 and hands it to the ledger, which is
|
|
178
|
+
``compiled-program.md`` 5.4 and the whole reason a strategy is
|
|
179
|
+
reproducible on a moving bar.
|
|
180
|
+
"""
|
|
181
|
+
self._context = context
|
|
182
|
+
held: Optional[Entry] = self._entries.get((name, len(arguments)))
|
|
183
|
+
if held is not None:
|
|
184
|
+
return held.call(self, list(arguments))
|
|
185
|
+
keeping = self._stateful.get((name, len(arguments)))
|
|
186
|
+
if keeping is not None:
|
|
187
|
+
return ABSENT if state is None else keeping.call(self, list(arguments), state)
|
|
188
|
+
if len(arguments) == 0 and name in self._facts():
|
|
189
|
+
return fact_value(
|
|
190
|
+
name, self._instrument(), self._now(), self._book, self._bar.get(SESSION_FIRST, ABSENT)
|
|
191
|
+
)
|
|
192
|
+
return ABSENT
|
|
193
|
+
|
|
194
|
+
# -- the six members a stateless call may ask for -----------------------
|
|
195
|
+
|
|
196
|
+
def bar(self, fact: str) -> Any:
|
|
197
|
+
return self._bar.get(fact, ABSENT)
|
|
198
|
+
|
|
199
|
+
def host(self, fact: str) -> Any:
|
|
200
|
+
if self._context is None:
|
|
201
|
+
return ABSENT
|
|
202
|
+
return self._context.instrument.get(fact, ABSENT)
|
|
203
|
+
|
|
204
|
+
def first_bar(self) -> bool:
|
|
205
|
+
return self._first
|
|
206
|
+
|
|
207
|
+
def _instrument(self) -> Any:
|
|
208
|
+
"""The record the host stated, which is what a ``chart`` fact is read from."""
|
|
209
|
+
return {} if self._context is None else self._context.instrument
|
|
210
|
+
|
|
211
|
+
def _now(self) -> Any:
|
|
212
|
+
"""The clock's fixed value, which a case states and no engine reads."""
|
|
213
|
+
return ABSENT if self._context is None else self._context.now
|
|
214
|
+
|
|
215
|
+
def kind_of(self, reference: Any) -> str:
|
|
216
|
+
return tag(reference)
|
|
217
|
+
|
|
218
|
+
def items_of(self, reference: Any) -> Optional[List[Any]]:
|
|
219
|
+
if isinstance(reference, ArrayValue):
|
|
220
|
+
return list(reference.elements)
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def make_array(self, items: List[Any]) -> Any:
|
|
224
|
+
return ArrayValue(list(items))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_reference(value: Any) -> bool:
|
|
228
|
+
"""Whether a value is one of the heap's, which a channel never carries out."""
|
|
229
|
+
return isinstance(value, Reference)
|