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,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