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/trading/types.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""The shapes an order takes from request to settlement.
|
|
2
|
+
|
|
3
|
+
Field names follow ccxt where ccxt has them and FIX where ccxt does not.
|
|
4
|
+
Where the two disagree, FIX wins for state and ccxt for naming: an order has
|
|
5
|
+
one `status` and separate `filled` / `remaining` quantities, so "partially
|
|
6
|
+
filled" is a question about those numbers, not a state of its own, and a
|
|
7
|
+
cancel or replace in flight has a name (`pending_cancel`, `pending_replace`)
|
|
8
|
+
because a fill arriving while one is in flight is the common race.
|
|
9
|
+
|
|
10
|
+
Every quantity of money is `Decimal`. Every timestamp is milliseconds since
|
|
11
|
+
epoch. Every object keeps the venue's raw payload in `info`.
|
|
12
|
+
|
|
13
|
+
Three things the venues disagree on are carried explicitly rather than
|
|
14
|
+
smoothed over:
|
|
15
|
+
|
|
16
|
+
* **Exposure versus inventory.** Kalshi and Polymarket US net a position:
|
|
17
|
+
selling YES with none is a long NO. Polymarket does not: YES and NO are
|
|
18
|
+
two token inventories until they are merged. `Position` carries both
|
|
19
|
+
views, and a venue fills whichever it has.
|
|
20
|
+
* **Fill finality.** A Polymarket fill is matched off-chain first and
|
|
21
|
+
confirmed on-chain later, and can fail in between. `Fill.settlement`
|
|
22
|
+
says which; the other venues report fills already final.
|
|
23
|
+
* **Buying power.** Polymarket US posts margin and computes buying power;
|
|
24
|
+
the others lock the full cost. `Balance.buying_power` is the figure the
|
|
25
|
+
venue defines, or `None` where it defines none.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from decimal import Decimal
|
|
30
|
+
from enum import Enum
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
34
|
+
|
|
35
|
+
from ..types import iso
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Side(str, Enum):
|
|
39
|
+
"""What an order does on a market, on its YES leg.
|
|
40
|
+
|
|
41
|
+
`buy` takes the YES side, `sell` takes the NO side, and the price is
|
|
42
|
+
always the YES price: selling at 0.70 is the same order as buying NO at
|
|
43
|
+
0.30. On a venue that nets a position (Kalshi, Polymarket US) that is
|
|
44
|
+
literally the order sent. On Polymarket, where YES and NO are separate
|
|
45
|
+
tokens, `sell` buys the NO token at `1 - price`; `sell` with
|
|
46
|
+
`reduce_only` sells the YES tokens held instead. `buy` with
|
|
47
|
+
`reduce_only` likewise sells NO tokens held rather than buying YES.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
BUY = "buy"
|
|
51
|
+
SELL = "sell"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OrderType(str, Enum):
|
|
55
|
+
"""What the venue holds (`limit`, `market`) and what the engine holds.
|
|
56
|
+
|
|
57
|
+
A venue adapter accepts only the first two; every other type is a parent
|
|
58
|
+
the execution engine manages, which submits limit or market children
|
|
59
|
+
when its condition is met. The engine, not the adapter, reports which of
|
|
60
|
+
these it offers.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
LIMIT = "limit"
|
|
64
|
+
MARKET = "market"
|
|
65
|
+
STOP_MARKET = "stop_market"
|
|
66
|
+
STOP_LIMIT = "stop_limit"
|
|
67
|
+
TRAILING_STOP = "trailing_stop"
|
|
68
|
+
ICEBERG = "iceberg"
|
|
69
|
+
OCO = "oco"
|
|
70
|
+
BRACKET = "bracket"
|
|
71
|
+
TWAP = "twap"
|
|
72
|
+
PEG = "peg"
|
|
73
|
+
SMART_TAKER = "smart_taker"
|
|
74
|
+
RFQ_TAKER = "rfq_taker"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
VENUE_ORDER_TYPES = frozenset({OrderType.LIMIT, OrderType.MARKET})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TimeInForce(str, Enum):
|
|
81
|
+
GTC = "gtc"
|
|
82
|
+
IOC = "ioc"
|
|
83
|
+
FOK = "fok"
|
|
84
|
+
GTD = "gtd"
|
|
85
|
+
DAY = "day"
|
|
86
|
+
"""Not a venue concept on any of the three; the engine rewrites it to
|
|
87
|
+
`gtd` at the configured session end before it reaches an adapter."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OrderStatus(str, Enum):
|
|
91
|
+
PENDING = "pending"
|
|
92
|
+
"""Accepted by this library, not yet acknowledged by the venue."""
|
|
93
|
+
OPEN = "open"
|
|
94
|
+
"""Resting at the venue. Read `filled` and `remaining` for how much."""
|
|
95
|
+
PENDING_CANCEL = "pending_cancel"
|
|
96
|
+
PENDING_REPLACE = "pending_replace"
|
|
97
|
+
CLOSED = "closed"
|
|
98
|
+
"""Fully filled."""
|
|
99
|
+
CANCELED = "canceled"
|
|
100
|
+
"""Ended by a cancel; `filled` may be non-zero."""
|
|
101
|
+
REJECTED = "rejected"
|
|
102
|
+
EXPIRED = "expired"
|
|
103
|
+
WAITING = "waiting"
|
|
104
|
+
"""An engine-held parent whose condition has not been met."""
|
|
105
|
+
TRIGGERED = "triggered"
|
|
106
|
+
"""An engine-held parent whose condition fired; its children are live."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
TERMINAL_STATUSES = frozenset({
|
|
110
|
+
OrderStatus.CLOSED, OrderStatus.CANCELED, OrderStatus.REJECTED, OrderStatus.EXPIRED,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class HeldBy(str, Enum):
|
|
115
|
+
VENUE = "venue"
|
|
116
|
+
ENGINE = "engine"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class SettlementState(str, Enum):
|
|
120
|
+
MATCHED = "matched"
|
|
121
|
+
"""Matched at the venue, not yet final. Polymarket only, before the chain
|
|
122
|
+
confirms."""
|
|
123
|
+
CONFIRMED = "confirmed"
|
|
124
|
+
"""Final."""
|
|
125
|
+
FAILED = "failed"
|
|
126
|
+
"""Matched and then failed on-chain; the shares never arrived."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Liquidity(str, Enum):
|
|
130
|
+
MAKER = "maker"
|
|
131
|
+
TAKER = "taker"
|
|
132
|
+
UNKNOWN = "unknown"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PositionSide(str, Enum):
|
|
136
|
+
"""`long` holds YES, `short` holds NO, on the market's YES leg."""
|
|
137
|
+
|
|
138
|
+
LONG = "long"
|
|
139
|
+
SHORT = "short"
|
|
140
|
+
FLAT = "flat"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _Base(BaseModel):
|
|
144
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True, use_enum_values=False)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Account(_Base):
|
|
148
|
+
"""One set of credentials at one venue, optionally one subaccount.
|
|
149
|
+
|
|
150
|
+
Balances are held here and nowhere else: cash sits at the venue, so a
|
|
151
|
+
consolidated balance is a roll-up computed on read, never something an
|
|
152
|
+
order can spend from.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
venue: str
|
|
156
|
+
name: str = "default"
|
|
157
|
+
"""Which credential set, for a caller with more than one at a venue."""
|
|
158
|
+
subaccount: str | None = None
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def key(self) -> str:
|
|
162
|
+
return f"{self.venue}:{self.name}" + (f":{self.subaccount}" if self.subaccount else "")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Precision(_Base):
|
|
166
|
+
"""What an order for this instrument must satisfy before it is signed."""
|
|
167
|
+
|
|
168
|
+
tick: Decimal
|
|
169
|
+
"""Minimum price increment. Polymarket varies it per market."""
|
|
170
|
+
min_amount: Decimal = Decimal("1")
|
|
171
|
+
amount_step: Decimal | None = None
|
|
172
|
+
"""Contract increment where the venue has one; `None` means any decimal."""
|
|
173
|
+
whole_contracts: bool = False
|
|
174
|
+
"""Polymarket US: integers only."""
|
|
175
|
+
face_value: Decimal = Decimal("1")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class OrderRequest(_Base):
|
|
179
|
+
"""What a caller asks for. Validated against the market before signing."""
|
|
180
|
+
|
|
181
|
+
market_id: str
|
|
182
|
+
"""Synpath id, `venue:native`. A bare native id is accepted by the venue's
|
|
183
|
+
own adapter."""
|
|
184
|
+
side: Side
|
|
185
|
+
"""`buy` takes YES, `sell` takes NO. See `Side`."""
|
|
186
|
+
amount: Decimal
|
|
187
|
+
"""Contracts."""
|
|
188
|
+
type: OrderType = OrderType.LIMIT
|
|
189
|
+
price: Decimal | None = None
|
|
190
|
+
"""Always the YES price, 0 to 1. Required for a limit order. For a market
|
|
191
|
+
order, the protection price the engine will not cross; refused without
|
|
192
|
+
one on a venue without native market orders."""
|
|
193
|
+
stop_price: Decimal | None = None
|
|
194
|
+
time_in_force: TimeInForce = TimeInForce.GTC
|
|
195
|
+
expires_at: int | None = None
|
|
196
|
+
"""Milliseconds since epoch. Required with `gtd`."""
|
|
197
|
+
post_only: bool = False
|
|
198
|
+
reduce_only: bool = False
|
|
199
|
+
"""Only reduce an existing position. On Polymarket this is what decides
|
|
200
|
+
whether `sell` buys NO tokens or sells YES tokens held; see `Side`."""
|
|
201
|
+
client_order_id: str | None = None
|
|
202
|
+
"""The idempotency key. Generated before the first write if absent, and
|
|
203
|
+
reused on every retry of the same intent."""
|
|
204
|
+
account: Account | None = None
|
|
205
|
+
book: str | None = None
|
|
206
|
+
"""Strategy or desk the order belongs to. Internal, independent of the
|
|
207
|
+
venue account; positions and P&L roll up by it."""
|
|
208
|
+
trader: str | None = None
|
|
209
|
+
tags: dict[str, str] = Field(default_factory=dict)
|
|
210
|
+
notes: str | None = None
|
|
211
|
+
params: dict[str, Any] = Field(default_factory=dict)
|
|
212
|
+
"""Venue-specific extras passed through untouched (Kalshi
|
|
213
|
+
`self_trade_prevention_type`, Polymarket `signatureType`, ...)."""
|
|
214
|
+
|
|
215
|
+
@model_validator(mode="after")
|
|
216
|
+
def _consistent(self) -> "OrderRequest":
|
|
217
|
+
if self.type == OrderType.LIMIT and self.price is None:
|
|
218
|
+
raise ValueError("a limit order needs a price")
|
|
219
|
+
if self.time_in_force == TimeInForce.GTD and self.expires_at is None:
|
|
220
|
+
raise ValueError("gtd needs expires_at")
|
|
221
|
+
if self.type in (OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.TRAILING_STOP) \
|
|
222
|
+
and self.stop_price is None:
|
|
223
|
+
raise ValueError(f"{self.type.value} needs a stop_price")
|
|
224
|
+
if self.type == OrderType.STOP_LIMIT and self.price is None:
|
|
225
|
+
raise ValueError("stop_limit needs the limit price to submit once triggered")
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class EditRequest(_Base):
|
|
230
|
+
"""A change to a resting order. Fields left `None` are left alone."""
|
|
231
|
+
|
|
232
|
+
order_id: str
|
|
233
|
+
price: Decimal | None = None
|
|
234
|
+
amount: Decimal | None = None
|
|
235
|
+
time_in_force: TimeInForce | None = None
|
|
236
|
+
expires_at: int | None = None
|
|
237
|
+
client_order_id: str | None = None
|
|
238
|
+
"""Idempotency key for the *edit*, so a retried edit is not applied twice."""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Order(_Base):
|
|
242
|
+
"""An order as the venue (or the engine) reports it."""
|
|
243
|
+
|
|
244
|
+
id: str
|
|
245
|
+
client_order_id: str | None = None
|
|
246
|
+
venue: str
|
|
247
|
+
account: Account | None = None
|
|
248
|
+
market_id: str
|
|
249
|
+
"""Synpath id, `venue:native`."""
|
|
250
|
+
side: Side
|
|
251
|
+
"""On the YES leg: `buy` is a YES order, `sell` a NO order, whatever the
|
|
252
|
+
venue's own wording. `price` is the YES price."""
|
|
253
|
+
type: OrderType
|
|
254
|
+
time_in_force: TimeInForce
|
|
255
|
+
status: OrderStatus
|
|
256
|
+
held_by: HeldBy = HeldBy.VENUE
|
|
257
|
+
price: Decimal | None = None
|
|
258
|
+
stop_price: Decimal | None = None
|
|
259
|
+
amount: Decimal
|
|
260
|
+
filled: Decimal = Decimal("0")
|
|
261
|
+
remaining: Decimal | None = None
|
|
262
|
+
"""`amount - filled` unless the venue reports otherwise."""
|
|
263
|
+
average_price: Decimal | None = None
|
|
264
|
+
cost: Decimal | None = None
|
|
265
|
+
"""Collateral committed so far."""
|
|
266
|
+
fee: Decimal | None = None
|
|
267
|
+
fee_currency: str | None = None
|
|
268
|
+
last_fill_price: Decimal | None = None
|
|
269
|
+
last_fill_amount: Decimal | None = None
|
|
270
|
+
post_only: bool = False
|
|
271
|
+
reduce_only: bool = False
|
|
272
|
+
expires_at: int | None = None
|
|
273
|
+
created_at: int | None = None
|
|
274
|
+
updated_at: int | None = None
|
|
275
|
+
parent_id: str | None = None
|
|
276
|
+
"""The engine-held parent this order is a child of."""
|
|
277
|
+
queue_priority_preserved: bool | None = None
|
|
278
|
+
"""After an edit: whether the venue kept the order's place in the queue.
|
|
279
|
+
Kalshi keeps it on a decrease and loses it on a price change; Polymarket
|
|
280
|
+
always loses it because an edit is a cancel and a new order."""
|
|
281
|
+
book: str | None = None
|
|
282
|
+
trader: str | None = None
|
|
283
|
+
tags: dict[str, str] = Field(default_factory=dict)
|
|
284
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
285
|
+
|
|
286
|
+
@model_validator(mode="after")
|
|
287
|
+
def _derive(self) -> "Order":
|
|
288
|
+
if self.remaining is None:
|
|
289
|
+
object.__setattr__(self, "remaining", self.amount - self.filled)
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def is_terminal(self) -> bool:
|
|
294
|
+
return self.status in TERMINAL_STATUSES
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def created_datetime(self) -> str | None:
|
|
298
|
+
return iso(self.created_at)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class Fill(_Base):
|
|
302
|
+
id: str
|
|
303
|
+
order_id: str
|
|
304
|
+
client_order_id: str | None = None
|
|
305
|
+
venue: str
|
|
306
|
+
account: Account | None = None
|
|
307
|
+
market_id: str
|
|
308
|
+
side: Side
|
|
309
|
+
"""On the YES leg, like `Order.side`."""
|
|
310
|
+
price: Decimal
|
|
311
|
+
"""The YES price."""
|
|
312
|
+
amount: Decimal
|
|
313
|
+
fee: Decimal | None = None
|
|
314
|
+
fee_currency: str | None = None
|
|
315
|
+
liquidity: Liquidity = Liquidity.UNKNOWN
|
|
316
|
+
settlement: SettlementState = SettlementState.CONFIRMED
|
|
317
|
+
"""`matched` until the chain confirms, on Polymarket. Final elsewhere."""
|
|
318
|
+
timestamp: int
|
|
319
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def datetime(self) -> str | None:
|
|
323
|
+
return iso(self.timestamp)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class Position(_Base):
|
|
327
|
+
"""One market's position: net on the YES leg, with the token inventories
|
|
328
|
+
kept beside it on a venue that does not net."""
|
|
329
|
+
|
|
330
|
+
venue: str
|
|
331
|
+
account: Account | None = None
|
|
332
|
+
market_id: str
|
|
333
|
+
side: PositionSide = PositionSide.FLAT
|
|
334
|
+
"""`long` holds YES, `short` holds NO."""
|
|
335
|
+
contracts: Decimal = Decimal("0")
|
|
336
|
+
"""Net exposure on the side named, positive. On a netting venue this is
|
|
337
|
+
the position; on Polymarket it is `|inventory_yes - inventory_no|`."""
|
|
338
|
+
inventory_yes: Decimal | None = None
|
|
339
|
+
inventory_no: Decimal | None = None
|
|
340
|
+
"""Tokens actually held on each side, on a venue that does not net
|
|
341
|
+
(Polymarket). `None` elsewhere."""
|
|
342
|
+
entry_price: Decimal | None = None
|
|
343
|
+
mark_price: Decimal | None = None
|
|
344
|
+
unrealized_pnl: Decimal | None = None
|
|
345
|
+
realized_pnl: Decimal | None = None
|
|
346
|
+
margin: Decimal | None = None
|
|
347
|
+
"""Collateral locked against this position, where the venue reports it."""
|
|
348
|
+
resolved: bool = False
|
|
349
|
+
final: bool = False
|
|
350
|
+
"""Resolved and past any dispute window. On Polymarket a resolved market
|
|
351
|
+
can still be challenged; `resolved` without `final` says so."""
|
|
352
|
+
won: bool | None = None
|
|
353
|
+
payout: Decimal | None = None
|
|
354
|
+
redeemable: Decimal | None = None
|
|
355
|
+
"""Payout waiting to be claimed on-chain. Polymarket only."""
|
|
356
|
+
timestamp: int | None = None
|
|
357
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class Settlement(_Base):
|
|
361
|
+
venue: str
|
|
362
|
+
account: Account | None = None
|
|
363
|
+
market_id: str
|
|
364
|
+
held: PositionSide | None = None
|
|
365
|
+
"""Which side was held into settlement, when the venue says."""
|
|
366
|
+
result: str | None = None
|
|
367
|
+
won: bool | None = None
|
|
368
|
+
amount: Decimal | None = None
|
|
369
|
+
cost: Decimal | None = None
|
|
370
|
+
payout: Decimal | None = None
|
|
371
|
+
pnl: Decimal | None = None
|
|
372
|
+
timestamp: int | None = None
|
|
373
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class Balance(_Base):
|
|
377
|
+
"""One currency in one venue account. Never pooled across accounts."""
|
|
378
|
+
|
|
379
|
+
venue: str
|
|
380
|
+
account: Account
|
|
381
|
+
currency: str
|
|
382
|
+
total: Decimal
|
|
383
|
+
available: Decimal
|
|
384
|
+
locked: Decimal | None = None
|
|
385
|
+
"""Committed to resting orders and margin, where the venue reports it.
|
|
386
|
+
`None` means it does not, not zero."""
|
|
387
|
+
buying_power: Decimal | None = None
|
|
388
|
+
"""The venue's own figure for what can still be bought, where it has one
|
|
389
|
+
(Polymarket US). `None` means the venue defines none, not zero."""
|
|
390
|
+
timestamp: int | None = None
|
|
391
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class FeeEstimate(_Base):
|
|
395
|
+
"""What an order would cost, before it is placed."""
|
|
396
|
+
|
|
397
|
+
venue: str
|
|
398
|
+
market_id: str
|
|
399
|
+
side: Side
|
|
400
|
+
price: Decimal
|
|
401
|
+
amount: Decimal
|
|
402
|
+
taker_fee: Decimal | None = None
|
|
403
|
+
maker_fee: Decimal | None = None
|
|
404
|
+
"""Negative where the venue pays makers."""
|
|
405
|
+
builder_fee: Decimal | None = None
|
|
406
|
+
currency: str = "USD"
|
|
407
|
+
info: dict[str, Any] = Field(default_factory=dict)
|
|
408
|
+
|
|
409
|
+
@field_validator("price", "amount")
|
|
410
|
+
@classmethod
|
|
411
|
+
def _positive(cls, value: Decimal) -> Decimal:
|
|
412
|
+
if value <= 0:
|
|
413
|
+
raise ValueError("must be positive")
|
|
414
|
+
return value
|