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,229 @@
|
|
|
1
|
+
"""How much each order of a call sends, and which position it is sent against, 17.1.
|
|
2
|
+
|
|
3
|
+
**This is the half of a call that is arithmetic.** ``placing`` says what each call
|
|
4
|
+
means and dispatches to the two functions here; these two decide how many orders
|
|
5
|
+
the call becomes, how large each is, and which position reference each one
|
|
6
|
+
carries.
|
|
7
|
+
|
|
8
|
+
The two entry points are ``entering``, for a call that states a side and a size,
|
|
9
|
+
and ``flattening``, for one that reduces what the leg holds. What they differ
|
|
10
|
+
about is not the split but the budget: an entry sends the size the script wrote
|
|
11
|
+
whatever the leg holds, so the only question is where its units land; a close
|
|
12
|
+
chooses its own number, so it may only choose one that has settled.
|
|
13
|
+
|
|
14
|
+
**No order crosses zero.** An instruction that would take a leg from long to
|
|
15
|
+
short is two orders, one that closes the outgoing position and one that opens the
|
|
16
|
+
replacement, each carrying its own position reference, so that a fill arriving
|
|
17
|
+
late can still say which of the two it settled.
|
|
18
|
+
|
|
19
|
+
**The reference is minted in every unit; only the arithmetic waits.** The split
|
|
20
|
+
subtracts a position folded from filled quantities from a quantity the script
|
|
21
|
+
stated, and those are the same kind of number only in a declaration counting in
|
|
22
|
+
units. Minting a reference needs none of that arithmetic, so an order the engine
|
|
23
|
+
cannot size against the leg is still sent on a position of its own rather than on
|
|
24
|
+
the outgoing one. What does not hold is written where a reader meets it:
|
|
25
|
+
``stdlib.md`` 17.1 and ``errors.md`` OS7005.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, replace
|
|
29
|
+
from typing import Any, List, Optional, Protocol, Sequence, Tuple
|
|
30
|
+
|
|
31
|
+
from .closable import Closing, closable_units
|
|
32
|
+
from .holdings import Book, Holding, divide, holdings, joining, opposing, outgoing_for
|
|
33
|
+
from .intents import Identity, IntentBar, Placement # noqa: F401 the context states them
|
|
34
|
+
from .rows import Reduction
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlacingContext(Book, Closing, Protocol):
|
|
38
|
+
"""What the mapping and the refusals read: the declaration, the leg and the ledger."""
|
|
39
|
+
|
|
40
|
+
instrument: Identity
|
|
41
|
+
product: str
|
|
42
|
+
#: The unit a quantity the script stated is counted in, ``language.md`` 13.3.
|
|
43
|
+
qty_type: str
|
|
44
|
+
#: The size an order that names none takes, from the declaration.
|
|
45
|
+
declared_qty: float
|
|
46
|
+
#: The instrument's tick size, absent where the host states none.
|
|
47
|
+
tick_size: Optional[float]
|
|
48
|
+
#: Entries allowed in one direction before one is refused.
|
|
49
|
+
pyramiding: float
|
|
50
|
+
bar: IntentBar
|
|
51
|
+
|
|
52
|
+
def avg_price(self) -> Optional[float]:
|
|
53
|
+
"""The average price of the open position, absent while flat."""
|
|
54
|
+
|
|
55
|
+
def mint(self) -> int:
|
|
56
|
+
"""A fresh position, for the replacement half of a flip."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class MappedOrder:
|
|
61
|
+
"""One order a call sends, and what that order takes out of the leg."""
|
|
62
|
+
|
|
63
|
+
placement: Placement
|
|
64
|
+
reduces: Optional[Reduction] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def adding(placement: Placement) -> MappedOrder:
|
|
68
|
+
"""An order that adds to a position, or whose size the engine cannot count."""
|
|
69
|
+
return MappedOrder(placement=placement, reduces=None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Entry:
|
|
74
|
+
"""What an entering order states, at one quantity and one position."""
|
|
75
|
+
|
|
76
|
+
side: str
|
|
77
|
+
limit: Optional[float]
|
|
78
|
+
trigger: Optional[float]
|
|
79
|
+
order_type: str
|
|
80
|
+
tag: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _placing(entry: Entry, qty: float, qty_type: str, position_ref: int) -> Placement:
|
|
84
|
+
return Placement(
|
|
85
|
+
kind="place",
|
|
86
|
+
side=entry.side,
|
|
87
|
+
qty=qty,
|
|
88
|
+
qty_type=qty_type,
|
|
89
|
+
order_type=entry.order_type,
|
|
90
|
+
limit=entry.limit,
|
|
91
|
+
trigger=entry.trigger,
|
|
92
|
+
tag=entry.tag,
|
|
93
|
+
position_ref=position_ref,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _opening_on(ctx: Any, book: Sequence[Holding], side: str) -> int:
|
|
98
|
+
"""The reference an order that opens or adds to a position carries."""
|
|
99
|
+
found = joining(book, side)
|
|
100
|
+
return ctx.mint() if found is None else found
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def entering(ctx: Any, entry: Entry, qty: Optional[float]) -> Tuple[MappedOrder, ...]:
|
|
104
|
+
"""The orders an entry sends, which is two where it crosses zero.
|
|
105
|
+
|
|
106
|
+
The closing half is what is left to close, not what the leg holds: an order
|
|
107
|
+
the destination has not answered has filled nothing, so a leg with a close
|
|
108
|
+
already going has nothing left for the outgoing half to take.
|
|
109
|
+
|
|
110
|
+
What it crosses is decided per position, not from the leg's net. The net is
|
|
111
|
+
folded from settled fills, so on a silent destination it reads zero while an
|
|
112
|
+
entry is still going and an opposing entry is not seen as opposing anything
|
|
113
|
+
at all. The question is about one position, so it is asked of one position,
|
|
114
|
+
and the answer is the same whether or not the destination has answered yet.
|
|
115
|
+
"""
|
|
116
|
+
# The declaration's own size where the call named none, ``stdlib.md`` 17.2.
|
|
117
|
+
wanted = ctx.declared_qty if qty is None else qty
|
|
118
|
+
book = holdings(ctx)
|
|
119
|
+
# Nothing to cross: the leg holds no position this order opposes, so it opens
|
|
120
|
+
# one or joins the one it is on the side of.
|
|
121
|
+
if not opposing(book, entry.side):
|
|
122
|
+
return (adding(_placing(entry, wanted, ctx.qty_type, _opening_on(ctx, book, entry.side))),)
|
|
123
|
+
|
|
124
|
+
if ctx.qty_type != "units":
|
|
125
|
+
return (
|
|
126
|
+
MappedOrder(
|
|
127
|
+
# A position of its own, in every unit, because minting one needs
|
|
128
|
+
# no lot size. On the outgoing reference this order was the
|
|
129
|
+
# crossing 17.1 refuses outright.
|
|
130
|
+
placement=_placing(entry, wanted, ctx.qty_type, ctx.mint()),
|
|
131
|
+
# Unreadable in units, so it is taken to have reduced the whole of
|
|
132
|
+
# what was left: ``closable`` says why that is the only safe
|
|
133
|
+
# reading.
|
|
134
|
+
reduces=Reduction(part=None, claimed=closable_units(ctx, None), counted=False),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# What each position it opposes can absorb, oldest first. A position holding
|
|
139
|
+
# six is sent six of the nine, whether those six have settled or are still
|
|
140
|
+
# going, because six of this order is what brings that position back to zero.
|
|
141
|
+
shares, left = divide(book, entry.side, wanted, lambda one: one.units)
|
|
142
|
+
out: List[MappedOrder] = [
|
|
143
|
+
MappedOrder(
|
|
144
|
+
placement=_placing(entry, share.units, "units", share.ref),
|
|
145
|
+
reduces=Reduction(part=None, claimed=share.units, counted=True),
|
|
146
|
+
)
|
|
147
|
+
for share in shares
|
|
148
|
+
]
|
|
149
|
+
if left == 0:
|
|
150
|
+
return tuple(out)
|
|
151
|
+
# The replacement is a position of its own where the leg holds none on this
|
|
152
|
+
# side, so that a fill on an outgoing order settles the position it belonged
|
|
153
|
+
# to.
|
|
154
|
+
out.append(adding(_placing(entry, left, "units", _opening_on(ctx, book, entry.side))))
|
|
155
|
+
return tuple(out)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _reducing(side: str, qty: float, qty_type: str, tag: str, position_ref: int) -> Placement:
|
|
159
|
+
return Placement(
|
|
160
|
+
kind="place",
|
|
161
|
+
side=side,
|
|
162
|
+
qty=qty,
|
|
163
|
+
qty_type=qty_type,
|
|
164
|
+
order_type="market",
|
|
165
|
+
tag=tag,
|
|
166
|
+
position_ref=position_ref,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def flattening(
|
|
171
|
+
ctx: Any,
|
|
172
|
+
units: float,
|
|
173
|
+
side: str,
|
|
174
|
+
qty: Optional[float],
|
|
175
|
+
tag: str,
|
|
176
|
+
part: Optional[str],
|
|
177
|
+
) -> Tuple[MappedOrder, ...]:
|
|
178
|
+
"""The orders a flattening call sends, which is one per position it reduces.
|
|
179
|
+
|
|
180
|
+
It is divided across the positions holding it, oldest first, each bounded by
|
|
181
|
+
what has settled on it and is not already working against it. Sized against
|
|
182
|
+
the whole leg and attached to a single reference, one order was large enough
|
|
183
|
+
to take the first of them through zero and out the other side.
|
|
184
|
+
|
|
185
|
+
A quantity the engine cannot read is one order. It cannot be divided at all,
|
|
186
|
+
so it goes on the position it is closing, oldest first, and that position is
|
|
187
|
+
what it may take past zero: the one shape of 17.1 an engine does not keep.
|
|
188
|
+
"""
|
|
189
|
+
# Nothing to flatten and no size named: an instruction about a position the
|
|
190
|
+
# strategy does not hold, which is not an error and is not an order either.
|
|
191
|
+
if units <= 0 and qty is None:
|
|
192
|
+
return ()
|
|
193
|
+
sending = units if qty is None else qty
|
|
194
|
+
# A quantity the engine worked out is counted as itself. One the script
|
|
195
|
+
# stated in a unit the engine cannot read is counted as the whole of what was
|
|
196
|
+
# left, which is what keeps a close after it from sending the position again.
|
|
197
|
+
counted = qty is None or ctx.qty_type == "units"
|
|
198
|
+
book = holdings(ctx)
|
|
199
|
+
if not counted:
|
|
200
|
+
outgoing = outgoing_for(ctx, book, side)
|
|
201
|
+
# Where the leg holds no position on the side the close reduces the call
|
|
202
|
+
# sends nothing, whatever it stated: a close is never minted a position,
|
|
203
|
+
# so there is nothing for it to be sent against.
|
|
204
|
+
if outgoing is None:
|
|
205
|
+
return ()
|
|
206
|
+
return (
|
|
207
|
+
MappedOrder(
|
|
208
|
+
placement=_reducing(side, sending, ctx.qty_type, tag, outgoing),
|
|
209
|
+
reduces=Reduction(part=part, claimed=units, counted=False),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
# Anything left when every position it reduces is full is left unsent rather
|
|
213
|
+
# than pushed onto one of them past its own size. By construction there is
|
|
214
|
+
# nothing left: what a close may send is the leg's settled net less what is
|
|
215
|
+
# working against it, and that is never more than the positions on that side
|
|
216
|
+
# hold.
|
|
217
|
+
shares, _left = divide(book, side, sending, lambda one: one.settled)
|
|
218
|
+
return tuple(
|
|
219
|
+
MappedOrder(
|
|
220
|
+
placement=_reducing(side, share.units, "units", tag, share.ref),
|
|
221
|
+
reduces=Reduction(part=part, claimed=share.units, counted=True),
|
|
222
|
+
)
|
|
223
|
+
for share in shares
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def at_position(order: MappedOrder, ref: int) -> MappedOrder:
|
|
228
|
+
"""The same order sent against another position, which a split makes."""
|
|
229
|
+
return MappedOrder(placement=replace(order.placement, position_ref=ref), reduces=order.reduces)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The status vocabulary of ``stdlib.md`` section 17.7, and what each word does.
|
|
2
|
+
|
|
3
|
+
The words themselves are the page's and the table there is the authority. What
|
|
4
|
+
is here is the three facts the fold needs to be able to ask: whether a word is
|
|
5
|
+
one of the seven, whether it ends an order, and how far along its life it sits.
|
|
6
|
+
``tests/test_statuses.py`` reads the table out of the page and fails the day one
|
|
7
|
+
of the three stops being what the page says, which is the arrangement this
|
|
8
|
+
engine already uses for every other constant it carries: the package a host
|
|
9
|
+
installs does not ship the specification, so the values live here and a test
|
|
10
|
+
holds them to the document.
|
|
11
|
+
|
|
12
|
+
**A rank rather than an ordering of the seven words.** Two of them share the
|
|
13
|
+
middle rank because an order that is live and one that is waiting for its
|
|
14
|
+
trigger are the same distance from the end, and either may follow the other
|
|
15
|
+
without the status going backwards.
|
|
16
|
+
|
|
17
|
+
**``placed`` is the engine's own.** It means an intent has left and nothing has
|
|
18
|
+
come back, and a host cannot report a state the destination has never described,
|
|
19
|
+
so it is the one word the last column of the table refuses a host.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import FrozenSet, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
#: The seven words a row's status may take, in the order the page prints them.
|
|
25
|
+
STATUSES: Tuple[str, ...] = (
|
|
26
|
+
"placed",
|
|
27
|
+
"working",
|
|
28
|
+
"triggerPending",
|
|
29
|
+
"filled",
|
|
30
|
+
"cancelled",
|
|
31
|
+
"rejected",
|
|
32
|
+
"expired",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
#: The four that end an order. A terminal row still takes a fill, under 17.8, so
|
|
36
|
+
#: this decides what the status may become and nothing about what the quantity
|
|
37
|
+
#: may do.
|
|
38
|
+
TERMINAL: FrozenSet[str] = frozenset({"filled", "cancelled", "rejected", "expired"})
|
|
39
|
+
|
|
40
|
+
#: The six a host may send in a frame, which is every word but the engine's own.
|
|
41
|
+
HOST_SENDABLE: Tuple[str, ...] = tuple(one for one in STATUSES if one != "placed")
|
|
42
|
+
|
|
43
|
+
#: The word a row is appended at.
|
|
44
|
+
PLACED = "placed"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_terminal(status: str) -> bool:
|
|
48
|
+
"""Whether this word ends an order."""
|
|
49
|
+
return status in TERMINAL
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def status_from(word: str) -> Optional[str]:
|
|
53
|
+
"""The word a host sent, or nothing where it is not one of the seven.
|
|
54
|
+
|
|
55
|
+
A word outside the vocabulary leaves a row's status alone and the rest of
|
|
56
|
+
the frame folds anyway, which is ``row``'s decision and is written there.
|
|
57
|
+
"""
|
|
58
|
+
return word if word in STATUSES else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def rank_of(status: str) -> int:
|
|
62
|
+
"""How far along its life a status sits: 0 sent, 1 live, 2 ended."""
|
|
63
|
+
if status == PLACED:
|
|
64
|
+
return 0
|
|
65
|
+
return 2 if is_terminal(status) else 1
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""The chart surface: what a run draws, in the channels a case asserts it through.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` section 11 maps a compiled program to a chart, and this
|
|
4
|
+
package is the half of that map an engine can perform: the declarations in
|
|
5
|
+
``outputs`` against the channels a bar wrote, folded into the ordered lists
|
|
6
|
+
``conformance.md`` section 4 compares. A host wanting the rest of the descriptor,
|
|
7
|
+
a legend, a pane, a settings dialog, builds it from the program, which carries
|
|
8
|
+
every declared field; nothing here decides how anything is drawn.
|
|
9
|
+
|
|
10
|
+
## The encoding, and the part of it the page does not fix
|
|
11
|
+
|
|
12
|
+
Section 4 fixes the shape of a channel, "an ordered list, and each element is a
|
|
13
|
+
flat object of named fields", and section 6 fixes how each kind of field is
|
|
14
|
+
compared, a colour as four integer channels and a string as an exact sequence of
|
|
15
|
+
code points. **It names no field of a surface row.** The first engine writes
|
|
16
|
+
none of these channels into a case either, so there is no second implementation
|
|
17
|
+
to read them off. What follows is therefore this engine's reading, stated here
|
|
18
|
+
rather than spread over five modules, and recorded in the stage's report as a
|
|
19
|
+
question for the page.
|
|
20
|
+
|
|
21
|
+
**A row is a thing the host was handed to draw, and there is no row for a thing
|
|
22
|
+
it was not.** That is ``stdlib.md`` section 18's closing sentence turned into an
|
|
23
|
+
encoding: "An absent value reaching any of these is a gap, never a zero: a line
|
|
24
|
+
breaks, a band stops, a level is not drawn, a bar keeps its own colour, a cell is
|
|
25
|
+
blank". So a marker fires or it does not, a bar is painted or it keeps its own
|
|
26
|
+
colour, a band spans a bar or stops at it, and each of those is a row present or
|
|
27
|
+
a row absent. A length difference is then the first thing a report names, which
|
|
28
|
+
section 6 says is what a reader of a failed case wants to see.
|
|
29
|
+
|
|
30
|
+
**A row carries what the run computed, and the identity of the declaration it
|
|
31
|
+
belongs to.** The bar index, the declaration's key or its ordinal, and the value
|
|
32
|
+
the channel held. It does not carry the declaration's own fields: a marker's
|
|
33
|
+
shape, a level's title, a band's opacity and a plot's colour are fixed before bar
|
|
34
|
+
0 and are in the compiled program that both engines were handed, so a row
|
|
35
|
+
repeating one would compare the compiler's output twice and call it an engine
|
|
36
|
+
agreeing with an engine. ``spec/decisions.md`` 14 reaches the same place from the
|
|
37
|
+
other side: the suite asserts columns, markers, fills, levels and paint, never a
|
|
38
|
+
plot's default style colour.
|
|
39
|
+
|
|
40
|
+
## What this engine does not draw
|
|
41
|
+
|
|
42
|
+
Two of section 2's channels are not answered, and they are named rather than
|
|
43
|
+
answered emptily, because an empty channel compares equal to an empty
|
|
44
|
+
expectation and is a pass nobody earned (``conformance.md`` section 8).
|
|
45
|
+
|
|
46
|
+
``table`` and ``drawings`` are the two, and the reason is the same for both:
|
|
47
|
+
4.11 keeps them out of the channels on purpose, a grid's cells are written by
|
|
48
|
+
library calls against a handle and a drawing is an object in the heap, and this
|
|
49
|
+
engine's library holds neither the grid calls nor the ``draw`` namespace. A
|
|
50
|
+
program that declares a grid or creates a drawing calls a function this engine's
|
|
51
|
+
manifest does not have, so it is already refused at load with OS6004 naming the
|
|
52
|
+
function; the sentences below are what a case asserting either channel is told.
|
|
53
|
+
|
|
54
|
+
The ``alerts`` and ``log`` channels are not this package's: an alert is raised by
|
|
55
|
+
the bar cycle (``run.py``, section 5.4) and a log line is written by a library
|
|
56
|
+
call this engine does not hold.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from typing import Any, Callable, Dict, List, Sequence
|
|
60
|
+
|
|
61
|
+
from ..program import LoadedProgram
|
|
62
|
+
from ..run import BarResult
|
|
63
|
+
from .bands import FILLS, fills_channel
|
|
64
|
+
from .levels import LEVELS, levels_channel
|
|
65
|
+
from .marks import MARKERS, markers_channel
|
|
66
|
+
from .paints import PAINTS, paint_channel
|
|
67
|
+
from .published import published
|
|
68
|
+
|
|
69
|
+
#: The surface channels of section 2 this engine answers.
|
|
70
|
+
ANSWERED = (MARKERS, FILLS, LEVELS) + tuple(name for name, _field in PAINTS)
|
|
71
|
+
|
|
72
|
+
#: The surface channels it does not, each with what a case asserting one is told.
|
|
73
|
+
UNANSWERED: Dict[str, str] = {
|
|
74
|
+
"table": (
|
|
75
|
+
"the table channel: a grid's cells are written by library calls against a handle rather "
|
|
76
|
+
"than through channels (compiled-program.md 4.11), and this engine's library holds "
|
|
77
|
+
"neither table nor cell, so a program declaring a grid is refused at load naming the "
|
|
78
|
+
"function"
|
|
79
|
+
),
|
|
80
|
+
"drawings": (
|
|
81
|
+
"the drawings channel: a free drawing is an object in the heap that the draw namespace "
|
|
82
|
+
"creates and mutates (compiled-program.md 4.11, stdlib.md 14.4), and this engine's "
|
|
83
|
+
"library holds none of those calls, so a program creating one is refused at load naming "
|
|
84
|
+
"the function"
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def surface_channels(
|
|
90
|
+
program: LoadedProgram,
|
|
91
|
+
executions: Sequence[BarResult],
|
|
92
|
+
spell: Callable[[Any], Any],
|
|
93
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
94
|
+
"""Every surface channel this engine answers, for one run.
|
|
95
|
+
|
|
96
|
+
``executions`` is what the caller driving the bars got back, in the order it
|
|
97
|
+
ran them, a moving bar's repeated executions included: the fold to one row
|
|
98
|
+
per bar is ``published.py``'s and not the caller's. ``spell`` is how one
|
|
99
|
+
value is written into a case file, which is ``adapter.spellings.as_reported``
|
|
100
|
+
for the adapter and identity for a caller reading the machine's own values.
|
|
101
|
+
A channel a case does not assert costs the list it is not asked for, which is
|
|
102
|
+
cheap enough that answering all five is simpler than answering some.
|
|
103
|
+
"""
|
|
104
|
+
bars = published(executions)
|
|
105
|
+
found: Dict[str, List[Dict[str, Any]]] = {
|
|
106
|
+
MARKERS: markers_channel(program, bars, spell),
|
|
107
|
+
FILLS: fills_channel(program, bars, spell),
|
|
108
|
+
LEVELS: levels_channel(program, bars, spell),
|
|
109
|
+
}
|
|
110
|
+
for name, declared in PAINTS:
|
|
111
|
+
found[name] = paint_channel(program, bars, declared, spell)
|
|
112
|
+
return found
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = ["ANSWERED", "UNANSWERED", "surface_channels"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""The fills channel: where a shaded band is drawn, and the colour it is drawn in.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` 2.8, the ``fills[]`` table: a band names the two plot
|
|
4
|
+
keys it is drawn between, a colour for each side, and a channel beside each
|
|
5
|
+
colour for the bars where the script computed one instead of declaring it.
|
|
6
|
+
|
|
7
|
+
**A band is drawn on a bar where both of its columns have a value.**
|
|
8
|
+
``stdlib.md`` 18: "An absent value reaching any of these is a gap, never a zero:
|
|
9
|
+
a line breaks, a band stops". So a row exists for a bar the band spans and there
|
|
10
|
+
is no row for a bar it does not, which is what makes the case the conformance
|
|
11
|
+
page names in section 7, a fill stopping across an absent bar, a difference in
|
|
12
|
+
this channel rather than a difference nobody can see. The two columns are read
|
|
13
|
+
through the plot keys, because ``between`` carries keys and not channels.
|
|
14
|
+
|
|
15
|
+
**A row carries the colours the bar computed and nothing the program declares.**
|
|
16
|
+
A band with two constant colours computes nothing per bar, so both fields are
|
|
17
|
+
null on every row and the row says only that the band is drawn there; the
|
|
18
|
+
colours are in the program both engines read. A band with a colour channel
|
|
19
|
+
carries what that channel held. Null therefore means "this bar computed no
|
|
20
|
+
colour for that side", which covers a side that was never per-bar and a per-bar
|
|
21
|
+
side that was absent on this bar. Whether a host draws the declared colour on
|
|
22
|
+
such a bar or leaves the band unpainted is not written down anywhere, and this
|
|
23
|
+
engine reports the fact rather than deciding it; the stage's report carries the
|
|
24
|
+
question.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
29
|
+
|
|
30
|
+
from ..diagnostics import ScriptError, malformed
|
|
31
|
+
from ..program import LoadedProgram
|
|
32
|
+
from ..values import ABSENT
|
|
33
|
+
from .plots import channel_of_key
|
|
34
|
+
from .published import Published, channel_named, value_of
|
|
35
|
+
|
|
36
|
+
#: Section 2's vocabulary for this channel.
|
|
37
|
+
FILLS = "fills"
|
|
38
|
+
|
|
39
|
+
#: The two sides of a band, as 2.8 spells them, each with the field that carries
|
|
40
|
+
#: a channel for it.
|
|
41
|
+
SIDES = (("colorUp", "colorUpChannel"), ("colorDown", "colorDownChannel"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Band:
|
|
46
|
+
"""One declared band, with every name it carries resolved to a channel."""
|
|
47
|
+
|
|
48
|
+
index: int
|
|
49
|
+
between: Sequence[int]
|
|
50
|
+
colours: Sequence[Optional[int]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def bands_of(program: LoadedProgram) -> List[Band]:
|
|
54
|
+
"""Every declared band, resolved once rather than on every bar.
|
|
55
|
+
|
|
56
|
+
Resolved before the first bar is read, so a program naming a plot key
|
|
57
|
+
nothing declares, or a channel past the end of the table, is refused whole
|
|
58
|
+
rather than on the bar the band would first have been drawn on.
|
|
59
|
+
"""
|
|
60
|
+
raw = program.raw
|
|
61
|
+
found: List[Band] = []
|
|
62
|
+
for at, one in enumerate(raw["outputs"][FILLS]):
|
|
63
|
+
path = f"outputs.{FILLS}[{at}]"
|
|
64
|
+
between = one.get("between")
|
|
65
|
+
if not isinstance(between, (list, tuple)) or len(between) != 2:
|
|
66
|
+
raise ScriptError(
|
|
67
|
+
malformed(f"{path}.between", "is not the two plot keys a band is drawn between")
|
|
68
|
+
)
|
|
69
|
+
found.append(
|
|
70
|
+
Band(
|
|
71
|
+
index=at,
|
|
72
|
+
between=[
|
|
73
|
+
channel_of_key(raw, key, f"{path}.between[{side}]")
|
|
74
|
+
for side, key in enumerate(between)
|
|
75
|
+
],
|
|
76
|
+
colours=[channel_named(raw, one, field, path) for _colour, field in SIDES],
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fills_channel(
|
|
83
|
+
program: LoadedProgram, bars: Sequence[Published], spell: Any
|
|
84
|
+
) -> List[Dict[str, Any]]:
|
|
85
|
+
"""Every band on every bar it is drawn on, oldest bar first.
|
|
86
|
+
|
|
87
|
+
Declaration order inside a bar, which is the order the bands are drawn in
|
|
88
|
+
and the order a legend lists them in.
|
|
89
|
+
"""
|
|
90
|
+
declared = bands_of(program)
|
|
91
|
+
if not declared:
|
|
92
|
+
return []
|
|
93
|
+
rows: List[Dict[str, Any]] = []
|
|
94
|
+
for bar in bars:
|
|
95
|
+
for band in declared:
|
|
96
|
+
if any(value_of(program, bar, one) is ABSENT for one in band.between):
|
|
97
|
+
continue
|
|
98
|
+
row: Dict[str, Any] = {"barIndex": bar.index, "fill": band.index}
|
|
99
|
+
for (colour, _field), channel in zip(SIDES, band.colours):
|
|
100
|
+
held = ABSENT if channel is None else value_of(program, bar, channel)
|
|
101
|
+
row[colour] = None if held is ABSENT else spell(held)
|
|
102
|
+
rows.append(row)
|
|
103
|
+
return rows
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""The levels channel: the horizontal lines a host draws after the run.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` 2.8, the ``levels[]`` table: "A level's price arrives
|
|
4
|
+
through a channel and is therefore evaluated every bar, and the level drawn is
|
|
5
|
+
the one from the last bar executed." So this channel is not one row per bar. It
|
|
6
|
+
is one row per level, holding the value the last bar left, which is the only
|
|
7
|
+
value a host ever draws.
|
|
8
|
+
|
|
9
|
+
A level whose last value is absent is not drawn (``stdlib.md`` 18: "a level is
|
|
10
|
+
not drawn"), so it has no row, and the ordinal on each row is what says which
|
|
11
|
+
of the declared levels is missing. The declaration's title, colour, style and
|
|
12
|
+
width are the program's and are not repeated here, for the reason the door's
|
|
13
|
+
docstring gives.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, List, Sequence
|
|
17
|
+
|
|
18
|
+
from ..program import LoadedProgram
|
|
19
|
+
from ..values import ABSENT
|
|
20
|
+
from .published import Published, value_of
|
|
21
|
+
|
|
22
|
+
#: Section 2's vocabulary for this channel.
|
|
23
|
+
LEVELS = "levels"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def levels_channel(
|
|
27
|
+
program: LoadedProgram, bars: Sequence[Published], spell: Any
|
|
28
|
+
) -> List[Dict[str, Any]]:
|
|
29
|
+
"""Each declared level, with the value the last bar executed left in it.
|
|
30
|
+
|
|
31
|
+
A run with no bars draws none: there is no last bar for a level to take its
|
|
32
|
+
price from, and a level drawn at a price nothing computed would be a line
|
|
33
|
+
across a chart with no data behind it.
|
|
34
|
+
"""
|
|
35
|
+
if not bars:
|
|
36
|
+
return []
|
|
37
|
+
last = bars[-1]
|
|
38
|
+
rows: List[Dict[str, Any]] = []
|
|
39
|
+
for at, one in enumerate(program.raw["outputs"][LEVELS]):
|
|
40
|
+
value = value_of(program, last, one["channel"])
|
|
41
|
+
if value is ABSENT:
|
|
42
|
+
continue
|
|
43
|
+
rows.append({"level": at, "value": spell(value)})
|
|
44
|
+
return rows
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The markers channel: one row per marker a bar drew.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` 2.8, the ``markers[]`` table: one entry per ``signal()``
|
|
4
|
+
call site, carrying the channel that holds the marker's text, and "a marker is
|
|
5
|
+
emitted when its channel holds a string for the bar, and not otherwise". Both
|
|
6
|
+
halves of that sentence are the rule below, and the second half is the one worth
|
|
7
|
+
stating: a channel holding a number, a colour or absence draws nothing, and a
|
|
8
|
+
call site that fired twice on a bar left the last text written, which is the
|
|
9
|
+
last write winning (2.7) rather than a rule of this surface's own.
|
|
10
|
+
|
|
11
|
+
The other half of when a marker is drawn is deferral, which ``published.py``
|
|
12
|
+
holds for every channel at once: 5.4 holds a marker back on a bar that is still
|
|
13
|
+
moving, so the first execution of a moving bar writes the text and step 9 throws
|
|
14
|
+
it away. Section 12.6's trace prints that row as ``held``, and the test beside
|
|
15
|
+
this module is measured against that column.
|
|
16
|
+
|
|
17
|
+
**A row carries what the engine computed and the identity it belongs to.** The
|
|
18
|
+
bar, the call site's key and the text. The marker's position, its shape and its
|
|
19
|
+
two colours are declared before bar 0 and are in the program every engine read,
|
|
20
|
+
so a row repeating them would assert the compiler's output through the engine's
|
|
21
|
+
channel; ``spec/decisions.md`` 14 says the same thing about a plot's declared
|
|
22
|
+
colour, that the suite asserts what a run produced. The door's docstring records
|
|
23
|
+
the encoding and what the page leaves open about it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, List, Sequence
|
|
27
|
+
|
|
28
|
+
from ..program import LoadedProgram
|
|
29
|
+
from .published import Published, value_of
|
|
30
|
+
|
|
31
|
+
#: Section 2's vocabulary for this channel, which a case names in ``asserts``.
|
|
32
|
+
MARKERS = "markers"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def markers_channel(
|
|
36
|
+
program: LoadedProgram, bars: Sequence[Published], spell: Any
|
|
37
|
+
) -> List[Dict[str, Any]]:
|
|
38
|
+
"""Every marker drawn, oldest bar first and declaration order inside a bar.
|
|
39
|
+
|
|
40
|
+
Declaration order rather than the order the call sites ran in: an engine
|
|
41
|
+
reporting the order of execution would report a script's branches, and two
|
|
42
|
+
engines agreeing on the branches is what the values channel is for.
|
|
43
|
+
"""
|
|
44
|
+
declared = program.raw["outputs"][MARKERS]
|
|
45
|
+
rows: List[Dict[str, Any]] = []
|
|
46
|
+
for bar in bars:
|
|
47
|
+
for one in declared:
|
|
48
|
+
text = value_of(program, bar, one["channel"])
|
|
49
|
+
if not isinstance(text, str):
|
|
50
|
+
continue
|
|
51
|
+
rows.append({"barIndex": bar.index, "key": one["key"], "text": spell(text)})
|
|
52
|
+
return rows
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The two paint channels: the price bars' colour, and the pane's background.
|
|
2
|
+
|
|
3
|
+
``compiled-program.md`` 2.8: ``barColor`` and ``background`` are each either
|
|
4
|
+
null or one channel, and there is one of each per program. A script with three
|
|
5
|
+
``barColor()`` calls writes the same channel three times and the last write on
|
|
6
|
+
the bar wins, which is 2.7's rule for every channel and needs no sentence here.
|
|
7
|
+
|
|
8
|
+
**A bar with no paint has no row.** ``stdlib.md`` 14.3 says ``barColor(none)``
|
|
9
|
+
and ``background(none)`` leave the bar alone and that passing an absent colour
|
|
10
|
+
is not an error, and 18 says an absent value reaching a surface is a gap and
|
|
11
|
+
never a zero. So a row is a bar that was painted, carrying the bar and the
|
|
12
|
+
colour, and a bar that was not painted is the row that is not there. The two
|
|
13
|
+
channels are projected by one function because they are one shape: an engine
|
|
14
|
+
with a rule for the background that it did not have for the bars would be two
|
|
15
|
+
answers to one question.
|
|
16
|
+
|
|
17
|
+
**What is in the row is what the channel held.** A colour reaches a case file as
|
|
18
|
+
the four integer channels of ``conformance.md`` section 6, spelled ``#rrggbbaa``
|
|
19
|
+
after the alpha conversion of ``compiled-program.md`` 3.1, and the spelling is
|
|
20
|
+
the caller's: ``adapter.spellings`` states that conversion once and this module
|
|
21
|
+
does not state it again. A channel declared ``color`` that held something else
|
|
22
|
+
is reported as what it held rather than dropped, because 3.5 has no check that
|
|
23
|
+
an ``EMIT`` matches its channel's declared type, and a value silently missing
|
|
24
|
+
from a channel is the failure this whole surface exists to avoid.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import Any, Dict, List, Sequence
|
|
28
|
+
|
|
29
|
+
from ..program import LoadedProgram
|
|
30
|
+
from ..values import ABSENT
|
|
31
|
+
from .published import Published, value_of
|
|
32
|
+
|
|
33
|
+
#: Section 2's vocabulary for the two channels, against the field of ``outputs``
|
|
34
|
+
#: each is read from. The names differ by a plural: a case asserts ``barColors``,
|
|
35
|
+
#: one row per painted bar, and a program declares ``barColor``, one channel.
|
|
36
|
+
PAINTS = (("barColors", "barColor"), ("background", "background"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def paint_channel(
|
|
40
|
+
program: LoadedProgram, bars: Sequence[Published], declared: str, spell: Any
|
|
41
|
+
) -> List[Dict[str, Any]]:
|
|
42
|
+
"""Every bar the named paint was applied to, oldest first.
|
|
43
|
+
|
|
44
|
+
``declared`` is the field of ``outputs``: ``barColor`` or ``background``. A
|
|
45
|
+
program that declares neither paints nothing, and its channel is null rather
|
|
46
|
+
than a channel that is never written.
|
|
47
|
+
"""
|
|
48
|
+
held = program.raw["outputs"].get(declared)
|
|
49
|
+
if held is None:
|
|
50
|
+
return []
|
|
51
|
+
channel = held["channel"]
|
|
52
|
+
rows: List[Dict[str, Any]] = []
|
|
53
|
+
for bar in bars:
|
|
54
|
+
colour = value_of(program, bar, channel)
|
|
55
|
+
if colour is ABSENT:
|
|
56
|
+
continue
|
|
57
|
+
rows.append({"barIndex": bar.index, "color": spell(colour)})
|
|
58
|
+
return rows
|