uselayer 0.1.0__tar.gz
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.
- uselayer-0.1.0/.gitignore +10 -0
- uselayer-0.1.0/CHANGELOG.md +24 -0
- uselayer-0.1.0/LICENSE +21 -0
- uselayer-0.1.0/PKG-INFO +190 -0
- uselayer-0.1.0/README.md +163 -0
- uselayer-0.1.0/examples/01_book_and_preview.py +22 -0
- uselayer-0.1.0/examples/02_paper_trade.py +26 -0
- uselayer-0.1.0/examples/03_rules_and_kill.py +24 -0
- uselayer-0.1.0/examples/04_backtest_saved_books.py +31 -0
- uselayer-0.1.0/examples/05_fees_by_date.py +21 -0
- uselayer-0.1.0/examples/06_pair_trade_backtest.py +47 -0
- uselayer-0.1.0/examples/_pick.py +14 -0
- uselayer-0.1.0/examples/recorded.json +5637 -0
- uselayer-0.1.0/pyproject.toml +63 -0
- uselayer-0.1.0/src/uselayer/AGENTS.md +67 -0
- uselayer-0.1.0/src/uselayer/__init__.py +69 -0
- uselayer-0.1.0/src/uselayer/__main__.py +43 -0
- uselayer-0.1.0/src/uselayer/_recorded.py +74 -0
- uselayer-0.1.0/src/uselayer/_switches.py +22 -0
- uselayer-0.1.0/src/uselayer/_version.py +1 -0
- uselayer-0.1.0/src/uselayer/backtest.py +91 -0
- uselayer-0.1.0/src/uselayer/books.py +151 -0
- uselayer-0.1.0/src/uselayer/calc.py +538 -0
- uselayer-0.1.0/src/uselayer/client.py +1146 -0
- uselayer-0.1.0/src/uselayer/errors.py +126 -0
- uselayer-0.1.0/src/uselayer/events.py +141 -0
- uselayer-0.1.0/src/uselayer/fees.py +123 -0
- uselayer-0.1.0/src/uselayer/fill.py +222 -0
- uselayer-0.1.0/src/uselayer/guardrails.py +643 -0
- uselayer-0.1.0/src/uselayer/http.py +328 -0
- uselayer-0.1.0/src/uselayer/layer_api.py +134 -0
- uselayer-0.1.0/src/uselayer/llms.txt +23 -0
- uselayer-0.1.0/src/uselayer/orders.py +113 -0
- uselayer-0.1.0/src/uselayer/paper.py +179 -0
- uselayer-0.1.0/src/uselayer/portfolio.py +96 -0
- uselayer-0.1.0/src/uselayer/py.typed +0 -0
- uselayer-0.1.0/src/uselayer/store.py +264 -0
- uselayer-0.1.0/src/uselayer/trading.py +506 -0
- uselayer-0.1.0/src/uselayer/venue_rules.py +211 -0
- uselayer-0.1.0/src/uselayer/venues/__init__.py +1 -0
- uselayer-0.1.0/src/uselayer/venues/base.py +139 -0
- uselayer-0.1.0/src/uselayer/venues/polymarket_us.py +187 -0
- uselayer-0.1.0/src/uselayer/venues/polymarket_us_live.py +634 -0
- uselayer-0.1.0/tests/conftest.py +150 -0
- uselayer-0.1.0/tests/test_backtest.py +71 -0
- uselayer-0.1.0/tests/test_examples.py +27 -0
- uselayer-0.1.0/tests/test_fees_and_rules.py +69 -0
- uselayer-0.1.0/tests/test_golden.py +97 -0
- uselayer-0.1.0/tests/test_guardrails.py +199 -0
- uselayer-0.1.0/tests/test_http.py +139 -0
- uselayer-0.1.0/tests/test_layer_traffic.py +68 -0
- uselayer-0.1.0/tests/test_live_polymarket_us.py +513 -0
- uselayer-0.1.0/tests/test_paper.py +140 -0
- uselayer-0.1.0/tests/test_public_repo.py +42 -0
- uselayer-0.1.0/tests/test_store.py +26 -0
- uselayer-0.1.0/tests/test_trade.py +220 -0
- uselayer-0.1.0/tests/test_types.py +76 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- `Client` in paper mode (the default): orders fill against Polymarket US's live public order books
|
|
6
|
+
through Layer's fill model; nothing is sent to the venue.
|
|
7
|
+
- Live mode for Polymarket US with your own API key (`PolymarketUS`): signed orders, cancel and
|
|
8
|
+
cancel-all, open orders, fills, positions and balance; fresh books from the venue's WebSocket; an
|
|
9
|
+
unknown order outcome is looked up and never resent; a new store starts killed when the account
|
|
10
|
+
already has open orders or positions.
|
|
11
|
+
- Backtest mode: replay books you saved with `uselayer.backtest.record_books`.
|
|
12
|
+
- Pairs: `quote()` prices YES on one market plus NO on its twin after both fees; `trade()` places both
|
|
13
|
+
legs with the leg-risk guard (thinner leg first, second leg up to break-even, chase, then unwind or
|
|
14
|
+
report the exposure); `run(strategy, pairs)` runs the same strategy in backtest, paper or live.
|
|
15
|
+
- One order shape for every venue and mode, published as `schema/order.json`.
|
|
16
|
+
- `preview()`: the fill, fees and every rule's decision, without sending anything.
|
|
17
|
+
- Guardrails: max position, budget, max daily loss, allowed venues/markets/actions, expiry,
|
|
18
|
+
approval above a size, stop-loss and take-profit; an always-on price collar, order throttle and
|
|
19
|
+
kill switch (`python -m uselayer kill | resume | status`).
|
|
20
|
+
- Dated venue rules: fee schedules looked up by the time of the trade.
|
|
21
|
+
- Fee math, `profit()` and `size()` that match Layer's API to the millionth of a dollar.
|
|
22
|
+
- One error type, `VenueError`, with `code`, `hint`, `next` and `retryable`; retries and pacing per
|
|
23
|
+
venue host.
|
|
24
|
+
- Local SQLite store per mode; no telemetry.
|
uselayer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Precious Emakenemi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uselayer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: uselayer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trade prediction markets with your own venue keys: one order shape, paper mode by default, guardrails, and fee math that matches Layer's API.
|
|
5
|
+
Project-URL: Homepage, https://uselayer.sh
|
|
6
|
+
Project-URL: Source, https://github.com/Dave-56/uselayer-sdk
|
|
7
|
+
Author: Layer
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: polymarket,prediction markets,sdk,trading
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: cryptography>=42
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: pydantic>=2.7
|
|
23
|
+
Requires-Dist: websockets>=13
|
|
24
|
+
Provides-Extra: yaml
|
|
25
|
+
Requires-Dist: pyyaml>=6; extra == 'yaml'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# uselayer
|
|
29
|
+
|
|
30
|
+
A Python SDK for trading prediction markets with your own venue keys.
|
|
31
|
+
|
|
32
|
+
This release (0.1) trades **Polymarket US**. **Paper mode** (the default) fills orders against the
|
|
33
|
+
venue's real order books with simulated money and sends nothing to the venue. **Live mode** sends
|
|
34
|
+
orders with your own Polymarket US API key. **Backtest mode** replays books you saved.
|
|
35
|
+
|
|
36
|
+
- One order shape for every venue and mode, published as a JSON Schema (`schema/order.json`).
|
|
37
|
+
- Paper mode is the default. `preview()` shows what an order would do and sends nothing.
|
|
38
|
+
- Guardrails check every order before it's sent: position size, budget, daily loss, allowed markets,
|
|
39
|
+
approvals, stop-loss and take-profit. A price collar, an order throttle and a kill switch are
|
|
40
|
+
always on.
|
|
41
|
+
- Fees come from each venue's published schedule in force at the time of the trade. They match
|
|
42
|
+
Layer's API (`POST /v0/profit`, `POST /v0/size`) to the millionth of a dollar.
|
|
43
|
+
- Everything stays on your machine: a local SQLite file per mode, no telemetry.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install uselayer
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.11 or newer.
|
|
52
|
+
|
|
53
|
+
## Paper trade in five lines
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from uselayer import Client
|
|
57
|
+
|
|
58
|
+
client = Client() # paper mode: real books, simulated fills
|
|
59
|
+
m = client.markets(limit=20)[0] # open Polymarket US markets, no key needed
|
|
60
|
+
book = client.book(m.slug)
|
|
61
|
+
order = client.order(
|
|
62
|
+
venue="polymarket_us", market=m.slug, side="yes", price=book.outcome("yes").best_ask.price, size=5
|
|
63
|
+
)
|
|
64
|
+
print(client.preview(order)) # fill, fees, every rule's decision
|
|
65
|
+
print(client.send(order)) # the order, filled against the book
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`client.positions()`, `client.fills()` and `client.orders()` read the local store. Every fill in paper
|
|
69
|
+
mode is a `SimulatedFill` with `simulated=True`.
|
|
70
|
+
|
|
71
|
+
## Live mode
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from uselayer import Client, PolymarketUS
|
|
75
|
+
|
|
76
|
+
client = Client(mode="live", polymarket_us=PolymarketUS(key_id="...", secret_key_path="~/.pmus/secret"))
|
|
77
|
+
client.balances()["polymarket_us"].cash
|
|
78
|
+
order = client.buy(venue="polymarket_us", market="<slug>", side="yes", price=0.42, size=5)
|
|
79
|
+
client.positions() # from the venue
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Create the key at polymarket.us/developer. It stays on your machine: requests are signed with it
|
|
83
|
+
locally and only the signature is sent. Books in live mode come from the venue's WebSocket, so they
|
|
84
|
+
aren't cached. An order whose answer never arrives raises `outcome_unknown` and is never sent again
|
|
85
|
+
on its own: call `client.sync()` and check `client.orders()`.
|
|
86
|
+
|
|
87
|
+
If the local store is new but your account already has open orders or positions, live mode starts
|
|
88
|
+
with the kill switch on, until you run `python -m uselayer resume --mode live`.
|
|
89
|
+
|
|
90
|
+
## Guardrails
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client = Client(
|
|
94
|
+
rules={
|
|
95
|
+
"max_position": {"per_market": 200}, # $ at risk in one market
|
|
96
|
+
"budget": 1000, # $ at risk in total
|
|
97
|
+
"max_daily_loss": {"amount": 150}, # stop opening positions after this loss today
|
|
98
|
+
"approve_above": 100, # ask before orders above $100
|
|
99
|
+
"stop_loss": {"pct": 25}, # exits sent by client.monitor()
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Rules can also come from a YAML or JSON file: `Client(rules="guardrails.yaml")` (YAML needs
|
|
105
|
+
`pip install "uselayer[yaml]"`). They're fixed when the client is created.
|
|
106
|
+
|
|
107
|
+
**Kill switch.** `client.kill()` cancels resting orders and blocks new ones. From another terminal:
|
|
108
|
+
`python -m uselayer kill`. It stays on, even after a restart, until a person runs
|
|
109
|
+
`python -m uselayer resume`. The client a strategy or agent holds can't resume.
|
|
110
|
+
|
|
111
|
+
## Pairs: both sides, with the leg-risk guard
|
|
112
|
+
|
|
113
|
+
When two markets are the same bet, buying YES on one and NO on the other pays $1 per contract
|
|
114
|
+
either way. `quote()` prices that after both fees; `trade()` places both legs:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
q = client.quote(pair) # pair: a Match from client.matches(), or two (venue, market)
|
|
118
|
+
t = client.trade(pair, size=100, min_edge=0.01)
|
|
119
|
+
t.status # "hedged" | "missed" | "unwound" | "exposed"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The thinner leg goes first, immediate-or-cancel. The other leg goes for what filled, up to its
|
|
123
|
+
break-even price. If it can't be completed within `chase_s`, the first leg is sold back, never below
|
|
124
|
+
its entry price minus `max_unwind_loss` (`on_miss="unwind"`, the default), or the open contracts are
|
|
125
|
+
reported (`on_miss="hold"`). Both legs pass the guardrails together before either is sent.
|
|
126
|
+
|
|
127
|
+
In this release `trade()` runs in paper and backtest mode.
|
|
128
|
+
|
|
129
|
+
## One strategy, every mode
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
def strategy(client, pair, quote):
|
|
133
|
+
if quote.net_profit_per_contract >= 0.02:
|
|
134
|
+
client.trade(pair, size=100)
|
|
135
|
+
|
|
136
|
+
Client(mode="backtest", books=saved_books).run(strategy, [pair]) # the past
|
|
137
|
+
Client().run(strategy, [pair], iterations=60) # now, paper
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Backtest on books you saved
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from uselayer import Client
|
|
144
|
+
from uselayer.backtest import load_books, record_books
|
|
145
|
+
|
|
146
|
+
record_books(Client(), ["<slug>"], "books.jsonl") # run on a schedule to build a history
|
|
147
|
+
|
|
148
|
+
bt = Client(mode="backtest", books=load_books("books.jsonl"))
|
|
149
|
+
bt.replay(lambda client, book: ...) # place orders as each book arrives
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The replay uses the same fill model, fees and rules as paper mode, on the replayed clock.
|
|
153
|
+
|
|
154
|
+
## Fees by date
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from datetime import UTC, datetime
|
|
158
|
+
from uselayer import FeeSettings, calculate_fee, rules_at
|
|
159
|
+
|
|
160
|
+
rules_at("polymarket_us", datetime.now(UTC)).source # the schedule's page
|
|
161
|
+
calculate_fee(
|
|
162
|
+
FeeSettings(venue="polymarket_us"), contracts=100, price=0.5, role="taker", at=datetime.now(UTC)
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Before the earliest schedule the SDK knows, it raises `no_venue_rules` instead of guessing.
|
|
167
|
+
|
|
168
|
+
## What paper mode can't tell you
|
|
169
|
+
|
|
170
|
+
- **Queue position.** A paper order that rests fills as soon as a later book reaches its price. A real
|
|
171
|
+
one waits in line, so paper fills look at least as good as live ones.
|
|
172
|
+
- **Freshness without a key.** In paper mode, Polymarket US's public book is cached for up to 30
|
|
173
|
+
seconds. The SDK stamps each book with the venue's time and, when a copy is older than
|
|
174
|
+
`max_quote_age_s` (10 s by default), waits for a fresh one before using it.
|
|
175
|
+
|
|
176
|
+
## For AI agents
|
|
177
|
+
|
|
178
|
+
`AGENTS.md` and `llms.txt` ship inside the package. Every public method has a docstring with an
|
|
179
|
+
example, every object has `.to_dict()`, and every error is a `VenueError` with `code`, `hint` and
|
|
180
|
+
`next`. The `examples/` folder runs in CI.
|
|
181
|
+
|
|
182
|
+
## Layer
|
|
183
|
+
|
|
184
|
+
Layer (uselayer.sh) finds markets that are the same bet on different venues. With a Layer API key,
|
|
185
|
+
`client.matches(q="...")` returns them. The SDK sends Layer your key, the market ids Layer gave you
|
|
186
|
+
and your filters, and nothing else: no prices, orders, positions or venue keys.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
uselayer-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# uselayer
|
|
2
|
+
|
|
3
|
+
A Python SDK for trading prediction markets with your own venue keys.
|
|
4
|
+
|
|
5
|
+
This release (0.1) trades **Polymarket US**. **Paper mode** (the default) fills orders against the
|
|
6
|
+
venue's real order books with simulated money and sends nothing to the venue. **Live mode** sends
|
|
7
|
+
orders with your own Polymarket US API key. **Backtest mode** replays books you saved.
|
|
8
|
+
|
|
9
|
+
- One order shape for every venue and mode, published as a JSON Schema (`schema/order.json`).
|
|
10
|
+
- Paper mode is the default. `preview()` shows what an order would do and sends nothing.
|
|
11
|
+
- Guardrails check every order before it's sent: position size, budget, daily loss, allowed markets,
|
|
12
|
+
approvals, stop-loss and take-profit. A price collar, an order throttle and a kill switch are
|
|
13
|
+
always on.
|
|
14
|
+
- Fees come from each venue's published schedule in force at the time of the trade. They match
|
|
15
|
+
Layer's API (`POST /v0/profit`, `POST /v0/size`) to the millionth of a dollar.
|
|
16
|
+
- Everything stays on your machine: a local SQLite file per mode, no telemetry.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install uselayer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Python 3.11 or newer.
|
|
25
|
+
|
|
26
|
+
## Paper trade in five lines
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from uselayer import Client
|
|
30
|
+
|
|
31
|
+
client = Client() # paper mode: real books, simulated fills
|
|
32
|
+
m = client.markets(limit=20)[0] # open Polymarket US markets, no key needed
|
|
33
|
+
book = client.book(m.slug)
|
|
34
|
+
order = client.order(
|
|
35
|
+
venue="polymarket_us", market=m.slug, side="yes", price=book.outcome("yes").best_ask.price, size=5
|
|
36
|
+
)
|
|
37
|
+
print(client.preview(order)) # fill, fees, every rule's decision
|
|
38
|
+
print(client.send(order)) # the order, filled against the book
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`client.positions()`, `client.fills()` and `client.orders()` read the local store. Every fill in paper
|
|
42
|
+
mode is a `SimulatedFill` with `simulated=True`.
|
|
43
|
+
|
|
44
|
+
## Live mode
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from uselayer import Client, PolymarketUS
|
|
48
|
+
|
|
49
|
+
client = Client(mode="live", polymarket_us=PolymarketUS(key_id="...", secret_key_path="~/.pmus/secret"))
|
|
50
|
+
client.balances()["polymarket_us"].cash
|
|
51
|
+
order = client.buy(venue="polymarket_us", market="<slug>", side="yes", price=0.42, size=5)
|
|
52
|
+
client.positions() # from the venue
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create the key at polymarket.us/developer. It stays on your machine: requests are signed with it
|
|
56
|
+
locally and only the signature is sent. Books in live mode come from the venue's WebSocket, so they
|
|
57
|
+
aren't cached. An order whose answer never arrives raises `outcome_unknown` and is never sent again
|
|
58
|
+
on its own: call `client.sync()` and check `client.orders()`.
|
|
59
|
+
|
|
60
|
+
If the local store is new but your account already has open orders or positions, live mode starts
|
|
61
|
+
with the kill switch on, until you run `python -m uselayer resume --mode live`.
|
|
62
|
+
|
|
63
|
+
## Guardrails
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = Client(
|
|
67
|
+
rules={
|
|
68
|
+
"max_position": {"per_market": 200}, # $ at risk in one market
|
|
69
|
+
"budget": 1000, # $ at risk in total
|
|
70
|
+
"max_daily_loss": {"amount": 150}, # stop opening positions after this loss today
|
|
71
|
+
"approve_above": 100, # ask before orders above $100
|
|
72
|
+
"stop_loss": {"pct": 25}, # exits sent by client.monitor()
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Rules can also come from a YAML or JSON file: `Client(rules="guardrails.yaml")` (YAML needs
|
|
78
|
+
`pip install "uselayer[yaml]"`). They're fixed when the client is created.
|
|
79
|
+
|
|
80
|
+
**Kill switch.** `client.kill()` cancels resting orders and blocks new ones. From another terminal:
|
|
81
|
+
`python -m uselayer kill`. It stays on, even after a restart, until a person runs
|
|
82
|
+
`python -m uselayer resume`. The client a strategy or agent holds can't resume.
|
|
83
|
+
|
|
84
|
+
## Pairs: both sides, with the leg-risk guard
|
|
85
|
+
|
|
86
|
+
When two markets are the same bet, buying YES on one and NO on the other pays $1 per contract
|
|
87
|
+
either way. `quote()` prices that after both fees; `trade()` places both legs:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
q = client.quote(pair) # pair: a Match from client.matches(), or two (venue, market)
|
|
91
|
+
t = client.trade(pair, size=100, min_edge=0.01)
|
|
92
|
+
t.status # "hedged" | "missed" | "unwound" | "exposed"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The thinner leg goes first, immediate-or-cancel. The other leg goes for what filled, up to its
|
|
96
|
+
break-even price. If it can't be completed within `chase_s`, the first leg is sold back, never below
|
|
97
|
+
its entry price minus `max_unwind_loss` (`on_miss="unwind"`, the default), or the open contracts are
|
|
98
|
+
reported (`on_miss="hold"`). Both legs pass the guardrails together before either is sent.
|
|
99
|
+
|
|
100
|
+
In this release `trade()` runs in paper and backtest mode.
|
|
101
|
+
|
|
102
|
+
## One strategy, every mode
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
def strategy(client, pair, quote):
|
|
106
|
+
if quote.net_profit_per_contract >= 0.02:
|
|
107
|
+
client.trade(pair, size=100)
|
|
108
|
+
|
|
109
|
+
Client(mode="backtest", books=saved_books).run(strategy, [pair]) # the past
|
|
110
|
+
Client().run(strategy, [pair], iterations=60) # now, paper
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Backtest on books you saved
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from uselayer import Client
|
|
117
|
+
from uselayer.backtest import load_books, record_books
|
|
118
|
+
|
|
119
|
+
record_books(Client(), ["<slug>"], "books.jsonl") # run on a schedule to build a history
|
|
120
|
+
|
|
121
|
+
bt = Client(mode="backtest", books=load_books("books.jsonl"))
|
|
122
|
+
bt.replay(lambda client, book: ...) # place orders as each book arrives
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The replay uses the same fill model, fees and rules as paper mode, on the replayed clock.
|
|
126
|
+
|
|
127
|
+
## Fees by date
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from datetime import UTC, datetime
|
|
131
|
+
from uselayer import FeeSettings, calculate_fee, rules_at
|
|
132
|
+
|
|
133
|
+
rules_at("polymarket_us", datetime.now(UTC)).source # the schedule's page
|
|
134
|
+
calculate_fee(
|
|
135
|
+
FeeSettings(venue="polymarket_us"), contracts=100, price=0.5, role="taker", at=datetime.now(UTC)
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Before the earliest schedule the SDK knows, it raises `no_venue_rules` instead of guessing.
|
|
140
|
+
|
|
141
|
+
## What paper mode can't tell you
|
|
142
|
+
|
|
143
|
+
- **Queue position.** A paper order that rests fills as soon as a later book reaches its price. A real
|
|
144
|
+
one waits in line, so paper fills look at least as good as live ones.
|
|
145
|
+
- **Freshness without a key.** In paper mode, Polymarket US's public book is cached for up to 30
|
|
146
|
+
seconds. The SDK stamps each book with the venue's time and, when a copy is older than
|
|
147
|
+
`max_quote_age_s` (10 s by default), waits for a fresh one before using it.
|
|
148
|
+
|
|
149
|
+
## For AI agents
|
|
150
|
+
|
|
151
|
+
`AGENTS.md` and `llms.txt` ship inside the package. Every public method has a docstring with an
|
|
152
|
+
example, every object has `.to_dict()`, and every error is a `VenueError` with `code`, `hint` and
|
|
153
|
+
`next`. The `examples/` folder runs in CI.
|
|
154
|
+
|
|
155
|
+
## Layer
|
|
156
|
+
|
|
157
|
+
Layer (uselayer.sh) finds markets that are the same bet on different venues. With a Layer API key,
|
|
158
|
+
`client.matches(q="...")` returns them. The SDK sends Layer your key, the market ids Layer gave you
|
|
159
|
+
and your filters, and nothing else: no prices, orders, positions or venue keys.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Read a market's book and preview an order. Nothing is sent.
|
|
2
|
+
|
|
3
|
+
python examples/01_book_and_preview.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from _pick import pick_market
|
|
7
|
+
|
|
8
|
+
from uselayer import Client
|
|
9
|
+
|
|
10
|
+
client = Client(store=":memory:") # paper mode (the default); this store lives in memory only
|
|
11
|
+
slug, book = pick_market(client)
|
|
12
|
+
yes = book.outcome("yes")
|
|
13
|
+
print(f"market {slug}: best bid {yes.best_bid.price}, best ask {yes.best_ask.price}, as of {book.as_of}")
|
|
14
|
+
|
|
15
|
+
order = client.order(venue="polymarket_us", market=slug, side="yes", price=yes.best_ask.price, size=5)
|
|
16
|
+
preview = client.preview(order)
|
|
17
|
+
print(
|
|
18
|
+
f"allowed: {preview.allowed}, would fill {preview.est_fill.filled} @ {preview.est_fill.avg_price}, fees ${preview.fees}"
|
|
19
|
+
)
|
|
20
|
+
for d in preview.verdict.decisions:
|
|
21
|
+
if d.result != "allow":
|
|
22
|
+
print(f" {d.rule}: {d.result} — {d.reason}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Buy in paper mode against the live book, then check the fee by hand.
|
|
2
|
+
|
|
3
|
+
python examples/02_paper_trade.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from _pick import pick_market
|
|
7
|
+
|
|
8
|
+
from uselayer import Client
|
|
9
|
+
|
|
10
|
+
client = Client(store=":memory:")
|
|
11
|
+
slug, book = pick_market(client)
|
|
12
|
+
ask = book.outcome("yes").best_ask
|
|
13
|
+
|
|
14
|
+
order = client.buy(venue="polymarket_us", market=slug, side="yes", price=ask.price, size=5)
|
|
15
|
+
print(f"{order.status}: {order.filled} @ {order.avg_price}, fees ${order.fees}")
|
|
16
|
+
|
|
17
|
+
coefficient = client.market(slug).fee_coefficient or 0.0695
|
|
18
|
+
for fill in client.fills():
|
|
19
|
+
by_hand = coefficient * fill.contracts * fill.price * (1 - fill.price)
|
|
20
|
+
print(
|
|
21
|
+
f" fill {fill.contracts} @ {fill.price}: fee ${fill.fee} (by hand ${by_hand:.4f}, billed to the cent)"
|
|
22
|
+
)
|
|
23
|
+
assert abs(fill.fee - by_hand) <= 0.005 + 1e-9
|
|
24
|
+
|
|
25
|
+
for p in client.positions():
|
|
26
|
+
print(f"position: {p.contracts} {p.side} of {p.market} at {p.avg_price} (simulated: {p.simulated})")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Set guardrails, see one block an order, then press the kill switch.
|
|
2
|
+
|
|
3
|
+
python examples/03_rules_and_kill.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from _pick import pick_market
|
|
7
|
+
|
|
8
|
+
from uselayer import Client, VenueError
|
|
9
|
+
|
|
10
|
+
client = Client(store=":memory:", rules={"max_position": {"per_market": 3}, "budget": 10})
|
|
11
|
+
slug, book = pick_market(client)
|
|
12
|
+
ask = book.outcome("yes").best_ask
|
|
13
|
+
|
|
14
|
+
big = client.order(venue="polymarket_us", market=slug, side="yes", price=ask.price, size=50)
|
|
15
|
+
print("preview of a big order:", client.preview(big).blocked_by) # max_position
|
|
16
|
+
|
|
17
|
+
small = client.buy(venue="polymarket_us", market=slug, side="yes", price=ask.price, size=1)
|
|
18
|
+
print("small order:", small.status)
|
|
19
|
+
|
|
20
|
+
client.kill() # cancels resting orders and blocks new ones (python -m uselayer kill does the same from a terminal)
|
|
21
|
+
try:
|
|
22
|
+
client.buy(venue="polymarket_us", market=slug, side="yes", price=ask.price, size=1)
|
|
23
|
+
except VenueError as e:
|
|
24
|
+
print(f"after kill: {e.code} by {e.rule}. {e.next}")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Save a few books, then replay them in backtest mode through the same fill model and rules.
|
|
2
|
+
|
|
3
|
+
python examples/04_backtest_saved_books.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from _pick import pick_market
|
|
10
|
+
|
|
11
|
+
from uselayer import Book, Client
|
|
12
|
+
from uselayer.backtest import load_books, record_books
|
|
13
|
+
|
|
14
|
+
live = Client(store=":memory:")
|
|
15
|
+
slug, _ = pick_market(live)
|
|
16
|
+
path = Path(tempfile.mkdtemp()) / "books.jsonl"
|
|
17
|
+
for _ in range(3):
|
|
18
|
+
record_books(live, [slug], path) # in real use, call this on a schedule to build a history
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def strategy(client: Client, book: Book) -> None:
|
|
22
|
+
ask = book.outcome("yes").best_ask
|
|
23
|
+
if ask and not client.positions():
|
|
24
|
+
client.buy(venue=book.venue, market=book.market, side="yes", price=ask.price, size=2)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
bt = Client(mode="backtest", books=load_books(path))
|
|
28
|
+
result = bt.replay(strategy)
|
|
29
|
+
print(f"replayed {result['books']} books, {result['fills']} simulated fills")
|
|
30
|
+
for p in bt.positions():
|
|
31
|
+
print(f"position: {p.contracts} @ {p.avg_price}, fees ${p.fees}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Fees come from the schedule in force at the time of the trade.
|
|
2
|
+
|
|
3
|
+
python examples/05_fees_by_date.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
from uselayer import FeeSettings, calculate_fee, rules_at
|
|
9
|
+
from uselayer.fees import dollars
|
|
10
|
+
|
|
11
|
+
now = datetime.now(UTC)
|
|
12
|
+
r = rules_at("polymarket_us", now)
|
|
13
|
+
print(f"Polymarket US schedule from {r.effective_from.date()} ({r.source})")
|
|
14
|
+
fee = calculate_fee(FeeSettings(venue="polymarket_us"), contracts=100, price=0.5, role="taker", at=now)
|
|
15
|
+
print(f"100 contracts at $0.50, taker: ${dollars(fee)}")
|
|
16
|
+
|
|
17
|
+
for when in (datetime(2026, 7, 9, tzinfo=UTC), datetime(2026, 7, 11, tzinfo=UTC)):
|
|
18
|
+
s = FeeSettings(venue="polymarket", category="sports")
|
|
19
|
+
print(
|
|
20
|
+
f"Polymarket sports, {when.date()}: ${dollars(calculate_fee(s, contracts=100, price=0.5, role='taker', at=when))}"
|
|
21
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Trade both sides of a pair with the leg-risk guard, in backtest mode.
|
|
2
|
+
|
|
3
|
+
The books below are made up for the example; they aren't real prices. Two markets that are the same
|
|
4
|
+
bet: buying YES on one and NO on the other pays $1 per contract either way.
|
|
5
|
+
|
|
6
|
+
python examples/06_pair_trade_backtest.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
|
+
|
|
11
|
+
from uselayer import Book, Client, Level
|
|
12
|
+
|
|
13
|
+
t0 = datetime(2026, 10, 1, 12, 0, tzinfo=UTC)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def book(market: str, seconds: int, bid: float, ask: float, size: float = 50) -> Book:
|
|
17
|
+
return Book(
|
|
18
|
+
venue="polymarket_us",
|
|
19
|
+
market=market,
|
|
20
|
+
bids=(Level(price=bid, size=size),),
|
|
21
|
+
asks=(Level(price=ask, size=size),),
|
|
22
|
+
as_of=t0 + timedelta(seconds=seconds),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
pair = [("polymarket_us", "example-a"), ("polymarket_us", "example-b")]
|
|
27
|
+
books = [
|
|
28
|
+
book("example-a", 0, 0.40, 0.42), # YES on A costs 0.42
|
|
29
|
+
book("example-b", 1, 0.60, 0.62), # NO on B costs 1 − 0.60 = 0.40
|
|
30
|
+
book("example-a", 60, 0.47, 0.49),
|
|
31
|
+
book("example-b", 61, 0.52, 0.54),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def strategy(client: Client, pair: list[tuple[str, str]], quote) -> None: # type: ignore[no-untyped-def]
|
|
36
|
+
print(
|
|
37
|
+
f"quote: {quote.contracts} contracts clear the edge, {quote.net_profit_per_contract} a contract after fees"
|
|
38
|
+
)
|
|
39
|
+
if quote.net_profit_per_contract >= 0.02 and not client.positions():
|
|
40
|
+
t = client.trade(pair, size=20, min_edge=0.01)
|
|
41
|
+
print(f"trade: {t.status}, {t.hedged} hedged, ${t.locked_in} locked in after fees")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
bt = Client(mode="backtest", books=books, rules={"max_position": {"per_market": 50}})
|
|
45
|
+
bt.run(strategy, [pair])
|
|
46
|
+
for p in bt.positions():
|
|
47
|
+
print(f"position: {p.contracts} {p.side} of {p.market} at {p.avg_price}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared by the examples: pick an open Polymarket US market with prices on both sides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uselayer import Book, Client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pick_market(client: Client) -> tuple[str, Book]:
|
|
9
|
+
for m in client.markets(limit=50):
|
|
10
|
+
book = client.book(m.slug)
|
|
11
|
+
yes = book.outcome("yes")
|
|
12
|
+
if yes.best_bid and yes.best_ask and 0.1 < yes.best_ask.price < 0.9 and yes.best_ask.size >= 5:
|
|
13
|
+
return m.slug, book
|
|
14
|
+
raise SystemExit("No open market with prices on both sides right now; try again later.")
|