viper-execution 0.2.2__tar.gz → 0.3.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.
Files changed (53) hide show
  1. {viper_execution-0.2.2 → viper_execution-0.3.0}/.gitignore +2 -0
  2. viper_execution-0.3.0/PKG-INFO +272 -0
  3. viper_execution-0.3.0/README.md +224 -0
  4. {viper_execution-0.2.2 → viper_execution-0.3.0}/pyproject.toml +2 -2
  5. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/__init__.py +30 -7
  6. viper_execution-0.3.0/src/viper/examples/account_snapshot.py +197 -0
  7. viper_execution-0.3.0/src/viper/examples/algo_actions.py +118 -0
  8. viper_execution-0.3.0/src/viper/examples/algo_lifecycle.py +111 -0
  9. viper_execution-0.3.0/src/viper/examples/basket.py +120 -0
  10. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/detect_and_fire_glidemaker.py +2 -1
  11. viper_execution-0.3.0/src/viper/examples/getting_started.py +133 -0
  12. viper_execution-0.3.0/src/viper/examples/instrument_detail.py +68 -0
  13. viper_execution-0.3.0/src/viper/examples/list_executions.py +119 -0
  14. viper_execution-0.3.0/src/viper/examples/list_instruments.py +87 -0
  15. viper_execution-0.3.0/src/viper/examples/monitor_actions.py +129 -0
  16. viper_execution-0.3.0/src/viper/examples/monitor_lifecycle.py +168 -0
  17. viper_execution-0.3.0/src/viper/examples/nuke.py +149 -0
  18. viper_execution-0.3.0/src/viper/examples/place_order.py +150 -0
  19. viper_execution-0.3.0/src/viper/examples/preview_algo.py +124 -0
  20. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/smart_exit.py +2 -1
  21. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/start_flowband.py +2 -1
  22. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/start_flowscale.py +2 -1
  23. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/start_ghostsweep.py +2 -1
  24. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/start_glidemaker.py +2 -1
  25. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/start_pacemaker.py +2 -1
  26. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/stream_account_state.py +3 -1
  27. viper_execution-0.3.0/src/viper/examples/stream_execution.py +164 -0
  28. viper_execution-0.3.0/src/viper/examples/stream_executions.py +136 -0
  29. viper_execution-0.3.0/src/viper/examples/stream_monitors.py +200 -0
  30. viper_execution-0.3.0/src/viper/examples/stream_orders_fills.py +149 -0
  31. viper_execution-0.3.0/src/viper/examples/ws_algo_control.py +152 -0
  32. viper_execution-0.3.0/src/viper/examples/ws_archive.py +114 -0
  33. viper_execution-0.3.0/src/viper/examples/ws_basket.py +142 -0
  34. viper_execution-0.3.0/src/viper/examples/ws_bulk_control.py +178 -0
  35. viper_execution-0.3.0/src/viper/examples/ws_order_control.py +122 -0
  36. viper_execution-0.3.0/src/viper/examples/ws_order_management.py +147 -0
  37. viper_execution-0.3.0/src/viper/examples/ws_read_snapshot.py +140 -0
  38. viper_execution-0.3.0/src/viper/examples/ws_sweep.py +146 -0
  39. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/rest.py +178 -3
  40. viper_execution-0.2.2/PKG-INFO +0 -183
  41. viper_execution-0.2.2/README.md +0 -135
  42. {viper_execution-0.2.2 → viper_execution-0.3.0}/.github/workflows/release.yml +0 -0
  43. {viper_execution-0.2.2 → viper_execution-0.3.0}/LICENSE +0 -0
  44. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/__init__.py +0 -0
  45. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/__main__.py +0 -0
  46. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/_algo_common.py +0 -0
  47. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/examples/_algo_rest_common.py +0 -0
  48. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/exceptions.py +0 -0
  49. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/py.typed +0 -0
  50. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/rest_types.py +0 -0
  51. {viper_execution-0.2.2 → viper_execution-0.3.0}/src/viper/ws.py +0 -0
  52. {viper_execution-0.2.2 → viper_execution-0.3.0}/tests/test_handshake_status.py +0 -0
  53. {viper_execution-0.2.2 → viper_execution-0.3.0}/tests/test_resync_map.py +0 -0
@@ -7,3 +7,5 @@ build/
7
7
  .venv/
8
8
  venv/
9
9
  .env
10
+
11
+ *.zip
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.4
2
+ Name: viper-execution
3
+ Version: 0.3.0
4
+ Summary: Institutional-grade Python SDK for the Viper Execution trading API on Hyperliquid.
5
+ Project-URL: Homepage, https://viperexecution.com
6
+ Project-URL: Documentation, https://docs.viperexecution.com
7
+ Project-URL: Repository, https://github.com/viperexecution/viper-sdk-python
8
+ Project-URL: Issues, https://github.com/viperexecution/viper-sdk-python/issues
9
+ Author: Viper Execution
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Viper Execution
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: algotrading,hyperliquid,trading,viper,websocket
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Financial and Insurance Industry
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Typing :: Typed
41
+ Requires-Python: >=3.10
42
+ Requires-Dist: httpx>=0.27.0
43
+ Requires-Dist: websockets>=13.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
46
+ Requires-Dist: pytest>=8.0; extra == 'dev'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Viper Execution Python SDK
50
+
51
+ Institutional-grade Python client for the [Viper Execution](https://viperexecution.com) trading API on Hyperliquid.
52
+
53
+ > **Status:** SDK `0.2.x`. Ships a typed async REST client (`ViperRestClient`) and a resilient WebSocket client (`ViperWSClient`). The SDK version is independent of the API version — this is SDK 0.x against API v1.
54
+
55
+ The SDK is a convenience layer over the raw HMAC + REST/WebSocket surface — never required. Every response is returned as a plain `dict`, so you are never boxed out of the raw payload; the typed signatures and `TypedDict` hints are there for editor and type-checker support only.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install viper-execution
61
+ ```
62
+
63
+ Requires Python ≥ 3.10.
64
+
65
+ ## Quickstart (REST)
66
+
67
+ ```python
68
+ import asyncio
69
+ from viper import ViperRestClient
70
+
71
+ async def main():
72
+ # from_env() reads VIPER_API_KEY / VIPER_API_SECRET / VIPER_HANDLE /
73
+ # VIPER_WALLET. Pass anything explicitly to override the environment.
74
+ async with ViperRestClient.from_env() as viper:
75
+ # market data
76
+ btc = (await viper.instrument("BTC"))["instrument"]
77
+ print("BTC mark:", btc["mark_price"])
78
+
79
+ # launch a Glidemaker (idempotency key is auto-generated)
80
+ res = await viper.execute(
81
+ algo="glidemaker", symbol="BTC", side="buy", total_size=0.001,
82
+ params={"strategy": "neutral", "limit_price": 65000},
83
+ )
84
+ print(res["execution_id"], res["status"])
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Quickstart (WebSocket)
90
+
91
+ ```python
92
+ import os
93
+ import asyncio
94
+ from viper import ViperWSClient
95
+
96
+ async def main():
97
+ wallet = os.environ["VIPER_WALLET"].lower()
98
+ client = ViperWSClient.from_env(
99
+ on_event=lambda f: print(f["channel"], f.get("event")),
100
+ )
101
+ await client.start()
102
+ await client.subscribe("account.state", wallet)
103
+ await asyncio.sleep(30)
104
+ await client.close()
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ ## Using credentials
110
+
111
+ Both clients take credentials two ways, both first-class — pick whichever fits how your process gets its secrets.
112
+
113
+ **From the environment (quickest, and production-correct).** `from_env()` reads `VIPER_API_KEY`, `VIPER_API_SECRET`, `VIPER_HANDLE`, and `VIPER_WALLET`. This is also the right pattern for deployment: containers, CI, and secret managers all inject secrets as env vars, so the same code runs unchanged from laptop to production. Anything passed explicitly overrides the environment:
114
+
115
+ ```python
116
+ viper = ViperRestClient.from_env(handle="override-handle")
117
+ ```
118
+
119
+ **Explicitly (your own secret store).** If your keys live in Vault, AWS Secrets Manager, an HSM, or a config file, fetch them in your code and pass them to the constructor directly — `from_env()` is never required:
120
+
121
+ ```python
122
+ api_key_id, api_secret = my_secret_store.get("viper") # however you fetch them
123
+ viper = ViperRestClient(
124
+ api_key_id=api_key_id,
125
+ api_secret=api_secret,
126
+ handle="your-handle",
127
+ wallet="0x...",
128
+ )
129
+ ```
130
+
131
+ `ViperWSClient` constructs identically.
132
+
133
+ ## Using the REST client
134
+
135
+ `ViperRestClient` is async and instance-based (no global singleton). It covers the core trading surface: execute and executions, orders, account, positions, market data (instruments, price, orderbook), leverage, and limits.
136
+
137
+ ```python
138
+ async with ViperRestClient.from_env() as viper:
139
+ # reads — return the raw dict as-is
140
+ state = await viper.account_state()
141
+ pos = await viper.positions()
142
+ price = await viper.price("BTC")
143
+
144
+ # mutating calls auto-generate an Idempotency-Key and are throttled
145
+ order = await viper.place_order(symbol="BTC", side="buy", size=0.001,
146
+ order_type="limit", price=60000, post_only=True)
147
+ await viper.cancel_order(symbol="BTC", order_id=order["order_ids"][0])
148
+ ```
149
+
150
+ What the client handles for you:
151
+
152
+ - **Signing** — HMAC-SHA256 over the canonical `{timestamp}{method}{path}{body}`, signed over the exact bytes sent. You never construct a signature.
153
+ - **Idempotency** — every mutating call (`execute`, `place_order`, `cancel_*`, `modify_order`, `close_*`, execution lifecycle, `set_leverage`, `update_settings`, `nuke`) auto-generates an `Idempotency-Key` unless you pass one. Reads don't.
154
+ - **Replay spacing** — a per-instance throttle keeps mutating calls ≥ 1.1s apart; reads are never blocked.
155
+ - **Configurable transport** — inject your own `httpx.AsyncClient` via `http_client=...` to set timeouts, proxies, or pools.
156
+
157
+ `nuke()` (cancel all orders + close all positions) requires `confirm=True` with no default — the conscious step the raw API gets from its mandatory `Idempotency-Key`, which the client otherwise fills for you.
158
+
159
+ ### Errors
160
+
161
+ The API error envelope is mapped to typed exceptions, all subclasses of `ViperError`:
162
+
163
+ | Exception | Maps from |
164
+ |---|---|
165
+ | `ViperValidationError` | `validation_error`, `bad_request`, `missing_field`, … (400/422) |
166
+ | `ViperAuthError` | `unauthorized`, `insufficient_scope`, `forbidden`, `tenancy_denied` (401/403) |
167
+ | `ViperConflictError` | `conflict`, `idempotency_mismatch`, `state_transition_forbidden` (409) |
168
+ | `ViperNotFoundError` | `not_found`, `unknown_route`, `scope_not_found` (404) |
169
+ | `ViperRateLimitError` | `rate_limited`, `venue_rate_limit` (429) — carries `retry_after` |
170
+ | `ViperAPIError` | anything else |
171
+
172
+ Every exception carries `.code` (the machine-readable error code), `.status`, and `.payload`, so you can branch precisely — e.g. tell a state `conflict` from an `idempotency_mismatch`:
173
+
174
+ ```python
175
+ from viper import ViperConflictError
176
+
177
+ try:
178
+ await viper.execute(algo="glidemaker", symbol="BTC", side="buy",
179
+ total_size=0.001, params={"strategy": "neutral"})
180
+ except ViperConflictError as e:
181
+ if e.code == "idempotency_mismatch":
182
+ ... # reused key with a different body — a client bug
183
+ ```
184
+
185
+ ## Runnable examples
186
+
187
+ Examples ship inside the package — no extra downloads. List the catalog and
188
+ run one by name or number:
189
+
190
+ ```bash
191
+ viper-examples # list the catalog
192
+ viper-examples stream-account-state # run by name
193
+ viper-examples 01 # ...or by number
194
+ ```
195
+
196
+ Set the env vars the examples read — bash/zsh:
197
+
198
+ ```bash
199
+ export VIPER_API_KEY=vk_...
200
+ export VIPER_API_SECRET=vs_...
201
+ export VIPER_HANDLE=your-handle # optional
202
+ export VIPER_WALLET=0x... # the wallet to trade/stream
203
+ ```
204
+
205
+ …or PowerShell:
206
+
207
+ ```powershell
208
+ $env:VIPER_API_KEY = "vk_..."
209
+ $env:VIPER_API_SECRET = "vs_..."
210
+ $env:VIPER_HANDLE = "your-handle" # optional
211
+ $env:VIPER_WALLET = "0x..." # the wallet to trade/stream
212
+ ```
213
+
214
+ ### Live algo examples
215
+
216
+ Several examples fire a real algo. They place **real orders on mainnet** (there
217
+ is no testnet). Each reads the live BTC mark, sizes to a USD notional (default
218
+ ~$250), discloses exactly what it will do with a short Ctrl-C abort window, fires
219
+ **once**, observes, then cancels.
220
+
221
+ Over the WebSocket command surface:
222
+
223
+ ```bash
224
+ viper-examples start-glidemaker # passive limit
225
+ viper-examples start-pacemaker # TWAP
226
+ ```
227
+
228
+ Over the REST client:
229
+
230
+ ```bash
231
+ viper-examples detect-and-fire-glidemaker # poll a signal, then fire on it
232
+ viper-examples start-ghostsweep # hidden stop
233
+ viper-examples start-flowscale # scaled ladder
234
+ viper-examples start-flowband # floating stealth scale
235
+ viper-examples smart-exit # reduce-only stop on an existing long
236
+ ```
237
+
238
+ Optional knobs:
239
+
240
+ ```bash
241
+ export VIPER_EXAMPLE_USD=250 # target notional (default 250)
242
+ export VIPER_EXAMPLE_OBSERVE_S=10 # seconds to observe before cancel (default 10)
243
+ export VIPER_EXAMPLE_NO_CANCEL=1 # leave the execution running instead of cancelling
244
+ ```
245
+
246
+ The source for each example lives in
247
+ [`src/viper/examples/`](src/viper/examples/).
248
+
249
+ ## What the WebSocket client handles for you
250
+
251
+ The `/v1/ws` stream has a number of behaviors a naive client gets wrong. `ViperWSClient` handles them as a built-in contract:
252
+
253
+ - **Liveness** — transport ping/pong plus a data-staleness watchdog; silent half-open connections are detected and reconnected.
254
+ - **Reconnect with resume** — on drop, it reconnects with exponential backoff and resubscribes every scope carrying its `last_seq` cursor, so you resume exactly where you left off (replay from the per-scope ring buffer).
255
+ - **Resync recovery** — when the server can't satisfy a cursor (`buffer_overflow` / `last_seq_ahead_of_server` / `scope_not_found`), it REST-fetches authoritative current state and resubscribes fresh.
256
+ - **Multi-wallet attribution** — every data frame is routed by `data.wallet`, so one socket can carry many wallets without cross-attribution. (Control markers such as `hydrated` carry no `data.wallet`; route those by `scope_id`.)
257
+ - **Slow hydration** — `account.state` hydration is server-slow (~5s; it gathers balance + HIP-3 collateral across all dexes, then bursts frames). The client does not mistake that for a dead stream, and neither should your application logic.
258
+ - **Terminal conditions** — credential revocation (close `4013`), a handshake auth rejection (HTTP `401`/`403` — revoked/invalid key or insufficient scope), or an exhausted reconnect budget all stop the loop permanently via `on_terminal` rather than reconnect-hammering.
259
+
260
+ ## Callbacks
261
+
262
+ | Callback | Fires on |
263
+ |---|---|
264
+ | `on_event(frame)` | Every classified data frame (the main path) |
265
+ | `on_meta(frame)` | `_meta` frames: welcome + upstream connectivity events |
266
+ | `on_terminal(code)` | Terminal stop. `code` is the WS close code (`4013` = credentials revoked), the handshake HTTP status (`401`/`403`), or `-1` (reconnect budget exhausted) |
267
+ | `on_command_result(frame)` | Subscribe acks / command errors (no correlation id) |
268
+ | `on_raw(frame)` | Optional advanced tap: every frame pre-classification (audit/metrics) |
269
+
270
+ ## License
271
+
272
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,224 @@
1
+ # Viper Execution Python SDK
2
+
3
+ Institutional-grade Python client for the [Viper Execution](https://viperexecution.com) trading API on Hyperliquid.
4
+
5
+ > **Status:** SDK `0.2.x`. Ships a typed async REST client (`ViperRestClient`) and a resilient WebSocket client (`ViperWSClient`). The SDK version is independent of the API version — this is SDK 0.x against API v1.
6
+
7
+ The SDK is a convenience layer over the raw HMAC + REST/WebSocket surface — never required. Every response is returned as a plain `dict`, so you are never boxed out of the raw payload; the typed signatures and `TypedDict` hints are there for editor and type-checker support only.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install viper-execution
13
+ ```
14
+
15
+ Requires Python ≥ 3.10.
16
+
17
+ ## Quickstart (REST)
18
+
19
+ ```python
20
+ import asyncio
21
+ from viper import ViperRestClient
22
+
23
+ async def main():
24
+ # from_env() reads VIPER_API_KEY / VIPER_API_SECRET / VIPER_HANDLE /
25
+ # VIPER_WALLET. Pass anything explicitly to override the environment.
26
+ async with ViperRestClient.from_env() as viper:
27
+ # market data
28
+ btc = (await viper.instrument("BTC"))["instrument"]
29
+ print("BTC mark:", btc["mark_price"])
30
+
31
+ # launch a Glidemaker (idempotency key is auto-generated)
32
+ res = await viper.execute(
33
+ algo="glidemaker", symbol="BTC", side="buy", total_size=0.001,
34
+ params={"strategy": "neutral", "limit_price": 65000},
35
+ )
36
+ print(res["execution_id"], res["status"])
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ ## Quickstart (WebSocket)
42
+
43
+ ```python
44
+ import os
45
+ import asyncio
46
+ from viper import ViperWSClient
47
+
48
+ async def main():
49
+ wallet = os.environ["VIPER_WALLET"].lower()
50
+ client = ViperWSClient.from_env(
51
+ on_event=lambda f: print(f["channel"], f.get("event")),
52
+ )
53
+ await client.start()
54
+ await client.subscribe("account.state", wallet)
55
+ await asyncio.sleep(30)
56
+ await client.close()
57
+
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ## Using credentials
62
+
63
+ Both clients take credentials two ways, both first-class — pick whichever fits how your process gets its secrets.
64
+
65
+ **From the environment (quickest, and production-correct).** `from_env()` reads `VIPER_API_KEY`, `VIPER_API_SECRET`, `VIPER_HANDLE`, and `VIPER_WALLET`. This is also the right pattern for deployment: containers, CI, and secret managers all inject secrets as env vars, so the same code runs unchanged from laptop to production. Anything passed explicitly overrides the environment:
66
+
67
+ ```python
68
+ viper = ViperRestClient.from_env(handle="override-handle")
69
+ ```
70
+
71
+ **Explicitly (your own secret store).** If your keys live in Vault, AWS Secrets Manager, an HSM, or a config file, fetch them in your code and pass them to the constructor directly — `from_env()` is never required:
72
+
73
+ ```python
74
+ api_key_id, api_secret = my_secret_store.get("viper") # however you fetch them
75
+ viper = ViperRestClient(
76
+ api_key_id=api_key_id,
77
+ api_secret=api_secret,
78
+ handle="your-handle",
79
+ wallet="0x...",
80
+ )
81
+ ```
82
+
83
+ `ViperWSClient` constructs identically.
84
+
85
+ ## Using the REST client
86
+
87
+ `ViperRestClient` is async and instance-based (no global singleton). It covers the core trading surface: execute and executions, orders, account, positions, market data (instruments, price, orderbook), leverage, and limits.
88
+
89
+ ```python
90
+ async with ViperRestClient.from_env() as viper:
91
+ # reads — return the raw dict as-is
92
+ state = await viper.account_state()
93
+ pos = await viper.positions()
94
+ price = await viper.price("BTC")
95
+
96
+ # mutating calls auto-generate an Idempotency-Key and are throttled
97
+ order = await viper.place_order(symbol="BTC", side="buy", size=0.001,
98
+ order_type="limit", price=60000, post_only=True)
99
+ await viper.cancel_order(symbol="BTC", order_id=order["order_ids"][0])
100
+ ```
101
+
102
+ What the client handles for you:
103
+
104
+ - **Signing** — HMAC-SHA256 over the canonical `{timestamp}{method}{path}{body}`, signed over the exact bytes sent. You never construct a signature.
105
+ - **Idempotency** — every mutating call (`execute`, `place_order`, `cancel_*`, `modify_order`, `close_*`, execution lifecycle, `set_leverage`, `update_settings`, `nuke`) auto-generates an `Idempotency-Key` unless you pass one. Reads don't.
106
+ - **Replay spacing** — a per-instance throttle keeps mutating calls ≥ 1.1s apart; reads are never blocked.
107
+ - **Configurable transport** — inject your own `httpx.AsyncClient` via `http_client=...` to set timeouts, proxies, or pools.
108
+
109
+ `nuke()` (cancel all orders + close all positions) requires `confirm=True` with no default — the conscious step the raw API gets from its mandatory `Idempotency-Key`, which the client otherwise fills for you.
110
+
111
+ ### Errors
112
+
113
+ The API error envelope is mapped to typed exceptions, all subclasses of `ViperError`:
114
+
115
+ | Exception | Maps from |
116
+ |---|---|
117
+ | `ViperValidationError` | `validation_error`, `bad_request`, `missing_field`, … (400/422) |
118
+ | `ViperAuthError` | `unauthorized`, `insufficient_scope`, `forbidden`, `tenancy_denied` (401/403) |
119
+ | `ViperConflictError` | `conflict`, `idempotency_mismatch`, `state_transition_forbidden` (409) |
120
+ | `ViperNotFoundError` | `not_found`, `unknown_route`, `scope_not_found` (404) |
121
+ | `ViperRateLimitError` | `rate_limited`, `venue_rate_limit` (429) — carries `retry_after` |
122
+ | `ViperAPIError` | anything else |
123
+
124
+ Every exception carries `.code` (the machine-readable error code), `.status`, and `.payload`, so you can branch precisely — e.g. tell a state `conflict` from an `idempotency_mismatch`:
125
+
126
+ ```python
127
+ from viper import ViperConflictError
128
+
129
+ try:
130
+ await viper.execute(algo="glidemaker", symbol="BTC", side="buy",
131
+ total_size=0.001, params={"strategy": "neutral"})
132
+ except ViperConflictError as e:
133
+ if e.code == "idempotency_mismatch":
134
+ ... # reused key with a different body — a client bug
135
+ ```
136
+
137
+ ## Runnable examples
138
+
139
+ Examples ship inside the package — no extra downloads. List the catalog and
140
+ run one by name or number:
141
+
142
+ ```bash
143
+ viper-examples # list the catalog
144
+ viper-examples stream-account-state # run by name
145
+ viper-examples 01 # ...or by number
146
+ ```
147
+
148
+ Set the env vars the examples read — bash/zsh:
149
+
150
+ ```bash
151
+ export VIPER_API_KEY=vk_...
152
+ export VIPER_API_SECRET=vs_...
153
+ export VIPER_HANDLE=your-handle # optional
154
+ export VIPER_WALLET=0x... # the wallet to trade/stream
155
+ ```
156
+
157
+ …or PowerShell:
158
+
159
+ ```powershell
160
+ $env:VIPER_API_KEY = "vk_..."
161
+ $env:VIPER_API_SECRET = "vs_..."
162
+ $env:VIPER_HANDLE = "your-handle" # optional
163
+ $env:VIPER_WALLET = "0x..." # the wallet to trade/stream
164
+ ```
165
+
166
+ ### Live algo examples
167
+
168
+ Several examples fire a real algo. They place **real orders on mainnet** (there
169
+ is no testnet). Each reads the live BTC mark, sizes to a USD notional (default
170
+ ~$250), discloses exactly what it will do with a short Ctrl-C abort window, fires
171
+ **once**, observes, then cancels.
172
+
173
+ Over the WebSocket command surface:
174
+
175
+ ```bash
176
+ viper-examples start-glidemaker # passive limit
177
+ viper-examples start-pacemaker # TWAP
178
+ ```
179
+
180
+ Over the REST client:
181
+
182
+ ```bash
183
+ viper-examples detect-and-fire-glidemaker # poll a signal, then fire on it
184
+ viper-examples start-ghostsweep # hidden stop
185
+ viper-examples start-flowscale # scaled ladder
186
+ viper-examples start-flowband # floating stealth scale
187
+ viper-examples smart-exit # reduce-only stop on an existing long
188
+ ```
189
+
190
+ Optional knobs:
191
+
192
+ ```bash
193
+ export VIPER_EXAMPLE_USD=250 # target notional (default 250)
194
+ export VIPER_EXAMPLE_OBSERVE_S=10 # seconds to observe before cancel (default 10)
195
+ export VIPER_EXAMPLE_NO_CANCEL=1 # leave the execution running instead of cancelling
196
+ ```
197
+
198
+ The source for each example lives in
199
+ [`src/viper/examples/`](src/viper/examples/).
200
+
201
+ ## What the WebSocket client handles for you
202
+
203
+ The `/v1/ws` stream has a number of behaviors a naive client gets wrong. `ViperWSClient` handles them as a built-in contract:
204
+
205
+ - **Liveness** — transport ping/pong plus a data-staleness watchdog; silent half-open connections are detected and reconnected.
206
+ - **Reconnect with resume** — on drop, it reconnects with exponential backoff and resubscribes every scope carrying its `last_seq` cursor, so you resume exactly where you left off (replay from the per-scope ring buffer).
207
+ - **Resync recovery** — when the server can't satisfy a cursor (`buffer_overflow` / `last_seq_ahead_of_server` / `scope_not_found`), it REST-fetches authoritative current state and resubscribes fresh.
208
+ - **Multi-wallet attribution** — every data frame is routed by `data.wallet`, so one socket can carry many wallets without cross-attribution. (Control markers such as `hydrated` carry no `data.wallet`; route those by `scope_id`.)
209
+ - **Slow hydration** — `account.state` hydration is server-slow (~5s; it gathers balance + HIP-3 collateral across all dexes, then bursts frames). The client does not mistake that for a dead stream, and neither should your application logic.
210
+ - **Terminal conditions** — credential revocation (close `4013`), a handshake auth rejection (HTTP `401`/`403` — revoked/invalid key or insufficient scope), or an exhausted reconnect budget all stop the loop permanently via `on_terminal` rather than reconnect-hammering.
211
+
212
+ ## Callbacks
213
+
214
+ | Callback | Fires on |
215
+ |---|---|
216
+ | `on_event(frame)` | Every classified data frame (the main path) |
217
+ | `on_meta(frame)` | `_meta` frames: welcome + upstream connectivity events |
218
+ | `on_terminal(code)` | Terminal stop. `code` is the WS close code (`4013` = credentials revoked), the handshake HTTP status (`401`/`403`), or `-1` (reconnect budget exhausted) |
219
+ | `on_command_result(frame)` | Subscribe acks / command errors (no correlation id) |
220
+ | `on_raw(frame)` | Optional advanced tap: every frame pre-classification (audit/metrics) |
221
+
222
+ ## License
223
+
224
+ MIT — see [LICENSE](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viper-execution"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Institutional-grade Python SDK for the Viper Execution trading API on Hyperliquid."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -12,7 +12,7 @@ license = { file = "LICENSE" }
12
12
  authors = [{ name = "Viper Execution" }]
13
13
  keywords = ["viper", "hyperliquid", "trading", "algotrading", "websocket"]
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  "Intended Audience :: Financial and Insurance Industry",
18
18
  "Programming Language :: Python :: 3",
@@ -38,7 +38,7 @@ _ENV_VARS = [
38
38
  ]
39
39
 
40
40
 
41
- def _discover() -> List[Tuple[str, str, int, str]]:
41
+ def _discover() -> List[Tuple[str, str, int, str, str]]:
42
42
  """Return [(slug, module_name, order, description), ...] sorted by order."""
43
43
  found = []
44
44
  for info in pkgutil.iter_modules(__path__):
@@ -49,13 +49,15 @@ def _discover() -> List[Tuple[str, str, int, str]]:
49
49
  if not hasattr(mod, "main") or not hasattr(mod, "DESCRIPTION"):
50
50
  continue
51
51
  order = int(getattr(mod, "ORDER", 999))
52
+ kind = str(getattr(mod, "KIND", "rest")).lower()
53
+ section = str(getattr(mod, "SECTION", "Other"))
52
54
  slug = name.replace("_", "-")
53
- found.append((slug, name, order, str(mod.DESCRIPTION)))
55
+ found.append((slug, name, order, str(mod.DESCRIPTION), kind, section))
54
56
  found.sort(key=lambda r: (r[2], r[0]))
55
57
  return found
56
58
 
57
59
 
58
- def list_examples() -> List[Tuple[str, str, int, str]]:
60
+ def list_examples() -> List[Tuple[str, str, int, str, str]]:
59
61
  """Public helper: the discovered example catalog."""
60
62
  return _discover()
61
63
 
@@ -63,7 +65,7 @@ def list_examples() -> List[Tuple[str, str, int, str]]:
63
65
  def _resolve(token: str, catalog) -> str | None:
64
66
  """Map a user token (slug, module name, or order number) to a module name."""
65
67
  t = token.strip().lower().replace("_", "-")
66
- for slug, mod_name, order, _desc in catalog:
68
+ for slug, mod_name, order, _desc, _kind, _section in catalog:
67
69
  if t == slug or t == mod_name.replace("_", "-"):
68
70
  return mod_name
69
71
  if t.lstrip("0") == str(order) or t == f"{order:02d}":
@@ -88,6 +90,18 @@ def _print_env_setup() -> None:
88
90
  print(f' export {name}="{val}"')
89
91
 
90
92
 
93
+ # Section display order for the catalog listing. Sections not listed here are
94
+ # appended afterward (alphabetically) under their own header.
95
+ _SECTION_ORDER = [
96
+ "Getting Started",
97
+ "Algorithms",
98
+ "Account & Market Data",
99
+ "Monitors",
100
+ "Streaming (WebSocket reads)",
101
+ "Trading over WebSocket (Tier-3 writes)",
102
+ ]
103
+
104
+
91
105
  def _print_catalog(catalog) -> None:
92
106
  print("Viper SDK — Examples")
93
107
  print("====================")
@@ -96,10 +110,19 @@ def _print_catalog(catalog) -> None:
96
110
  if not catalog:
97
111
  print(" (no examples found)")
98
112
  else:
99
- print("Available examples:")
100
113
  width = max(len(s) for s, *_ in catalog)
101
- for slug, _mod, order, desc in catalog:
102
- print(f" {order:02d} {slug.ljust(width)} {desc}")
114
+ # Group by section, ordered by _SECTION_ORDER (others appended A-Z).
115
+ by_section = {}
116
+ for row in catalog:
117
+ by_section.setdefault(row[5], []).append(row)
118
+ ordered = [s for s in _SECTION_ORDER if s in by_section]
119
+ ordered += sorted(s for s in by_section if s not in _SECTION_ORDER)
120
+ for sec in ordered:
121
+ print(f"\n{sec}")
122
+ print(" " + "-" * len(sec))
123
+ for slug, _mod, order, desc, kind, _section in by_section[sec]:
124
+ tag = "[WS] " if kind == "ws" else "[REST]"
125
+ print(f" {order:02d} {tag} {slug.ljust(width)} {desc}")
103
126
  print()
104
127
  _print_env_setup()
105
128
  print()