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,67 @@
|
|
|
1
|
+
"""What a case expects, read out of its own files into one mapping by channel.
|
|
2
|
+
|
|
3
|
+
``conformance.md`` section 4 splits the expectation across two files for a reason
|
|
4
|
+
it gives: ``expected.csv`` is one value per bar and one column per asserted
|
|
5
|
+
channel, and ``expected.json`` is everything that is not, each channel an ordered
|
|
6
|
+
list of flat objects. The comparison wants one shape, so both are read here into
|
|
7
|
+
the shape the answer already has, and a channel the case asserts that neither
|
|
8
|
+
file holds is a malformed case rather than a failure of the engine.
|
|
9
|
+
|
|
10
|
+
**A cell is read as the channel's own type, and the case file does not carry
|
|
11
|
+
one.** ``expected.csv`` has a header of column names and no types, so the four
|
|
12
|
+
letters ``true`` in a string column and the bool are the same eight bytes on
|
|
13
|
+
disk. The type comes from the compiled program's channel table
|
|
14
|
+
(``compiled-program.md`` section 2.7), which the run has already resolved, and it
|
|
15
|
+
is passed in here rather than guessed from the shape of the text: a guess would
|
|
16
|
+
read a string channel that emitted ``true`` as a bool and report a difference
|
|
17
|
+
that is in the reader rather than in either engine.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
|
21
|
+
|
|
22
|
+
from .reading import Case
|
|
23
|
+
from .spellings import Malformed, read_cell
|
|
24
|
+
|
|
25
|
+
#: Section 4: the channel whose expectation is columnar, and the file it is in.
|
|
26
|
+
COLUMNAR = "values"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def values_expected(
|
|
30
|
+
columns: Sequence[str], rows: Sequence[Sequence[str]], types: Mapping[str, str]
|
|
31
|
+
) -> list:
|
|
32
|
+
"""``expected.csv`` as one flat object per bar, in the answer's own shape."""
|
|
33
|
+
out = []
|
|
34
|
+
for at, row in enumerate(rows):
|
|
35
|
+
held: Dict[str, Any] = {}
|
|
36
|
+
for which, name in enumerate(columns):
|
|
37
|
+
kind = types.get(name)
|
|
38
|
+
if kind is None:
|
|
39
|
+
raise Malformed(
|
|
40
|
+
f"expected.csv names the column {name} and this program has no channel for it"
|
|
41
|
+
)
|
|
42
|
+
held[name] = read_cell(row[which], kind, f"expected.csv line {at + 2}, {name}")
|
|
43
|
+
out.append(held)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def expected_channels(
|
|
48
|
+
case: Case, types: Mapping[str, str]
|
|
49
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
50
|
+
"""Every asserted channel as the case holds it, or the sentence refusing it."""
|
|
51
|
+
found: Dict[str, Any] = {}
|
|
52
|
+
for channel in case.asserts:
|
|
53
|
+
if channel == COLUMNAR:
|
|
54
|
+
if not case.expected_columns:
|
|
55
|
+
return None, "the case asserts values and holds no expected.csv (section 4)"
|
|
56
|
+
found[channel] = values_expected(case.expected_columns, case.expected_rows, types)
|
|
57
|
+
continue
|
|
58
|
+
if case.expected_json is None or channel not in case.expected_json:
|
|
59
|
+
return None, (
|
|
60
|
+
f"expected.json holds no {channel}, which case.json asserts. A case asserts the "
|
|
61
|
+
"channels it names, and one it names with nothing to compare against is malformed"
|
|
62
|
+
)
|
|
63
|
+
held = case.expected_json[channel]
|
|
64
|
+
if not isinstance(held, list):
|
|
65
|
+
return None, f"expected.json's {channel} is not an ordered list (section 4)"
|
|
66
|
+
found[channel] = held
|
|
67
|
+
return found, None
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The two namespaces whose answer is the host's or the ledger's, not an argument's.
|
|
2
|
+
|
|
3
|
+
``stdlib.md`` 3.4 is the ``chart`` namespace, 17.4 is the ``pos`` namespace and
|
|
4
|
+
12.4 is the session's one derived entry. None of the three is in ``library/``,
|
|
5
|
+
and that is the library's decision rather than an omission: every function there
|
|
6
|
+
computes its answer from its arguments and its own region, and these read the
|
|
7
|
+
instrument record the host stated, the fills this strategy settled and the
|
|
8
|
+
boundary ``sessions.py`` derives. All of those arrive at this seam and nowhere
|
|
9
|
+
else, so the manifest rows for them are here, beside the join that can answer
|
|
10
|
+
them.
|
|
11
|
+
|
|
12
|
+
**A fact nobody stated is absent**, which is ``host-interface.md`` 4.1's column
|
|
13
|
+
for every entry but one: ``chart.symbol`` is the empty string, because 3.4 says
|
|
14
|
+
so. A default anywhere else would make "the smallest increment is five paise"
|
|
15
|
+
and "nobody said" the same reading.
|
|
16
|
+
|
|
17
|
+
**The two derived facts are not here.** ``chart.intervalMinutes`` and
|
|
18
|
+
``chart.isIntraday`` are computed from the interval string by section 15.2's
|
|
19
|
+
canonical form, which this engine does not read yet, so a program calling one is
|
|
20
|
+
refused at load by name (OS6004) rather than answered from a guess at what the
|
|
21
|
+
string meant. The planned entries of both tables are absent for the same reason:
|
|
22
|
+
a name the language does not serve yet is a refusal naming it.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any, Dict, Optional, Protocol, Tuple
|
|
26
|
+
|
|
27
|
+
from ..values import ABSENT
|
|
28
|
+
from .sessions import SESSION_FACTS
|
|
29
|
+
|
|
30
|
+
#: Section 3.4 against ``host-interface.md`` 4.1: the call, and the fact of the
|
|
31
|
+
#: instrument record it reads. Every one is arity 0, holds no state and has no
|
|
32
|
+
#: effect, which is what the manifest check holds a program's own table to.
|
|
33
|
+
CHART_FACTS: Dict[str, str] = {
|
|
34
|
+
"chart.symbol": "symbol",
|
|
35
|
+
"chart.exchange": "exchange",
|
|
36
|
+
"chart.interval": "interval",
|
|
37
|
+
"chart.timezone": "timezone",
|
|
38
|
+
"chart.tickSize": "tickSize",
|
|
39
|
+
"chart.lotSize": "lotSize",
|
|
40
|
+
"chart.pointValue": "pointValue",
|
|
41
|
+
"chart.currency": "currency",
|
|
42
|
+
"chart.instrumentType": "instrumentType",
|
|
43
|
+
"chart.hasVolume": "hasVolume",
|
|
44
|
+
"chart.hasOpenInterest": "hasOpenInterest",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#: The one entry of that table whose absent case is a value rather than absence.
|
|
48
|
+
EMPTY_WHEN_UNSTATED: Tuple[str, ...] = ("chart.symbol",)
|
|
49
|
+
|
|
50
|
+
#: Section 3.4: the clock, whose value a case fixes so that a script using it is
|
|
51
|
+
#: still reproducible. It is the one entry of the namespace that is not a fact of
|
|
52
|
+
#: the instrument.
|
|
53
|
+
CHART_CLOCK = "chart.now"
|
|
54
|
+
|
|
55
|
+
#: Section 17.4: the entries of the ``pos`` namespace that are not planned. Each
|
|
56
|
+
#: is folded from this strategy's own fills and reflects fills rather than
|
|
57
|
+
#: intentions, which is the position book's whole job.
|
|
58
|
+
POSITION_FACTS: Tuple[str, ...] = (
|
|
59
|
+
"pos.size",
|
|
60
|
+
"pos.isLong",
|
|
61
|
+
"pos.isShort",
|
|
62
|
+
"pos.isFlat",
|
|
63
|
+
"pos.avgPrice",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Book(Protocol):
|
|
68
|
+
"""What a position fact is read from, and the whole of what it is asked.
|
|
69
|
+
|
|
70
|
+
The ledger answers it. Two methods rather than the ledger itself, so that
|
|
71
|
+
this module states what a reading of the ``pos`` namespace reaches: the
|
|
72
|
+
settled position and nothing about the orders that made it.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def size(self) -> float:
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def avg_price(self) -> Optional[float]:
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def chart_value(name: str, instrument: Any, now: Any) -> Any:
|
|
83
|
+
"""One ``chart`` entry, from the record the host stated and the fixed clock."""
|
|
84
|
+
if name == CHART_CLOCK:
|
|
85
|
+
return now
|
|
86
|
+
held = instrument.get(CHART_FACTS[name], ABSENT)
|
|
87
|
+
if held is ABSENT and name in EMPTY_WHEN_UNSTATED:
|
|
88
|
+
return ""
|
|
89
|
+
return held
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def position_value(name: str, book: Book) -> Any:
|
|
93
|
+
"""One ``pos`` entry, from the ledger's own position book.
|
|
94
|
+
|
|
95
|
+
``pos.size`` is ``0`` while flat rather than absent, because zero is the true
|
|
96
|
+
size and a script adding it to something should get the right answer.
|
|
97
|
+
``pos.avgPrice`` is absent while flat rather than zero, because zero is a
|
|
98
|
+
price and a script comparing against it would take a branch that looks
|
|
99
|
+
correct. Section 17.4 states both, and the difference is the reason the two
|
|
100
|
+
are not one reading with a default.
|
|
101
|
+
"""
|
|
102
|
+
if name == "pos.avgPrice":
|
|
103
|
+
return book.avg_price()
|
|
104
|
+
size = book.size()
|
|
105
|
+
if name == "pos.size":
|
|
106
|
+
return size
|
|
107
|
+
if name == "pos.isLong":
|
|
108
|
+
return size > 0
|
|
109
|
+
if name == "pos.isShort":
|
|
110
|
+
return size < 0
|
|
111
|
+
return size == 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
#: Every name this module answers. The manifest rows built from it are all arity
|
|
115
|
+
#: 0, hold no state and have no effect, which is what section 2.5's check holds a
|
|
116
|
+
#: program's own library table to. The session's own entry is beside them because
|
|
117
|
+
#: it is read the same way and answered from the bar rather than from a record.
|
|
118
|
+
FACT_NAMES: Tuple[str, ...] = (*CHART_FACTS, CHART_CLOCK, *POSITION_FACTS, *SESSION_FACTS)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def fact_value(name: str, instrument: Any, now: Any, book: Book, bar: Any) -> Any:
|
|
122
|
+
"""Whichever of the three namespaces this name belongs to, read once."""
|
|
123
|
+
if name in POSITION_FACTS:
|
|
124
|
+
return position_value(name, book)
|
|
125
|
+
if name in SESSION_FACTS:
|
|
126
|
+
return bar
|
|
127
|
+
return chart_value(name, instrument, now)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""The comparison of ``conformance.md`` section 6, in this language.
|
|
2
|
+
|
|
3
|
+
The page prints the function and every step of it, and this is that function with
|
|
4
|
+
nothing added. Each step is written out below beside the wrong implementation it
|
|
5
|
+
refuses, because the whole value of the suite rests on this being the same rule
|
|
6
|
+
in both engines: a comparison that was a little more forgiving in one of them
|
|
7
|
+
would report agreement that is not there, which is worse than reporting none.
|
|
8
|
+
|
|
9
|
+
1. Two absent values match, asked before anything numeric is.
|
|
10
|
+
2. One absent and one present fail, whatever the tolerance. A number is not
|
|
11
|
+
nearly absent, and warmup length is a specified property, so a value one bar
|
|
12
|
+
early is a defect however small it is.
|
|
13
|
+
3. A result that is not finite is its own outcome. ``language.md`` section 5.1
|
|
14
|
+
says an infinity and a not-a-number never appear as values, so producing one
|
|
15
|
+
is a defect rather than a near miss, and burying it among numeric failures
|
|
16
|
+
would hide the one failure that is never a rounding difference.
|
|
17
|
+
4. Signed zero is normalised. Nothing in the language can observe the sign of a
|
|
18
|
+
zero, so comparing raw bits would fail a case over a difference no script can
|
|
19
|
+
see.
|
|
20
|
+
5. Equality is over the eight bytes, not over a rendering, so a formatting
|
|
21
|
+
decision can never make two different values look equal.
|
|
22
|
+
6. With both bounds zero, anything past step 5 fails.
|
|
23
|
+
7. The bound is ``max(abs, rel * |e|)`` and never a sum: exactly one bound is in
|
|
24
|
+
force at any magnitude, and a failure can name which of the two it broke.
|
|
25
|
+
|
|
26
|
+
**A declared tolerance is held to the caps section 6 prints.** A case past either
|
|
27
|
+
is not a conformance case at all, and it is reported ``error`` rather than run
|
|
28
|
+
under a bound the page refuses.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import struct
|
|
32
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
33
|
+
|
|
34
|
+
from .page import TOLERANCE_CAP_ABS, TOLERANCE_CAP_REL
|
|
35
|
+
from .spellings import written
|
|
36
|
+
|
|
37
|
+
#: Section 4: what a diagnostic is compared on, and nothing else. The message
|
|
38
|
+
#: text and the suggested fix are deliberately not compared, because improving
|
|
39
|
+
#: the wording of an error is something this project wants to keep doing.
|
|
40
|
+
DIAGNOSTIC_FIELDS: Tuple[str, ...] = ("code", "line", "column", "severity")
|
|
41
|
+
|
|
42
|
+
#: Section 6: exact, which is what a comparison is until a case declares otherwise.
|
|
43
|
+
EXACT = (0.0, 0.0)
|
|
44
|
+
|
|
45
|
+
_PASS: Dict[str, Any] = {"outcome": "pass"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bits(value: float) -> bytes:
|
|
49
|
+
"""The eight bytes of a binary64, so equality is over bits and not over text."""
|
|
50
|
+
return struct.pack(">d", value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_number(value: Any) -> bool:
|
|
54
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compare_numbers(actual: Any, expected: Any, abs_bound: float, rel_bound: float) -> Dict[str, Any]:
|
|
58
|
+
"""Steps 1 to 8, in the page's order, over one asserted numeric value."""
|
|
59
|
+
if expected is None and actual is None:
|
|
60
|
+
return _PASS
|
|
61
|
+
if expected is None or actual is None:
|
|
62
|
+
return {"outcome": "fail", "bound": "absence"}
|
|
63
|
+
if not _is_number(actual) or not _finite(actual):
|
|
64
|
+
return {"outcome": "nonFinite"}
|
|
65
|
+
if not _is_number(expected) or not _finite(expected):
|
|
66
|
+
return {"outcome": "nonFinite"}
|
|
67
|
+
left = 0.0 if actual == 0 else float(actual)
|
|
68
|
+
right = 0.0 if expected == 0 else float(expected)
|
|
69
|
+
if _bits(left) == _bits(right):
|
|
70
|
+
return _PASS
|
|
71
|
+
difference = abs(left - right)
|
|
72
|
+
if abs_bound == 0 and rel_bound == 0:
|
|
73
|
+
return {"outcome": "fail", "bound": "exact", "difference": difference}
|
|
74
|
+
relative = rel_bound * abs(right)
|
|
75
|
+
if difference <= max(abs_bound, relative):
|
|
76
|
+
return _PASS
|
|
77
|
+
return {
|
|
78
|
+
"outcome": "fail",
|
|
79
|
+
"bound": "abs" if abs_bound >= relative else "rel",
|
|
80
|
+
"difference": difference,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _finite(value: Any) -> bool:
|
|
85
|
+
return value == value and value not in (float("inf"), float("-inf"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _kind_of(value: Any) -> str:
|
|
89
|
+
if value is None:
|
|
90
|
+
return "absent"
|
|
91
|
+
if isinstance(value, bool):
|
|
92
|
+
return "bool"
|
|
93
|
+
if _is_number(value):
|
|
94
|
+
return "number"
|
|
95
|
+
if isinstance(value, str):
|
|
96
|
+
return "string"
|
|
97
|
+
return "list" if isinstance(value, list) else "object"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compare_values(actual: Any, expected: Any, tolerance: Tuple[float, float]) -> Dict[str, Any]:
|
|
101
|
+
"""One value of any kind, under section 6's table.
|
|
102
|
+
|
|
103
|
+
Absence is asked first for every kind, because the table says it is never
|
|
104
|
+
subject to a tolerance and that is as true of a string as of a number. Two
|
|
105
|
+
values of different kinds fail as ``kind``: a bool where a number was
|
|
106
|
+
expected is not a near miss.
|
|
107
|
+
"""
|
|
108
|
+
if expected is None or actual is None:
|
|
109
|
+
return compare_numbers(actual, expected, tolerance[0], tolerance[1])
|
|
110
|
+
kind = _kind_of(expected)
|
|
111
|
+
if kind != _kind_of(actual):
|
|
112
|
+
return {"outcome": "fail", "bound": "kind"}
|
|
113
|
+
if kind == "number":
|
|
114
|
+
return compare_numbers(actual, expected, tolerance[0], tolerance[1])
|
|
115
|
+
if kind in ("bool", "string"):
|
|
116
|
+
return _PASS if actual == expected else {"outcome": "fail", "bound": "exact"}
|
|
117
|
+
return _PASS if _stable(actual) == _stable(expected) else {"outcome": "fail", "bound": "exact"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _stable(value: Any) -> str:
|
|
121
|
+
"""A nested value as one piece of text, keys sorted, so two spellings are one."""
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
return "[" + ",".join(_stable(one) for one in value) + "]"
|
|
124
|
+
if isinstance(value, dict):
|
|
125
|
+
members = (f"{key}:{_stable(value[key])}" for key in sorted(value))
|
|
126
|
+
return "{" + ",".join(members) + "}"
|
|
127
|
+
return written(value)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _fields_of(channel: str, expected: Dict[str, Any]) -> Sequence[str]:
|
|
131
|
+
"""Section 4: four fields for a diagnostic, else the expected element's own.
|
|
132
|
+
|
|
133
|
+
"Compared on the fields the case names and no others" is what lets a case
|
|
134
|
+
assert the part of a ledger row it is about without freezing every other
|
|
135
|
+
field of it.
|
|
136
|
+
"""
|
|
137
|
+
if channel == "diagnostics":
|
|
138
|
+
return DIAGNOSTIC_FIELDS
|
|
139
|
+
return sorted(expected)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _difference(
|
|
143
|
+
result: Dict[str, Any],
|
|
144
|
+
channel: str,
|
|
145
|
+
index: Optional[int],
|
|
146
|
+
column: Optional[str],
|
|
147
|
+
actual: Any,
|
|
148
|
+
expected: Any,
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
found = {
|
|
151
|
+
"outcome": result["outcome"],
|
|
152
|
+
"channel": channel,
|
|
153
|
+
"index": index,
|
|
154
|
+
"column": column,
|
|
155
|
+
"expected": written(expected),
|
|
156
|
+
"actual": written(actual),
|
|
157
|
+
}
|
|
158
|
+
if "bound" in result:
|
|
159
|
+
found["bound"] = result["bound"]
|
|
160
|
+
if "difference" in result:
|
|
161
|
+
found["difference"] = written(result["difference"])
|
|
162
|
+
return found
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def compare_channel(
|
|
166
|
+
channel: str, actual: Any, expected: Any, tolerance: Tuple[float, float]
|
|
167
|
+
) -> Optional[Dict[str, Any]]:
|
|
168
|
+
"""One channel: an ordered list, length first, then element by element.
|
|
169
|
+
|
|
170
|
+
Nothing when the channel matches, else the first difference with its index,
|
|
171
|
+
its column and both values as a report writes them. A length mismatch is
|
|
172
|
+
reported before any element is compared, which is section 6's table, and at
|
|
173
|
+
the first index one list has and the other lacks.
|
|
174
|
+
"""
|
|
175
|
+
if not isinstance(expected, list) or not isinstance(actual, list):
|
|
176
|
+
return {
|
|
177
|
+
"outcome": "fail",
|
|
178
|
+
"channel": channel,
|
|
179
|
+
"index": None,
|
|
180
|
+
"column": None,
|
|
181
|
+
"bound": "kind",
|
|
182
|
+
"expected": _kind_of(expected),
|
|
183
|
+
"actual": _kind_of(actual),
|
|
184
|
+
}
|
|
185
|
+
if len(actual) != len(expected):
|
|
186
|
+
return {
|
|
187
|
+
"outcome": "fail",
|
|
188
|
+
"channel": channel,
|
|
189
|
+
"index": min(len(actual), len(expected)),
|
|
190
|
+
"column": None,
|
|
191
|
+
"bound": "length",
|
|
192
|
+
"expected": str(len(expected)),
|
|
193
|
+
"actual": str(len(actual)),
|
|
194
|
+
}
|
|
195
|
+
for index, want in enumerate(expected):
|
|
196
|
+
got = actual[index]
|
|
197
|
+
if not isinstance(want, dict):
|
|
198
|
+
result = compare_values(got, want, tolerance)
|
|
199
|
+
if result["outcome"] != "pass":
|
|
200
|
+
return _difference(result, channel, index, None, got, want)
|
|
201
|
+
continue
|
|
202
|
+
for column in _fields_of(channel, want):
|
|
203
|
+
wanted = want.get(column)
|
|
204
|
+
found = got.get(column) if isinstance(got, dict) else None
|
|
205
|
+
result = compare_values(found, wanted, tolerance)
|
|
206
|
+
if result["outcome"] != "pass":
|
|
207
|
+
return _difference(result, channel, index, column, found, wanted)
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def compare_channels(
|
|
212
|
+
asserted: Sequence[str],
|
|
213
|
+
actual: Dict[str, Any],
|
|
214
|
+
expected: Dict[str, Any],
|
|
215
|
+
tolerance: Tuple[float, float],
|
|
216
|
+
) -> Dict[str, Any]:
|
|
217
|
+
"""Every asserted channel, in the case's own order, to one outcome."""
|
|
218
|
+
for channel in asserted:
|
|
219
|
+
found = compare_channel(channel, actual.get(channel), expected.get(channel), tolerance)
|
|
220
|
+
if found is not None:
|
|
221
|
+
return found
|
|
222
|
+
return dict(_PASS)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def tolerance_from(declared: Any) -> Tuple[Optional[Tuple[float, float]], Optional[str]]:
|
|
226
|
+
"""The case's tolerance, held to section 6, or the sentence refusing it.
|
|
227
|
+
|
|
228
|
+
Absent means exact. A non-zero bound needs a stated reason, which is the rule
|
|
229
|
+
that stops a tolerance from being the thing an engine author widens until the
|
|
230
|
+
suite goes green. A bound past the cap is not a conformance case at all.
|
|
231
|
+
"""
|
|
232
|
+
if declared is None:
|
|
233
|
+
return EXACT, None
|
|
234
|
+
if not isinstance(declared, dict):
|
|
235
|
+
return None, "tolerance is not an object"
|
|
236
|
+
bounds: List[float] = []
|
|
237
|
+
for name in ("abs", "rel"):
|
|
238
|
+
value = declared.get(name, 0)
|
|
239
|
+
if value is None:
|
|
240
|
+
value = 0
|
|
241
|
+
if not _is_number(value) or not _finite(value):
|
|
242
|
+
return None, f"tolerance.{name} is not a finite number"
|
|
243
|
+
if value < 0:
|
|
244
|
+
return None, f"tolerance.{name} is below zero"
|
|
245
|
+
bounds.append(float(value))
|
|
246
|
+
abs_bound, rel_bound = bounds[0], bounds[1]
|
|
247
|
+
reason = declared.get("reason")
|
|
248
|
+
stated = isinstance(reason, str) and reason.strip() != ""
|
|
249
|
+
if (abs_bound != 0 or rel_bound != 0) and not stated:
|
|
250
|
+
return None, "a non-zero tolerance is declared with no reason (section 6)"
|
|
251
|
+
if abs_bound > TOLERANCE_CAP_ABS or rel_bound > TOLERANCE_CAP_REL:
|
|
252
|
+
return None, (
|
|
253
|
+
f"a tolerance of abs {written(abs_bound)} and rel {written(rel_bound)} is looser than "
|
|
254
|
+
f"the cap of abs {written(TOLERANCE_CAP_ABS)} and rel {written(TOLERANCE_CAP_REL)}, so "
|
|
255
|
+
"this is not a conformance case (section 6)"
|
|
256
|
+
)
|
|
257
|
+
return (abs_bound, rel_bound), None
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The nine order calls as this engine's manifest holds them, and the desk behind them.
|
|
2
|
+
|
|
3
|
+
``stdlib.md`` 17.2 and 17.3 are the calls, ``host-interface.md`` 7.2 and 7.4 are
|
|
4
|
+
the frames and the boundary they fold at, and ``strategy/`` is all of the
|
|
5
|
+
behaviour. What is here is the join: the manifest rows a program's own library
|
|
6
|
+
table is checked against, and the object that carries a ledger, its intents and
|
|
7
|
+
its fills through one case.
|
|
8
|
+
|
|
9
|
+
**An order call is an effect and is not performed when it executes.**
|
|
10
|
+
``compiled-program.md`` 5.4 has a function with an effect leave a record and push
|
|
11
|
+
absence, and step 9 applies the record only on a bar the engine decided. So the
|
|
12
|
+
machine hands this desk the calls a decided bar left behind, in the order the bar
|
|
13
|
+
made them, and a condition that was true halfway through a moving bar and false
|
|
14
|
+
when it closed places no order at all.
|
|
15
|
+
|
|
16
|
+
**A frame is delivered after a bar and folded before the next one.**
|
|
17
|
+
``conformance.md`` section 3 says ``afterBar`` is "the index of the bar after
|
|
18
|
+
whose execution the frame is delivered, so the fold happens at a bar boundary
|
|
19
|
+
before the next execution". Read as it is written, a frame naming the last bar is
|
|
20
|
+
delivered and never folded, because there is no next execution to fold before.
|
|
21
|
+
That is a hole in the page rather than a decision to make here, so such a case is
|
|
22
|
+
named ``unsupported`` by the caller rather than answered under one reading of
|
|
23
|
+
two.
|
|
24
|
+
|
|
25
|
+
**A refusal takes back the whole bar.** ``Ledger.discard`` is what that costs, and
|
|
26
|
+
the reason is in its own docstring: every call of a bar is mapped before any of
|
|
27
|
+
them is routed, so a refusal after one has appended a row would otherwise leave a
|
|
28
|
+
row no destination was ever handed.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
32
|
+
|
|
33
|
+
from ..contracts import LibraryEntry
|
|
34
|
+
from ..diagnostics import Diagnostic
|
|
35
|
+
from ..strategy import (
|
|
36
|
+
Fills,
|
|
37
|
+
Identity,
|
|
38
|
+
IntentBar,
|
|
39
|
+
Intents,
|
|
40
|
+
Ledger,
|
|
41
|
+
LedgerOptions,
|
|
42
|
+
LedgerRow,
|
|
43
|
+
OrderFrame,
|
|
44
|
+
)
|
|
45
|
+
from ..strategy.calls import PARAMETERS
|
|
46
|
+
from .reading import Frame
|
|
47
|
+
|
|
48
|
+
#: Section 2.5's row for each of the nine, by name and argument count. The count
|
|
49
|
+
#: is one more than the signature's, which is ``compiled-program.md`` 4.10's
|
|
50
|
+
#: extra argument: the names of the arguments the script wrote. None of them
|
|
51
|
+
#: holds state, and every one of them has the ``order`` effect, which is what
|
|
52
|
+
#: makes the machine hold the call until step 9 rather than calling into here.
|
|
53
|
+
ORDER_ENTRIES: Dict[Tuple[str, int], LibraryEntry] = {
|
|
54
|
+
(name, len(parameters) + 1): LibraryEntry(name, len(parameters) + 1, False, "order")
|
|
55
|
+
for name, parameters in PARAMETERS.items()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Desk:
|
|
60
|
+
"""One strategy's ledger, its intents and its fills, driven over one case.
|
|
61
|
+
|
|
62
|
+
The caller owns the bar loop, because the bars are the case's and the
|
|
63
|
+
execution is the machine's. What is owned here is the order of the three
|
|
64
|
+
things that happen between two executions, and that order is
|
|
65
|
+
``host-interface.md`` 7.4's rather than this class's choice.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, frames: Sequence[Frame] = ()) -> None:
|
|
69
|
+
self.ledger = Ledger()
|
|
70
|
+
self.intents = Intents()
|
|
71
|
+
self.fills = Fills()
|
|
72
|
+
#: The destination's own reference per intent, recorded at delivery, so
|
|
73
|
+
#: that a fill carries the reference the frame that made it carried.
|
|
74
|
+
self._refs: Dict[int, str] = {}
|
|
75
|
+
self._after: Dict[int, List[Frame]] = {}
|
|
76
|
+
for frame in frames:
|
|
77
|
+
self._after.setdefault(frame.after_bar, []).append(frame)
|
|
78
|
+
self._waiting: Tuple[Frame, ...] = ()
|
|
79
|
+
|
|
80
|
+
def begin(self, options: LedgerOptions) -> None:
|
|
81
|
+
"""The declaration's own settings, before bar 0 and after the load.
|
|
82
|
+
|
|
83
|
+
A ledger exists before the program is loaded, because the manifest has to
|
|
84
|
+
answer whether this engine holds ``pos.size`` before anything has read a
|
|
85
|
+
declaration, and the declaration's settings can only be read once the
|
|
86
|
+
inputs behind them are resolved, which is what a load does. Nothing has
|
|
87
|
+
placed an order in between: the first bar is what places one.
|
|
88
|
+
"""
|
|
89
|
+
self.ledger.options = options
|
|
90
|
+
|
|
91
|
+
# -- the boundary between two bars --------------------------------------
|
|
92
|
+
|
|
93
|
+
def fold(self, index: int, time: Optional[float]) -> None:
|
|
94
|
+
"""Deliver what the destination sent after the last bar, then fold it.
|
|
95
|
+
|
|
96
|
+
Delivered first and folded second, in one call, because the two are one
|
|
97
|
+
boundary: a driver that folded before delivering would hold every frame
|
|
98
|
+
for a bar longer, and a script would read a position one bar late.
|
|
99
|
+
"""
|
|
100
|
+
for frame in self._waiting:
|
|
101
|
+
named = self.intents.at_ordinal(frame.intent)
|
|
102
|
+
if named is not None and frame.order_ref != "":
|
|
103
|
+
self._refs[named.intent_id] = frame.order_ref
|
|
104
|
+
self.ledger.deliver(
|
|
105
|
+
OrderFrame(
|
|
106
|
+
# An ordinal naming no intent this run placed is how a case
|
|
107
|
+
# hands an engine a frame about an order its ledger does not
|
|
108
|
+
# hold. Step 1 of the fold refuses it, which is the ledger's
|
|
109
|
+
# to do and not this driver's, so an id no run can mint goes
|
|
110
|
+
# over rather than the frame being dropped here.
|
|
111
|
+
intent_id=-1 if named is None else named.intent_id,
|
|
112
|
+
status=frame.status,
|
|
113
|
+
filled_qty=frame.filled_qty,
|
|
114
|
+
avg_fill_price=frame.avg_fill_price,
|
|
115
|
+
order_ref=frame.order_ref,
|
|
116
|
+
text=frame.text,
|
|
117
|
+
# When the destination answered. Left off, every frame
|
|
118
|
+
# carried the instant of the bar that placed the order, so a
|
|
119
|
+
# case whose destination answered later was answered one way
|
|
120
|
+
# here and another by the first engine, at `updatedAt` and
|
|
121
|
+
# at nothing else.
|
|
122
|
+
time=frame.time,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
self._waiting = ()
|
|
126
|
+
self.fills.record(self.ledger.settle(), self.intents, index, time, self._refs)
|
|
127
|
+
|
|
128
|
+
def deliver_after(self, index: int) -> None:
|
|
129
|
+
"""The frames this case says the destination sent after bar ``index``."""
|
|
130
|
+
self._waiting = tuple(self._after.get(index, ()))
|
|
131
|
+
|
|
132
|
+
# -- what a decided bar sent --------------------------------------------
|
|
133
|
+
|
|
134
|
+
def apply(self, effects: Iterable[Any], bar: IntentBar) -> Optional[Diagnostic]:
|
|
135
|
+
"""Step 9's order calls, in the order the bar made them, or the refusal.
|
|
136
|
+
|
|
137
|
+
Every effect a program of this engine's can leave is one of the nine,
|
|
138
|
+
because they are the only entries in its manifest with an effect at all,
|
|
139
|
+
so the name goes to the ledger rather than being sorted here.
|
|
140
|
+
"""
|
|
141
|
+
appended = len(self.ledger.rows())
|
|
142
|
+
for effect in effects:
|
|
143
|
+
placed = self.ledger.place(effect.name, effect.arguments, bar, effect.position)
|
|
144
|
+
if placed.refusal is not None:
|
|
145
|
+
self.ledger.discard(appended)
|
|
146
|
+
return placed.refusal
|
|
147
|
+
self.intents.record(placed.intents)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# -- what a script and a report read ------------------------------------
|
|
151
|
+
|
|
152
|
+
def size(self) -> float:
|
|
153
|
+
return self.ledger.size()
|
|
154
|
+
|
|
155
|
+
def avg_price(self) -> Optional[float]:
|
|
156
|
+
return self.ledger.avg_price()
|
|
157
|
+
|
|
158
|
+
def rows(self) -> Sequence[LedgerRow]:
|
|
159
|
+
return self.ledger.rows()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def unfoldable(frames: Sequence[Frame], bars: int) -> Tuple[Frame, ...]:
|
|
163
|
+
"""The frames of a case that no boundary folds, which is the hole above.
|
|
164
|
+
|
|
165
|
+
A frame named after the last bar is delivered and never folded, so a case
|
|
166
|
+
carrying one is answered ``unsupported`` naming it rather than run to a
|
|
167
|
+
ledger that is missing whatever it said.
|
|
168
|
+
"""
|
|
169
|
+
return tuple(frame for frame in frames if frame.after_bar >= bars - 1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def options_for(declared: Dict[str, Any], instrument: Dict[str, Any]) -> LedgerOptions:
|
|
173
|
+
"""What the declaration and the instrument record fix before bar 0.
|
|
174
|
+
|
|
175
|
+
``declared`` is the strategy block of the compiled program's meta, with every
|
|
176
|
+
input reference already resolved: a script may declare its own quantity from
|
|
177
|
+
an input, and a ledger handed the reference rather than the value would size
|
|
178
|
+
every order from a shape.
|
|
179
|
+
"""
|
|
180
|
+
return LedgerOptions(
|
|
181
|
+
instrument=Identity(instrument.get("symbol"), instrument.get("exchange")),
|
|
182
|
+
product=declared["product"],
|
|
183
|
+
qty_type=declared["qtyType"],
|
|
184
|
+
declared_qty=declared["qty"],
|
|
185
|
+
tick_size=instrument.get("tickSize"),
|
|
186
|
+
pyramiding=declared["pyramiding"],
|
|
187
|
+
)
|