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,293 @@
1
+ """What the order layer will not do, ``errors.md`` OS7002 to OS7013 and OS7017.
2
+
3
+ **An order is the one place in the language where doing nothing quietly is worse
4
+ than stopping loudly.** Everywhere else an absent value propagates and the study
5
+ draws a gap; here it would be a position the script believes it holds and does
6
+ not, or one it holds and does not know about.
7
+
8
+ **A refusal happens before anything is sent.** The ledger asks this file about
9
+ every order a call produces before any of them takes an id, so a refused call
10
+ appends no row and returns no intent: there is nothing for a host to send and
11
+ nothing to take back afterwards.
12
+
13
+ **What is not refused here.** A rule this file cannot evaluate truthfully is not
14
+ evaluated at all. A quantity that is not a whole number of lots, OS7005, and an
15
+ order outside the instrument's session, OS7012, are facts about a leg no file can
16
+ declare yet. The capital a strategy has left, OS7011, is money and the money
17
+ layer does not join the execution path in this release. OS7014 and OS7015 are the
18
+ host's, under ``host-interface.md`` 7.6. And a stated close quantity held against
19
+ a position that is still there is checked in full only in a declaration counting
20
+ in units, which is the narrowing OS7017 itself carries.
21
+
22
+ Nothing here holds a message. ``spec/errors.json`` is the catalogue and the
23
+ authority, and a second engine that typed the sentences out again would be a
24
+ second place for them to be wrong: what travels is the code, the position and the
25
+ values the message names.
26
+ """
27
+
28
+ from typing import Any, Optional, Sequence, Tuple
29
+
30
+ from ..diagnostics import Diagnostic, failure
31
+ from .calls import OrderCall
32
+ from .closable import closable
33
+ from .intents import Placement
34
+ from .positions import sign
35
+ from .sizing import MappedOrder
36
+ from .statuses import is_terminal
37
+
38
+ #: How a message names an instrument the host gave no symbol for. The same words
39
+ #: the engine uses for the same gap elsewhere, because a reader meeting both
40
+ #: should not have to work out that they mean one thing.
41
+ UNNAMED_INSTRUMENT = "the chart's instrument"
42
+
43
+ #: The calls that take the declaration's size when they are given none.
44
+ SIZED_BY_DECLARATION: Tuple[str, ...] = ("buy", "sell", "order.place")
45
+
46
+ #: How far off a tick a price may be and still be on it. A price computed from a
47
+ #: tick size rarely divides by it exactly in binary, and a tolerance relative to
48
+ #: the count keeps that true at every magnitude while staying far tighter than
49
+ #: the half tick that would let a price between two ticks through.
50
+ TICK_TOLERANCE = 1e-9
51
+
52
+
53
+ class SentOnBar:
54
+ """An order this bar has already sent, which is what OS7013 is asked about.
55
+
56
+ The bar's own record, and the only rule left that is scoped to a bar. What a
57
+ close is measured against is a different question with a different scope, and
58
+ it is answered from the ledger's rows. The record is the bar's rather than
59
+ the execution's, so a bar declared ``onUnconfirmed`` and executed again is
60
+ still one bar for it: the orders of its earlier executions really were handed
61
+ over.
62
+ """
63
+
64
+ __slots__ = ("name", "line", "side")
65
+
66
+ def __init__(self, name: str, line: int, side: str) -> None:
67
+ self.name = name
68
+ self.line = line
69
+ self.side = side
70
+
71
+
72
+ def _name_of(instrument: Any) -> str:
73
+ symbol = getattr(instrument, "symbol", None)
74
+ return UNNAMED_INSTRUMENT if symbol is None else symbol
75
+
76
+
77
+ def _spell(name: str, line: int) -> str:
78
+ """An order call as a message names one: what was called, and where."""
79
+ return f"{name}() on line {line}"
80
+
81
+
82
+ def _part_of(tag: Optional[str]) -> str:
83
+ """What a close is closing, as a message names it: the leg, or one tag of it."""
84
+ return "the leg" if tag is None else f'the tag "{tag}"'
85
+
86
+
87
+ def _price_missing(call: OrderCall) -> Optional[str]:
88
+ """The price argument a named type needs and was not given, 17.2.
89
+
90
+ Only a call that names a type can disagree with its own prices. ``buy`` and
91
+ ``sell`` take no type and their prices imply one, so the two always agree.
92
+ """
93
+ named = call.order_type
94
+ if named in ("limit", "stopLimit") and call.limit is None:
95
+ return "price"
96
+ if named in ("stop", "stopLimit") and call.trigger is None:
97
+ return "trigger"
98
+ return None
99
+
100
+
101
+ def _on_tick(price: float, tick: float) -> bool:
102
+ steps = price / tick
103
+ nearest = round(steps)
104
+ return abs(steps - nearest) <= TICK_TOLERANCE * max(1.0, abs(steps))
105
+
106
+
107
+ def _entries_open(ctx: Any, side: str) -> int:
108
+ """The entries already open in one direction, which is what pyramiding counts.
109
+
110
+ Over every position the leg still holds on that side rather than over one of
111
+ them: an entry placed while the whole of a position is on its way out opens a
112
+ position of its own, so a count keyed to a single reference would report none
113
+ and let a declaration of one entry hold two.
114
+
115
+ An order that has not filled is not an open entry, because OS7008's own fix
116
+ tells a reader to test ``pos.size`` and that is folded from settled fills. A
117
+ count including an order still waiting at the venue would refuse a script that
118
+ had done exactly what the fix asked, which is the one thing a fix may never do.
119
+ """
120
+ holding = 1 if side == "buy" else -1
121
+ found = 0
122
+ for row in ctx.rows():
123
+ if row.side != side or row.filled_qty == 0:
124
+ continue
125
+ if sign(ctx.size_of(row.position_ref)) != holding:
126
+ continue
127
+ found += 1
128
+ return found
129
+
130
+
131
+ def _beyond_the_close(call: OrderCall, ctx: Any) -> Optional[Diagnostic]:
132
+ """A close asked to send more than it is closing, OS7017.
133
+
134
+ Refused rather than clamped, because the quantity is an argument the script
135
+ wrote and is therefore a claim about the strategy's own position. Sending
136
+ what is there would leave the script believing it closed the number it asked
137
+ for, and reading the call as a reversal would make ``close`` open a position.
138
+
139
+ In full only where the two numbers count the same thing, and in part
140
+ everywhere: nothing left to close is zero in every unit, so a close of any
141
+ quantity against nothing is a false claim whatever the declaration counts in.
142
+ An assumed zero is not a measurement, so it is not named in a message either.
143
+ """
144
+ if call.name != "close":
145
+ return None
146
+ stated = call.qty
147
+ if stated is None:
148
+ return None
149
+ left = closable(ctx, call.tag)
150
+ held = left.units
151
+ if stated <= held:
152
+ return None
153
+ if ctx.qty_type != "units" and not (held == 0 and left.counted):
154
+ return None
155
+ return failure(
156
+ "OS7017", call.position, qty=stated, part=_part_of(call.tag), held=held
157
+ )
158
+
159
+
160
+ def refusal_in_call(call: OrderCall, ctx: Any) -> Optional[Diagnostic]:
161
+ """What the call itself is wrong about, before anything is computed from it.
162
+
163
+ Four refusals. Three are about what the script wrote and nothing else, so they
164
+ are the same on every bar and on every host. The fourth, OS7017, is the one
165
+ quantity a script writes that is a claim about the leg rather than about
166
+ itself, so it is answered here, against the leg, before the call is mapped.
167
+ """
168
+ # OS7002. Every argument the script wrote that came out absent, named in
169
+ # signature order, so a call with two is reported on the first one a reader
170
+ # would fix. An argument the script did not write is not one of them.
171
+ if call.absent:
172
+ return failure("OS7002", call.position, name=call.name, argument=call.absent[0])
173
+
174
+ # OS7004. Direction is chosen by the function and not by the sign, so a
175
+ # negative quantity is a calculation that went the wrong way.
176
+ stated = call.qty
177
+ if stated is not None and not stated > 0:
178
+ return failure("OS7004", call.position, name=call.name, qty=stated)
179
+ if stated is None and call.name in SIZED_BY_DECLARATION and not ctx.declared_qty > 0:
180
+ return failure("OS7004", call.position, name=call.name, qty=ctx.declared_qty)
181
+
182
+ # OS7017. After OS7004, so a negative quantity is still answered by the code
183
+ # that is about the sign rather than by the one that is about the size.
184
+ crossing = _beyond_the_close(call, ctx)
185
+ if crossing is not None:
186
+ return crossing
187
+
188
+ # OS7007. Filling the price in from the bar's close would make the order a
189
+ # market order wearing another name.
190
+ missing = _price_missing(call)
191
+ if missing is not None and call.order_type is not None:
192
+ return failure("OS7007", call.position, type=call.order_type, argument=missing)
193
+
194
+ return None
195
+
196
+
197
+ def _unknown_tag(call: OrderCall, placement: Placement, ctx: Any) -> Optional[Diagnostic]:
198
+ """A cancellation naming an order that is not there to cancel, OS7009."""
199
+ for row in ctx.rows():
200
+ if row.tag == placement.tag and not is_terminal(row.status):
201
+ return None
202
+ return failure("OS7009", call.position, tag=placement.tag)
203
+
204
+
205
+ def _wrong_side(call: OrderCall, placement: Placement, ctx: Any) -> Optional[Diagnostic]:
206
+ """A protective level on the wrong side of the entry, OS7010.
207
+
208
+ Only against a position that is open, because the level is measured from the
209
+ position's average entry price and a leg that holds nothing has none. An entry
210
+ and its bracket on one bar is the common shape and is not this case: the entry
211
+ has not filled, so there is nothing yet for the level to be on the wrong side
212
+ of, which is why a bracket may also state its levels as distances.
213
+
214
+ A level exactly at the entry is not refused. Moving every stop to its own
215
+ entry is a rule the language names (17.11), so the price that rule produces
216
+ cannot be one the language will not take.
217
+ """
218
+ entry = ctx.avg_price()
219
+ size = ctx.size()
220
+ if entry is None or size == 0:
221
+ return None
222
+ long = size > 0
223
+ side = "long" if long else "short"
224
+
225
+ stop = placement.stop
226
+ if stop is not None and (stop > entry if long else stop < entry):
227
+ return failure("OS7010", call.position, side=side, entry=entry, leg="stop", price=stop)
228
+ target = placement.target
229
+ if target is not None and (target < entry if long else target > entry):
230
+ return failure("OS7010", call.position, side=side, entry=entry, leg="limit", price=target)
231
+ return None
232
+
233
+
234
+ def _ordering(
235
+ call: OrderCall, order: MappedOrder, ctx: Any, sent: Sequence[SentOnBar]
236
+ ) -> Optional[Diagnostic]:
237
+ """What one order is wrong about, given the instrument and the leg."""
238
+ placement = order.placement
239
+ side = placement.side
240
+ if side is None:
241
+ return None
242
+
243
+ # OS7006. A price between two ticks cannot exist at the exchange, and
244
+ # rounding it here would move the order off the level the script computed.
245
+ tick = ctx.tick_size
246
+ if tick is not None and tick > 0:
247
+ for price in (placement.limit, placement.trigger):
248
+ if price is None or _on_tick(price, tick):
249
+ continue
250
+ return failure(
251
+ "OS7006", call.position, symbol=_name_of(ctx.instrument), tick=tick, price=price
252
+ )
253
+
254
+ # OS7008. Refusing rather than silently adding keeps a backtest from building
255
+ # a position the declaration forbade. Whether this order is an entry is the
256
+ # mapping's answer and not the leg's net: the net reads flat while an entry is
257
+ # still going, and an order that comes off a position is not an entry whatever
258
+ # it says.
259
+ if order.reduces is None:
260
+ found = _entries_open(ctx, side)
261
+ if found >= ctx.pyramiding:
262
+ return failure("OS7008", call.position, max=ctx.pyramiding, found=found)
263
+
264
+ # OS7013. Which of the two to honour has no defensible answer, so neither is
265
+ # placed: this call sends nothing, and the bar stops before anything the bar
266
+ # decided reaches the destination.
267
+ for one in sent:
268
+ if one.side != side:
269
+ return failure(
270
+ "OS7013",
271
+ call.position,
272
+ first=_spell(one.name, one.line),
273
+ second=_spell(call.name, call.position.line),
274
+ bar=ctx.bar.index,
275
+ )
276
+
277
+ return None
278
+
279
+
280
+ def refusal_in_order(
281
+ call: OrderCall, order: MappedOrder, ctx: Any, sent: Sequence[SentOnBar]
282
+ ) -> Optional[Diagnostic]:
283
+ """What one order this call produced is wrong about.
284
+
285
+ Asked of every order a call sends, before any of them is given an id, so a
286
+ call that sends two sends both or neither.
287
+ """
288
+ placement = order.placement
289
+ if placement.kind == "cancel":
290
+ return _unknown_tag(call, placement, ctx)
291
+ if placement.kind == "bracket":
292
+ return _wrong_side(call, placement, ctx)
293
+ return _ordering(call, order, ctx, sent)
@@ -0,0 +1,219 @@
1
+ """One row of the ledger, and the fold of one frame into it, ``stdlib.md`` 17.7 and 17.8.
2
+
3
+ **A row is appended when the order is sent and is never rewritten by
4
+ guesswork.** Every change to it comes from a frame the destination sent, so the
5
+ sequence that produced a position can be replayed rather than inferred.
6
+
7
+ **The fold is where double counting happens**, which is why it is written once,
8
+ here, and why every step of it is the numbered step 17.8 gives. Frames repeat,
9
+ cross in flight and arrive after the order they are about has ended, and an
10
+ engine that added each frame's quantity to a running total would report a
11
+ position the strategy never held. Taking the greatest cumulative quantity
12
+ instead makes a repeat cost nothing and makes a terminal frame that overtook a
13
+ partial one carry the whole remainder in one piece.
14
+
15
+ **A row also carries what its order takes out of the position**, because that is
16
+ the only record of it that outlives the bar the order was sent on. A position
17
+ moves when a fill settles, so an order that has been sent and not answered has
18
+ moved nothing and is invisible to every position figure; the row is where it is
19
+ visible. ``closable`` reads the reduction back, and what makes the reading work
20
+ is that a row already says whether the order is still going: an order neither
21
+ terminal nor fully filled is one the destination still has.
22
+
23
+ Steps 1 and 6 are not here. Locating a row is a question about the whole ledger
24
+ and settling a fill is the position book's, so this file says how many units
25
+ settled and at what price, and the ledger says where.
26
+ """
27
+
28
+ from dataclasses import dataclass, field
29
+ from typing import Optional
30
+
31
+ from .intents import Identity
32
+ from .statuses import PLACED, is_terminal, rank_of, status_from
33
+
34
+ #: Why a frame was refused, ``stdlib.md`` 17.14. Both are the host's mistake and
35
+ #: both are recorded rather than raised: a frame naming an order this strategy
36
+ #: never placed is a fact about the host, and one reporting a fill with no price
37
+ #: cannot be marked against anything.
38
+ UNKNOWN_INTENT = "unknownIntent"
39
+ FILL_WITH_NO_PRICE = "fillWithNoPrice"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Reduction:
44
+ """What one order claimed of the part it reduces, in units, when it was sent.
45
+
46
+ Absent on an order that adds to a position, and absent on one whose quantity
47
+ the engine cannot count in units, which are two different facts with the
48
+ same consequence: neither subtracts anything from what is left to close.
49
+
50
+ ``claimed`` is a claim and not a size. On an order the engine sized itself
51
+ the two are the same number; on one it could not read they are not, and
52
+ there it is the whole of what was left to close at the moment the order
53
+ left, which is the reading ``stdlib.md`` 17.1 makes for an order it cannot
54
+ count.
55
+ """
56
+
57
+ #: The tag the close named, or absent where it reduces the leg as a whole.
58
+ part: Optional[str]
59
+ #: The units of that part this order claimed when it was sent.
60
+ claimed: float
61
+ #: Whether ``claimed`` is the order's own quantity or the most it could have
62
+ #: been. False on an order whose quantity the engine cannot read in units.
63
+ counted: bool
64
+
65
+
66
+ @dataclass
67
+ class LedgerRow:
68
+ """A row of the ledger, ``stdlib.md`` 17.7's own field list."""
69
+
70
+ intent_id: int
71
+ tag: str
72
+ leg: str
73
+ position_ref: int
74
+ instrument: Identity
75
+ product: str
76
+ side: str
77
+ qty: float
78
+ order_type: str
79
+ price: Optional[float]
80
+ trigger: Optional[float]
81
+ placed_at: Optional[float]
82
+ updated_at: Optional[float]
83
+ #: What this order claimed of the position, absent where it claimed nothing.
84
+ #: Fixed when the order was sent and never rewritten, because it is a record
85
+ #: of what the engine measured at that moment and not a running total.
86
+ reduces: Optional[Reduction] = None
87
+ #: This order's own quantity in units, absent where the engine cannot read
88
+ #: it. ``reduces`` says what an order takes out of a position and this says
89
+ #: what it puts into one.
90
+ units: Optional[float] = None
91
+ #: The destination's own reference, recorded, shown, and never parsed.
92
+ order_ref: str = ""
93
+ status: str = PLACED
94
+ filled_qty: float = 0.0
95
+ avg_fill_price: Optional[float] = None
96
+ rejection: str = ""
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class FrameOutcome:
101
+ """What one frame did.
102
+
103
+ A refused frame changes nothing and is recorded. ``after_terminal`` is the
104
+ ``fillAfterTerminal`` of 17.11: a fill the strategy could not have expected
105
+ is the event a trader most needs named.
106
+ """
107
+
108
+ intent_id: object
109
+ refused: Optional[str] = None
110
+ changed: bool = False
111
+ #: Units this frame added, which is what settles against a position.
112
+ delta: float = 0.0
113
+ price: Optional[float] = None
114
+ after_terminal: bool = False
115
+ #: What ``fillAfterTerminal`` carries, and empty on every other outcome: the
116
+ #: tag, the quantity this frame added and the terminal word it arrived after.
117
+ #: A fill the strategy could not have expected is the event a trader most
118
+ #: needs named, so the payload travels with the outcome rather than being
119
+ #: rebuilt by whoever logs it.
120
+ event: dict = field(default_factory=dict)
121
+
122
+
123
+ def working_units(row: LedgerRow) -> float:
124
+ """How much of this row's claim on the part it reduces is still outstanding.
125
+
126
+ Four facts decide the number, and each of them is a case a strategy meets. A
127
+ partial fill of an order the engine counted releases what settled. A partial
128
+ fill of one it could not count leaves the claim whole until the order ends,
129
+ because the claim is in units and the answer is in whatever unit the order
130
+ was written in, and subtracting one from the other is drift. An order that
131
+ has ended releases the rest, which is how a strategy whose close was refused
132
+ closes again. And an order that adds claims nothing at all: an entry that has
133
+ not settled is not a position, and counting it would send a close for units
134
+ that may never exist.
135
+ """
136
+ reduces = row.reduces
137
+ if reduces is None or is_terminal(row.status):
138
+ return 0.0
139
+ if reduces.counted:
140
+ return max(0.0, reduces.claimed - row.filled_qty)
141
+ return reduces.claimed
142
+
143
+
144
+ def refused(intent_id: object, why: str) -> FrameOutcome:
145
+ """A frame that folded nothing, carrying which of 17.14's two it was."""
146
+ return FrameOutcome(intent_id=intent_id, refused=why)
147
+
148
+
149
+ def fold_frame(row: LedgerRow, frame) -> FrameOutcome:
150
+ """Folds one frame into one row, ``stdlib.md`` 17.8 steps 2 to 5 and 7."""
151
+ # A word outside the vocabulary leaves the status alone and the rest of the
152
+ # frame folds anyway. Mapping a destination's own words onto the vocabulary
153
+ # is the host's job under 17.7, and an engine that guessed at one would
154
+ # decide an order was dead on a word it had never seen. Refusing the whole
155
+ # frame instead would throw away the cumulative quantity it carries, which
156
+ # is real whatever the word beside it says.
157
+ status = status_from(frame.status)
158
+ was_terminal = is_terminal(row.status)
159
+
160
+ # Step 2. The cumulative quantity never decreases, so a frame reporting less
161
+ # than the row already holds contributes nothing.
162
+ filled = max(row.filled_qty, frame.filled_qty)
163
+ delta = filled - row.filled_qty
164
+
165
+ # Step 3. The destination computed its average over the cumulative quantity,
166
+ # so the row takes that average whole. The engine never averages two
167
+ # averages of its own.
168
+ price = frame.avg_fill_price
169
+ if delta > 0 and price is None:
170
+ return refused(frame.intent_id, FILL_WITH_NO_PRICE)
171
+
172
+ # Step 4, which a frame arriving at a terminal row does not run: the status
173
+ # records how the order ended and the quantity records what traded, and the
174
+ # two are both true.
175
+ moved = (
176
+ status is not None
177
+ and status != PLACED
178
+ and not was_terminal
179
+ and rank_of(status) >= rank_of(row.status)
180
+ and status != row.status
181
+ )
182
+
183
+ text = frame.text if frame.text is not None else ""
184
+ new_text = text != "" and text != row.rejection
185
+
186
+ # Step 5, and then step 7: a repeated frame and one overtaken by a later
187
+ # frame both end here, having changed nothing. No fill, no event, no report
188
+ # row, no recalculation.
189
+ if not (moved or delta > 0 or new_text):
190
+ return FrameOutcome(intent_id=frame.intent_id)
191
+
192
+ if moved and status is not None:
193
+ row.status = status
194
+ if delta > 0:
195
+ row.filled_qty = filled
196
+ row.avg_fill_price = price
197
+ if new_text:
198
+ row.rejection = text
199
+ if isinstance(frame.order_ref, str):
200
+ row.order_ref = frame.order_ref
201
+ if frame.sent_instrument is not None:
202
+ row.instrument = frame.sent_instrument
203
+ if isinstance(frame.sent_product, str):
204
+ row.product = frame.sent_product
205
+ if isinstance(frame.time, (int, float)) and not isinstance(frame.time, bool):
206
+ row.updated_at = float(frame.time)
207
+
208
+ return FrameOutcome(
209
+ intent_id=frame.intent_id,
210
+ changed=True,
211
+ delta=delta,
212
+ price=price if delta > 0 else None,
213
+ after_terminal=was_terminal and delta > 0,
214
+ event=(
215
+ {"tag": row.tag, "added": delta, "after": row.status}
216
+ if was_terminal and delta > 0
217
+ else {}
218
+ ),
219
+ )