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.
Files changed (77) hide show
  1. synpath/__init__.py +183 -0
  2. synpath/__main__.py +66 -0
  3. synpath/base.py +723 -0
  4. synpath/bucket.py +154 -0
  5. synpath/client.py +356 -0
  6. synpath/engine/__init__.py +37 -0
  7. synpath/engine/__main__.py +354 -0
  8. synpath/engine/alerts.py +170 -0
  9. synpath/engine/engine.py +888 -0
  10. synpath/engine/eod.py +154 -0
  11. synpath/engine/events.py +140 -0
  12. synpath/engine/fair_values.py +117 -0
  13. synpath/engine/feeds.py +220 -0
  14. synpath/engine/journal.py +907 -0
  15. synpath/engine/ledger.py +353 -0
  16. synpath/engine/orders/__init__.py +42 -0
  17. synpath/engine/orders/base.py +441 -0
  18. synpath/engine/orders/day.py +72 -0
  19. synpath/engine/orders/iceberg.py +121 -0
  20. synpath/engine/orders/manager.py +223 -0
  21. synpath/engine/orders/oco.py +255 -0
  22. synpath/engine/orders/peg.py +168 -0
  23. synpath/engine/orders/routed.py +496 -0
  24. synpath/engine/orders/stop.py +240 -0
  25. synpath/engine/orders/taker.py +187 -0
  26. synpath/engine/orders/twap.py +190 -0
  27. synpath/engine/paper.py +532 -0
  28. synpath/engine/reconcile.py +279 -0
  29. synpath/engine/risk.py +403 -0
  30. synpath/engine/router.py +261 -0
  31. synpath/errors.py +98 -0
  32. synpath/history.py +71 -0
  33. synpath/hosted.py +86 -0
  34. synpath/hosted_auth.py +201 -0
  35. synpath/ids.py +61 -0
  36. synpath/kalshi.py +1378 -0
  37. synpath/matching.py +86 -0
  38. synpath/polymarket.py +1004 -0
  39. synpath/polymarket_us.py +989 -0
  40. synpath/remote.py +195 -0
  41. synpath/server/__init__.py +98 -0
  42. synpath/server/__main__.py +118 -0
  43. synpath/server/api.py +439 -0
  44. synpath/server/errors.py +87 -0
  45. synpath/server/local.py +96 -0
  46. synpath/server/models.py +75 -0
  47. synpath/server/serve.py +236 -0
  48. synpath/server/store.py +363 -0
  49. synpath/server/trading.py +764 -0
  50. synpath/trading/__init__.py +79 -0
  51. synpath/trading/__main__.py +69 -0
  52. synpath/trading/base.py +126 -0
  53. synpath/trading/credentials.py +400 -0
  54. synpath/trading/errors.py +94 -0
  55. synpath/trading/init.py +233 -0
  56. synpath/trading/instruments.py +162 -0
  57. synpath/trading/kalshi.py +957 -0
  58. synpath/trading/limiter.py +177 -0
  59. synpath/trading/money.py +172 -0
  60. synpath/trading/polymarket.py +1362 -0
  61. synpath/trading/polymarket_signing.py +478 -0
  62. synpath/trading/polymarket_us.py +705 -0
  63. synpath/trading/polymarket_us_exchange.py +825 -0
  64. synpath/trading/types.py +414 -0
  65. synpath/types.py +608 -0
  66. synpath/ws/__init__.py +55 -0
  67. synpath/ws/base.py +544 -0
  68. synpath/ws/grpc.py +578 -0
  69. synpath/ws/kalshi.py +418 -0
  70. synpath/ws/polymarket.py +430 -0
  71. synpath/ws/polymarket_us.py +299 -0
  72. synpath/ws/polymarket_us_exchange.py +754 -0
  73. synpath-0.1.0.dist-info/METADATA +224 -0
  74. synpath-0.1.0.dist-info/RECORD +77 -0
  75. synpath-0.1.0.dist-info/WHEEL +4 -0
  76. synpath-0.1.0.dist-info/entry_points.txt +2 -0
  77. synpath-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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