openscript 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openscript/__init__.py +40 -0
- openscript/__main__.py +62 -0
- openscript/accounting/__init__.py +74 -0
- openscript/accounting/analysis.py +174 -0
- openscript/accounting/charges.py +397 -0
- openscript/accounting/equity.py +234 -0
- openscript/accounting/report.py +82 -0
- openscript/accounting/shapes.py +74 -0
- openscript/accounting/statistics.py +300 -0
- openscript/accounting/trades.py +294 -0
- openscript/adapter/__init__.py +32 -0
- openscript/adapter/answers.py +215 -0
- openscript/adapter/channels.py +137 -0
- openscript/adapter/expectations.py +67 -0
- openscript/adapter/facts.py +127 -0
- openscript/adapter/matching.py +257 -0
- openscript/adapter/ordering.py +187 -0
- openscript/adapter/page.py +130 -0
- openscript/adapter/reading.py +357 -0
- openscript/adapter/reporting.py +244 -0
- openscript/adapter/running.py +449 -0
- openscript/adapter/serving.py +229 -0
- openscript/adapter/sessions.py +168 -0
- openscript/adapter/spellings.py +184 -0
- openscript/bars.py +157 -0
- openscript/budget.py +342 -0
- openscript/canonical.py +192 -0
- openscript/civil.py +196 -0
- openscript/contracts.py +165 -0
- openscript/dates.py +302 -0
- openscript/diagnostics.py +104 -0
- openscript/hours.py +165 -0
- openscript/inputs.py +239 -0
- openscript/intervals.py +60 -0
- openscript/library/__init__.py +76 -0
- openscript/library/arithmetic.py +128 -0
- openscript/library/averages.py +133 -0
- openscript/library/bars.py +60 -0
- openscript/library/bookkeeping.py +166 -0
- openscript/library/code_points.py +85 -0
- openscript/library/colour.py +202 -0
- openscript/library/composites.py +208 -0
- openscript/library/counting.py +218 -0
- openscript/library/deviation.py +155 -0
- openscript/library/elementary.py +206 -0
- openscript/library/extremes.py +122 -0
- openscript/library/flows.py +220 -0
- openscript/library/momentum.py +203 -0
- openscript/library/number_text.py +223 -0
- openscript/library/prices.py +36 -0
- openscript/library/ranges.py +105 -0
- openscript/library/rounding.py +123 -0
- openscript/library/series.py +213 -0
- openscript/library/stateful.py +442 -0
- openscript/library/stateless.py +261 -0
- openscript/library/strength.py +180 -0
- openscript/library/strings.py +228 -0
- openscript/library/trend.py +260 -0
- openscript/library/values.py +91 -0
- openscript/logbook.py +119 -0
- openscript/machine.py +499 -0
- openscript/memory.py +204 -0
- openscript/opcodes.py +166 -0
- openscript/program.py +146 -0
- openscript/run.py +368 -0
- openscript/strategy/__init__.py +78 -0
- openscript/strategy/calls.py +201 -0
- openscript/strategy/closable.py +182 -0
- openscript/strategy/fills.py +131 -0
- openscript/strategy/holdings.py +277 -0
- openscript/strategy/intents.py +162 -0
- openscript/strategy/ledger.py +270 -0
- openscript/strategy/placing.py +206 -0
- openscript/strategy/positions.py +124 -0
- openscript/strategy/refusals.py +293 -0
- openscript/strategy/rows.py +219 -0
- openscript/strategy/sizing.py +229 -0
- openscript/strategy/statuses.py +65 -0
- openscript/surface/__init__.py +115 -0
- openscript/surface/bands.py +103 -0
- openscript/surface/levels.py +44 -0
- openscript/surface/marks.py +52 -0
- openscript/surface/paints.py +58 -0
- openscript/surface/plots.py +44 -0
- openscript/surface/published.py +119 -0
- openscript/values.py +210 -0
- openscript/verify.py +301 -0
- openscript/verify_code.py +290 -0
- openscript/verify_requests.py +271 -0
- openscript/verify_shape.py +162 -0
- openscript/verify_tables.py +256 -0
- openscript/version.py +39 -0
- openscript/zones.py +118 -0
- openscript-0.4.0.dist-info/METADATA +82 -0
- openscript-0.4.0.dist-info/RECORD +97 -0
- openscript-0.4.0.dist-info/WHEEL +5 -0
- openscript-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)
|