viper-execution 0.2.0__tar.gz → 0.2.2__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 (27) hide show
  1. viper_execution-0.2.2/.github/workflows/release.yml +74 -0
  2. {viper_execution-0.2.0 → viper_execution-0.2.2}/PKG-INFO +1 -1
  3. {viper_execution-0.2.0 → viper_execution-0.2.2}/pyproject.toml +1 -1
  4. viper_execution-0.2.2/src/viper/examples/_algo_rest_common.py +123 -0
  5. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/detect_and_fire_glidemaker.py +10 -7
  6. viper_execution-0.2.2/src/viper/examples/smart_exit.py +85 -0
  7. viper_execution-0.2.2/src/viper/examples/start_flowband.py +32 -0
  8. viper_execution-0.2.2/src/viper/examples/start_flowscale.py +32 -0
  9. viper_execution-0.2.2/src/viper/examples/start_ghostsweep.py +32 -0
  10. viper_execution-0.2.0/.github/workflows/release.yml +0 -43
  11. {viper_execution-0.2.0 → viper_execution-0.2.2}/.gitignore +0 -0
  12. {viper_execution-0.2.0 → viper_execution-0.2.2}/LICENSE +0 -0
  13. {viper_execution-0.2.0 → viper_execution-0.2.2}/README.md +0 -0
  14. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/__init__.py +0 -0
  15. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/__init__.py +0 -0
  16. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/__main__.py +0 -0
  17. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/_algo_common.py +0 -0
  18. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/start_glidemaker.py +0 -0
  19. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/start_pacemaker.py +0 -0
  20. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/examples/stream_account_state.py +0 -0
  21. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/exceptions.py +0 -0
  22. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/py.typed +0 -0
  23. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/rest.py +0 -0
  24. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/rest_types.py +0 -0
  25. {viper_execution-0.2.0 → viper_execution-0.2.2}/src/viper/ws.py +0 -0
  26. {viper_execution-0.2.0 → viper_execution-0.2.2}/tests/test_handshake_status.py +0 -0
  27. {viper_execution-0.2.0 → viper_execution-0.2.2}/tests/test_resync_map.py +0 -0
@@ -0,0 +1,74 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ # full history + main, so the guard can compare the tag to main's tip
16
+ fetch-depth: 0
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ # Release guard. The publish job (needs: build) runs only if this passes,
22
+ # so a tag failing either check never reaches PyPI, where versions are
23
+ # immutable. Two checks:
24
+ # 1. Tag name matches the pyproject version.
25
+ # 2. Tag commit is the tip of main. A version comparison alone is
26
+ # insufficient: a tag can reference a commit behind main while the
27
+ # version string is unchanged, so commit identity is compared directly.
28
+ - name: Validate release tag
29
+ run: |
30
+ set -euo pipefail
31
+ TAG="${GITHUB_REF_NAME}"
32
+ VER="v$(grep -E '^version *= *"' pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
33
+ echo "tag=$TAG pyproject=$VER"
34
+ if [ "$TAG" != "$VER" ]; then
35
+ echo "::error::Tag $TAG does not match pyproject version $VER."
36
+ exit 1
37
+ fi
38
+ git fetch --quiet origin main
39
+ TAG_SHA="$(git rev-parse HEAD)"
40
+ MAIN_SHA="$(git rev-parse origin/main)"
41
+ echo "tag_sha=$TAG_SHA main_sha=$MAIN_SHA"
42
+ if [ "$TAG_SHA" != "$MAIN_SHA" ]; then
43
+ echo "::error::Tag commit is not the tip of main. Re-point the tag to main before releasing."
44
+ exit 1
45
+ fi
46
+ echo "Release tag validated: $TAG at $TAG_SHA"
47
+
48
+ - name: Build sdist and wheel
49
+ run: |
50
+ python -m pip install --upgrade build
51
+ python -m build
52
+ - name: Verify metadata
53
+ run: |
54
+ python -m pip install --upgrade twine
55
+ twine check dist/*
56
+ - uses: actions/upload-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ publish:
62
+ name: Publish to PyPI
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ environment: pypi
66
+ permissions:
67
+ id-token: write
68
+ steps:
69
+ - uses: actions/download-artifact@v4
70
+ with:
71
+ name: dist
72
+ path: dist/
73
+ - name: Publish
74
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viper-execution
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Institutional-grade Python SDK for the Viper Execution trading API on Hyperliquid.
5
5
  Project-URL: Homepage, https://viperexecution.com
6
6
  Project-URL: Documentation, https://docs.viperexecution.com
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viper-execution"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -0,0 +1,123 @@
1
+ """
2
+ Shared REST launch flow for the price-level algo examples (GhostSweep,
3
+ FlowScale, FlowBand). NOT discovered as an example itself (leading underscore).
4
+
5
+ run_buy_algo does, in order:
6
+ 1. Read live BTC mark + sizing fields via rest.instrument("BTC").
7
+ 2. Size to a USD notional (default $250), floored to the instrument's lot.
8
+ 3. Build the algo's params from the live mark (per-algo callback).
9
+ 4. Disclose exactly what will be sent, with a Ctrl-C abort window (nothing
10
+ sent yet).
11
+ 5. Fire EXACTLY ONCE via rest.execute(...) — one idempotency key up front.
12
+ 6. Poll rest.execution(id) for status, then cancel unless told otherwise.
13
+
14
+ These place REAL orders on mainnet. There is no testnet. The default params
15
+ arm each algo away from the market (a few % out), so within the short observe
16
+ window they rest rather than fill — then get cancelled.
17
+
18
+ Env: VIPER_API_KEY, VIPER_API_SECRET, VIPER_HANDLE, VIPER_WALLET (required);
19
+ VIPER_EXAMPLE_USD (250), VIPER_EXAMPLE_ABORT_S (3),
20
+ VIPER_EXAMPLE_OBSERVE_S (10), VIPER_EXAMPLE_POLL_S (2.0),
21
+ VIPER_EXAMPLE_NO_CANCEL (set to leave it running).
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import math
27
+ import uuid
28
+ import asyncio
29
+ from typing import Callable, Optional
30
+
31
+ from viper import ViperRestClient, ViperError
32
+
33
+ SYMBOL = "BTC"
34
+ USD = float(os.environ.get("VIPER_EXAMPLE_USD", "250"))
35
+ ABORT_S = float(os.environ.get("VIPER_EXAMPLE_ABORT_S", "3"))
36
+ OBSERVE_S = float(os.environ.get("VIPER_EXAMPLE_OBSERVE_S", "10"))
37
+ POLL_S = float(os.environ.get("VIPER_EXAMPLE_POLL_S", "2.0"))
38
+ NO_CANCEL = bool(os.environ.get("VIPER_EXAMPLE_NO_CANCEL"))
39
+
40
+
41
+ def require_env() -> None:
42
+ missing = [k for k in ("VIPER_API_KEY", "VIPER_API_SECRET") if not os.environ.get(k)]
43
+ if missing:
44
+ raise SystemExit(f"# missing required env: {', '.join(missing)}")
45
+ if not os.environ.get("VIPER_WALLET"):
46
+ print("# note: VIPER_WALLET not set — the resolved wallet comes from your handle.")
47
+
48
+
49
+ def compute_size(usd: float, mark: float, sz_decimals: int, min_size: float) -> float:
50
+ """USD notional -> base size, floored to the instrument's lot, min-clamped."""
51
+ q = 10 ** int(sz_decimals)
52
+ sized = math.floor((usd / mark) * q) / q
53
+ if min_size and sized < min_size:
54
+ sized = min_size
55
+ return sized
56
+
57
+
58
+ async def instrument_mark(rest: ViperRestClient) -> tuple:
59
+ """Return (mark, sz_decimals, min_order_value_size) for BTC."""
60
+ rec = (await rest.instrument(SYMBOL))["instrument"]
61
+ return (float(rec["mark_price"]), int(rec.get("sz_decimals", 5)),
62
+ float(rec.get("min_order_value_size") or 0.0))
63
+
64
+
65
+ async def poll_and_cancel(rest: ViperRestClient, exec_id: str, label: str) -> None:
66
+ """Poll execution status for the observe window, then cancel (unless
67
+ VIPER_EXAMPLE_NO_CANCEL). Status/fill live under the `state` object."""
68
+ deadline = asyncio.get_event_loop().time() + OBSERVE_S
69
+ while asyncio.get_event_loop().time() < deadline:
70
+ await asyncio.sleep(max(2.0, POLL_S))
71
+ try:
72
+ st = await rest.execution(exec_id)
73
+ except ViperError as e:
74
+ print(f"# status poll failed: {type(e).__name__} code={e.code}")
75
+ break
76
+ state = st.get("state") or {}
77
+ print(f"# status={state.get('status')} "
78
+ f"filled={state.get('filled_size')}/{state.get('total_size')} "
79
+ f"({state.get('filled_pct')}%)")
80
+ if NO_CANCEL:
81
+ print(f"# VIPER_EXAMPLE_NO_CANCEL set — leaving {exec_id} running.")
82
+ else:
83
+ try:
84
+ c = await rest.cancel_execution(exec_id)
85
+ print(f"# {label}: cancel -> {c.get('status') or c.get('result') or 'ok'}")
86
+ except ViperError as e:
87
+ print(f"# cancel failed: {type(e).__name__} code={e.code}")
88
+
89
+
90
+ async def run_buy_algo(*, algo: str, build_params: Callable[[float], dict],
91
+ label: str) -> None:
92
+ """Size off live mark, disclose + abort window, fire one buy via REST, poll,
93
+ cancel. `build_params(mark)` returns the per-algo params dict."""
94
+ require_env()
95
+ rest = ViperRestClient.from_env()
96
+ async with rest:
97
+ mark, sz_decimals, min_size = await instrument_mark(rest)
98
+ size = compute_size(USD, mark, sz_decimals, min_size)
99
+ params = build_params(mark)
100
+
101
+ print(f"# {label}: LIVE buy {size} {SYMBOL} (~${USD:.0f} at mark {mark}) "
102
+ f"on MAINNET — this places a real order.")
103
+ print(f"# params={params}")
104
+ print(f"# Ctrl-C within {ABORT_S:.0f}s to abort (nothing sent yet)...")
105
+ try:
106
+ await asyncio.sleep(ABORT_S)
107
+ except (KeyboardInterrupt, asyncio.CancelledError):
108
+ print("\n# aborted — nothing was sent.")
109
+ return
110
+
111
+ idem = uuid.uuid4().hex
112
+ try:
113
+ res = await rest.execute(algo=algo, symbol=SYMBOL, side="buy",
114
+ total_size=size, params=params,
115
+ idempotency_key=idem)
116
+ except ViperError as e:
117
+ print(f"# execute failed: {type(e).__name__} code={e.code} status={e.status}")
118
+ return
119
+
120
+ exec_id = res.get("execution_id") or res.get("id")
121
+ print(f"# {label}: launched execution_id={exec_id} status={res.get('status')}")
122
+ if exec_id:
123
+ await poll_and_cancel(rest, exec_id, label)
@@ -101,11 +101,12 @@ async def main():
101
101
  _require_env()
102
102
  rest = ViperRestClient.from_env()
103
103
  async with rest:
104
- # 1) size off the live instrument
105
- inst = await rest.instrument(SYMBOL)
106
- mark = float(inst["mark_price"])
107
- size = _compute_size(USD, mark, inst.get("sz_decimals", 5),
108
- float(inst.get("min_order_value_size") or 0))
104
+ # 1) size off the live instrument. GET /v1/instruments/{symbol} wraps the
105
+ # record under "instrument" (alongside a validation_example).
106
+ rec = (await rest.instrument(SYMBOL))["instrument"]
107
+ mark = float(rec["mark_price"])
108
+ size = _compute_size(USD, mark, rec.get("sz_decimals", 5),
109
+ float(rec.get("min_order_value_size") or 0))
109
110
 
110
111
  # 2) watch for the signal
111
112
  if not await _detect_signal(rest):
@@ -149,8 +150,10 @@ async def main():
149
150
  except ViperError as e:
150
151
  print(f"# status poll failed: {type(e).__name__} code={e.code}")
151
152
  break
152
- print(f"# execution status={st.get('status')} "
153
- f"filled={st.get('filled_size')}/{st.get('total_size')}")
153
+ state = st.get("state") or {}
154
+ print(f"# execution status={state.get('status')} "
155
+ f"filled={state.get('filled_size')}/{state.get('total_size')} "
156
+ f"({state.get('filled_pct')}%) maker={state.get('maker_fill_size')}")
154
157
  if NO_CANCEL:
155
158
  print(f"# VIPER_EXAMPLE_NO_CANCEL set — leaving {exec_id} running.")
156
159
  else:
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fire Smart Exit (conditional reduce-only exit) on an existing BTC long — REST,
4
+ live mainnet.
5
+
6
+ Smart Exit is a reduce-only stop: for a SELL exit it fires when ask < limit_price.
7
+ This example finds your existing BTC long, arms a sell-stop 5% BELOW mark (so it
8
+ rests as protection and does NOT fire within the observe window), then cancels.
9
+ reduce_only is forced by the algo — the request does not declare it.
10
+
11
+ Smart Exit exits a position; it does not open one. If you have no BTC long, the
12
+ example tells you and exits without firing. Open a small long (~$250) first, then
13
+ re-run. There is no testnet.
14
+
15
+ Env: VIPER_API_KEY, VIPER_API_SECRET, VIPER_HANDLE, VIPER_WALLET (+ optional
16
+ VIPER_EXAMPLE_OBSERVE_S / _NO_CANCEL). Run:
17
+ viper-examples smart-exit
18
+ """
19
+ import uuid
20
+ import asyncio
21
+
22
+ from viper import ViperRestClient, ViperError
23
+ from viper.examples._algo_rest_common import (
24
+ require_env, instrument_mark, poll_and_cancel, ABORT_S, SYMBOL,
25
+ )
26
+
27
+ ORDER = 8
28
+ DESCRIPTION = "Arm Smart Exit (reduce-only stop) on an existing BTC long over REST — live, auto-cancels."
29
+
30
+
31
+ def _find_long(positions: dict):
32
+ """Return the signed size of the BTC long, or None. size>0 is long."""
33
+ for p in (positions or {}).get("items", []):
34
+ if str(p.get("symbol", "")).upper() == SYMBOL:
35
+ size = float(p.get("size") or 0)
36
+ if size > 0:
37
+ return size
38
+ return None
39
+
40
+
41
+ async def main():
42
+ require_env()
43
+ rest = ViperRestClient.from_env()
44
+ async with rest:
45
+ long_size = _find_long(await rest.positions())
46
+ if not long_size:
47
+ print(f"# Smart Exit exits an existing {SYMBOL} long; none found.")
48
+ print(f"# Open a small {SYMBOL} long (~$250) first, then re-run.")
49
+ return
50
+
51
+ mark, _, _ = await instrument_mark(rest)
52
+ # SELL exit fires when ask < limit_price; 5% below mark -> arms as a
53
+ # stop, won't fire now.
54
+ limit_price = round(mark * 0.95)
55
+ params = {"strategy": "neutral", "limit_price": limit_price}
56
+
57
+ print(f"# Smart Exit: LIVE reduce-only sell-stop on {long_size} {SYMBOL} "
58
+ f"long — limit {limit_price} (~5% below mark {mark}).")
59
+ print(f"# params={params}")
60
+ print(f"# Ctrl-C within {ABORT_S:.0f}s to abort (nothing sent yet)...")
61
+ try:
62
+ await asyncio.sleep(ABORT_S)
63
+ except (KeyboardInterrupt, asyncio.CancelledError):
64
+ print("\n# aborted — nothing was sent.")
65
+ return
66
+
67
+ idem = uuid.uuid4().hex
68
+ try:
69
+ # No reduce_only kwarg: Smart Exit forces it at the algo layer, and
70
+ # the request schema rejects a declared reduce_only.
71
+ res = await rest.execute(algo="smart_exit", symbol=SYMBOL, side="sell",
72
+ total_size=long_size, params=params,
73
+ idempotency_key=idem)
74
+ except ViperError as e:
75
+ print(f"# execute failed: {type(e).__name__} code={e.code} status={e.status}")
76
+ return
77
+
78
+ exec_id = res.get("execution_id") or res.get("id")
79
+ print(f"# Smart Exit: launched execution_id={exec_id} status={res.get('status')}")
80
+ if exec_id:
81
+ await poll_and_cancel(rest, exec_id, "Smart Exit")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ asyncio.run(main())
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fire FlowBand (floating stealth scale) on BTC over REST — live mainnet.
4
+
5
+ FlowBand floats a stealth scale of levels across a percentage band. This example
6
+ uses a 0.5%-2% band (the required range params); for a BUY the levels rest below
7
+ market and won't fully fill in the short observe window, then get cancelled.
8
+ Places real orders (~$250 total). There is no testnet.
9
+
10
+ Env: VIPER_API_KEY, VIPER_API_SECRET, VIPER_HANDLE, VIPER_WALLET (+ optional
11
+ VIPER_EXAMPLE_USD / _OBSERVE_S / _NO_CANCEL). Run:
12
+ viper-examples start-flowband
13
+ """
14
+ import asyncio
15
+
16
+ from viper.examples._algo_rest_common import run_buy_algo
17
+
18
+ ORDER = 7
19
+ DESCRIPTION = "Fire FlowBand (floating stealth scale) on BTC over REST — live ~$250, auto-cancels."
20
+
21
+
22
+ def _params(mark: float) -> dict:
23
+ # required percentage band; num_levels/flow_band/bias take server defaults.
24
+ return {"range_from_pct": 0.5, "range_to_pct": 2.0}
25
+
26
+
27
+ async def main():
28
+ await run_buy_algo(algo="flowband", build_params=_params, label="FlowBand")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ asyncio.run(main())
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fire FlowScale (scaled ladder) on BTC over REST — live mainnet.
4
+
5
+ FlowScale lays a scaled ladder of clips across a percentage band relative to the
6
+ market. This example uses a 0.5%-2% band with the minimum 5 clips; for a BUY the
7
+ ladder rests below market and won't fully fill in the short observe window, then
8
+ gets cancelled. Places real orders (~$250 total). There is no testnet.
9
+
10
+ Env: VIPER_API_KEY, VIPER_API_SECRET, VIPER_HANDLE, VIPER_WALLET (+ optional
11
+ VIPER_EXAMPLE_USD / _OBSERVE_S / _NO_CANCEL). Run:
12
+ viper-examples start-flowscale
13
+ """
14
+ import asyncio
15
+
16
+ from viper.examples._algo_rest_common import run_buy_algo
17
+
18
+ ORDER = 6
19
+ DESCRIPTION = "Fire FlowScale (scaled ladder) on BTC over REST — live ~$250, auto-cancels."
20
+
21
+
22
+ def _params(mark: float) -> dict:
23
+ # scaled ladder 0.5%-2% out, 5 clips (the minimum). pct band -> no absolute price.
24
+ return {"range_from_pct": 0.5, "range_to_pct": 2.0, "num_clips": 5}
25
+
26
+
27
+ async def main():
28
+ await run_buy_algo(algo="flowscale", build_params=_params, label="FlowScale")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ asyncio.run(main())
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fire GhostSweep (hidden stop/take) on BTC over REST — live mainnet.
4
+
5
+ GhostSweep arms a hidden sweep at a trigger price. For a BUY it activates when
6
+ ask <= trigger_price, so this example sets trigger 5% BELOW mark: the sweep arms
7
+ as a hidden buy-stop on a dip and does NOT fire within the observe window, then
8
+ gets cancelled. Places a real order (~$250). There is no testnet.
9
+
10
+ Env: VIPER_API_KEY, VIPER_API_SECRET, VIPER_HANDLE, VIPER_WALLET (+ optional
11
+ VIPER_EXAMPLE_USD / _OBSERVE_S / _NO_CANCEL). Run:
12
+ viper-examples start-ghostsweep
13
+ """
14
+ import asyncio
15
+
16
+ from viper.examples._algo_rest_common import run_buy_algo
17
+
18
+ ORDER = 5
19
+ DESCRIPTION = "Fire GhostSweep (hidden stop) on BTC over REST — live ~$250, auto-cancels."
20
+
21
+
22
+ def _params(mark: float) -> dict:
23
+ # BUY sweep arms when ask <= trigger_price; 5% below mark -> arms, won't fire now.
24
+ return {"strategy": "neutral", "trigger_price": round(mark * 0.95)}
25
+
26
+
27
+ async def main():
28
+ await run_buy_algo(algo="ghostsweep", build_params=_params, label="GhostSweep")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ asyncio.run(main())
@@ -1,43 +0,0 @@
1
- name: release
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- jobs:
9
- build:
10
- name: Build distribution
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: actions/setup-python@v5
15
- with:
16
- python-version: "3.12"
17
- - name: Build sdist and wheel
18
- run: |
19
- python -m pip install --upgrade build
20
- python -m build
21
- - name: Verify metadata
22
- run: |
23
- python -m pip install --upgrade twine
24
- twine check dist/*
25
- - uses: actions/upload-artifact@v4
26
- with:
27
- name: dist
28
- path: dist/
29
-
30
- publish:
31
- name: Publish to PyPI
32
- needs: build
33
- runs-on: ubuntu-latest
34
- environment: pypi
35
- permissions:
36
- id-token: write
37
- steps:
38
- - uses: actions/download-artifact@v4
39
- with:
40
- name: dist
41
- path: dist/
42
- - name: Publish
43
- uses: pypa/gh-action-pypi-publish@release/v1
File without changes