langbot-sdk 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.
- langbot_sdk-0.1.0/.gitignore +8 -0
- langbot_sdk-0.1.0/LICENSE +21 -0
- langbot_sdk-0.1.0/PKG-INFO +410 -0
- langbot_sdk-0.1.0/README.md +368 -0
- langbot_sdk-0.1.0/pyproject.toml +77 -0
- langbot_sdk-0.1.0/src/langbot/__init__.py +147 -0
- langbot_sdk-0.1.0/src/langbot/adapter.py +310 -0
- langbot_sdk-0.1.0/src/langbot/backtest.py +272 -0
- langbot_sdk-0.1.0/src/langbot/cli.py +2785 -0
- langbot_sdk-0.1.0/src/langbot/client.py +1490 -0
- langbot_sdk-0.1.0/src/langbot/config.py +144 -0
- langbot_sdk-0.1.0/src/langbot/kalshi.py +890 -0
- langbot_sdk-0.1.0/src/langbot/lab/__init__.py +90 -0
- langbot_sdk-0.1.0/src/langbot/lab/base.py +346 -0
- langbot_sdk-0.1.0/src/langbot/lab/engine.py +228 -0
- langbot_sdk-0.1.0/src/langbot/lab/env.py +151 -0
- langbot_sdk-0.1.0/src/langbot/lab/fees.py +310 -0
- langbot_sdk-0.1.0/src/langbot/lab/history.py +42 -0
- langbot_sdk-0.1.0/src/langbot/lab/paper.py +351 -0
- langbot_sdk-0.1.0/src/langbot/lab/payrolls.py +257 -0
- langbot_sdk-0.1.0/src/langbot/lab/scorecard.py +383 -0
- langbot_sdk-0.1.0/src/langbot/lab/strategies.py +524 -0
- langbot_sdk-0.1.0/src/langbot/live/__init__.py +76 -0
- langbot_sdk-0.1.0/src/langbot/live/audit.py +108 -0
- langbot_sdk-0.1.0/src/langbot/live/guard.py +782 -0
- langbot_sdk-0.1.0/src/langbot/live/kalshi_venue.py +124 -0
- langbot_sdk-0.1.0/src/langbot/live/killswitch.py +95 -0
- langbot_sdk-0.1.0/src/langbot/live/ledger.py +304 -0
- langbot_sdk-0.1.0/src/langbot/live/limits.py +235 -0
- langbot_sdk-0.1.0/src/langbot/live/polymarket_venue.py +141 -0
- langbot_sdk-0.1.0/src/langbot/live/venue.py +171 -0
- langbot_sdk-0.1.0/src/langbot/notify.py +151 -0
- langbot_sdk-0.1.0/src/langbot/polymarket_trading.py +879 -0
- langbot_sdk-0.1.0/src/langbot/py.typed +0 -0
- langbot_sdk-0.1.0/src/langbot/receipts.py +662 -0
- langbot_sdk-0.1.0/src/langbot/runtime.py +650 -0
- langbot_sdk-0.1.0/src/langbot/schemas.py +1233 -0
- langbot_sdk-0.1.0/src/langbot/sim/__init__.py +153 -0
- langbot_sdk-0.1.0/src/langbot/sim/bench.py +297 -0
- langbot_sdk-0.1.0/src/langbot/sim/engine.py +1389 -0
- langbot_sdk-0.1.0/src/langbot/sim/local.py +112 -0
- langbot_sdk-0.1.0/src/langbot/sim/scorecard.py +390 -0
- langbot_sdk-0.1.0/src/langbot/sim/stats.py +148 -0
- langbot_sdk-0.1.0/src/langbot/sim/strategies.py +367 -0
- langbot_sdk-0.1.0/src/langbot/sim/study.py +581 -0
- langbot_sdk-0.1.0/src/langbot/strategy.py +691 -0
- langbot_sdk-0.1.0/src/langbot/x402.py +276 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Langbot
|
|
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.
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langbot-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Langbot SDK and CLI: data and simulation for every market on Kalshi and Polymarket. Market history, replays against recorded order books, a strategy runtime and a Kalshi adapter.
|
|
5
|
+
Project-URL: Homepage, https://langbot.ai
|
|
6
|
+
Project-URL: Documentation, https://langbot.ai/docs
|
|
7
|
+
Author-email: Langbot <hello@langbot.ai>
|
|
8
|
+
Maintainer-email: Langbot <hello@langbot.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backtesting,kalshi,mcp,order-book,polymarket,prediction-markets,trading-agents
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: cryptography>=42
|
|
24
|
+
Requires-Dist: fastapi>=0.110
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pydantic>=2.7
|
|
27
|
+
Requires-Dist: python-ulid>=2.2
|
|
28
|
+
Requires-Dist: rich>=13
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Requires-Dist: uvicorn>=0.29
|
|
31
|
+
Requires-Dist: websockets>=13
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: jsonschema>=4.21; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
36
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Provides-Extra: x402
|
|
39
|
+
Requires-Dist: eth-account>=0.13; extra == 'x402'
|
|
40
|
+
Requires-Dist: x402[evm]>=2.26; extra == 'x402'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# langbot-sdk
|
|
44
|
+
|
|
45
|
+
Python SDK and CLI for [Langbot](https://langbot.ai), the data and simulation layer for every
|
|
46
|
+
market on Kalshi and Polymarket. Look up any market and its price history, replay a strategy
|
|
47
|
+
against the order books Langbot recorded, with real fees and latency, then run the same file
|
|
48
|
+
live with your own Kalshi keys: paper first, live only when you say so.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install langbot-sdk # Python 3.12+; the import name and the command are `langbot`
|
|
52
|
+
uv tool install langbot-sdk # or: just the `langbot` command, isolated
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The PyPI name `langbot` belongs to an unrelated project; install `langbot-sdk`.
|
|
56
|
+
|
|
57
|
+
**No token needed** for the first step:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
langbot markets senate # search every Kalshi and Polymarket market
|
|
61
|
+
langbot market KXEARNINGSMENTIONGS-26OCT13-PRIV # one market: price history, result, replayable books
|
|
62
|
+
langbot events # live and upcoming events with markets
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With a free token (`langbot login`):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
langbot sim events # events with recorded books
|
|
69
|
+
langbot sim run -e ev_kxearningsmentiongs_26oct13 -s my_strategy.py # replay, scorecard
|
|
70
|
+
langbot run my_strategy.py --mode demo # the same file, paper, on the live feed
|
|
71
|
+
langbot calendar --days 7 # upcoming events, speakers, rights
|
|
72
|
+
langbot context KXFEDMENTION-26OCT-TARIFF # rules + book + base rate + event + mentions
|
|
73
|
+
langbot rates --speaker powell --phrase transitory [--series KXFEDMENTION] [--window-days 365]
|
|
74
|
+
langbot outcomes --speaker powell --since 2026-01-01 # settled markets, paginated (--cursor)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every command takes `--json` for the raw response.
|
|
78
|
+
|
|
79
|
+
## Client
|
|
80
|
+
|
|
81
|
+
Runs as is, without a token (anonymous calls get the Free delay, +60 s):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
from langbot import Langbot # alias of LangbotClient
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def main() -> None:
|
|
90
|
+
# token: LANGBOT_API_TOKEN, else the one `langbot login` saved, else anonymous
|
|
91
|
+
# api_url: LANGBOT_API_URL, else https://api.langbot.ai (stream: wss://api.langbot.ai)
|
|
92
|
+
async with Langbot() as lb:
|
|
93
|
+
hits = await lb.recent_events(types=["phrase_detected"], limit=5)
|
|
94
|
+
for e in hits:
|
|
95
|
+
print(e.event_id, e.phrase, e.t_spoken, e.verdict.counts if e.verdict else None)
|
|
96
|
+
if hits:
|
|
97
|
+
page = await lb.transcript(hits[-1].event_id) # words, text, last_seq
|
|
98
|
+
print(page.text[:200] or f"words withheld for {page.redacted_sources}")
|
|
99
|
+
async for evt in lb.stream(types=["phrase_detected"]):
|
|
100
|
+
print(evt.phrase, evt.t_spoken, evt.lag_vs_public_ms)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`transcript()` returns one page (`words`, `text`, `last_seq`, `held`, `delay_ms`,
|
|
107
|
+
`redacted_sources`); poll with `since=page.last_seq`. Restricted sources return no words.
|
|
108
|
+
|
|
109
|
+
Feed errors raise `LangbotAPIError` (`.status`, `.detail`, `.url`); `LangbotApiError` is the
|
|
110
|
+
same class under the TypeScript SDK's spelling. A stream refused on policy (close `1008`: your
|
|
111
|
+
plan's connection cap) raises `StreamRefused` instead of reconnecting. A local feed:
|
|
112
|
+
`Langbot("http://localhost:8000")`. Examples that run against the hosted feed:
|
|
113
|
+
[`examples/`](examples/) (`recent_hits.py`, `transcript_pages.py`, `stream_hits.py`).
|
|
114
|
+
|
|
115
|
+
### Same calls in the TypeScript SDK
|
|
116
|
+
|
|
117
|
+
The TypeScript SDK uses the Python SDK's names in camelCase (`recent_events` → `recentEvents`).
|
|
118
|
+
The old TypeScript names (`getEvent`, `getTranscript`, `listMarkets`, `listSubscriptions`,
|
|
119
|
+
`listTokens`) still work for one release, marked deprecated.
|
|
120
|
+
|
|
121
|
+
| | Python (`langbot`) | TypeScript (`@langbotai/sdk`) |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| client | `LangbotClient` / `Langbot` | `LangbotClient` / `Langbot` |
|
|
124
|
+
| live + upcoming events | `live_events()` | `liveEvents()` |
|
|
125
|
+
| one event | `event(id)` | `event(id)` |
|
|
126
|
+
| recent hits (one page) | `recent_events(event_id, phrases=, types=, since=, until=, cursor=, limit=)` | `recentEvents({eventId, phrases, types, since, until, cursor, limit})` |
|
|
127
|
+
| every page, newest first | `iter_recent_events(...)` | `iterRecentEvents({...})` |
|
|
128
|
+
| transcript page | `transcript(id, since=)` → `Transcript` | `transcript(id, {since})` → `Transcript` |
|
|
129
|
+
| every market (no token) | `public_markets(q, venue=, status=, category=, limit=)` | `publicMarkets({q, venue, status, category, limit})` |
|
|
130
|
+
| one market + history (no token) | `public_market(ticker)` | `publicMarket(ticker)` |
|
|
131
|
+
| markets with recorded events | `markets(q=, venue=, status=)` | `markets({q, venue, status})` |
|
|
132
|
+
| subscriptions / tokens | `subscriptions()`, `tokens()` | `subscriptions()`, `tokens()` |
|
|
133
|
+
| live stream | `stream(...)`, `stream_sse(...)` | `stream(...)`, `streamSSE(...)` |
|
|
134
|
+
| feed error | `LangbotAPIError` (`.status`, `.code`, `.detail`, `.url`) | `LangbotAPIError` (`.status`, `.code`, `.detail`, `.url`, `.body`) |
|
|
135
|
+
| refused stream (1008) | raises `StreamRefused` (`.code`) | `onStatus({state: "closed", code: 1008, willRetry: false})` after a `server_error` |
|
|
136
|
+
| token default | `LANGBOT_API_TOKEN`, else `~/.config/langbot/config.toml` | `LANGBOT_API_TOKEN` (the npm CLI also reads `config.toml`) |
|
|
137
|
+
| config file (both CLIs) | `~/.config/langbot/config.toml`, `LANGBOT_CONFIG` | the same file (falls back to the old `config.json`) |
|
|
138
|
+
|
|
139
|
+
Both read unknown keys and unknown enum values without failing, and both are tested against
|
|
140
|
+
the feed's own response shapes (`packages/schemas/fixtures/feed-responses.json`).
|
|
141
|
+
|
|
142
|
+
## History, calendar and context
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from langbot import LangbotClient
|
|
146
|
+
|
|
147
|
+
async with LangbotClient() as lb:
|
|
148
|
+
# list[CalendarEntry]: .rights, .speaker_keys, .phrases, .restricted
|
|
149
|
+
cal = await lb.calendar(days=7)
|
|
150
|
+
# MarketContext: .market .book .base_rates .event .mentions .rules
|
|
151
|
+
ctx = await lb.context("KXFEDMENTION-26OCT-TARIFF")
|
|
152
|
+
# BaseRates: .yes_rate (P(YES)), .markets (sample), .mispricing (avg result - open)
|
|
153
|
+
br = await lb.base_rates(speaker="powell", phrase="transitory", window_days=365)
|
|
154
|
+
# OutcomesPage: .items (list[MarketOutcome]), .next_cursor
|
|
155
|
+
page = await lb.outcomes(speaker="powell", limit=50)
|
|
156
|
+
# list[Mention]: phrase facts only, never transcript text
|
|
157
|
+
facts = await lb.mentions(phrase="blackwell", since=1_760_000_000_000)
|
|
158
|
+
who = await lb.speakers(q="pow") # list[SpeakerProfile]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Base rates and outcomes are the venue's public settlement record; mentions are Langbot's own
|
|
162
|
+
`phrase_detected` facts. For `restricted` / `unknown` sources only phrase facts are ever returned
|
|
163
|
+
(no transcript text) — `CalendarEntry.restricted` tells you ahead of time.
|
|
164
|
+
|
|
165
|
+
## Write a strategy
|
|
166
|
+
|
|
167
|
+
One protocol for `langbot run`, `langbot backtest` and `langbot sim run`: a class with a
|
|
168
|
+
synchronous `on_event(evt, ctx)` (the sim protocol below; `langbot.adapter` drives it live).
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
class Mention:
|
|
172
|
+
markets = ["KXEARNINGSMENTIONNVDA-*"] # glob over tickers
|
|
173
|
+
phrases = ["blackwell"]
|
|
174
|
+
|
|
175
|
+
def on_event(self, evt, ctx):
|
|
176
|
+
if evt["type"] == "phrase_detected" and evt.get("phrase") == "blackwell":
|
|
177
|
+
for ticker in evt["market_refs"]:
|
|
178
|
+
book = ctx.book(ticker)
|
|
179
|
+
if book and book.yes_ask and book.yes_ask < 0.60:
|
|
180
|
+
ctx.buy(ticker, "yes", 10, limit=book.yes_ask, tif="ioc")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`ctx`: `now`, `tickers`, `book(ticker)`, `position(ticker)`, `orders(ticker)`,
|
|
184
|
+
`buy(ticker, side, qty, limit=None, tif=None)`, `sell(...)`, `cancel_all(ticker=None)`.
|
|
185
|
+
The older `class X(langbot.Strategy)` with async hooks still runs, with a deprecation warning.
|
|
186
|
+
|
|
187
|
+
Every real order passes through the **live guard** (`langbot.live`): hard limits from
|
|
188
|
+
`~/.config/langbot/live.toml` (per order, per market, total exposure, open orders, orders per
|
|
189
|
+
minute, daily loss, price through the book, series allow/deny; a missing file means the
|
|
190
|
+
smallest limits), a kill switch (`langbot live halt` / `resume`, plus automatic halts on daily
|
|
191
|
+
loss, reconciliation mismatch, repeated API errors, stale books and clock skew), reconciliation
|
|
192
|
+
against Kalshi every 15 s, idempotent client order ids, and an append-only audit log.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
langbot live run my_strategy.py --dry-run # Kalshi demo, nothing sent
|
|
196
|
+
langbot live run my_strategy.py # Kalshi demo
|
|
197
|
+
langbot live run my_strategy.py --env prod --yes-real-money
|
|
198
|
+
langbot live halt --cancel-orders # stop now, pull resting orders
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Demo is the default. Prod needs `--yes-real-money` and a non-empty `series_allow`. See
|
|
202
|
+
`docs/risk.md` for every limit and halt.
|
|
203
|
+
|
|
204
|
+
## Langbot Sim: test a bot against a past event
|
|
205
|
+
|
|
206
|
+
`langbot.sim` replays a recorded event through your strategy against the Kalshi order books we
|
|
207
|
+
recorded while it happened (1 s snapshots), then scores it. Your code runs on your machine; the
|
|
208
|
+
feed serves the timeline (`GET /v1/sim/events/{id}/timeline`) and only ever runs its own
|
|
209
|
+
built-in strategies (`POST /v1/sim/runs`). Both use this same engine, so the same strategy on
|
|
210
|
+
the same event gives the same scorecard locally and on the server.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# my_bot.py
|
|
214
|
+
class FirstHit:
|
|
215
|
+
name = "first_hit"
|
|
216
|
+
|
|
217
|
+
def __init__(self, qty=10, max_price=0.90):
|
|
218
|
+
self.qty, self.max_price = qty, max_price
|
|
219
|
+
|
|
220
|
+
def on_event(self, evt, ctx): # every feed event, as the stream sends it
|
|
221
|
+
if evt["type"] != "phrase_detected":
|
|
222
|
+
return
|
|
223
|
+
if (evt.get("verdict") or {}).get("counts") is False: # §21: does not settle
|
|
224
|
+
return
|
|
225
|
+
for ticker in evt["market_refs"]:
|
|
226
|
+
if not ctx.position(ticker) and not ctx.orders(ticker):
|
|
227
|
+
ctx.buy(ticker, "yes", self.qty, limit=self.max_price, tif="ioc")
|
|
228
|
+
|
|
229
|
+
# optional: def on_book(self, ticker, book, ctx), on_fill(self, fill, ctx),
|
|
230
|
+
# on_start(self, ctx), on_end(self, ctx)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
langbot sim events # what can be replayed, with coverage
|
|
235
|
+
langbot sim run --event ev_kxearningsmentiongs_26oct13 \
|
|
236
|
+
--strategy my_bot.py:FirstHit --latency 250,2000,5000 --params '{"qty": 5}'
|
|
237
|
+
langbot sim run --event ev_… --strategy my_bot.py --timeline gs.ndjson # cache, then offline
|
|
238
|
+
langbot sim strategies # the built-ins
|
|
239
|
+
langbot sim run --event ev_… --strategy fade_rule_mismatch --server # a built-in, on the feed
|
|
240
|
+
langbot research bench --strategy my_bot.py --latency 250,2250 # every recorded event, one document
|
|
241
|
+
langbot research calibration --speaker powell --basis mid # openings vs settlements, with intervals
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Hooks are synchronous (no `await`); the clock is `ctx.now` (ms). `ctx.buy(ticker, side="yes"|"no",
|
|
245
|
+
qty, limit=None, tif=None)` / `ctx.sell(...)` return an `Order`; `ctx.position(ticker)` (`.yes`,
|
|
246
|
+
`.no`, `.net`), `ctx.book(ticker)` (`yes_bid`, `yes_ask`, `no_bid`, `no_ask`, `mid`, `bids`,
|
|
247
|
+
`asks`), `ctx.orders(ticker)`, `ctx.cancel(order_id)`, `ctx.cash`, `ctx.tickers`. Strategies never
|
|
248
|
+
see settlement results.
|
|
249
|
+
|
|
250
|
+
**Fill model** (`langbot/sim/engine.py` has the details):
|
|
251
|
+
|
|
252
|
+
- An order placed at `ctx.now` arrives at `now + latency` and matches the first snapshot at or
|
|
253
|
+
after that, walking its depth levels; size that runs out leaves a partial fill. A market order
|
|
254
|
+
(no `limit`) or `tif="ioc"` cancels the rest; a limit order rests.
|
|
255
|
+
- A resting order fills only when a later snapshot's best opposite price moves *through* its
|
|
256
|
+
limit (not on touch), at the limit, up to the size shown through it.
|
|
257
|
+
- No market impact: the recording never reacts to you (size you take is gone only within that
|
|
258
|
+
snapshot). `--queue-position` makes resting orders wait behind the size recorded at their price.
|
|
259
|
+
- Fees per order execution: `ceil(0.07 · Σ C·P·(1−P) · 100) / 100` taker; resting fills pay 0.0175.
|
|
260
|
+
- Positions settle at the market's result (1.00 / 0.00); a market without one is marked to its
|
|
261
|
+
last mid and flagged.
|
|
262
|
+
|
|
263
|
+
The scorecard: P&L (gross, fees, net), per market, orders and fills, fill rate, max drawdown,
|
|
264
|
+
exposure, a latency table (by default your latency, +2 s, +5 s) and the caveats that apply.
|
|
265
|
+
|
|
266
|
+
In code:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from langbot import LangbotClient
|
|
270
|
+
from langbot.sim import load_strategy, run_scorecard
|
|
271
|
+
|
|
272
|
+
async with LangbotClient() as lb:
|
|
273
|
+
timeline = await lb.sim_timeline("ev_kxearningsmentiongs_26oct13")
|
|
274
|
+
card = run_scorecard(timeline, load_strategy("my_bot.py:FirstHit"), latencies=[250, 2250, 5250])
|
|
275
|
+
print(card["pnl"], card["latency_sensitivity"])
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For a timeline too big to hold, stream it: `ScorecardRun(Timeline.header(event_id, markets,
|
|
279
|
+
booked=tickers_with_books), strategy, latencies=[...])`, then `feed(page)` for each page of
|
|
280
|
+
`TimelineItem`s in timeline order and `finish()`. Every latency runs in the same pass, memory
|
|
281
|
+
stays at a book per ticker plus your orders and fills, and the scorecard is identical to
|
|
282
|
+
`run_scorecard` on the whole timeline. The feed's server runs work this way.
|
|
283
|
+
|
|
284
|
+
**Event study.** How do prices move after the speaker says a word? `langbot sim study` pools
|
|
285
|
+
every trigger (detected phrases, and transcript words when you pass `--words`; multi-word phrases
|
|
286
|
+
match consecutive words) across events and reports, per (word, ticker), the mean / median / stdev
|
|
287
|
+
move of the YES mid in cents and the share up / down / flat at each horizon, plus a chronological
|
|
288
|
+
train/test split and caveats (small n, how many pairs you tested):
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
langbot sim study --event ev_a --event ev_b --words "tariff,rate cut" --horizons 5,30,300
|
|
292
|
+
langbot sim study --event ev_a --words tariff --tickers KX… --clock spoken --json
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Triggers are timed by default when the data was emitted (`--clock emitted`, plus `--delay-ms`),
|
|
296
|
+
i.e. when you could have traded. `--clock spoken` uses when the words were said and is not
|
|
297
|
+
tradeable: the broadcast can run 20-30 s behind the market, so it credits words with moves that
|
|
298
|
+
already happened. In code: `event_study(timeline, words=[...], tickers=None)` or
|
|
299
|
+
`event_study_many([tl_a, tl_b], ...)` from `langbot.sim`.
|
|
300
|
+
|
|
301
|
+
Plans: every plan runs Sim on any recorded event. Free has 20 runs a month, Builder 100,
|
|
302
|
+
Standard and up unlimited. A run is one server run or one timeline download (its first page).
|
|
303
|
+
Past the allowance the feed answers `429` with `code == "quota_exceeded"`; the error's `.quota`
|
|
304
|
+
says what was used and when it resets (`resets_at`).
|
|
305
|
+
|
|
306
|
+
## Modules
|
|
307
|
+
|
|
308
|
+
| module | what |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `langbot.schemas` | pydantic models mirroring `packages/schemas/event.schema.json` (`Event`, `Speaker`, `EventType`, `SourceKind`) plus `Book`, `Fill`, `Order`, `Market`, `LiveEvent`, `MarketOutcome`, `BaseRates`, `SpeakerProfile`, `Mention`, `CalendarEntry`, `MarketContext`, `new_event_id()` |
|
|
311
|
+
| `langbot.client` | `Langbot` / `LangbotClient` — every feed endpoint incl. `calendar()`, `context()`, `base_rates()`, `outcomes()`, `mentions()`, `speakers()`; `stream()` (WS, auto-reconnect), `stream_sse()` |
|
|
312
|
+
| `langbot.kalshi` | `KalshiClient` — Trade API v2 with RSA-PSS / Ed25519 signing, token-bucket rate limiter, `ws_subscribe()` with orderbook reconstruction |
|
|
313
|
+
| `langbot.strategy` | `Strategy`, `RiskRails`, `PaperBroker`, `KalshiBroker`, `Position` |
|
|
314
|
+
| `langbot.live` | `LiveGuard`, `GuardedBroker`, `Limits`/`load_limits`, `KillSwitch`, `AuditLog`, the `Venue` protocol, `KalshiVenue`, `PolymarketUSVenue` |
|
|
315
|
+
| `langbot.runtime` | `langbot run`: feed stream + Kalshi books + local daemon on `127.0.0.1:8787` |
|
|
316
|
+
| `langbot.backtest` | replay archived events + book snapshots (feed or JSONL) at original timestamps |
|
|
317
|
+
| `langbot.sim` | Langbot Sim: the deterministic engine (fill model, fees, settlement; `queue_position`, `taker_delay_ms`, `maker_fee_rate` settings), scorecard and built-in strategies the feed also runs (`quote_both_sides` is the market-making baseline); `event_study` for price moves after words; `langbot.sim.bench` aggregates a strategy across events (bootstrap interval, p-values with FDR, fee sensitivity, walk-forward) and `langbot.sim.stats` holds the shared tests |
|
|
318
|
+
| `langbot.notify` | Discord / Slack / Telegram notifiers (`notifier_from_env()`) |
|
|
319
|
+
| `langbot.cli` | the `langbot` command |
|
|
320
|
+
|
|
321
|
+
## Paying per call
|
|
322
|
+
|
|
323
|
+
Some endpoints are sold by the call (evidence packs, provenance reports, replay bundles,
|
|
324
|
+
history, facts — CONTRACTS §17). Without a plan that covers them the feed answers
|
|
325
|
+
`402 Payment Required` in the [x402](https://x402.org) v2 shape, and the client can pay it in
|
|
326
|
+
USDC with a wallet you hold:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
pip install "langbot-sdk[x402]" # adds the official x402 client + eth-account (pulls web3)
|
|
330
|
+
export LANGBOT_X402_WALLET=0x… # hex private key of a funded wallet; yours, never Langbot's
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
import os
|
|
335
|
+
from langbot import LangbotClient, PaymentRequired
|
|
336
|
+
|
|
337
|
+
async with LangbotClient(x402_wallet=os.environ["LANGBOT_X402_WALLET"], x402_max_usd=1.0) as lb:
|
|
338
|
+
bundle = await lb.replay("ev_nvda_q3_2026") # 402 → signed retry → 200, one call
|
|
339
|
+
print(lb.last_payment) # {price_usd, header, network, settlement: {transaction, payer, …}}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
What happens on a 402 whose `PAYMENT-REQUIRED` header and JSON body carry the x402 v2
|
|
343
|
+
`PaymentRequired` object `{x402Version: 2, resource: {url, …}, accepts: [{scheme: "exact",
|
|
344
|
+
network: "eip155:8453", amount: "250000", asset: <USDC>, payTo, maxTimeoutSeconds, extra: {name, version}}]}`:
|
|
345
|
+
|
|
346
|
+
- the price is `accepts[0].amount` ÷ asset decimals (USDC: 6). At or under `x402_max_usd`
|
|
347
|
+
(default **$1**) the client signs an EIP-3009 `transferWithAuthorization` for exactly that
|
|
348
|
+
amount to `payTo` (EIP-712 domain from `extra.name` / `extra.version`) and retries the same
|
|
349
|
+
request **once** with `PAYMENT-SIGNATURE` = base64 `{x402Version: 2, accepted: <accepts[0]
|
|
350
|
+
verbatim>, payload: {signature, authorization}}`;
|
|
351
|
+
- over the cap, without a wallet, or for a scheme other than `exact`, it raises
|
|
352
|
+
`PaymentRequired(price_usd, requirements)` (a `LangbotAPIError` with status 402) and signs
|
|
353
|
+
nothing;
|
|
354
|
+
- a 402 that is not x402-shaped (a plan gate) is the usual `LangbotAPIError`; a failed
|
|
355
|
+
settlement (402 + `PAYMENT-RESPONSE {success: false}`) is a `LangbotAPIError`
|
|
356
|
+
"payment failed: <reason>" and is never retried;
|
|
357
|
+
- the feed's `PAYMENT-RESPONSE` settlement is decoded into `client.last_payment["settlement"]`.
|
|
358
|
+
|
|
359
|
+
CLI: `langbot --x402-wallet-env LANGBOT_X402_WALLET replay ev_…` reads the key from that env
|
|
360
|
+
var (never from an argument; `LANGBOT_X402_WALLET` is used when set even without the flag),
|
|
361
|
+
`--x402-max-usd` sets the cap, and a paid call prints `paid $0.5000 USDC on eip155:8453 …` on stderr.
|
|
362
|
+
The key is never logged, printed or stored; only signatures leave the process.
|
|
363
|
+
|
|
364
|
+
## Configuration
|
|
365
|
+
|
|
366
|
+
`~/.config/langbot/config.toml` (written by `langbot init`), overridden by env:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
LANGBOT_API_URL, LANGBOT_API_TOKEN
|
|
370
|
+
KALSHI_KEY_ID, KALSHI_PRIVATE_KEY_PATH, KALSHI_ENV=demo|prod
|
|
371
|
+
LANGBOT_MAX_POSITION_USD, LANGBOT_MAX_DAILY_LOSS_USD, LANGBOT_MARKET_ALLOWLIST
|
|
372
|
+
DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL, TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
|
|
373
|
+
LANGBOT_I_UNDERSTAND_LIVE=1 # required for --mode live (`langbot live run --env prod` uses --yes-real-money)
|
|
374
|
+
LANGBOT_LIVE_CONFIG # live limits file (default ~/.config/langbot/live.toml)
|
|
375
|
+
LANGBOT_LIVE_DIR # HALT flag + audit logs (default ~/.config/langbot/live)
|
|
376
|
+
LANGBOT_X402_WALLET # hex wallet key for paying per call (langbot-sdk[x402])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Local daemon
|
|
380
|
+
|
|
381
|
+
`langbot run` serves `http://127.0.0.1:8787`: `GET /status`, `GET /strategies`,
|
|
382
|
+
`POST /strategies` `{path, mode}`, `POST /strategies/{id}/start|stop`, `GET /positions`,
|
|
383
|
+
`GET /fills`. The MCP server's `run_paper` tool talks to it.
|
|
384
|
+
|
|
385
|
+
## Kalshi notes
|
|
386
|
+
|
|
387
|
+
Prices are dollars (`0.42`), sent to Kalshi as fixed-point strings (`"0.4200"`); counts
|
|
388
|
+
are contracts (`"10.00"`). Orders are bids/asks on the **YES** book. Signing message is
|
|
389
|
+
`timestamp_ms + METHOD + path` where `path` includes `/trade-api/v2` and no query string.
|
|
390
|
+
|
|
391
|
+
## Install from source
|
|
392
|
+
|
|
393
|
+
From a checkout of this repository (Python 3.12+, [uv](https://docs.astral.sh/uv/)):
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
uv sync --all-packages # every workspace package into .venv
|
|
397
|
+
uv run langbot --help
|
|
398
|
+
uv run python packages/sdk-py/examples/recent_hits.py
|
|
399
|
+
# or into another project's environment:
|
|
400
|
+
pip install ./packages/sdk-py
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Develop
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
uv run --package langbot-sdk pytest packages/sdk-py -q
|
|
407
|
+
uv run ruff check packages/sdk-py
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
MIT. The SDK, CLI and runtime are public so you can read exactly what runs on your machine with your keys; the hosted feed is a separate, proprietary service at [langbot.ai](https://langbot.ai).
|