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/bucket.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Buckets: one tradable thing made of several venues' listings.
|
|
2
|
+
|
|
3
|
+
A bucket sits where a single market does. You place an order on it the way
|
|
4
|
+
you place one on `kalshi:KXFOO-25`; the engine's router turns that into legs
|
|
5
|
+
on the member markets. It is the user's own definition, a list of member
|
|
6
|
+
markets and, for each, whether that member's YES is the bucket's YES or the
|
|
7
|
+
opposite (`flip`). Nothing here judges whether the members really are the
|
|
8
|
+
same question; that is the caller's call, or a matching service's, later.
|
|
9
|
+
|
|
10
|
+
Ids are UUIDs made locally, so a bucket created offline stays unique when
|
|
11
|
+
its definition later moves to a hosted account. Ownership is the `book`, the
|
|
12
|
+
same cross-venue strategy label orders and P&L already roll up by.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import uuid
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from . import ids
|
|
23
|
+
from .errors import BadRequest
|
|
24
|
+
from .trading.types import Side
|
|
25
|
+
from .types import BookSide, _Base
|
|
26
|
+
|
|
27
|
+
ONE = Decimal("1")
|
|
28
|
+
BUCKET_PREFIX = "bucket:"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BucketMember(_Base):
|
|
32
|
+
market_id: str
|
|
33
|
+
"""A Synpath id, `venue:native`."""
|
|
34
|
+
flip: bool = False
|
|
35
|
+
"""True when this member's YES is the opposite of the bucket's proposition."""
|
|
36
|
+
|
|
37
|
+
def book_side(self) -> BookSide:
|
|
38
|
+
"""Which of the member's two books is the bucket's YES book. Reading
|
|
39
|
+
the NO book of a flipped member gives prices already in bucket terms,
|
|
40
|
+
so nothing downstream converts."""
|
|
41
|
+
return "no" if self.flip else "yes"
|
|
42
|
+
|
|
43
|
+
def to_member(self, side: Side, price: Decimal) -> tuple[Side, Decimal]:
|
|
44
|
+
"""A bucket-side order as the member venue must receive it. Prices in
|
|
45
|
+
this library are always the YES price and `sell` is the NO side, so
|
|
46
|
+
buying a flipped member's NO at p is `sell` at 1 - p."""
|
|
47
|
+
if not self.flip:
|
|
48
|
+
return side, price
|
|
49
|
+
return (Side.SELL if side == Side.BUY else Side.BUY), ONE - price
|
|
50
|
+
|
|
51
|
+
def to_bucket(self, side: Side, price: Decimal) -> tuple[Side, Decimal]:
|
|
52
|
+
"""A member fill back in bucket terms. The same map; it is its own inverse."""
|
|
53
|
+
return self.to_member(side, price)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Bucket(_Base):
|
|
57
|
+
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
58
|
+
book: str
|
|
59
|
+
name: str
|
|
60
|
+
members: list[BucketMember]
|
|
61
|
+
status: Literal["active", "archived"] = "active"
|
|
62
|
+
created_ms: int | None = None
|
|
63
|
+
updated_ms: int | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def market_id(self) -> str:
|
|
67
|
+
"""What an order names to target this bucket: `bucket:<id>`."""
|
|
68
|
+
return BUCKET_PREFIX + self.id
|
|
69
|
+
|
|
70
|
+
def market_ids(self) -> list[str]:
|
|
71
|
+
return [m.market_id for m in self.members]
|
|
72
|
+
|
|
73
|
+
def venues(self) -> set[str]:
|
|
74
|
+
return {ids.venue_of(m.market_id) for m in self.members}
|
|
75
|
+
|
|
76
|
+
def member(self, market_id: str) -> BucketMember:
|
|
77
|
+
for m in self.members:
|
|
78
|
+
if m.market_id == market_id:
|
|
79
|
+
return m
|
|
80
|
+
raise KeyError(market_id)
|
|
81
|
+
|
|
82
|
+
def check(self) -> None:
|
|
83
|
+
"""The shape checks a definition must pass before it is stored or
|
|
84
|
+
traded: well-formed ids on known venues, no duplicates, at least two
|
|
85
|
+
members. Whether the members mean the same thing is not checked."""
|
|
86
|
+
if len(self.members) < 2:
|
|
87
|
+
raise BadRequest("a bucket needs at least two members")
|
|
88
|
+
seen: set[str] = set()
|
|
89
|
+
for m in self.members:
|
|
90
|
+
ids.venue_of(m.market_id) # raises BadRequest on a bare or unknown id
|
|
91
|
+
if m.market_id in seen:
|
|
92
|
+
raise BadRequest(f"{m.market_id} is listed twice")
|
|
93
|
+
seen.add(m.market_id)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BucketVenueFill(_Base):
|
|
97
|
+
venue: str
|
|
98
|
+
filled: Decimal
|
|
99
|
+
average_price: Decimal | None = Field(description="In bucket terms")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class BucketOrderReport(_Base):
|
|
103
|
+
"""What an order on a bucket has done, in bucket terms, and where."""
|
|
104
|
+
|
|
105
|
+
order_id: str
|
|
106
|
+
bucket_id: str
|
|
107
|
+
status: str = Field(description="As on the order: `open`, `closed`, `canceled`, ...")
|
|
108
|
+
side: str
|
|
109
|
+
amount: Decimal
|
|
110
|
+
worst_price: Decimal = Field(description="The worst price the order accepts, in bucket terms")
|
|
111
|
+
filled: Decimal
|
|
112
|
+
unfilled: Decimal
|
|
113
|
+
average_price: Decimal | None = Field(description="Weighted across every venue, in bucket terms")
|
|
114
|
+
fees_paid: Decimal
|
|
115
|
+
rounds: int = Field(description="Re-allocations that moved a leg")
|
|
116
|
+
stop_reason: str | None = Field(description="`worst_price` (nothing left at or better than it), `liquidity`, `min_amount`, `max_rounds`, `max_age`, "
|
|
117
|
+
"or null while working or once filled")
|
|
118
|
+
per_venue: list[BucketVenueFill]
|
|
119
|
+
detail: str | None = Field(default=None, description="Why the order was rejected or stopped, in words; "
|
|
120
|
+
"null while it works normally")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class BucketMemberPosition(_Base):
|
|
124
|
+
market_id: str
|
|
125
|
+
venue: str
|
|
126
|
+
book: str
|
|
127
|
+
account: str
|
|
128
|
+
flip: bool
|
|
129
|
+
contracts: Decimal = Field(description="On the member's own YES leg")
|
|
130
|
+
bucket_contracts: Decimal = Field(description="The same, in bucket terms: negated when `flip`")
|
|
131
|
+
entry_price: Decimal | None
|
|
132
|
+
mark: Decimal | None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class BucketPosition(_Base):
|
|
136
|
+
bucket_id: str
|
|
137
|
+
name: str
|
|
138
|
+
book: str
|
|
139
|
+
contracts: Decimal = Field(description="Net across members, in bucket terms; negative is short")
|
|
140
|
+
side: Literal["long", "short", "flat"]
|
|
141
|
+
entry_price: Decimal | None
|
|
142
|
+
realized: Decimal
|
|
143
|
+
fees: Decimal
|
|
144
|
+
members: list[BucketMemberPosition]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_bucket_id(market_id: str) -> bool:
|
|
148
|
+
return market_id.startswith(BUCKET_PREFIX)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def bucket_id_of(market_id: str) -> str:
|
|
152
|
+
if not is_bucket_id(market_id):
|
|
153
|
+
raise BadRequest(f"{market_id!r} is not a bucket id")
|
|
154
|
+
return market_id[len(BUCKET_PREFIX):]
|
synpath/client.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""One client for every venue, routed by the venue in each Synpath id.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
import synpath
|
|
5
|
+
|
|
6
|
+
client = synpath.Client() # market data needs no credentials
|
|
7
|
+
market = client.fetch_market("kalshi:KXFEDDECISION-26SEP-C25")
|
|
8
|
+
book = client.fetch_order_book("polymarket:2252244", side="no")
|
|
9
|
+
|
|
10
|
+
client = synpath.Client(synpath.load_credentials()) # KALSHI_*, POLYMARKET_* from .env
|
|
11
|
+
async with client:
|
|
12
|
+
order = await client.create_order(OrderRequest(
|
|
13
|
+
market_id=market.id, side="buy", type="limit", price="0.05", amount=5,
|
|
14
|
+
))
|
|
15
|
+
await client.cancel_order(order.id, market_id=order.market_id)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
client = synpath.Client(server="http://127.0.0.1:8000") # your own `synpath serve`
|
|
20
|
+
order = await client.create_order(OrderRequest(..., type="stop_market", stop_price="0.40"))
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With `server=`, every trading call goes to that server's engine instead of to
|
|
24
|
+
the venues, which is how the engine's order types (stops, icebergs, TWAP,
|
|
25
|
+
orders on a bucket) are reached from Python. Market data stays direct. The
|
|
26
|
+
server holds the venue credentials; this side holds only the server's access token,
|
|
27
|
+
found in `~/.synpath/servers.json` for a server on this machine, or given as
|
|
28
|
+
`access_token=` / `SYNPATH_ACCESS_TOKEN` for one across the network.
|
|
29
|
+
|
|
30
|
+
Every id in this library starts with its venue (`kalshi:...`, `polymarket:...`),
|
|
31
|
+
so a call that names a market knows where to go. The per-venue adapters
|
|
32
|
+
(`synpath.Kalshi`, `synpath.KalshiTrading`, ...) are what this routes to,
|
|
33
|
+
built on first use and shared after; a caller that wants one venue only can
|
|
34
|
+
keep using them directly.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from decimal import Decimal
|
|
39
|
+
from typing import Any, Iterable, Mapping
|
|
40
|
+
|
|
41
|
+
from . import ids
|
|
42
|
+
from .base import Exchange
|
|
43
|
+
from .bucket import Bucket, BucketMember, BucketOrderReport, BucketPosition
|
|
44
|
+
from .errors import BadRequest
|
|
45
|
+
from .trading.base import TradingExchange
|
|
46
|
+
from .trading.credentials import Credentials, require
|
|
47
|
+
from .trading.types import (
|
|
48
|
+
Balance, EditRequest, FeeEstimate, Fill, Order, OrderRequest, Position, Settlement, Side,
|
|
49
|
+
)
|
|
50
|
+
from .types import BookSide, Candle, FeeSchedule, Market, OrderBook, Page, Trade
|
|
51
|
+
|
|
52
|
+
__all__ = ["Client"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Client:
|
|
56
|
+
"""Reads on any venue, and order entry on the venues you hold credentials for."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
credentials: Mapping[str, Credentials | None] | None = None,
|
|
61
|
+
*,
|
|
62
|
+
exchanges: Mapping[str, Exchange] | None = None,
|
|
63
|
+
trading: Mapping[str, TradingExchange] | None = None,
|
|
64
|
+
server: str | None = None,
|
|
65
|
+
access_token: str | None = None,
|
|
66
|
+
server_http: Any = None,
|
|
67
|
+
):
|
|
68
|
+
"""`credentials` is what `synpath.load_credentials()` returns. `exchanges`
|
|
69
|
+
and `trading` let a caller hand in adapters it already built (a paper
|
|
70
|
+
venue, a test double); anything not given is built on first use.
|
|
71
|
+
`server` is the root URL of your own `synpath serve`; with it, trading
|
|
72
|
+
goes through that server's engine. `server_http` injects an httpx
|
|
73
|
+
client for tests."""
|
|
74
|
+
self.credentials: dict[str, Credentials | None] = dict(credentials or {})
|
|
75
|
+
self._exchanges: dict[str, Exchange] = dict(exchanges or {})
|
|
76
|
+
self._trading: dict[str, TradingExchange] = dict(trading or {})
|
|
77
|
+
self.server = server
|
|
78
|
+
self._remote: Any = None
|
|
79
|
+
if server:
|
|
80
|
+
from .remote import RemoteTrading
|
|
81
|
+
self._remote = RemoteTrading(server, access_token, http_client=server_http)
|
|
82
|
+
|
|
83
|
+
# -- adapters -------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def exchange(self, venue: str) -> Exchange:
|
|
86
|
+
"""The read adapter for a venue, built once."""
|
|
87
|
+
if venue not in self._exchanges:
|
|
88
|
+
from . import exchange
|
|
89
|
+
self._exchanges[venue] = exchange(venue)
|
|
90
|
+
return self._exchanges[venue]
|
|
91
|
+
|
|
92
|
+
def trading(self, venue: str) -> TradingExchange:
|
|
93
|
+
"""The trading adapter for a venue, built once from its credentials.
|
|
94
|
+
`CredentialsMissing` names the variables to set when there are none."""
|
|
95
|
+
if venue not in self._trading:
|
|
96
|
+
creds = require(venue, self.credentials)
|
|
97
|
+
self._trading[venue] = _TRADING[venue](creds) # type: ignore[index]
|
|
98
|
+
return self._trading[venue]
|
|
99
|
+
|
|
100
|
+
def _venue(self, market_id: str) -> str:
|
|
101
|
+
return ids.venue_of(market_id)
|
|
102
|
+
|
|
103
|
+
# -- market data (sync, like the read adapters) ---------------------------
|
|
104
|
+
|
|
105
|
+
def fetch_market(self, market_id: str) -> Market:
|
|
106
|
+
return self.exchange(self._venue(market_id)).fetch_market(market_id)
|
|
107
|
+
|
|
108
|
+
def fetch_markets_by_ids(self, market_ids: list[str]) -> list[Market]:
|
|
109
|
+
"""Many markets, across venues, in the order asked for."""
|
|
110
|
+
found: dict[str, Market] = {}
|
|
111
|
+
for venue, batch in _by_venue(market_ids).items():
|
|
112
|
+
for market in self.exchange(venue).fetch_markets_by_ids(batch):
|
|
113
|
+
found[market.id] = market
|
|
114
|
+
return [found[m] for m in (ids.qualify(self._venue(m), ids.split(m)[1]) for m in market_ids) if m in found]
|
|
115
|
+
|
|
116
|
+
def fetch_markets(
|
|
117
|
+
self, *, venue: str | None = None, query: str | None = None, limit: int | None = None,
|
|
118
|
+
cursor: str | None = None, status: str = "open", sort: str | None = None,
|
|
119
|
+
) -> Page[Market]:
|
|
120
|
+
"""One venue's page, or without `venue` the first page of every
|
|
121
|
+
venue concatenated (no cursor across venues)."""
|
|
122
|
+
if venue is not None:
|
|
123
|
+
return self.exchange(venue).fetch_markets(query=query, limit=limit, cursor=cursor, status=status, sort=sort)
|
|
124
|
+
if cursor is not None:
|
|
125
|
+
raise BadRequest("a cursor belongs to one venue; pass venue= to continue paging")
|
|
126
|
+
markets: list[Market] = []
|
|
127
|
+
for name in ids.VENUES:
|
|
128
|
+
markets.extend(self.exchange(name).fetch_markets(query=query, limit=limit, status=status, sort=sort))
|
|
129
|
+
return Page(markets, next_cursor=None)
|
|
130
|
+
|
|
131
|
+
def fetch_order_book(self, market_id: str, *, side: BookSide = "yes", depth: int | None = None) -> OrderBook:
|
|
132
|
+
return self.exchange(self._venue(market_id)).fetch_order_book(market_id, side=side, depth=depth)
|
|
133
|
+
|
|
134
|
+
def fetch_order_books(
|
|
135
|
+
self, market_ids: list[str], *, side: BookSide = "yes", depth: int | None = None,
|
|
136
|
+
) -> dict[str, OrderBook]:
|
|
137
|
+
books: dict[str, OrderBook] = {}
|
|
138
|
+
for venue, batch in _by_venue(market_ids).items():
|
|
139
|
+
books.update(self.exchange(venue).fetch_order_books(batch, side=side, depth=depth))
|
|
140
|
+
return books
|
|
141
|
+
|
|
142
|
+
def fetch_trades(
|
|
143
|
+
self, market_id: str, *, since: int | None = None, limit: int | None = None, cursor: str | None = None,
|
|
144
|
+
) -> Page[Trade]:
|
|
145
|
+
return self.exchange(self._venue(market_id)).fetch_trades(market_id, since=since, limit=limit, cursor=cursor)
|
|
146
|
+
|
|
147
|
+
def fetch_ohlcv(
|
|
148
|
+
self, market_id: str, *, timeframe: str = "1h", since: int | None = None,
|
|
149
|
+
until: int | None = None, limit: int | None = None,
|
|
150
|
+
) -> list[Candle]:
|
|
151
|
+
return self.exchange(self._venue(market_id)).fetch_ohlcv(
|
|
152
|
+
market_id, timeframe=timeframe, since=since, until=until, limit=limit,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def fetch_fee_schedule(self, market_id: str) -> FeeSchedule:
|
|
156
|
+
return self.exchange(self._venue(market_id)).fetch_fee_schedule(market_id)
|
|
157
|
+
|
|
158
|
+
# -- order entry (async, like the trading adapters) -----------------------
|
|
159
|
+
|
|
160
|
+
async def create_order(self, request: OrderRequest) -> Order:
|
|
161
|
+
if self._remote:
|
|
162
|
+
return await self._remote.create_order(request)
|
|
163
|
+
return await self.trading(self._venue(request.market_id)).create_order(request)
|
|
164
|
+
|
|
165
|
+
async def create_orders(self, requests: list[OrderRequest]) -> list[Order | Exception]:
|
|
166
|
+
"""Many orders, each sent to its own venue, results in the order given."""
|
|
167
|
+
if self._remote:
|
|
168
|
+
return await self._remote.create_orders(requests)
|
|
169
|
+
results: list[Order | Exception | None] = [None] * len(requests)
|
|
170
|
+
groups: dict[str, list[int]] = {}
|
|
171
|
+
for i, request in enumerate(requests):
|
|
172
|
+
try:
|
|
173
|
+
groups.setdefault(self._venue(request.market_id), []).append(i)
|
|
174
|
+
except BadRequest as exc:
|
|
175
|
+
results[i] = exc
|
|
176
|
+
for venue, indexes in groups.items():
|
|
177
|
+
answers = await self.trading(venue).create_orders([requests[i] for i in indexes])
|
|
178
|
+
for i, answer in zip(indexes, answers):
|
|
179
|
+
results[i] = answer
|
|
180
|
+
return results # type: ignore[return-value]
|
|
181
|
+
|
|
182
|
+
async def create_bucket(self, *, book: str, name: str, members: list[BucketMember]) -> Bucket:
|
|
183
|
+
"""Define a bucket on your server; place orders on it by `bucket.market_id`.
|
|
184
|
+
|
|
185
|
+
A bucket lives in the server's journal, beside the engine that routes
|
|
186
|
+
its orders, so it needs `Client(server=...)`."""
|
|
187
|
+
return await self._server().create_bucket(book=book, name=name, members=members)
|
|
188
|
+
|
|
189
|
+
def _server(self) -> Any:
|
|
190
|
+
if not self._remote:
|
|
191
|
+
raise BadRequest("buckets are kept by your synpath serve: create the client with server=")
|
|
192
|
+
return self._remote
|
|
193
|
+
|
|
194
|
+
async def fetch_buckets(self, *, book: str | None = None, status: str = "active") -> list[Bucket]:
|
|
195
|
+
"""Your server's buckets: `active` (default), `archived` or `all`."""
|
|
196
|
+
return await self._server().fetch_buckets(book=book, status=status)
|
|
197
|
+
|
|
198
|
+
async def fetch_bucket(self, bucket_id: str) -> Bucket:
|
|
199
|
+
"""One bucket, by `<id>` or by its market id `bucket:<id>`."""
|
|
200
|
+
return await self._server().fetch_bucket(bucket_id)
|
|
201
|
+
|
|
202
|
+
async def archive_bucket(self, bucket_id: str) -> Bucket:
|
|
203
|
+
"""Retire a bucket: no new orders on it. Orders already on it keep working."""
|
|
204
|
+
return await self._server().archive_bucket(bucket_id)
|
|
205
|
+
|
|
206
|
+
async def fetch_bucket_position(self, bucket_id: str, *, book: str | None = None) -> BucketPosition:
|
|
207
|
+
"""The position across a bucket's members, netted in bucket terms."""
|
|
208
|
+
return await self._server().fetch_bucket_position(bucket_id, book=book)
|
|
209
|
+
|
|
210
|
+
async def fetch_bucket_orders(self, bucket_id: str) -> list[BucketOrderReport]:
|
|
211
|
+
"""Every order placed on a bucket, live or finished, with where it filled."""
|
|
212
|
+
return await self._server().fetch_bucket_orders(bucket_id)
|
|
213
|
+
|
|
214
|
+
async def fetch_bucket_order(self, bucket_id: str, order_id: str) -> BucketOrderReport:
|
|
215
|
+
"""One order on a bucket: filled, weighted average, and the split by venue."""
|
|
216
|
+
return await self._server().fetch_bucket_order(bucket_id, order_id)
|
|
217
|
+
|
|
218
|
+
async def cancel_order(self, order_id: str, *, market_id: str | None = None, venue: str | None = None) -> Order:
|
|
219
|
+
"""Cancel by id. The venue comes from `market_id` or `venue`; an order
|
|
220
|
+
id alone does not say where it lives, except to a server, which knows."""
|
|
221
|
+
if self._remote:
|
|
222
|
+
return await self._remote.cancel_order(order_id, market_id=market_id, venue=venue)
|
|
223
|
+
return await self.trading(_venue_of(market_id, venue)).cancel_order(order_id, market_id=market_id)
|
|
224
|
+
|
|
225
|
+
async def cancel_all_orders(self, *, market_id: str | None = None, venue: str | None = None) -> int | None:
|
|
226
|
+
"""One market's, one venue's, or every venue's resting orders."""
|
|
227
|
+
if self._remote:
|
|
228
|
+
return await self._remote.cancel_all_orders(market_id=market_id, venue=venue)
|
|
229
|
+
if market_id is None and venue is None:
|
|
230
|
+
total: int | None = 0
|
|
231
|
+
for name in list(self._trading):
|
|
232
|
+
count = await self._trading[name].cancel_all_orders()
|
|
233
|
+
total = None if count is None or total is None else total + count
|
|
234
|
+
return total
|
|
235
|
+
return await self.trading(_venue_of(market_id, venue)).cancel_all_orders(market_id=market_id)
|
|
236
|
+
|
|
237
|
+
async def edit_order(self, request: EditRequest, *, venue: str | None = None, current: Order | None = None) -> Order:
|
|
238
|
+
if self._remote:
|
|
239
|
+
return await self._remote.edit_order(request, venue=venue, current=current)
|
|
240
|
+
if venue is None:
|
|
241
|
+
raise BadRequest("edit_order needs venue= when trading the venues directly")
|
|
242
|
+
return await self.trading(venue).edit_order(request, current=current)
|
|
243
|
+
|
|
244
|
+
async def fetch_order(self, order_id: str, *, market_id: str | None = None, venue: str | None = None) -> Order:
|
|
245
|
+
if self._remote:
|
|
246
|
+
return await self._remote.fetch_order(order_id, market_id=market_id, venue=venue)
|
|
247
|
+
return await self.trading(_venue_of(market_id, venue)).fetch_order(order_id)
|
|
248
|
+
|
|
249
|
+
async def fetch_open_orders(self, *, market_id: str | None = None, venue: str | None = None) -> list[Order]:
|
|
250
|
+
"""One market's, one venue's, or every connected venue's open orders."""
|
|
251
|
+
if self._remote:
|
|
252
|
+
return await self._remote.fetch_open_orders(market_id=market_id, venue=venue)
|
|
253
|
+
if market_id is None and venue is None:
|
|
254
|
+
orders: list[Order] = []
|
|
255
|
+
for name in list(self._trading):
|
|
256
|
+
orders.extend(await self._trading[name].fetch_open_orders())
|
|
257
|
+
return orders
|
|
258
|
+
return await self.trading(_venue_of(market_id, venue)).fetch_open_orders(market_id=market_id)
|
|
259
|
+
|
|
260
|
+
async def fetch_my_trades(
|
|
261
|
+
self, *, market_id: str | None = None, venue: str | None = None, since: int | None = None,
|
|
262
|
+
limit: int | None = None, cursor: str | None = None,
|
|
263
|
+
) -> Page[Fill]:
|
|
264
|
+
if self._remote:
|
|
265
|
+
return await self._remote.fetch_my_trades(market_id=market_id, venue=venue, since=since,
|
|
266
|
+
limit=limit, cursor=cursor)
|
|
267
|
+
return await self.trading(_venue_of(market_id, venue)).fetch_my_trades(
|
|
268
|
+
market_id=market_id, since=since, limit=limit, cursor=cursor,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def fetch_positions(self, *, market_id: str | None = None, venue: str | None = None) -> list[Position]:
|
|
272
|
+
"""One market's, one venue's, or every connected venue's positions."""
|
|
273
|
+
if self._remote:
|
|
274
|
+
return await self._remote.fetch_positions(market_id=market_id, venue=venue)
|
|
275
|
+
if market_id is None and venue is None:
|
|
276
|
+
positions: list[Position] = []
|
|
277
|
+
for name in list(self._trading):
|
|
278
|
+
positions.extend(await self._trading[name].fetch_positions())
|
|
279
|
+
return positions
|
|
280
|
+
return await self.trading(_venue_of(market_id, venue)).fetch_positions(market_id=market_id)
|
|
281
|
+
|
|
282
|
+
async def fetch_balance(self, venue: str) -> Balance:
|
|
283
|
+
if self._remote:
|
|
284
|
+
return await self._remote.fetch_balance(venue)
|
|
285
|
+
return await self.trading(venue).fetch_balance()
|
|
286
|
+
|
|
287
|
+
async def fetch_settlements(
|
|
288
|
+
self, *, market_id: str | None = None, venue: str | None = None, since: int | None = None,
|
|
289
|
+
limit: int | None = None, cursor: str | None = None,
|
|
290
|
+
) -> Page[Settlement]:
|
|
291
|
+
if self._remote and not self.credentials:
|
|
292
|
+
return await self._remote.fetch_settlements()
|
|
293
|
+
return await self.trading(_venue_of(market_id, venue)).fetch_settlements(
|
|
294
|
+
market_id=market_id, since=since, limit=limit, cursor=cursor,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
async def fetch_fee_estimate(self, market_id: str, side: Side, price: Decimal, amount: Decimal) -> FeeEstimate:
|
|
298
|
+
if self._remote and not self.credentials:
|
|
299
|
+
return await self._remote.fetch_fee_estimate(market_id, side, price, amount)
|
|
300
|
+
return await self.trading(self._venue(market_id)).fetch_fee_estimate(market_id, side, price, amount)
|
|
301
|
+
|
|
302
|
+
# -- housekeeping ---------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
async def close(self) -> None:
|
|
305
|
+
if self._remote is not None:
|
|
306
|
+
await self._remote.close()
|
|
307
|
+
for adapter in self._trading.values():
|
|
308
|
+
await adapter.close()
|
|
309
|
+
for exchange in self._exchanges.values():
|
|
310
|
+
exchange.close()
|
|
311
|
+
|
|
312
|
+
async def __aenter__(self) -> "Client":
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
316
|
+
await self.close()
|
|
317
|
+
|
|
318
|
+
def __repr__(self) -> str:
|
|
319
|
+
via = f" server={self.server}" if self.server else ""
|
|
320
|
+
return f"<synpath.Client reads={sorted(self._exchanges)} trades={sorted(self._trading)}{via}>"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _by_venue(market_ids: Iterable[str]) -> dict[str, list[str]]:
|
|
324
|
+
groups: dict[str, list[str]] = {}
|
|
325
|
+
for market_id in market_ids:
|
|
326
|
+
groups.setdefault(ids.venue_of(market_id), []).append(market_id)
|
|
327
|
+
return groups
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _venue_of(market_id: str | None, venue: str | None) -> str:
|
|
331
|
+
if market_id is not None:
|
|
332
|
+
found = ids.venue_of(market_id)
|
|
333
|
+
if venue is not None and venue != found:
|
|
334
|
+
raise BadRequest(f"{market_id!r} belongs to {found}, not {venue}")
|
|
335
|
+
return found
|
|
336
|
+
if venue is None:
|
|
337
|
+
raise BadRequest("pass market_id= or venue= so the call can be routed")
|
|
338
|
+
return venue
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _kalshi(creds: Credentials) -> TradingExchange:
|
|
342
|
+
from .trading.kalshi import KalshiTrading
|
|
343
|
+
return KalshiTrading(creds) # type: ignore[arg-type]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _polymarket(creds: Credentials) -> TradingExchange:
|
|
347
|
+
from .trading.polymarket import PolymarketTrading
|
|
348
|
+
return PolymarketTrading(creds) # type: ignore[arg-type]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _polymarket_us(creds: Credentials) -> TradingExchange:
|
|
352
|
+
from .trading.polymarket_us import PolymarketUSTrading
|
|
353
|
+
return PolymarketUSTrading(creds) # type: ignore[arg-type]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
_TRADING = {"kalshi": _kalshi, "polymarket": _polymarket, "polymarket_us": _polymarket_us}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""synpath.engine -- order entry with a journal, a ledger and risk rules.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from synpath.engine import Engine, EngineConfig, RiskConfig
|
|
5
|
+
|
|
6
|
+
async with Engine({"kalshi": kalshi}, EngineConfig(journal_path="trading.db"),
|
|
7
|
+
risk=RiskConfig(max_order_contracts=Decimal("100"))) as engine:
|
|
8
|
+
order = await engine.submit(request)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The engine writes every decision to a journal before it acts, so a process
|
|
12
|
+
killed mid-submit restarts knowing what was in doubt; runs pre-trade risk
|
|
13
|
+
rules that name themselves when they refuse; books fills into a ledger that
|
|
14
|
+
nets on the YES leg and rolls up by instrument, market, strategy and
|
|
15
|
+
account; and refuses to trade at all if another engine holds the journal.
|
|
16
|
+
|
|
17
|
+
Part of the base install: `pip install synpath`.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .engine import Engine, EngineConfig, Recovery
|
|
22
|
+
from .events import EngineEvent, EventBus, Subscription
|
|
23
|
+
from .fair_values import FairValue, FairValues
|
|
24
|
+
from .journal import Intent, IntentState, Journal, JournalEvent, LeaseLost
|
|
25
|
+
from .ledger import Ledger, PositionState, Rollup
|
|
26
|
+
from .orders import ManagedOrder, ManagedOrders
|
|
27
|
+
from .risk import Decision, KillSwitch, RiskConfig, RiskEngine
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Engine", "EngineConfig", "Recovery",
|
|
31
|
+
"Journal", "JournalEvent", "Intent", "IntentState", "LeaseLost",
|
|
32
|
+
"EventBus", "EngineEvent", "Subscription",
|
|
33
|
+
"Ledger", "PositionState", "Rollup",
|
|
34
|
+
"RiskConfig", "RiskEngine", "Decision", "KillSwitch",
|
|
35
|
+
"FairValues", "FairValue",
|
|
36
|
+
"ManagedOrder", "ManagedOrders",
|
|
37
|
+
]
|