synpath 0.1.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.
- synpath/__init__.py +183 -0
- synpath/__main__.py +66 -0
- synpath/base.py +723 -0
- synpath/bucket.py +154 -0
- synpath/client.py +356 -0
- synpath/engine/__init__.py +37 -0
- synpath/engine/__main__.py +354 -0
- synpath/engine/alerts.py +170 -0
- synpath/engine/engine.py +888 -0
- synpath/engine/eod.py +154 -0
- synpath/engine/events.py +140 -0
- synpath/engine/fair_values.py +117 -0
- synpath/engine/feeds.py +220 -0
- synpath/engine/journal.py +907 -0
- synpath/engine/ledger.py +353 -0
- synpath/engine/orders/__init__.py +42 -0
- synpath/engine/orders/base.py +441 -0
- synpath/engine/orders/day.py +72 -0
- synpath/engine/orders/iceberg.py +121 -0
- synpath/engine/orders/manager.py +223 -0
- synpath/engine/orders/oco.py +255 -0
- synpath/engine/orders/peg.py +168 -0
- synpath/engine/orders/routed.py +496 -0
- synpath/engine/orders/stop.py +240 -0
- synpath/engine/orders/taker.py +187 -0
- synpath/engine/orders/twap.py +190 -0
- synpath/engine/paper.py +532 -0
- synpath/engine/reconcile.py +279 -0
- synpath/engine/risk.py +403 -0
- synpath/engine/router.py +261 -0
- synpath/errors.py +98 -0
- synpath/history.py +71 -0
- synpath/hosted.py +86 -0
- synpath/hosted_auth.py +201 -0
- synpath/ids.py +61 -0
- synpath/kalshi.py +1378 -0
- synpath/matching.py +86 -0
- synpath/polymarket.py +1004 -0
- synpath/polymarket_us.py +989 -0
- synpath/remote.py +195 -0
- synpath/server/__init__.py +98 -0
- synpath/server/__main__.py +118 -0
- synpath/server/api.py +439 -0
- synpath/server/errors.py +87 -0
- synpath/server/local.py +96 -0
- synpath/server/models.py +75 -0
- synpath/server/serve.py +236 -0
- synpath/server/store.py +363 -0
- synpath/server/trading.py +764 -0
- synpath/trading/__init__.py +79 -0
- synpath/trading/__main__.py +69 -0
- synpath/trading/base.py +126 -0
- synpath/trading/credentials.py +400 -0
- synpath/trading/errors.py +94 -0
- synpath/trading/init.py +233 -0
- synpath/trading/instruments.py +162 -0
- synpath/trading/kalshi.py +957 -0
- synpath/trading/limiter.py +177 -0
- synpath/trading/money.py +172 -0
- synpath/trading/polymarket.py +1362 -0
- synpath/trading/polymarket_signing.py +478 -0
- synpath/trading/polymarket_us.py +705 -0
- synpath/trading/polymarket_us_exchange.py +825 -0
- synpath/trading/types.py +414 -0
- synpath/types.py +608 -0
- synpath/ws/__init__.py +55 -0
- synpath/ws/base.py +544 -0
- synpath/ws/grpc.py +578 -0
- synpath/ws/kalshi.py +418 -0
- synpath/ws/polymarket.py +430 -0
- synpath/ws/polymarket_us.py +299 -0
- synpath/ws/polymarket_us_exchange.py +754 -0
- synpath-0.1.0.dist-info/METADATA +224 -0
- synpath-0.1.0.dist-info/RECORD +77 -0
- synpath-0.1.0.dist-info/WHEEL +4 -0
- synpath-0.1.0.dist-info/entry_points.txt +2 -0
- synpath-0.1.0.dist-info/licenses/LICENSE +21 -0
synpath/engine/ledger.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""The ledger: what the fills add up to.
|
|
2
|
+
|
|
3
|
+
A venue tells you your position. It does not tell you what each strategy
|
|
4
|
+
paid for it, what is realized, or what the same exposure looks like across
|
|
5
|
+
three venues. That is this file's job, and it works only from fills the
|
|
6
|
+
journal already holds, so it can be rebuilt from the log at any time.
|
|
7
|
+
|
|
8
|
+
Three decisions shape it:
|
|
9
|
+
|
|
10
|
+
**Everything nets on the YES leg.** Buying NO at 0.40 is selling YES at
|
|
11
|
+
0.60, and a book holding both is holding neither. Fills on a `:no`
|
|
12
|
+
instrument are converted before they are applied, so a position is one
|
|
13
|
+
number per market instead of two that quietly cancel. What was actually
|
|
14
|
+
bought is kept as well, per instrument, because on Polymarket those are real
|
|
15
|
+
tokens that must be redeemed or merged.
|
|
16
|
+
|
|
17
|
+
**Average cost, and realized profit taken on the way out.** A reducing fill
|
|
18
|
+
realizes against the average cost of what is being closed; a fill that
|
|
19
|
+
crosses through zero closes the old side first and opens the new one at the
|
|
20
|
+
fill price. Fees are realized when they are charged, never amortized into
|
|
21
|
+
cost, so a flat book's realized profit is what the venue actually paid.
|
|
22
|
+
|
|
23
|
+
**Unrealized profit needs a mark, and a mark is a choice.** It comes from
|
|
24
|
+
fair values the engine stores per account and instrument (`fair_values.py`),
|
|
25
|
+
not from a book this module cannot see. Without a mark a position reports
|
|
26
|
+
its cost and no unrealized number, rather than a made-up one.
|
|
27
|
+
|
|
28
|
+
Settlement closes a position at 1 or 0 and moves the whole remaining amount
|
|
29
|
+
into realized.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from decimal import Decimal
|
|
35
|
+
from typing import Any, Iterable, Literal
|
|
36
|
+
|
|
37
|
+
from ..trading.types import Account, Fill, Position, PositionSide, Settlement, SettlementState, Side
|
|
38
|
+
|
|
39
|
+
ZERO = Decimal("0")
|
|
40
|
+
ONE = Decimal("1")
|
|
41
|
+
|
|
42
|
+
Level = Literal["market", "book", "account"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class PositionState:
|
|
47
|
+
"""One book's position in one market, netted on the YES leg."""
|
|
48
|
+
|
|
49
|
+
account_key: str
|
|
50
|
+
book: str
|
|
51
|
+
market_id: str
|
|
52
|
+
venue: str
|
|
53
|
+
contracts: Decimal = ZERO
|
|
54
|
+
"""Signed: positive is long YES, negative is short YES (long NO)."""
|
|
55
|
+
average_cost: Decimal = ZERO
|
|
56
|
+
"""Per contract, of what is currently open; for a short, what was received."""
|
|
57
|
+
realized: Decimal = ZERO
|
|
58
|
+
fees: Decimal = ZERO
|
|
59
|
+
bought: Decimal = ZERO
|
|
60
|
+
sold: Decimal = ZERO
|
|
61
|
+
volume: Decimal = ZERO
|
|
62
|
+
"""Contracts traded, both directions."""
|
|
63
|
+
inventory: dict[str, Decimal] = field(default_factory=dict)
|
|
64
|
+
"""Contracts held per side (`"yes"`, `"no"`) before netting, as the
|
|
65
|
+
engine's own fills imply: a sell adds to `no`."""
|
|
66
|
+
resolved: bool = False
|
|
67
|
+
last_ts: int | None = None
|
|
68
|
+
|
|
69
|
+
def average_price_in_bucket(self, flip: bool, face_value: Decimal = ONE) -> Decimal:
|
|
70
|
+
"""The average cost as a price of the bucket's YES: unchanged for a
|
|
71
|
+
member that is the bucket's YES, reflected through the face value
|
|
72
|
+
for one that is its NO."""
|
|
73
|
+
return face_value - self.average_cost if flip else self.average_cost
|
|
74
|
+
fills: int = 0
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def side(self) -> PositionSide:
|
|
78
|
+
if self.contracts > 0:
|
|
79
|
+
return PositionSide.LONG
|
|
80
|
+
if self.contracts < 0:
|
|
81
|
+
return PositionSide.SHORT
|
|
82
|
+
return PositionSide.FLAT
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def cost(self) -> Decimal:
|
|
86
|
+
"""What the open position tied up, positive either way."""
|
|
87
|
+
return abs(self.contracts) * self.average_cost
|
|
88
|
+
|
|
89
|
+
def unrealized(self, mark: Decimal | None) -> Decimal | None:
|
|
90
|
+
"""Profit if the position were closed at `mark`. `None` without a mark."""
|
|
91
|
+
if mark is None or self.contracts == 0:
|
|
92
|
+
return None
|
|
93
|
+
if self.contracts > 0:
|
|
94
|
+
return (mark - self.average_cost) * self.contracts
|
|
95
|
+
# Short YES: sold at average_cost, bought back at mark.
|
|
96
|
+
return (self.average_cost - mark) * abs(self.contracts)
|
|
97
|
+
|
|
98
|
+
def net(self, mark: Decimal | None = None) -> Decimal:
|
|
99
|
+
unreal = self.unrealized(mark) or ZERO
|
|
100
|
+
return self.realized + unreal
|
|
101
|
+
|
|
102
|
+
def apply(self, *, side: Side, price: Decimal, amount: Decimal, fee: Decimal | None, ts: int | None) -> Decimal:
|
|
103
|
+
"""Apply one fill already converted to the YES leg. Returns the profit
|
|
104
|
+
realized by this fill, fee included."""
|
|
105
|
+
signed = amount if side == Side.BUY else -amount
|
|
106
|
+
realized = ZERO
|
|
107
|
+
if self.contracts == 0 or (self.contracts > 0) == (signed > 0):
|
|
108
|
+
# Opening or adding: weighted average of what it cost.
|
|
109
|
+
total = abs(self.contracts) + amount
|
|
110
|
+
self.average_cost = ((self.average_cost * abs(self.contracts)) + (price * amount)) / total if total else ZERO
|
|
111
|
+
self.contracts += signed
|
|
112
|
+
else:
|
|
113
|
+
closing = min(amount, abs(self.contracts))
|
|
114
|
+
direction = ONE if self.contracts > 0 else -ONE
|
|
115
|
+
realized += (price - self.average_cost) * closing * direction
|
|
116
|
+
self.contracts += signed
|
|
117
|
+
if self.contracts == 0:
|
|
118
|
+
self.average_cost = ZERO
|
|
119
|
+
elif (self.contracts > 0) != (direction > 0):
|
|
120
|
+
# Crossed through zero: the remainder opens the other side.
|
|
121
|
+
self.average_cost = price
|
|
122
|
+
if fee:
|
|
123
|
+
self.fees += fee
|
|
124
|
+
realized -= fee
|
|
125
|
+
self.realized += realized
|
|
126
|
+
self.volume += amount
|
|
127
|
+
if side == Side.BUY:
|
|
128
|
+
self.bought += amount
|
|
129
|
+
else:
|
|
130
|
+
self.sold += amount
|
|
131
|
+
held = "yes" if side == Side.BUY else "no"
|
|
132
|
+
self.inventory[held] = self.inventory.get(held, ZERO) + amount
|
|
133
|
+
self.fills += 1
|
|
134
|
+
if ts is not None:
|
|
135
|
+
self.last_ts = max(ts, self.last_ts or 0)
|
|
136
|
+
return realized
|
|
137
|
+
|
|
138
|
+
def settle(self, price: Decimal) -> Decimal:
|
|
139
|
+
"""Resolution: everything still open pays out at `price` (1 or 0)."""
|
|
140
|
+
if self.contracts == 0:
|
|
141
|
+
self.resolved = True
|
|
142
|
+
return ZERO
|
|
143
|
+
realized = (price - self.average_cost) * self.contracts
|
|
144
|
+
self.realized += realized
|
|
145
|
+
self.contracts = ZERO
|
|
146
|
+
self.average_cost = ZERO
|
|
147
|
+
self.resolved = True
|
|
148
|
+
return realized
|
|
149
|
+
|
|
150
|
+
def to_position(self, account: Account | None = None, mark: Decimal | None = None) -> Position:
|
|
151
|
+
"""The unified `Position` a caller sees."""
|
|
152
|
+
return Position(
|
|
153
|
+
venue=self.venue, account=account, market_id=self.market_id,
|
|
154
|
+
side=self.side, contracts=abs(self.contracts),
|
|
155
|
+
inventory_yes=self.inventory.get("yes"), inventory_no=self.inventory.get("no"),
|
|
156
|
+
entry_price=self.average_cost if self.contracts else None, mark_price=mark,
|
|
157
|
+
unrealized_pnl=self.unrealized(mark), realized_pnl=self.realized, resolved=self.resolved,
|
|
158
|
+
timestamp=self.last_ts,
|
|
159
|
+
info={"book": self.book, "fees": str(self.fees), "volume": str(self.volume), "fills": self.fills},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(slots=True)
|
|
164
|
+
class Rollup:
|
|
165
|
+
"""A level of the tree: one instrument, market, book or account."""
|
|
166
|
+
|
|
167
|
+
level: Level
|
|
168
|
+
key: str
|
|
169
|
+
contracts: Decimal = ZERO
|
|
170
|
+
"""Net signed contracts; for a roll-up above one market it is the sum,
|
|
171
|
+
which nets long one market against short another only if they are the
|
|
172
|
+
same market."""
|
|
173
|
+
cost: Decimal = ZERO
|
|
174
|
+
realized: Decimal = ZERO
|
|
175
|
+
unrealized: Decimal | None = None
|
|
176
|
+
fees: Decimal = ZERO
|
|
177
|
+
volume: Decimal = ZERO
|
|
178
|
+
positions: int = 0
|
|
179
|
+
marked: int = 0
|
|
180
|
+
"""How many of the open positions had a mark; the rest are cost only."""
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def total(self) -> Decimal:
|
|
184
|
+
return self.realized + (self.unrealized or ZERO)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Ledger:
|
|
188
|
+
"""Positions and profit, per account, book and market.
|
|
189
|
+
|
|
190
|
+
Built by replaying fills in order. A fill already applied is ignored, so
|
|
191
|
+
replaying the journal twice cannot double a position.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, *, face_value: Decimal = ONE):
|
|
195
|
+
self.face_value = face_value
|
|
196
|
+
self.positions: dict[tuple[str, str, str], PositionState] = {}
|
|
197
|
+
self.seen: set[tuple[str, str]] = set()
|
|
198
|
+
self.settled: set[tuple[str, str, str]] = set()
|
|
199
|
+
|
|
200
|
+
# -- applying -------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def apply_fill(self, fill: Fill, *, book: str | None = None) -> Decimal:
|
|
203
|
+
"""Book one fill. Returns the realized profit it produced."""
|
|
204
|
+
if (fill.venue, fill.id) in self.seen:
|
|
205
|
+
return ZERO
|
|
206
|
+
self.seen.add((fill.venue, fill.id))
|
|
207
|
+
if fill.settlement == SettlementState.FAILED:
|
|
208
|
+
# Matched and then lost on chain: it never happened.
|
|
209
|
+
return ZERO
|
|
210
|
+
state = self.state_for(fill, book=book)
|
|
211
|
+
# Fills arrive on the YES leg already: `sell` is the NO side at the YES price.
|
|
212
|
+
return state.apply(side=fill.side, price=fill.price, amount=fill.amount, fee=fill.fee, ts=fill.timestamp)
|
|
213
|
+
|
|
214
|
+
def apply_fills(self, fills: Iterable[Fill], *, book: str | None = None) -> Decimal:
|
|
215
|
+
return sum((self.apply_fill(f, book=book) for f in fills), ZERO)
|
|
216
|
+
|
|
217
|
+
def apply_settlement(self, settlement: Settlement, *, book: str | None = None) -> Decimal:
|
|
218
|
+
"""A market resolved. Closes every book's position in it at the payout."""
|
|
219
|
+
price = _settlement_price(settlement, self.face_value)
|
|
220
|
+
realized = ZERO
|
|
221
|
+
for key, state in self.positions.items():
|
|
222
|
+
if state.venue == settlement.venue and state.market_id == settlement.market_id:
|
|
223
|
+
if key in self.settled:
|
|
224
|
+
continue
|
|
225
|
+
if book is not None and state.book != book:
|
|
226
|
+
continue
|
|
227
|
+
realized += state.settle(price)
|
|
228
|
+
self.settled.add(key)
|
|
229
|
+
return realized
|
|
230
|
+
|
|
231
|
+
def state_for(self, fill: Fill, *, book: str | None = None) -> PositionState:
|
|
232
|
+
account_key = fill.account.key if fill.account else f"{fill.venue}:default"
|
|
233
|
+
name = book or (fill.info.get("book") if isinstance(fill.info, dict) else None) or "default"
|
|
234
|
+
key = (account_key, name, fill.market_id)
|
|
235
|
+
state = self.positions.get(key)
|
|
236
|
+
if state is None:
|
|
237
|
+
state = self.positions[key] = PositionState(
|
|
238
|
+
account_key=account_key, book=name, market_id=fill.market_id, venue=fill.venue,
|
|
239
|
+
)
|
|
240
|
+
return state
|
|
241
|
+
|
|
242
|
+
# -- reading --------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def open_positions(self) -> list[PositionState]:
|
|
245
|
+
return [p for p in self.positions.values() if p.contracts != 0]
|
|
246
|
+
|
|
247
|
+
def position(self, account_key: str, book: str, venue: str, market_id: str) -> PositionState | None:
|
|
248
|
+
return self.positions.get((account_key, book, market_id))
|
|
249
|
+
|
|
250
|
+
def rollup(self, level: Level, marks: dict[tuple[str, str], Decimal] | None = None) -> dict[str, Rollup]:
|
|
251
|
+
"""Aggregate every position to one of the four levels.
|
|
252
|
+
|
|
253
|
+
`marks` is keyed by `(account_key, market_id)`, as the journal
|
|
254
|
+
stores fair values.
|
|
255
|
+
"""
|
|
256
|
+
out: dict[str, Rollup] = {}
|
|
257
|
+
for state in self.positions.values():
|
|
258
|
+
if level in ("market", "instrument"):
|
|
259
|
+
key = state.market_id
|
|
260
|
+
elif level == "market":
|
|
261
|
+
key = f"{state.venue}:{state.market_id}"
|
|
262
|
+
elif level == "book":
|
|
263
|
+
key = state.book
|
|
264
|
+
else:
|
|
265
|
+
key = state.account_key
|
|
266
|
+
row = out.get(key)
|
|
267
|
+
if row is None:
|
|
268
|
+
row = out[key] = Rollup(level=level, key=key)
|
|
269
|
+
mark = (marks or {}).get((state.account_key, state.market_id))
|
|
270
|
+
unreal = state.unrealized(mark)
|
|
271
|
+
row.contracts += state.contracts
|
|
272
|
+
row.cost += state.cost
|
|
273
|
+
row.realized += state.realized
|
|
274
|
+
row.fees += state.fees
|
|
275
|
+
row.volume += state.volume
|
|
276
|
+
row.positions += 1 if state.contracts else 0
|
|
277
|
+
if unreal is not None:
|
|
278
|
+
row.unrealized = (row.unrealized or ZERO) + unreal
|
|
279
|
+
row.marked += 1
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
def total(self, marks: dict[tuple[str, str], Decimal] | None = None) -> Rollup:
|
|
283
|
+
"""The firm's line: everything, every account, every book."""
|
|
284
|
+
row = Rollup(level="account", key="*")
|
|
285
|
+
for part in self.rollup("account", marks).values():
|
|
286
|
+
row.contracts += part.contracts
|
|
287
|
+
row.cost += part.cost
|
|
288
|
+
row.realized += part.realized
|
|
289
|
+
row.fees += part.fees
|
|
290
|
+
row.volume += part.volume
|
|
291
|
+
row.positions += part.positions
|
|
292
|
+
row.marked += part.marked
|
|
293
|
+
if part.unrealized is not None:
|
|
294
|
+
row.unrealized = (row.unrealized or ZERO) + part.unrealized
|
|
295
|
+
return row
|
|
296
|
+
|
|
297
|
+
def merged(self, venue_positions: Iterable[Position]) -> list[dict[str, Any]]:
|
|
298
|
+
"""The ledger and the venue side by side, read only.
|
|
299
|
+
|
|
300
|
+
One row per account and market: what this engine's books add up to,
|
|
301
|
+
what the venue says, and the difference. A non-zero difference is a
|
|
302
|
+
position somebody else in the account opened, or a fill this engine
|
|
303
|
+
has not seen; `reconcile.py` turns it into an event.
|
|
304
|
+
"""
|
|
305
|
+
mine: dict[tuple[str, str], Decimal] = {}
|
|
306
|
+
books: dict[tuple[str, str], dict[str, Decimal]] = {}
|
|
307
|
+
for state in self.positions.values():
|
|
308
|
+
key = (state.account_key, state.market_id)
|
|
309
|
+
mine[key] = mine.get(key, ZERO) + state.contracts
|
|
310
|
+
books.setdefault(key, {})[state.book] = state.contracts
|
|
311
|
+
rows: list[dict[str, Any]] = []
|
|
312
|
+
seen: set[tuple[str, str]] = set()
|
|
313
|
+
for position in venue_positions:
|
|
314
|
+
account_key = position.account.key if position.account else f"{position.venue}:default"
|
|
315
|
+
signed = position.contracts if position.side != PositionSide.SHORT else -position.contracts
|
|
316
|
+
key = (account_key, position.market_id)
|
|
317
|
+
seen.add(key)
|
|
318
|
+
engine = mine.get(key, ZERO)
|
|
319
|
+
rows.append({
|
|
320
|
+
"account": account_key, "venue": position.venue,
|
|
321
|
+
"market_id": position.market_id, "engine": engine, "venue_contracts": signed,
|
|
322
|
+
"difference": engine - signed, "books": books.get(key, {}),
|
|
323
|
+
})
|
|
324
|
+
for key, engine in mine.items():
|
|
325
|
+
if key in seen or engine == 0:
|
|
326
|
+
continue
|
|
327
|
+
account_key, market_id = key
|
|
328
|
+
venue, _, _ = market_id.partition(":")
|
|
329
|
+
rows.append({
|
|
330
|
+
"account": account_key, "venue": venue,
|
|
331
|
+
"market_id": market_id, "engine": engine, "venue_contracts": ZERO,
|
|
332
|
+
"difference": engine, "books": books.get(key, {}),
|
|
333
|
+
})
|
|
334
|
+
return sorted(rows, key=lambda r: (r["account"], r["venue"], r["market_id"]))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _settlement_price(settlement: Settlement, face_value: Decimal) -> Decimal:
|
|
338
|
+
"""What one YES contract paid at resolution.
|
|
339
|
+
|
|
340
|
+
The venues say this three different ways, so take them in order of how
|
|
341
|
+
directly they answer the question: the outcome word, then the flag, then
|
|
342
|
+
the payout divided by what it was paid on.
|
|
343
|
+
"""
|
|
344
|
+
result = (settlement.result or "").strip().lower()
|
|
345
|
+
if result in ("yes", "true", "1"):
|
|
346
|
+
return face_value
|
|
347
|
+
if result in ("no", "false", "0"):
|
|
348
|
+
return ZERO
|
|
349
|
+
if settlement.won is not None:
|
|
350
|
+
return face_value if settlement.won else ZERO
|
|
351
|
+
if settlement.payout is not None and settlement.amount:
|
|
352
|
+
return Decimal(settlement.payout) / Decimal(settlement.amount)
|
|
353
|
+
return ZERO
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""synpath.engine.orders -- the order types the venues do not hold.
|
|
2
|
+
|
|
3
|
+
Stops (market, limit, trailing), icebergs, one-cancels-the-other and
|
|
4
|
+
brackets, TWAP, pegs, and the two taking types: an engine-held market order
|
|
5
|
+
that walks the book inside a price bound, and a smart taker that does it in
|
|
6
|
+
clips over time. Day is not here: it is rewritten to a venue-held GTD at the
|
|
7
|
+
session end, because an expiry must outlive the process (`day.py`).
|
|
8
|
+
|
|
9
|
+
Each type is a `ManagedOrder`: a state machine whose state is written to the
|
|
10
|
+
journal on every change and restored on start, submitting ordinary venue
|
|
11
|
+
orders as children so the risk rules, the ledger and reconciliation see them
|
|
12
|
+
like anything else.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from synpath.trading.types import OrderRequest, OrderType, Side
|
|
17
|
+
|
|
18
|
+
await engine.submit(OrderRequest(
|
|
19
|
+
market_id="kalshi:KXX", side=Side.SELL, amount=Decimal("20"),
|
|
20
|
+
type=OrderType.TRAILING_STOP, stop_price=Decimal("0.40"),
|
|
21
|
+
params={"trail": "0.03", "trigger_source": "touch"},
|
|
22
|
+
))
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .base import Child, Context, ManagedOrder, State
|
|
28
|
+
from .day import as_gtd, session_expiry
|
|
29
|
+
from .iceberg import Iceberg
|
|
30
|
+
from .manager import REGISTRY, ManagedOrders, register
|
|
31
|
+
from .oco import OCO, Bracket
|
|
32
|
+
from .peg import Peg
|
|
33
|
+
from .routed import RoutedLimit
|
|
34
|
+
from .stop import StopLimit, StopMarket, TrailingStop
|
|
35
|
+
from .taker import MarketOrder, SmartTaker
|
|
36
|
+
from .twap import TWAP
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"ManagedOrder", "ManagedOrders", "Context", "Child", "State", "register", "REGISTRY",
|
|
40
|
+
"StopMarket", "StopLimit", "TrailingStop", "Iceberg", "OCO", "Bracket", "TWAP", "Peg",
|
|
41
|
+
"MarketOrder", "SmartTaker", "as_gtd", "session_expiry",
|
|
42
|
+
]
|