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,277 @@
1
+ """What a leg holds, one entry per position reference, including what is still working.
2
+
3
+ **This file answers which position an order is sent against**, which the leg's
4
+ net cannot answer. The net is folded from settled fills, so with the destination
5
+ silent an entry of six on one bar and an opposing nine on the next would read as
6
+ opposing nothing at all, and both would go on one reference: that reference would
7
+ open six long and settle three short, which is the one failure ``stdlib.md`` 17.1
8
+ names, because a fill arriving late could no longer say which position it settled.
9
+
10
+ **So a reference is measured by what is on it, settled and working together.** An
11
+ order the destination has not answered has moved no position figure, and the
12
+ ledger's rows are where it is visible. A reference with six units of buy still to
13
+ come is a long position whether or not any of it has settled, and an opposing
14
+ order has to take those six off it before it opens anything new: otherwise
15
+ nothing can ever bring that reference back to zero, which is 17.7's sentence
16
+ about how a position ends.
17
+
18
+ **Two numbers, because two questions are asked and they are not the same
19
+ question.** ``units`` is what an opposing order may take, and it counts what is
20
+ working: the order is being sent either way, at the size the script wrote, and
21
+ the only thing being decided is which reference each unit lands on. ``settled``
22
+ is what a close may send, and it counts only what has settled less what is
23
+ already working against it, because a close chooses its own quantity and one
24
+ counting a working entry would sell units that may never exist.
25
+
26
+ **What this costs, stated rather than discovered.** An entry that opposes an
27
+ unanswered entry is sent against it, so if the first order is rejected and the
28
+ second fills, that reference settles on the side it did not open on. No engine
29
+ avoids that without holding an order back until the destination answers, which is
30
+ an engine that stops trading when a destination is slow. What 17.1 asks for is
31
+ kept either way: every order names exactly one position.
32
+ """
33
+
34
+ from dataclasses import dataclass
35
+ from typing import Callable, List, Optional, Protocol, Sequence, Tuple
36
+
37
+ from .positions import sign
38
+ from .rows import LedgerRow
39
+ from .statuses import is_terminal
40
+
41
+
42
+ class Book(Protocol):
43
+ """What this file reads: the rows of the ledger and the position book."""
44
+
45
+ def rows(self) -> Sequence[LedgerRow]:
46
+ """The rows this strategy placed, newest last."""
47
+
48
+ def size_of(self, ref: int) -> float:
49
+ """The settled size of one position reference, signed the way a position is."""
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Holding:
54
+ """One position reference a leg holds, and what may still be done to it."""
55
+
56
+ ref: int
57
+ #: ``buy`` for a long position, ``sell`` for a short one.
58
+ side: str
59
+ #: The units an opposing order may take, or absent where the engine cannot
60
+ #: read one of its quantities. Absent is not zero: it means the reference
61
+ #: holds something whose size is in the declaration's own unit.
62
+ units: Optional[float]
63
+ #: The settled units nothing is working against, which is what a close sends.
64
+ settled: float
65
+
66
+
67
+ @dataclass
68
+ class _Tally:
69
+ """One reference under construction, before it is decided what it holds."""
70
+
71
+ settled: float
72
+ working: float = 0.0
73
+ #: Working units on the side that reduces what has settled here, unsigned.
74
+ #: Which side that is depends on what settled, not on what an order was when
75
+ #: it left: an order recorded as adding is a reduction now on a position that
76
+ #: has since changed sign, and a reduction recorded then is adding now.
77
+ against: float = 0.0
78
+ #: The side of a working order whose quantity is not readable, or none.
79
+ unreadable: Optional[str] = None
80
+ #: Whether a quantity it cannot read is working against what settled here.
81
+ blocked: bool = False
82
+
83
+
84
+ def _side_of(signed: float) -> str:
85
+ return "buy" if signed > 0 else "sell"
86
+
87
+
88
+ def holdings(ctx: Book) -> Tuple[Holding, ...]:
89
+ """Every position reference this leg holds, oldest first.
90
+
91
+ Oldest first is the order the references were minted in, which is the order
92
+ the rows first name them in, and it is the order a reducing order takes them
93
+ in: the position opened first is the position closed first.
94
+
95
+ A reference with nothing left on it is not here. It has no room for an
96
+ opposing order and nothing for a close to send, and it is either already back
97
+ at zero or has its whole quantity spoken for by orders that are still going.
98
+ """
99
+ tallies = {}
100
+ minted: List[int] = []
101
+
102
+ for row in ctx.rows():
103
+ tally = tallies.get(row.position_ref)
104
+ if tally is None:
105
+ tally = _Tally(settled=ctx.size_of(row.position_ref))
106
+ tallies[row.position_ref] = tally
107
+ minted.append(row.position_ref)
108
+ # An order that has ended has nothing more coming from it, whatever it
109
+ # filled: the fill is already in the settled figure above and the rest is
110
+ # released. That is how a strategy whose order was rejected gets its room
111
+ # back.
112
+ if is_terminal(row.status):
113
+ continue
114
+ way = 1 if row.side == "buy" else -1
115
+ # Every row is read the same way, by the order's own quantity in units,
116
+ # because this sum is compared against what has settled on this reference
117
+ # and both halves have to fall together when a fill arrives. Read instead
118
+ # by what a reduction claimed of the leg, the settled half fell and the
119
+ # claimed half did not, the sum went negative, and an entry opposing the
120
+ # reference was handed it as an order that adds.
121
+ remaining = None if row.units is None else max(0.0, row.units - row.filled_qty)
122
+ reduces = sign(tally.settled) == -way
123
+ if remaining is None:
124
+ tally.unreadable = row.side
125
+ # Nothing is sent against a position an order the engine cannot read
126
+ # is working against. Between a close that sends nothing and an order
127
+ # that crosses zero, 17.1 has already chosen.
128
+ if reduces:
129
+ tally.blocked = True
130
+ continue
131
+ tally.working += way * remaining
132
+ if reduces:
133
+ tally.against += remaining
134
+
135
+ held: List[Holding] = []
136
+ for ref in minted:
137
+ tally = tallies[ref]
138
+ outstanding = tally.settled + tally.working
139
+ if outstanding == 0 and tally.unreadable is None:
140
+ continue
141
+ side = tally.unreadable if outstanding == 0 else _side_of(outstanding)
142
+ # A reference holding a quantity the engine cannot read holds an unknown
143
+ # number of units, and understating it is the safe half of that: an
144
+ # opposing order divided against too small a number opens the remainder
145
+ # on a reference of its own, which crosses nothing.
146
+ units = None if outstanding == 0 else abs(outstanding)
147
+ # What a close may send is what settled here and is not already coming
148
+ # off, and only where what settled is on this side. A reference whose
149
+ # working orders run past its settled quantity reads as the other side,
150
+ # because that is what it will hold, and nothing of that side has settled.
151
+ facing = 1 if side == "buy" else -1
152
+ if tally.blocked or sign(tally.settled) != facing:
153
+ settled = 0.0
154
+ else:
155
+ settled = max(0.0, abs(tally.settled) - tally.against)
156
+ held.append(Holding(ref=ref, side=side, units=units, settled=settled))
157
+ return tuple(held)
158
+
159
+
160
+ def opposing(book: Sequence[Holding], side: str) -> Tuple[Holding, ...]:
161
+ """The references an order of this side reduces, oldest first."""
162
+ return tuple(one for one in book if one.side != side)
163
+
164
+
165
+ def outgoing_for(ctx: Book, book: Sequence[Holding], side: str) -> Optional[int]:
166
+ """The position an order the engine cannot size is sent against, or none.
167
+
168
+ A close is never minted a position of its own. It is named for reducing, so
169
+ it goes on the position it is closing, and where the engine cannot read the
170
+ quantity it states, that position is the one the order may take past zero:
171
+ the one shape of 17.1 an engine does not keep.
172
+
173
+ What has settled comes first and the book's own list second. A reference
174
+ whose settled quantity is entirely spoken for by orders still going is not in
175
+ the book, because it has nothing left for an engine-sized close to send; an
176
+ order the engine cannot size is not choosing a quantity, so that exclusion
177
+ does not apply to it. Taking the book's answer alone sent a close on the
178
+ reference a short entry had just opened, which is a call named close adding
179
+ to a position.
180
+ """
181
+ facing = -1 if side == "buy" else 1
182
+ seen = set()
183
+ for row in ctx.rows():
184
+ if row.position_ref in seen:
185
+ continue
186
+ seen.add(row.position_ref)
187
+ if sign(ctx.size_of(row.position_ref)) == facing:
188
+ return row.position_ref
189
+ rest = opposing(book, side)
190
+ return rest[0].ref if rest else None
191
+
192
+
193
+ def protecting(ctx: Book) -> Optional[int]:
194
+ """The position a bracket protects, or none where the leg holds none.
195
+
196
+ Holding first, opening second, which is the order 17.7 states them in, and
197
+ the newest of them where the leg holds more than one. A bracket appends no
198
+ row and moves no position, so it has nothing of its own to name: it names
199
+ what the leg has, and what the leg has is what its own fills settled.
200
+
201
+ Derived here rather than kept as the last reference minted. A stored slot
202
+ answered neither question: cleared when one reference returned to zero it
203
+ reported a leg still holding an older position as holding none, and kept for
204
+ an order the destination then refused it named a position that never opened.
205
+ Both are wrong in the direction a host cannot check, because ``0`` is the one
206
+ value ``host-interface.md`` 7.1 tells a host means there is nothing to look up.
207
+ """
208
+ held: Optional[int] = None
209
+ seen = set()
210
+ for row in ctx.rows():
211
+ if row.position_ref in seen:
212
+ continue
213
+ seen.add(row.position_ref)
214
+ # Rows are oldest first and references are minted in order, so the last
215
+ # one this loop keeps is the newest reference something has settled on.
216
+ if ctx.size_of(row.position_ref) != 0:
217
+ held = row.position_ref
218
+ if held is not None:
219
+ return held
220
+ book = holdings(ctx)
221
+ return None if not book else book[-1].ref
222
+
223
+
224
+ def joining(book: Sequence[Holding], side: str) -> Optional[int]:
225
+ """The reference an order that adds to a position joins, or none to mint one.
226
+
227
+ The newest open reference on that side, so that two entries sent on two bars
228
+ before either fills belong to one position rather than to two. A reference
229
+ whose whole quantity is already going is not open to join: an entry joining
230
+ it would settle into a position that reaches zero and ends, and 17.7's
231
+ sentence about how a position ends would have to happen twice for one
232
+ reference.
233
+ """
234
+ for one in reversed(list(book)):
235
+ if one.side == side:
236
+ return one.ref
237
+ return None
238
+
239
+
240
+ @dataclass(frozen=True)
241
+ class Share:
242
+ """One order of a call: the units it sends and the reference it sends them on."""
243
+
244
+ ref: int
245
+ units: float
246
+
247
+
248
+ def divide(
249
+ book: Sequence[Holding],
250
+ side: str,
251
+ units: float,
252
+ room: Callable[[Holding], Optional[float]],
253
+ ) -> Tuple[Tuple[Share, ...], float]:
254
+ """How a quantity is divided across the references it reduces, oldest first.
255
+
256
+ A reducing order that spans two positions is two orders, for the reason 17.1
257
+ gives for a flip: one order against two positions would leave a late fill
258
+ with no way to say which of them it settled.
259
+
260
+ ``room`` is which of the two numbers a holding offers is the ceiling here:
261
+ what an opposing order may take, or what has settled. The caller chooses,
262
+ because the caller knows whether the quantity is the script's or the
263
+ engine's. What is left over when every reference is full is the caller's to
264
+ open or to drop, and nothing is ever sent past a reference's own ceiling.
265
+ """
266
+ shares: List[Share] = []
267
+ left = units
268
+ for one in opposing(book, side):
269
+ if left <= 0:
270
+ break
271
+ ceiling = room(one)
272
+ if ceiling is None or ceiling <= 0:
273
+ continue
274
+ take = min(left, ceiling)
275
+ shares.append(Share(ref=one.ref, units=take))
276
+ left -= take
277
+ return tuple(shares), left
@@ -0,0 +1,162 @@
1
+ """The two shapes that cross the order boundary, ``host-interface.md`` 7.1 and 7.2.
2
+
3
+ **An intent is not an order.** The engine states what the strategy decided and
4
+ the destination makes the order, which is why the duty is two messages rather
5
+ than one call. An engine that named a destination's own order id at the moment a
6
+ script called ``buy()`` would be handing the script an identifier for something
7
+ that may never exist, which is also why an order function returns absence
8
+ (``compiled-program.md`` section 5.4).
9
+
10
+ **A frame is cumulative.** Every frame restates the whole life of one order
11
+ rather than what changed since the frame before it, which is what makes a
12
+ repeat, a pair that crossed in flight and a reconnecting session that resends
13
+ its last frames all harmless. The fold that depends on it is ``rows``.
14
+
15
+ Nothing here parses an identity. A resolved identity may be a string, a number,
16
+ a pair or a row in the host's own table (``host-interface.md`` section 9.1), so
17
+ the engine carries the one it was given and hands it back unchanged. That is the
18
+ whole of what this engine knows about where an order goes: the destination is
19
+ the host's, and ``spec/decisions.md`` says the engine learns nothing about it.
20
+
21
+ A field a destination has not spoken about yet is ``None`` rather than a
22
+ substituted zero or an empty string, because absence and zero are different
23
+ answers everywhere else in this language and an order is the last place to stop
24
+ telling them apart.
25
+ """
26
+
27
+ from dataclasses import dataclass, replace
28
+ from typing import Any, Optional, Tuple
29
+
30
+ #: The two sides, ``stdlib.md`` 17.2.
31
+ SIDES: Tuple[str, ...] = ("buy", "sell")
32
+
33
+ #: The four types, ``stdlib.md`` 17.2.
34
+ TYPES: Tuple[str, ...] = ("market", "limit", "stop", "stopLimit")
35
+
36
+ #: What an intent asks the destination to do, ``host-interface.md`` 7.1.
37
+ KINDS: Tuple[str, ...] = ("place", "cancel", "bracket")
38
+
39
+ #: The reference an intent about no position of its own carries. No reference an
40
+ #: engine mints is this, so a host looks one up only when it is not this.
41
+ NO_POSITION_REF = 0
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Identity:
46
+ """The instrument an order names, as the chart's record states it.
47
+
48
+ A pair, which is one of the spellings section 9.1 allows an identity to
49
+ take. It is carried and compared and never split, formatted or inferred
50
+ from.
51
+ """
52
+
53
+ symbol: Optional[str] = None
54
+ exchange: Optional[str] = None
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class IntentBar:
59
+ """The bar whose close decided an order, ``host-interface.md`` 7.1."""
60
+
61
+ index: int = 0
62
+ time: Optional[float] = None
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Placement:
67
+ """An order this run is about to send, before it is given an id.
68
+
69
+ The intent's own shape less the four fields that are the same for every
70
+ order this run sends, so that the mapping cannot state one of them
71
+ differently from one call to the next.
72
+ """
73
+
74
+ kind: str = "place"
75
+ side: Optional[str] = None
76
+ qty: Optional[float] = None
77
+ #: The unit ``qty`` is counted in, ``language.md`` 13.3, passed untranslated.
78
+ qty_type: str = "units"
79
+ order_type: Optional[str] = None
80
+ limit: Optional[float] = None
81
+ trigger: Optional[float] = None
82
+ #: A bracket's target and stop, as prices.
83
+ target: Optional[float] = None
84
+ stop: Optional[float] = None
85
+ #: A bracket's target and stop as distances from the entry, carried as
86
+ #: distances because the entry they are measured from is a fill, and on the
87
+ #: bar a script writes an entry and its bracket together nothing has filled.
88
+ profit: Optional[float] = None
89
+ loss: Optional[float] = None
90
+ tag: str = ""
91
+ position_ref: int = NO_POSITION_REF
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class OrderIntent:
96
+ """What the engine hands over, ``host-interface.md`` 7.1.
97
+
98
+ ``qty`` carries the unit its ``qty_type`` names rather than a count of units
99
+ the engine worked out for itself. A lot is the venue's own fact and the host
100
+ owns symbology, so an engine that multiplied by a lot size the host never
101
+ stated would send a quantity nobody asked for. A quantity the engine folded
102
+ from filled quantities, as a flattening order's is, states units.
103
+ """
104
+
105
+ intent_id: int
106
+ placement: Placement
107
+ instrument: Identity
108
+ product: str
109
+ bar: IntentBar
110
+
111
+ @property
112
+ def kind(self) -> str:
113
+ return self.placement.kind
114
+
115
+ @property
116
+ def side(self) -> Optional[str]:
117
+ return self.placement.side
118
+
119
+ @property
120
+ def qty(self) -> Optional[float]:
121
+ return self.placement.qty
122
+
123
+ @property
124
+ def qty_type(self) -> str:
125
+ return self.placement.qty_type
126
+
127
+ @property
128
+ def order_type(self) -> Optional[str]:
129
+ return self.placement.order_type
130
+
131
+ @property
132
+ def tag(self) -> str:
133
+ return self.placement.tag
134
+
135
+ @property
136
+ def position_ref(self) -> int:
137
+ return self.placement.position_ref
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class OrderFrame:
142
+ """What a host reports back about one order, ``host-interface.md`` 7.2.
143
+
144
+ ``filled_qty`` is cumulative, from the beginning of this order's life, and
145
+ ``avg_fill_price`` is the destination's average over the whole of it.
146
+ """
147
+
148
+ intent_id: Any
149
+ status: str
150
+ filled_qty: float
151
+ avg_fill_price: Optional[float] = None
152
+ order_ref: Optional[str] = None
153
+ sent_instrument: Optional[Identity] = None
154
+ sent_product: Optional[str] = None
155
+ time: Optional[float] = None
156
+ text: Optional[str] = None
157
+ seq: Optional[float] = None
158
+
159
+
160
+ def at_position(placement: Placement, ref: int) -> Placement:
161
+ """The same placement sent against another position, which is what a split makes."""
162
+ return replace(placement, position_ref=ref)