openscript 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ """What ``spec/conformance.md`` fixes, carried here because the engine reads no page.
2
+
3
+ Every value below is printed in that document, and a copy is a fact stated twice
4
+ unless something holds the two equal. So the arrangement is the one section 6
5
+ already uses for the tolerance caps, in the page's own words: the figures "are
6
+ read out of this section by a test and held to the constants the projection
7
+ carries, because the engine reads no page". ``tests/test_adapter_page.py`` is
8
+ that test here. It reads the document and fails the build the day a value below
9
+ stops being the page's.
10
+
11
+ The reason the constants are not simply read at run time is the same reason
12
+ ``version.py`` next door carries two numbers rather than reading them: a package
13
+ a host installs does not carry the specification, and an adapter that could only
14
+ answer with the repository beside it would be an adapter nobody outside this
15
+ tree could run.
16
+
17
+ Nothing here is a default this file chose. Section 3's instrument block is that
18
+ section's, "chosen to be boring rather than realistic"; the channels are section
19
+ 2's ``asserts`` vocabulary; the outcomes and the profiles are sections 9 and 8.
20
+ """
21
+
22
+ from typing import Any, Dict, Tuple
23
+
24
+ #: Section 2: the file names a case directory may hold. A file the table does
25
+ #: not name is not input, so a directory holding one is malformed rather than
26
+ #: read past.
27
+ CASE_FILES: Tuple[str, ...] = (
28
+ "case.json",
29
+ "script.os",
30
+ "bars.csv",
31
+ "expected.csv",
32
+ "expected.json",
33
+ "instrument.json",
34
+ "settings.json",
35
+ "backtest.json",
36
+ "ticks.csv",
37
+ "frames.csv",
38
+ "notes.md",
39
+ )
40
+
41
+ #: The one row of that table written with a name in it: a secondary series, one
42
+ #: file per series the script asks for.
43
+ SECONDARY_PREFIX = "bars."
44
+ SECONDARY_SUFFIX = ".csv"
45
+
46
+ #: Section 2: the channels a case may assert. A case asserts the ones it names
47
+ #: and no others, which is what keeps a change to one output from failing a case
48
+ #: about something else.
49
+ CHANNELS: Tuple[str, ...] = (
50
+ "diagnostics",
51
+ "values",
52
+ "markers",
53
+ "fills",
54
+ "levels",
55
+ "barColors",
56
+ "background",
57
+ "table",
58
+ "drawings",
59
+ "alerts",
60
+ "orders",
61
+ "trades",
62
+ "performance",
63
+ "log",
64
+ )
65
+
66
+ #: Section 9: the outcomes a case result may carry. The runner refuses one this
67
+ #: list does not name, and so does this adapter's own test.
68
+ OUTCOMES: Tuple[str, ...] = ("pass", "fail", "nonFinite", "error", "unsupported", "skipped")
69
+
70
+ #: Section 8: the profiles, in the order they include one another.
71
+ PROFILES: Tuple[str, ...] = ("core", "chart", "strategy")
72
+
73
+ #: Section 3: the header of ``bars.csv``, in this order. An extra column is an
74
+ #: error rather than ignored, so a typo in a header cannot drop an input.
75
+ BARS_HEADER: Tuple[str, ...] = ("time", "open", "high", "low", "close", "volume")
76
+
77
+ #: Section 3: the fields of one row of ``frames.csv``, the last three optional
78
+ #: and dropped from the right, so a header the page allows is a prefix of this.
79
+ FRAMES_HEADER: Tuple[str, ...] = (
80
+ "afterBar",
81
+ "intent",
82
+ "status",
83
+ "filledQty",
84
+ "avgFillPrice",
85
+ "orderRef",
86
+ "text",
87
+ "time",
88
+ )
89
+
90
+ #: Section 4: how an absent value is written in a case file, in every column of
91
+ #: every one of them.
92
+ ABSENT_TEXT = "none"
93
+
94
+ #: Section 6: the loosest tolerance that is still a conformance case. A case
95
+ #: past either bound is reported ``error`` rather than run.
96
+ TOLERANCE_CAP_ABS = 1e-12
97
+ TOLERANCE_CAP_REL = 1e-9
98
+
99
+ #: Section 3: the instrument facts a case that states no ``instrument.json``
100
+ #: runs under.
101
+ DEFAULT_INSTRUMENT: Dict[str, Any] = {
102
+ "symbol": "TEST",
103
+ "exchange": "TEST",
104
+ "interval": "60",
105
+ "timezone": "UTC",
106
+ "tickSize": 0.01,
107
+ "lotSize": 1,
108
+ "hasVolume": True,
109
+ "session": {"start": "00:00", "end": "24:00", "days": [1, 2, 3, 4, 5, 6, 7]},
110
+ }
111
+
112
+ #: Section 3: the fields of ``backtest.json``, all three always stated.
113
+ BACKTEST_FIELDS: Tuple[str, ...] = ("digits", "costs", "range")
114
+
115
+
116
+ def is_case_file(name: str) -> bool:
117
+ """Whether section 2's table names this file, including a secondary series."""
118
+ if name in CASE_FILES:
119
+ return True
120
+ return (
121
+ name.startswith(SECONDARY_PREFIX)
122
+ and name.endswith(SECONDARY_SUFFIX)
123
+ and len(name) > len(SECONDARY_PREFIX) + len(SECONDARY_SUFFIX)
124
+ and name != "bars.csv"
125
+ )
126
+
127
+
128
+ def is_secondary(name: str) -> bool:
129
+ """Whether this file is a secondary series rather than the case's own bars."""
130
+ return name != "bars.csv" and is_case_file(name) and name.startswith(SECONDARY_PREFIX)
@@ -0,0 +1,357 @@
1
+ """One case directory, read by the names ``conformance.md`` section 2 gives it.
2
+
3
+ Section 2's table "is the whole of a case directory. A runner reads no other
4
+ file from it, and a file the table does not name is not input", so a directory
5
+ holding a file the table does not name is refused rather than read past: a case
6
+ that needed something no row covers is a case the suite cannot run until the row
7
+ exists, and an adapter that quietly ignored the file would run it under
8
+ different input from the engine next door.
9
+
10
+ Every refusal here is one sentence naming the file and what was wrong with it,
11
+ and every one of them reaches the caller as ``error`` (section 9): a malformed
12
+ case is not a failure of the engine, and reporting it as one would put the blame
13
+ on whoever is running the suite rather than on whoever wrote the directory.
14
+
15
+ **Nothing here is computed.** Bars are the file's, settings are the file's,
16
+ instrument facts are the file's or, when the file is absent, section 3's default
17
+ set carried in ``page.py``. That is the reason a case is reproducible on a
18
+ machine that has never seen this repository.
19
+ """
20
+
21
+ import csv
22
+ import io
23
+ import json
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
27
+
28
+ from .page import (
29
+ ABSENT_TEXT,
30
+ BACKTEST_FIELDS,
31
+ BARS_HEADER,
32
+ CHANNELS,
33
+ DEFAULT_INSTRUMENT,
34
+ FRAMES_HEADER,
35
+ PROFILES,
36
+ is_case_file,
37
+ is_secondary,
38
+ )
39
+ from .spellings import Malformed, read_number, read_whole
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Bar:
44
+ """One row of ``bars.csv``, with an absent field held as absence."""
45
+
46
+ time: int
47
+ open: Optional[float]
48
+ high: Optional[float]
49
+ low: Optional[float]
50
+ close: Optional[float]
51
+ volume: Optional[float]
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Frame:
56
+ """One row of ``frames.csv``, with the boundary it is delivered at.
57
+
58
+ ``intent`` is an ordinal and not an id: 1 is the first intent the run placed.
59
+ A case cannot know the id an engine minted and must not depend on its
60
+ spelling, so the ordinal is what a case names an order by and the driver maps
61
+ it to whatever this engine minted.
62
+ """
63
+
64
+ after_bar: int
65
+ intent: int
66
+ status: str
67
+ filled_qty: float
68
+ avg_fill_price: Optional[float]
69
+ order_ref: str
70
+ text: str
71
+ #: The destination's own instant for this frame, absent where it stated
72
+ #: none. ``stdlib.md`` 17.7 folds a row's ``updatedAt`` from it, so a reader
73
+ #: that dropped this column would hand the ledger frames that leave that
74
+ #: field where the placement put it, whatever the case says its destination
75
+ #: did. Defaulted because a file written before the column existed is a file
76
+ #: whose frames state no instant.
77
+ time: Optional[float] = None
78
+
79
+
80
+ @dataclass
81
+ class Case:
82
+ """A case directory, read. Every field is a file's, or the page's default."""
83
+
84
+ directory: Path
85
+ declared: Dict[str, Any]
86
+ script: str
87
+ bars: Optional[List[Bar]] = None
88
+ expected_columns: Tuple[str, ...] = ()
89
+ expected_rows: List[List[str]] = field(default_factory=list)
90
+ expected_json: Optional[Dict[str, Any]] = None
91
+ instrument: Dict[str, Any] = field(default_factory=dict)
92
+ stated_instrument: bool = False
93
+ settings: Dict[str, Any] = field(default_factory=dict)
94
+ backtest: Optional[Dict[str, Any]] = None
95
+ ticks: bool = False
96
+ frames: Optional[Tuple[Frame, ...]] = None
97
+ secondary: Tuple[str, ...] = ()
98
+
99
+ @property
100
+ def identity(self) -> str:
101
+ return str(self.declared.get("id"))
102
+
103
+ @property
104
+ def asserts(self) -> Tuple[str, ...]:
105
+ return tuple(self.declared["asserts"])
106
+
107
+
108
+ def _text_of(path: Path) -> str:
109
+ try:
110
+ return path.read_text(encoding="utf-8")
111
+ except OSError as reason:
112
+ raise Malformed(f"{path.name} could not be read: {reason}") from None
113
+
114
+
115
+ def _json_of(path: Path) -> Any:
116
+ try:
117
+ return json.loads(_text_of(path))
118
+ except ValueError as reason:
119
+ raise Malformed(f"{path.name} is not JSON: {reason}") from None
120
+
121
+
122
+ def _rows_of(text: str, name: str) -> List[List[str]]:
123
+ """A comma separated file as rows, with the blank lines refused not dropped.
124
+
125
+ Section 3 says no blank lines, so one is a defect in the file rather than
126
+ something to skip: a dropped row and an empty row look the same afterwards,
127
+ and the whole reason the expected file carries a bar index is that a dropped
128
+ row is caught where it was dropped.
129
+ """
130
+ rows = [row for row in csv.reader(io.StringIO(text))]
131
+ while rows and rows[-1] == []:
132
+ rows.pop()
133
+ for at, row in enumerate(rows):
134
+ if row == []:
135
+ raise Malformed(f"{name} line {at + 1} is blank, and a case file holds no blank line")
136
+ if not rows:
137
+ raise Malformed(f"{name} is empty, and a case file that holds nothing is not input")
138
+ return rows
139
+
140
+
141
+ def _header_held(row: Sequence[str], wanted: Sequence[str], name: str) -> None:
142
+ if tuple(row) != tuple(wanted):
143
+ raise Malformed(
144
+ f"{name} has the header {','.join(row)} and section 3 gives it {','.join(wanted)}. "
145
+ "An extra or missing column is an error rather than ignored, so a typo in a header "
146
+ "cannot silently drop an input"
147
+ )
148
+
149
+
150
+ def read_bars(text: str, name: str = "bars.csv") -> List[Bar]:
151
+ """``bars.csv``: the header section 3 prints, then one row per bar."""
152
+ rows = _rows_of(text, name)
153
+ _header_held(rows[0], BARS_HEADER, name)
154
+ bars: List[Bar] = []
155
+ for at, row in enumerate(rows[1:]):
156
+ where = f"{name} line {at + 2}"
157
+ if len(row) != len(BARS_HEADER):
158
+ raise Malformed(f"{where} holds {len(row)} fields and the header names {len(BARS_HEADER)}")
159
+ time = read_whole(row[0], f"{where}, time")
160
+ prices = [
161
+ None if cell == ABSENT_TEXT else read_number(cell, f"{where}, {BARS_HEADER[which + 1]}")
162
+ for which, cell in enumerate(row[1:])
163
+ ]
164
+ if bars and time <= bars[-1].time:
165
+ raise Malformed(
166
+ f"{where} is at {time} and the bar before it is at {bars[-1].time}. A bar's open "
167
+ "time is strictly increasing, and a run over bars in another order is not the run "
168
+ "the expected file came from"
169
+ )
170
+ bars.append(Bar(time, prices[0], prices[1], prices[2], prices[3], prices[4]))
171
+ if not bars:
172
+ raise Malformed(f"{name} holds a header and no bar")
173
+ return bars
174
+
175
+
176
+ def read_frames(text: str, name: str = "frames.csv") -> Tuple[Frame, ...]:
177
+ """``frames.csv``: one row per frame, in the order the destination sent them.
178
+
179
+ Section 3 gives the header and makes the last three columns optional, dropped
180
+ from the right, and "an omitted column is absent on every row". An extra
181
+ column is an error, as in ``bars.csv``, and so is a column out of order: the
182
+ fields are read by position, and a file that named them in another order
183
+ would be folded into another ledger without anything saying so.
184
+
185
+ Nothing is sorted. "Several rows may name one bar and are delivered in file
186
+ order, which is how a case orders two frames that cross", so the file's order
187
+ is the delivery order and this reader is the last place it could be lost.
188
+ """
189
+ rows = _rows_of(text, name)
190
+ header = tuple(rows[0])
191
+ if header != FRAMES_HEADER[: len(header)]:
192
+ raise Malformed(
193
+ f"{name} has the header {','.join(header)} and section 3 gives it "
194
+ f"{','.join(FRAMES_HEADER)}, of which only the last three columns may be left out"
195
+ )
196
+ found: List[Frame] = []
197
+ for at, row in enumerate(rows[1:]):
198
+ where = f"{name} line {at + 2}"
199
+ if len(row) != len(header):
200
+ raise Malformed(f"{where} holds {len(row)} fields and the header names {len(header)}")
201
+ cell = dict(zip(header, row))
202
+ price = cell["avgFillPrice"]
203
+ instant = cell.get("time", ABSENT_TEXT)
204
+ found.append(
205
+ Frame(
206
+ after_bar=read_whole(cell["afterBar"], f"{where}, afterBar"),
207
+ intent=read_whole(cell["intent"], f"{where}, intent"),
208
+ status=cell["status"],
209
+ filled_qty=read_number(cell["filledQty"], f"{where}, filledQty"),
210
+ avg_fill_price=(
211
+ None if price == ABSENT_TEXT else read_number(price, f"{where}, avgFillPrice")
212
+ ),
213
+ order_ref=cell.get("orderRef", ""),
214
+ text=cell.get("text", ""),
215
+ time=(
216
+ None if instant == ABSENT_TEXT else read_number(instant, f"{where}, time")
217
+ ),
218
+ )
219
+ )
220
+ return tuple(found)
221
+
222
+
223
+ def _read_expected_csv(text: str) -> Tuple[Tuple[str, ...], List[List[str]]]:
224
+ """``expected.csv``: the bar index, then one column per asserted channel.
225
+
226
+ The ``bar`` column is redundant on purpose and is held to the row position
227
+ here, which is section 4's own reason for it: a dropped row is caught at the
228
+ row it was dropped at rather than at the end.
229
+ """
230
+ rows = _rows_of(text, "expected.csv")
231
+ header = rows[0]
232
+ if not header or header[0] != "bar":
233
+ raise Malformed("expected.csv: the first column is the zero-based bar index, named bar")
234
+ for at, row in enumerate(rows[1:]):
235
+ where = f"expected.csv line {at + 2}"
236
+ if len(row) != len(header):
237
+ raise Malformed(f"{where} holds {len(row)} fields and the header names {len(header)}")
238
+ if read_whole(row[0], f"{where}, bar") != at:
239
+ raise Malformed(
240
+ f"{where} says it is bar {row[0]} and it is at row {at}. The index is redundant so "
241
+ "that a dropped row is caught where it was dropped"
242
+ )
243
+ return tuple(header[1:]), [row[1:] for row in rows[1:]]
244
+
245
+
246
+ def _instrument_from(held: Any) -> Dict[str, Any]:
247
+ if not isinstance(held, dict):
248
+ raise Malformed("instrument.json is not an object of instrument facts")
249
+ if not isinstance(held.get("hasVolume"), bool):
250
+ raise Malformed(
251
+ "instrument.json states no hasVolume. host-interface.md 4.1 requires the volume flag "
252
+ "of every host, and it is the one fact an engine does not refuse a run without, so a "
253
+ "file that omits it would hand the engine a study the expected output did not come from"
254
+ )
255
+ return dict(held)
256
+
257
+
258
+ def _backtest_from(held: Any) -> Dict[str, Any]:
259
+ """``backtest.json``, held to the three fields section 3 names and their shapes."""
260
+ if not isinstance(held, dict):
261
+ raise Malformed("backtest.json is not an object")
262
+ for key in held:
263
+ if key not in BACKTEST_FIELDS:
264
+ raise Malformed(f"backtest.json states {key}, which section 3 does not name")
265
+ for key in BACKTEST_FIELDS:
266
+ if key not in held:
267
+ raise Malformed(f"backtest.json states no {key}, which section 3 requires")
268
+ digits = held["digits"]
269
+ if not isinstance(digits, int) or isinstance(digits, bool) or digits < 0:
270
+ raise Malformed("backtest.json: digits is a whole number of decimal places")
271
+ costs = held["costs"]
272
+ if costs is not None and not isinstance(costs, dict):
273
+ raise Malformed("backtest.json: costs is a charge schedule or null")
274
+ window = held["range"]
275
+ if not isinstance(window, dict):
276
+ raise Malformed("backtest.json: range is an object of two bounds")
277
+ for bound in ("from", "to"):
278
+ value = window.get(bound)
279
+ if value is not None and (not isinstance(value, int) or isinstance(value, bool)):
280
+ raise Malformed(f"backtest.json: range.{bound} is a whole number of milliseconds or null")
281
+ return dict(held)
282
+
283
+
284
+ def _declared_from(held: Any, directory: Path) -> Dict[str, Any]:
285
+ """``case.json``, held to the fields section 2's table requires of every case."""
286
+ if not isinstance(held, dict):
287
+ raise Malformed("case.json is not an object")
288
+ identity = held.get("id")
289
+ if not isinstance(identity, str) or identity == "":
290
+ raise Malformed("case.json states no id, and section 2 says the id is the directory path")
291
+ walked = directory.as_posix().rstrip("/")
292
+ if not walked.endswith(identity):
293
+ raise Malformed(
294
+ f"case.json says its id is {identity!r} and it was read from {walked}. The id is "
295
+ "duplicated on purpose so that a moved directory is caught"
296
+ )
297
+ if held.get("profile") not in PROFILES:
298
+ raise Malformed(f"case.json names the profile {held.get('profile')!r}, which section 8 does not list")
299
+ version = held.get("languageVersion")
300
+ if not isinstance(version, int) or isinstance(version, bool):
301
+ raise Malformed("case.json states no languageVersion, and a case always pins one")
302
+ asserted = held.get("asserts")
303
+ if not isinstance(asserted, list) or asserted == []:
304
+ raise Malformed("case.json asserts nothing, and a case that asserts nothing cannot fail")
305
+ for channel in asserted:
306
+ if channel not in CHANNELS:
307
+ raise Malformed(f"case.json asserts {channel!r}, which section 2 does not list")
308
+ return dict(held)
309
+
310
+
311
+ def read_case(directory: str) -> Case:
312
+ """Every file section 2's table names, from one directory, read once."""
313
+ path = Path(directory)
314
+ if not path.is_dir():
315
+ raise Malformed(f"{directory} is not a directory, and one case is one directory")
316
+ names = sorted(one.name for one in path.iterdir() if one.is_file())
317
+ for name in names:
318
+ if not is_case_file(name):
319
+ raise Malformed(
320
+ f"{name} is not a file section 2's table names, so it is not input. A case that "
321
+ "needs something no row covers is a case the suite cannot run until the row exists"
322
+ )
323
+ held = {name: path / name for name in names}
324
+ if "case.json" not in held:
325
+ raise Malformed("the directory holds no case.json, which section 2 requires of every case")
326
+ if "script.os" not in held:
327
+ raise Malformed("the directory holds no script.os, which section 2 requires of every case")
328
+
329
+ declared = _declared_from(_json_of(held["case.json"]), path)
330
+ case = Case(directory=path, declared=declared, script=_text_of(held["script.os"]))
331
+
332
+ if "bars.csv" in held:
333
+ case.bars = read_bars(_text_of(held["bars.csv"]))
334
+ if "expected.csv" in held:
335
+ case.expected_columns, case.expected_rows = _read_expected_csv(_text_of(held["expected.csv"]))
336
+ if "expected.json" in held:
337
+ expected = _json_of(held["expected.json"])
338
+ if not isinstance(expected, dict):
339
+ raise Malformed("expected.json is not an object of channels")
340
+ case.expected_json = expected
341
+ if "instrument.json" in held:
342
+ case.instrument = _instrument_from(_json_of(held["instrument.json"]))
343
+ case.stated_instrument = True
344
+ else:
345
+ case.instrument = dict(DEFAULT_INSTRUMENT)
346
+ if "settings.json" in held:
347
+ settings = _json_of(held["settings.json"])
348
+ if not isinstance(settings, dict):
349
+ raise Malformed("settings.json is not an object of input values by name")
350
+ case.settings = settings
351
+ if "backtest.json" in held:
352
+ case.backtest = _backtest_from(_json_of(held["backtest.json"]))
353
+ case.ticks = "ticks.csv" in held
354
+ if "frames.csv" in held:
355
+ case.frames = read_frames(_text_of(held["frames.csv"]))
356
+ case.secondary = tuple(name for name in names if is_secondary(name))
357
+ return case