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,78 @@
1
+ """Orders, frames, fills and the ledger: ``stdlib.md`` section 17, in Python.
2
+
3
+ What a strategy decided, what the destination said became of it, and the position
4
+ every figure in the language is folded from. Nothing here decides where an order
5
+ goes: a destination is the host's, and the minute on it in ``spec/decisions.md``
6
+ says the engine learns nothing about it.
7
+
8
+ What is here, by the page it is written from:
9
+
10
+ - ``statuses`` 17.7's vocabulary: the seven words, the four that end an order
11
+ - ``intents`` ``host-interface.md`` 7.1 and 7.2, the two shapes that cross over
12
+ - ``rows`` 17.7's row and 17.8's fold, which is where double counting happens
13
+ - ``positions`` the position book: what settled, per reference, and at what price
14
+ - ``holdings`` 17.1, which position an order is sent against
15
+ - ``closable`` 17.1 and 17.2, what is left to reduce and which side reduces it
16
+ - ``calls`` 17.2 and 17.3's signatures, read once into named fields
17
+ - ``sizing`` how much each order sends, and the split that crosses no zero
18
+ - ``placing`` what each of the nine calls means
19
+ - ``refusals`` OS7002 to OS7013 and OS7017, asked before anything is sent
20
+ - ``ledger`` the run's own record, and the one way a frame gets in
21
+ - ``fills`` what settled, and the ordinal a case names an intent by
22
+
23
+ **Two things this package will not do.** It reaches no destination: an intent
24
+ leaves through the caller, and a frame arrives through ``Ledger.deliver``, which
25
+ is the only way in (``host-interface.md`` 7.4). And it holds no money. What a run
26
+ made is ``accounting``'s, computed after the fact from the fills, and the two are
27
+ separate so that a stored record can be reported again with no engine present.
28
+ """
29
+
30
+ from .calls import ORDER_CALLS, OrderCall, call_of
31
+ from .closable import Closable, closable, closable_units, closing_for, closing_side
32
+ from .fills import Fills, Intents
33
+ from .holdings import Holding, holdings, joining, opposing, outgoing_for, protecting
34
+ from .intents import Identity, IntentBar, OrderFrame, OrderIntent, Placement
35
+ from .ledger import Ledger, LedgerOptions, PlacedCall
36
+ from .positions import Positions
37
+ from .rows import FrameOutcome, LedgerRow, Reduction, fold_frame, working_units
38
+ from .sizing import MappedOrder
39
+ from .statuses import HOST_SENDABLE, STATUSES, TERMINAL, is_terminal, status_from
40
+
41
+ __all__ = [
42
+ "Closable",
43
+ "Fills",
44
+ "HOST_SENDABLE",
45
+ "Holding",
46
+ "Identity",
47
+ "IntentBar",
48
+ "Intents",
49
+ "Ledger",
50
+ "LedgerOptions",
51
+ "LedgerRow",
52
+ "MappedOrder",
53
+ "ORDER_CALLS",
54
+ "OrderCall",
55
+ "OrderFrame",
56
+ "OrderIntent",
57
+ "Placement",
58
+ "PlacedCall",
59
+ "Positions",
60
+ "Reduction",
61
+ "FrameOutcome",
62
+ "STATUSES",
63
+ "TERMINAL",
64
+ "call_of",
65
+ "closable",
66
+ "closable_units",
67
+ "closing_for",
68
+ "closing_side",
69
+ "fold_frame",
70
+ "holdings",
71
+ "is_terminal",
72
+ "joining",
73
+ "opposing",
74
+ "outgoing_for",
75
+ "protecting",
76
+ "status_from",
77
+ "working_units",
78
+ ]
@@ -0,0 +1,201 @@
1
+ """One order call, read once into the fields ``stdlib.md`` 17.2 and 17.3 name.
2
+
3
+ **The positions the arguments arrive in are read here and nowhere else.** Which
4
+ slot holds a quantity and which holds a trigger is a fact about the library
5
+ surface, and a fact read in two places is one that has to be corrected in two
6
+ places. The mapping to orders and the refusals both read this record, so neither
7
+ of them counts arguments.
8
+
9
+ **An argument the script left out and an argument it wrote as absent are not the
10
+ same thing, and an order is where the difference matters most.** ``buy()`` means
11
+ "use the size I declared" and takes the declaration's own quantity.
12
+ ``buy(qty = none)`` means "I computed a size and it came out absent", which is a
13
+ sizing calculation that has not warmed up or a divisor that was zero, and it is
14
+ OS7002: an order is the one place in the language where doing nothing quietly is
15
+ worse than stopping loudly (``language.md`` 6.8).
16
+
17
+ ``compiled-program.md`` 4.10 is what makes the two readable apart. An order call
18
+ carries one argument more than the language surface shows, and it is the last
19
+ one: the names of the arguments the script wrote, in parameter order, separated
20
+ by single spaces. Everything the script did not write takes the default the page
21
+ documents, which the mapping applies.
22
+
23
+ The parameter names below are those two sections' signatures, which is the one
24
+ fact this file copies out of the page, and ``tests/test_calls_surface.py`` reads
25
+ them back out of it. The engine reads no page, so the alternative to a copy held
26
+ by a test is a copy held by nobody.
27
+ """
28
+
29
+ from dataclasses import dataclass
30
+ from typing import Any, Dict, Optional, Sequence, Tuple
31
+
32
+ from .intents import SIDES, TYPES
33
+
34
+ #: The parameters of each order call, in signature order, without the extra
35
+ #: argument 4.10 adds. A call's arity is one more than the length of its row.
36
+ PARAMETERS: Dict[str, Tuple[str, ...]] = {
37
+ "buy": ("qty", "limit", "stop", "tag", "leg"),
38
+ "sell": ("qty", "limit", "stop", "tag", "leg"),
39
+ "close": ("tag", "qty", "leg"),
40
+ "exit": ("tag", "qty", "limit", "stop", "profit", "loss", "leg"),
41
+ "cancel": ("tag",),
42
+ "cancelAll": (),
43
+ "order.place": ("side", "qty", "type", "price", "trigger", "tag", "leg"),
44
+ "order.reverse": ("qty", "tag", "leg"),
45
+ "order.bracket": ("tag", "profit", "loss", "leg"),
46
+ }
47
+
48
+ #: The nine calls that place, cancel or protect, which is what a ledger reads.
49
+ ORDER_CALLS: Tuple[str, ...] = tuple(PARAMETERS)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class OrderCall:
54
+ """An order call, with every argument under the name its signature gives it.
55
+
56
+ ``trigger`` is an order's stop price, which ``buy`` and ``sell`` spell
57
+ ``stop`` and ``order.place`` spells ``trigger``. ``stop`` and ``target`` are
58
+ a bracket's two levels, which ``exit`` spells ``stop`` and ``limit``. The two
59
+ are separate fields because they are separate things: one rests at a venue
60
+ and one is a level the strategy holds.
61
+ """
62
+
63
+ name: str
64
+ position: Any
65
+ #: Arguments the script wrote that came out absent, in signature order.
66
+ absent: Tuple[str, ...] = ()
67
+ side: Optional[str] = None
68
+ qty: Optional[float] = None
69
+ order_type: Optional[str] = None
70
+ limit: Optional[float] = None
71
+ trigger: Optional[float] = None
72
+ target: Optional[float] = None
73
+ stop: Optional[float] = None
74
+ profit: Optional[float] = None
75
+ loss: Optional[float] = None
76
+ tag: Optional[str] = None
77
+
78
+
79
+ def _number(args: Sequence[Any], index: int) -> Optional[float]:
80
+ """A number the script stated, or nothing where it stated none.
81
+
82
+ A boolean is not a number here, as it is not anywhere else in this engine:
83
+ this interpreter's own ``True`` equals ``1``, and an order sized from one
84
+ would be an order nobody wrote.
85
+ """
86
+ if index >= len(args):
87
+ return None
88
+ value = args[index]
89
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
90
+ return None
91
+ return float(value) if value == value and abs(value) != float("inf") else None
92
+
93
+
94
+ def _text(args: Sequence[Any], index: int) -> Optional[str]:
95
+ if index >= len(args):
96
+ return None
97
+ value = args[index]
98
+ return value if isinstance(value, str) else None
99
+
100
+
101
+ def _one_of(words: Sequence[str], word: Optional[str]) -> Optional[str]:
102
+ return word if word in words else None
103
+
104
+
105
+ def _absent_of(name: str, args: Sequence[Any]) -> Tuple[str, ...]:
106
+ """The arguments the script wrote that came out absent, in signature order.
107
+
108
+ Read from the names 4.10 puts in the last argument rather than from a list of
109
+ this file's own, because the signature is the library's and a second copy
110
+ here is one more thing to correct when a signature changes.
111
+ """
112
+ params = PARAMETERS.get(name, ())
113
+ written = _text(args, len(params))
114
+ named = set(written.split(" ")) if written else set()
115
+ found = []
116
+ for at, one in enumerate(params):
117
+ if one in named and at < len(args) and args[at] is None:
118
+ found.append(one)
119
+ return tuple(found)
120
+
121
+
122
+ def _placing(side: str, args: Sequence[Any], position: Any) -> OrderCall:
123
+ """``buy`` and ``sell``, whose name is their side."""
124
+ return OrderCall(
125
+ name=side,
126
+ position=position,
127
+ side=side,
128
+ qty=_number(args, 0),
129
+ limit=_number(args, 1),
130
+ trigger=_number(args, 2),
131
+ tag=_text(args, 3),
132
+ )
133
+
134
+
135
+ def _read(name: str, args: Sequence[Any], position: Any) -> OrderCall:
136
+ if name in ("buy", "sell"):
137
+ return _placing(name, args, position)
138
+ if name == "close":
139
+ # The tag defaults to absence rather than to the empty string, because a
140
+ # call that names none flattens the whole leg and the empty string is a
141
+ # tag an order can carry.
142
+ return OrderCall(name=name, position=position, qty=_number(args, 1), tag=_text(args, 0))
143
+ if name == "exit":
144
+ return OrderCall(
145
+ name=name,
146
+ position=position,
147
+ qty=_number(args, 1),
148
+ target=_number(args, 2),
149
+ stop=_number(args, 3),
150
+ profit=_number(args, 4),
151
+ loss=_number(args, 5),
152
+ tag=_text(args, 0),
153
+ )
154
+ if name == "cancel":
155
+ return OrderCall(name=name, position=position, tag=_text(args, 0))
156
+ if name == "order.place":
157
+ return OrderCall(
158
+ name=name,
159
+ position=position,
160
+ side=_one_of(SIDES, _text(args, 0)),
161
+ qty=_number(args, 1),
162
+ order_type=_one_of(TYPES, _text(args, 2)),
163
+ limit=_number(args, 3),
164
+ trigger=_number(args, 4),
165
+ tag=_text(args, 5),
166
+ )
167
+ if name == "order.reverse":
168
+ return OrderCall(name=name, position=position, qty=_number(args, 0), tag=_text(args, 1))
169
+ if name == "order.bracket":
170
+ # Distances only, which is the whole of this spelling.
171
+ return OrderCall(
172
+ name=name,
173
+ position=position,
174
+ profit=_number(args, 1),
175
+ loss=_number(args, 2),
176
+ tag=_text(args, 0),
177
+ )
178
+ return OrderCall(name=name, position=position)
179
+
180
+
181
+ def call_of(name: str, args: Sequence[Any], position: Any) -> OrderCall:
182
+ """Reads one call, whichever of the nine it is."""
183
+ read = _read(name, args, position)
184
+ absent = _absent_of(name, args)
185
+ if not absent:
186
+ return read
187
+ return OrderCall(
188
+ name=read.name,
189
+ position=read.position,
190
+ absent=absent,
191
+ side=read.side,
192
+ qty=read.qty,
193
+ order_type=read.order_type,
194
+ limit=read.limit,
195
+ trigger=read.trigger,
196
+ target=read.target,
197
+ stop=read.stop,
198
+ profit=read.profit,
199
+ loss=read.loss,
200
+ tag=read.tag,
201
+ )
@@ -0,0 +1,182 @@
1
+ """What is left to reduce and which side reduces it, ``stdlib.md`` 17.1 and 17.2.
2
+
3
+ **Both questions are about a part rather than about the leg**, and the leg is the
4
+ case of a part that no tag names. A leg can be long under one tag and short under
5
+ another, and then its net answers neither question for either of them: a close
6
+ taking its side from the net sent an order that added to the part it was told to
7
+ flatten, and a count taken from the net did not see the close already on its way
8
+ to that part.
9
+
10
+ **What is available to reduce is the settled position less everything already
11
+ working against it.** A position is folded from settled fills and from nothing
12
+ else (17.8), which is right and is what makes this file necessary: an order the
13
+ destination has not answered has filled nothing, so the leg still reads what it
14
+ held before that order left, and measured against that alone every reducing order
15
+ sends the whole position again.
16
+
17
+ **The scope is the run and the position, not the bar.** Counted over one call,
18
+ two bare closes on one bar took a leg holding three long to three short. Counted
19
+ over one bar, the same two closes one bar apart did the same thing, because the
20
+ bar's own record is emptied when the bar index changes. So the record is the
21
+ ledger's, which is the run's: an order the destination still has is exactly a row
22
+ that is neither terminal nor fully filled.
23
+
24
+ **In units, because a position is.** A quantity the script stated is in the
25
+ declaration's own unit (``host-interface.md`` 7.1), so it is a number this file
26
+ can read only where that unit is units. What is done with an order it cannot read
27
+ is the only reading that cannot cross zero: that the order took the whole of the
28
+ part. So what is left carries whether it was counted or assumed. The mapping
29
+ reads the number, because sending nothing is safe either way; the refusal reads
30
+ both, because a refusal may only name a number it measured.
31
+ """
32
+
33
+ from dataclasses import dataclass
34
+ from typing import Optional, Protocol, Sequence
35
+
36
+ from .positions import sign
37
+ from .rows import LedgerRow, working_units
38
+ from .statuses import is_terminal
39
+
40
+
41
+ class Closing(Protocol):
42
+ """What the arithmetic below needs: the leg and the rows of its own ledger."""
43
+
44
+ def size(self) -> float:
45
+ """The leg's net position in units, folded from settled fills."""
46
+
47
+ def rows(self) -> Sequence[LedgerRow]:
48
+ """The rows this strategy placed, newest last."""
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Closable:
53
+ """What one part of the leg can still close, and whether that was measured."""
54
+
55
+ #: The units a close may still send against it.
56
+ units: float
57
+ #: Whether every order counted against it stated a quantity in units.
58
+ counted: bool
59
+
60
+
61
+ def closing_side(size: float) -> Optional[str]:
62
+ """The side that reduces a position, or nothing when there is none to reduce.
63
+
64
+ One fact with two readers: the mapping gives a close its direction with it,
65
+ and the arithmetic below tells an order on its way out of the position from
66
+ one on its way in, which is the difference between a reduction that is still
67
+ working and an entry that holds nothing.
68
+ """
69
+ if size > 0:
70
+ return "sell"
71
+ if size < 0:
72
+ return "buy"
73
+ return None
74
+
75
+
76
+ def held_under(ctx: Closing, tag: str) -> float:
77
+ """The settled units held under one tag, signed the way a position is."""
78
+ held = 0.0
79
+ for row in ctx.rows():
80
+ if row.tag != tag:
81
+ continue
82
+ held += row.filled_qty if row.side == "buy" else -row.filled_qty
83
+ return held
84
+
85
+
86
+ def _holding_of(ctx: Closing, tag: Optional[str]) -> float:
87
+ """What the part a call names holds, signed the way a position is.
88
+
89
+ The leg where the call names no tag, and the settled quantity of that tag's
90
+ own rows where it names one. One function because it is one question, and
91
+ every number this file produces about a part is taken from it.
92
+ """
93
+ return ctx.size() if tag is None else held_under(ctx, tag)
94
+
95
+
96
+ def closing_for(ctx: Closing, tag: Optional[str]) -> Optional[str]:
97
+ """The side that reduces the part a call names, or nothing where it holds none.
98
+
99
+ Taken from the leg instead, a call named ``close`` opened a position: a leg
100
+ holding ten long under one tag and four short under another nets six long, so
101
+ closing the short part was answered with a sell, which took that part to eight
102
+ short and cut the other one to six. A part whose own rows have netted to
103
+ nothing has no side and the call sends nothing, which is 17.2's idempotence.
104
+ """
105
+ return closing_side(_holding_of(ctx, tag))
106
+
107
+
108
+ def _committed(ctx: Closing, tag: Optional[str]) -> Closable:
109
+ """The units already working against the position, across the whole run.
110
+
111
+ Only an order on the side that reduces what the leg holds now, and every
112
+ order on that side. A row records what it reduced at the moment it was sent,
113
+ and a leg that has changed sign since is being reduced from the other side:
114
+ an order still working on the old side is adding to the leg rather than
115
+ taking from it, and an order recorded as adding is a reduction now whatever
116
+ it was then. The side is the part's own, which is the leg's where no tag
117
+ names one.
118
+ """
119
+ held = _holding_of(ctx, tag)
120
+ side = closing_side(held)
121
+ if side is None:
122
+ return Closable(units=0.0, counted=True)
123
+ units = 0.0
124
+ counted = True
125
+ for row in ctx.rows():
126
+ if row.side != side or is_terminal(row.status):
127
+ continue
128
+ reduces = row.reduces
129
+ if reduces is not None:
130
+ if tag is not None and reduces.part != tag:
131
+ continue
132
+ working = working_units(row)
133
+ if working == 0:
134
+ continue
135
+ units += working
136
+ counted = counted and reduces.counted
137
+ continue
138
+ # An order that was an entry when it left. Its own tag is what names it,
139
+ # because a reduction's part is the tag a close named and an entry named
140
+ # no part of anything.
141
+ if tag is not None and row.tag != tag:
142
+ continue
143
+ if row.units is None:
144
+ # A quantity in the declaration's own unit, working against this part
145
+ # and unreadable as a number of units. Taken to cover the whole of
146
+ # what is there, which is the reading 17.1 already makes for the
147
+ # other order it cannot read.
148
+ units += abs(held)
149
+ counted = False
150
+ continue
151
+ units += max(0.0, row.units - row.filled_qty)
152
+ return Closable(units=units, counted=counted)
153
+
154
+
155
+ def closable(ctx: Closing, tag: Optional[str]) -> Closable:
156
+ """What a ``close`` can still close, in units, whether or not it names a quantity.
157
+
158
+ A part on the leg's own side is bounded by the leg, so that closing a part can
159
+ never take the leg through zero. A part on the other side is not: closing a
160
+ short part under a long leg is a buy, which moves the leg away from zero
161
+ rather than towards it, so there is nothing for the leg's own number to bound.
162
+ What that order may actually send is still bounded per position by what has
163
+ settled there, so a part whose reference has already returned to zero sends
164
+ nothing rather than opening it again.
165
+ """
166
+ spent = _committed(ctx, None)
167
+ leg = max(0.0, abs(ctx.size()) - spent.units)
168
+ if tag is None:
169
+ return Closable(units=leg, counted=spent.counted)
170
+ held = held_under(ctx, tag)
171
+ own = _committed(ctx, tag)
172
+ part = max(0.0, abs(held) - own.units)
173
+ if sign(held) != sign(ctx.size()):
174
+ return Closable(units=part, counted=own.counted)
175
+ if part <= leg:
176
+ return Closable(units=part, counted=own.counted)
177
+ return Closable(units=leg, counted=spent.counted)
178
+
179
+
180
+ def closable_units(ctx: Closing, tag: Optional[str]) -> float:
181
+ """What a close can still close, for the mapping, which sends a number."""
182
+ return closable(ctx, tag).units
@@ -0,0 +1,131 @@
1
+ """What a run keeps about its intents: which one a frame names, and what settled.
2
+
3
+ Two things a run holds beside the ledger, because neither is the ledger's.
4
+
5
+ **An ordinal is how a case names an intent.** ``conformance.md`` section 3 says a
6
+ frame names its intent by ordinal and never by an engine's own id: 1 is the first
7
+ intent the run placed, 2 the second, and a case cannot know the id another engine
8
+ minted, so it must not depend on its spelling. An ordinal greater than the number
9
+ of intents the run placed is how a case hands an engine a frame naming an order
10
+ its ledger does not hold, and that is step 1 of the fold rather than a mistake
11
+ here, so the map answers with nothing and lets the ledger refuse it.
12
+
13
+ **A fill is what the money is folded from**, and the two sizes of a position
14
+ either side of the settlement are kept here rather than read back from the leg.
15
+ The leg reports a net and a fill settles against one position reference, and the
16
+ two are different numbers whenever a leg holds more than one position, which is
17
+ every flip. They are folded in the order the frames were folded in, from the same
18
+ deltas, so they are the same arithmetic the position book did rather than a second
19
+ reading of its result.
20
+
21
+ A refused frame settles nothing and is not a fill.
22
+ """
23
+
24
+ from typing import Dict, List, Optional, Sequence, Tuple
25
+
26
+ from ..accounting.shapes import RecordedFill
27
+ from .intents import OrderIntent
28
+ from .rows import FrameOutcome
29
+
30
+
31
+ class Intents:
32
+ """Every intent a run has placed, in the order it placed them.
33
+
34
+ The ordinal is the position in that order, counting from one, and it counts
35
+ every intent rather than every ledger row: a bracket and a cancellation take
36
+ an ordinal and append no row, because an ordinal names what the engine handed
37
+ over and a row records what was ordered.
38
+ """
39
+
40
+ def __init__(self) -> None:
41
+ self._placed: List[OrderIntent] = []
42
+ self._by_id: Dict[int, OrderIntent] = {}
43
+
44
+ def record(self, intents: Sequence[OrderIntent]) -> None:
45
+ """The intents one bar handed over, in the order the bar applied them."""
46
+ for intent in intents:
47
+ self._placed.append(intent)
48
+ self._by_id[intent.intent_id] = intent
49
+
50
+ def count(self) -> int:
51
+ return len(self._placed)
52
+
53
+ def at_ordinal(self, ordinal: int) -> Optional[OrderIntent]:
54
+ """The intent a case's ordinal names, or nothing where it names none."""
55
+ if ordinal < 1 or ordinal > len(self._placed):
56
+ return None
57
+ return self._placed[ordinal - 1]
58
+
59
+ def by_id(self, intent_id: int) -> Optional[OrderIntent]:
60
+ return self._by_id.get(intent_id)
61
+
62
+ def ordinal_of(self, intent_id: int) -> int:
63
+ """Which intent of the run this is, or ``0`` for one this run never placed.
64
+
65
+ What a record writes beside a ledger row, so that a case names an order by
66
+ a number every engine agrees about.
67
+ """
68
+ for at, intent in enumerate(self._placed):
69
+ if intent.intent_id == intent_id:
70
+ return at + 1
71
+ return 0
72
+
73
+
74
+ class Fills:
75
+ """The fills a run has settled, in fold order, with what each moved."""
76
+
77
+ def __init__(self) -> None:
78
+ self._fills: List[RecordedFill] = []
79
+ self._sizes: Dict[int, float] = {}
80
+ self._seq = 0
81
+
82
+ def settled(self) -> Tuple[RecordedFill, ...]:
83
+ return tuple(self._fills)
84
+
85
+ def record(
86
+ self,
87
+ outcomes: Sequence[FrameOutcome],
88
+ intents: Intents,
89
+ bar_index: int,
90
+ bar_time: Optional[float],
91
+ refs: Optional[Dict[int, str]] = None,
92
+ ) -> Tuple[RecordedFill, ...]:
93
+ """The fills one bar's fold settled, appended in the order they folded.
94
+
95
+ The bar is the bar the fold happened at, which is the bar the position
96
+ exists from. Where the fill itself traded is the bar before it or this
97
+ bar's own open, and neither is a bar the strategy could have acted on it
98
+ in.
99
+ """
100
+ made: List[RecordedFill] = []
101
+ for outcome in outcomes:
102
+ if outcome.refused is not None:
103
+ continue
104
+ if outcome.delta <= 0 or outcome.price is None:
105
+ continue
106
+ intent = intents.by_id(outcome.intent_id)
107
+ if intent is None or intent.side is None:
108
+ continue
109
+
110
+ before = self._sizes.get(intent.position_ref, 0.0)
111
+ after = before + (-outcome.delta if intent.side == "sell" else outcome.delta)
112
+ self._sizes[intent.position_ref] = after
113
+ self._seq += 1
114
+
115
+ fill = RecordedFill(
116
+ seq=self._seq,
117
+ intent_id=outcome.intent_id,
118
+ order_ref="" if refs is None else refs.get(outcome.intent_id, ""),
119
+ tag=intent.tag,
120
+ position_ref=intent.position_ref,
121
+ side=intent.side,
122
+ units=outcome.delta,
123
+ price=outcome.price,
124
+ bar_index=bar_index,
125
+ bar_time=bar_time,
126
+ ref_size_before=before,
127
+ ref_size_after=after,
128
+ )
129
+ self._fills.append(fill)
130
+ made.append(fill)
131
+ return tuple(made)