orcalayer 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.
- orcalayer-0.1.0/.gitignore +8 -0
- orcalayer-0.1.0/CHANGELOG.md +10 -0
- orcalayer-0.1.0/LICENSE +21 -0
- orcalayer-0.1.0/PKG-INFO +88 -0
- orcalayer-0.1.0/README.md +63 -0
- orcalayer-0.1.0/pyproject.toml +34 -0
- orcalayer-0.1.0/src/orcalayer/__init__.py +24 -0
- orcalayer-0.1.0/src/orcalayer/client.py +303 -0
- orcalayer-0.1.0/src/orcalayer/errors.py +67 -0
- orcalayer-0.1.0/tests/test_retry.py +63 -0
- orcalayer-0.1.0/tests/test_smoke.py +78 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-06-12
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `OrcaLayer` client with anonymous and Premium (Bearer key) access tiers.
|
|
8
|
+
- Methods: `leaderboard`, `wallet_overview`, `wallet_positions`, `markets`, `whale_alerts` (Premium).
|
|
9
|
+
- Automatic retry on HTTP 429 (honours `Retry-After`, exponential backoff) and single retry on 502.
|
|
10
|
+
- Typed exceptions: `PremiumRequiredError`, `AuthenticationError`, `RateLimitError`, `ServerError`.
|
orcalayer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OrcaLayer
|
|
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.
|
orcalayer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orcalayer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the OrcaLayer API — Polymarket whale and market analytics
|
|
5
|
+
Project-URL: Homepage, https://orcalayer.com
|
|
6
|
+
Project-URL: Documentation, https://orcalayer.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/orcalayer/orcalayer-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/orcalayer/orcalayer-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: OrcaLayer
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: analytics,api-client,polymarket,prediction-markets,trading,whales
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.24
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# orcalayer
|
|
27
|
+
|
|
28
|
+
Official Python client for the [OrcaLayer API](https://orcalayer.com) — Polymarket whale and market analytics: wallet P&L, open positions, smart-whale leaderboard, market search and real-time whale alerts.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install orcalayer
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.10+. Single dependency: `httpx`.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
Three lines to a first result — no API key needed for public endpoints:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from orcalayer import OrcaLayer
|
|
44
|
+
|
|
45
|
+
ol = OrcaLayer()
|
|
46
|
+
print(ol.leaderboard(limit=5))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Anonymous access is limited to 200 requests/min per IP, and wallet endpoints (`wallet_overview`, `wallet_positions`) additionally to 300 requests/day per IP. With a Premium API key ([get one here](https://orcalayer.com/pricing)) you get 600 req/min, no daily cap, and access to Premium endpoints such as whale alerts:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
ol = OrcaLayer(api_key="ol_your_key")
|
|
53
|
+
alerts = ol.whale_alerts(minutes=30, min_usd=1000)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Methods
|
|
57
|
+
|
|
58
|
+
| Method | Endpoint | Access |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `leaderboard(sort, category, limit, ...)` | Smart-whale leaderboard with server-side filters | Public |
|
|
61
|
+
| `wallet_overview(address)` | Wallet profile + trading stats (accepts 0x address or nickname) | Public |
|
|
62
|
+
| `wallet_positions(address, limit, offset)` | Open positions, sorted by current value | Public |
|
|
63
|
+
| `markets(q, category, min_volume, ...)` | Market search (accepts free text or a Polymarket URL) | Public |
|
|
64
|
+
| `whale_alerts(minutes, min_usd, ...)` | Recent smart-whale trades feed | Premium |
|
|
65
|
+
|
|
66
|
+
All methods return the JSON response as a plain `dict`, exactly as the API sends it. Full field reference: [orcalayer.com/docs](https://orcalayer.com/docs).
|
|
67
|
+
|
|
68
|
+
## Behavior notes
|
|
69
|
+
|
|
70
|
+
- **Rate limits**: on HTTP 429 the client reads `Retry-After` and retries with exponential backoff (default 3 attempts). Disable with `OrcaLayer(retry_on_rate_limit=False)`. A `Retry-After` beyond 5 minutes signals the anonymous daily cap rather than a burst — the client then raises `RateLimitError` immediately instead of retrying.
|
|
71
|
+
- **Transient 502** responses are retried once automatically.
|
|
72
|
+
- **Premium endpoints without a key** raise `PremiumRequiredError` with a link to [pricing](https://orcalayer.com/pricing) — no network call is made.
|
|
73
|
+
- **Wallet overview freshness**: responses include `as_of` (data timestamp) and `degraded` (heavy side-stats timed out, core stats still present).
|
|
74
|
+
- **Cold heavy wallets** answer HTTP 202 while their stats are computed server-side. The client retries once automatically after the server's `Retry-After` interval; if the wallet is still not ready it raises `WalletComputingError` (carrying `retry_after`) so a 202 body is never mistaken for wallet data.
|
|
75
|
+
|
|
76
|
+
## Errors
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from orcalayer import PremiumRequiredError, RateLimitError, AuthenticationError, ServerError
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
All inherit from `orcalayer.OrcaLayerError`.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT. See [LICENSE](LICENSE).
|
|
87
|
+
|
|
88
|
+
Data is provided for informational purposes only and is not financial advice.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# orcalayer
|
|
2
|
+
|
|
3
|
+
Official Python client for the [OrcaLayer API](https://orcalayer.com) — Polymarket whale and market analytics: wallet P&L, open positions, smart-whale leaderboard, market search and real-time whale alerts.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install orcalayer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10+. Single dependency: `httpx`.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
Three lines to a first result — no API key needed for public endpoints:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from orcalayer import OrcaLayer
|
|
19
|
+
|
|
20
|
+
ol = OrcaLayer()
|
|
21
|
+
print(ol.leaderboard(limit=5))
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Anonymous access is limited to 200 requests/min per IP, and wallet endpoints (`wallet_overview`, `wallet_positions`) additionally to 300 requests/day per IP. With a Premium API key ([get one here](https://orcalayer.com/pricing)) you get 600 req/min, no daily cap, and access to Premium endpoints such as whale alerts:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
ol = OrcaLayer(api_key="ol_your_key")
|
|
28
|
+
alerts = ol.whale_alerts(minutes=30, min_usd=1000)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Methods
|
|
32
|
+
|
|
33
|
+
| Method | Endpoint | Access |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `leaderboard(sort, category, limit, ...)` | Smart-whale leaderboard with server-side filters | Public |
|
|
36
|
+
| `wallet_overview(address)` | Wallet profile + trading stats (accepts 0x address or nickname) | Public |
|
|
37
|
+
| `wallet_positions(address, limit, offset)` | Open positions, sorted by current value | Public |
|
|
38
|
+
| `markets(q, category, min_volume, ...)` | Market search (accepts free text or a Polymarket URL) | Public |
|
|
39
|
+
| `whale_alerts(minutes, min_usd, ...)` | Recent smart-whale trades feed | Premium |
|
|
40
|
+
|
|
41
|
+
All methods return the JSON response as a plain `dict`, exactly as the API sends it. Full field reference: [orcalayer.com/docs](https://orcalayer.com/docs).
|
|
42
|
+
|
|
43
|
+
## Behavior notes
|
|
44
|
+
|
|
45
|
+
- **Rate limits**: on HTTP 429 the client reads `Retry-After` and retries with exponential backoff (default 3 attempts). Disable with `OrcaLayer(retry_on_rate_limit=False)`. A `Retry-After` beyond 5 minutes signals the anonymous daily cap rather than a burst — the client then raises `RateLimitError` immediately instead of retrying.
|
|
46
|
+
- **Transient 502** responses are retried once automatically.
|
|
47
|
+
- **Premium endpoints without a key** raise `PremiumRequiredError` with a link to [pricing](https://orcalayer.com/pricing) — no network call is made.
|
|
48
|
+
- **Wallet overview freshness**: responses include `as_of` (data timestamp) and `degraded` (heavy side-stats timed out, core stats still present).
|
|
49
|
+
- **Cold heavy wallets** answer HTTP 202 while their stats are computed server-side. The client retries once automatically after the server's `Retry-After` interval; if the wallet is still not ready it raises `WalletComputingError` (carrying `retry_after`) so a 202 body is never mistaken for wallet data.
|
|
50
|
+
|
|
51
|
+
## Errors
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from orcalayer import PremiumRequiredError, RateLimitError, AuthenticationError, ServerError
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All inherit from `orcalayer.OrcaLayerError`.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT. See [LICENSE](LICENSE).
|
|
62
|
+
|
|
63
|
+
Data is provided for informational purposes only and is not financial advice.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orcalayer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for the OrcaLayer API — Polymarket whale and market analytics"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "OrcaLayer" }]
|
|
13
|
+
keywords = ["polymarket", "prediction-markets", "whales", "trading", "analytics", "api-client"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["httpx>=0.24"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://orcalayer.com"
|
|
29
|
+
Documentation = "https://orcalayer.com/docs"
|
|
30
|
+
Repository = "https://github.com/orcalayer/orcalayer-python"
|
|
31
|
+
Changelog = "https://github.com/orcalayer/orcalayer-python/blob/main/CHANGELOG.md"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/orcalayer"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""orcalayer — Python client for the OrcaLayer API (https://orcalayer.com)."""
|
|
2
|
+
|
|
3
|
+
from .client import OrcaLayer
|
|
4
|
+
from .errors import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
OrcaLayerError,
|
|
7
|
+
PremiumRequiredError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServerError,
|
|
10
|
+
WalletComputingError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OrcaLayer",
|
|
17
|
+
"OrcaLayerError",
|
|
18
|
+
"PremiumRequiredError",
|
|
19
|
+
"AuthenticationError",
|
|
20
|
+
"RateLimitError",
|
|
21
|
+
"ServerError",
|
|
22
|
+
"WalletComputingError",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""OrcaLayer API client.
|
|
2
|
+
|
|
3
|
+
Two access tiers, selected automatically by whether an API key is given:
|
|
4
|
+
|
|
5
|
+
* Anonymous — public endpoints under ``/api/v2`` (200 requests/min per IP).
|
|
6
|
+
* Premium — same data plus Premium endpoints under ``/api/public/v1``
|
|
7
|
+
with ``Authorization: Bearer`` auth (600 requests/min per key).
|
|
8
|
+
|
|
9
|
+
All methods return the JSON response as a plain ``dict`` — exactly what the
|
|
10
|
+
API sends, no client-side reshaping.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from .errors import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
OrcaLayerError,
|
|
23
|
+
PremiumRequiredError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
WalletComputingError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://orcalayer.com"
|
|
30
|
+
PUBLIC_PREFIX = "/api/v2"
|
|
31
|
+
PREMIUM_PREFIX = "/api/public/v1"
|
|
32
|
+
USER_AGENT = "Mozilla/5.0 (compatible; orcalayer-python/0.1.0)"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OrcaLayer:
|
|
36
|
+
"""Client for the OrcaLayer API.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
api_key: Premium API key. Omit for anonymous access to public
|
|
40
|
+
endpoints; Premium endpoints then raise ``PremiumRequiredError``.
|
|
41
|
+
base_url: API host. Defaults to ``https://orcalayer.com``.
|
|
42
|
+
timeout: Per-request timeout in seconds.
|
|
43
|
+
retry_on_rate_limit: When True (default), HTTP 429 responses are
|
|
44
|
+
retried using the server's ``Retry-After`` header with
|
|
45
|
+
exponential backoff. Set False to raise ``RateLimitError``
|
|
46
|
+
immediately.
|
|
47
|
+
max_retries: Maximum retry attempts for 429 responses.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> from orcalayer import OrcaLayer
|
|
51
|
+
>>> ol = OrcaLayer()
|
|
52
|
+
>>> top = ol.leaderboard(limit=10)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: str | None = None,
|
|
58
|
+
*,
|
|
59
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
60
|
+
timeout: float = 30.0,
|
|
61
|
+
retry_on_rate_limit: bool = True,
|
|
62
|
+
max_retries: int = 3,
|
|
63
|
+
):
|
|
64
|
+
self.api_key = api_key
|
|
65
|
+
self.base_url = base_url.rstrip("/")
|
|
66
|
+
self.retry_on_rate_limit = retry_on_rate_limit
|
|
67
|
+
self.max_retries = max_retries
|
|
68
|
+
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
|
69
|
+
if api_key:
|
|
70
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
71
|
+
self._client = httpx.Client(timeout=timeout, headers=headers)
|
|
72
|
+
|
|
73
|
+
# ── HTTP core ────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _get(
|
|
76
|
+
self,
|
|
77
|
+
path: str,
|
|
78
|
+
params: dict[str, Any] | None = None,
|
|
79
|
+
*,
|
|
80
|
+
premium_only: bool = False,
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""GET ``path`` (no leading slash) and return the parsed JSON dict.
|
|
83
|
+
|
|
84
|
+
With an API key every call is routed through the Premium surface
|
|
85
|
+
(``/api/public/v1``, Bearer auth, higher rate limit). Without a key,
|
|
86
|
+
public endpoints go to ``/api/v2`` and Premium-only endpoints raise
|
|
87
|
+
``PremiumRequiredError``.
|
|
88
|
+
"""
|
|
89
|
+
if premium_only and not self.api_key:
|
|
90
|
+
raise PremiumRequiredError(path)
|
|
91
|
+
prefix = PREMIUM_PREFIX if self.api_key else PUBLIC_PREFIX
|
|
92
|
+
url = f"{self.base_url}{prefix}/{path}"
|
|
93
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
94
|
+
|
|
95
|
+
attempt = 0
|
|
96
|
+
retried_202 = False
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
resp = self._client.get(url, params=clean)
|
|
100
|
+
except httpx.HTTPError as exc:
|
|
101
|
+
raise OrcaLayerError(f"Request to {url} failed: {exc}") from exc
|
|
102
|
+
|
|
103
|
+
if resp.status_code == 202:
|
|
104
|
+
# Cold heavy wallet: stats are being computed server-side.
|
|
105
|
+
# One automatic retry after Retry-After, then a typed error
|
|
106
|
+
# so callers never mistake the 202 body for real data.
|
|
107
|
+
retry_after = _retry_after_seconds(resp)
|
|
108
|
+
if retried_202:
|
|
109
|
+
raise WalletComputingError(retry_after)
|
|
110
|
+
retried_202 = True
|
|
111
|
+
time.sleep(min(retry_after, 60))
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if resp.status_code == 429:
|
|
115
|
+
retry_after = _retry_after_seconds(resp)
|
|
116
|
+
# A Retry-After beyond 5 minutes signals a daily quota, not a
|
|
117
|
+
# sliding-window burst — retrying within this process is futile.
|
|
118
|
+
if (
|
|
119
|
+
not self.retry_on_rate_limit
|
|
120
|
+
or attempt >= self.max_retries
|
|
121
|
+
or retry_after > 300
|
|
122
|
+
):
|
|
123
|
+
raise RateLimitError(retry_after, _detail(resp))
|
|
124
|
+
# Server window is sliding 60s; honour Retry-After and add
|
|
125
|
+
# exponential backoff across attempts so bursts drain cleanly.
|
|
126
|
+
time.sleep(min(max(retry_after, 2.0 ** attempt), 120.0))
|
|
127
|
+
attempt += 1
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if resp.status_code == 502 and attempt == 0:
|
|
131
|
+
# Transient gateway hiccup: a single retry, then give up.
|
|
132
|
+
attempt += 1
|
|
133
|
+
time.sleep(1)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if resp.status_code in (401, 403):
|
|
137
|
+
raise AuthenticationError(resp.status_code, _detail(resp))
|
|
138
|
+
|
|
139
|
+
if resp.status_code >= 500:
|
|
140
|
+
raise ServerError(resp.status_code, resp.text)
|
|
141
|
+
|
|
142
|
+
resp.raise_for_status()
|
|
143
|
+
return resp.json()
|
|
144
|
+
|
|
145
|
+
# ── Public endpoints (work with or without a key) ────────────────────
|
|
146
|
+
|
|
147
|
+
def leaderboard(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
sort: str = "pnl",
|
|
151
|
+
category: str | None = None,
|
|
152
|
+
filter: str = "smart",
|
|
153
|
+
limit: int = 50,
|
|
154
|
+
offset: int = 0,
|
|
155
|
+
min_markets: int | None = None,
|
|
156
|
+
min_wr: float | None = None,
|
|
157
|
+
min_pnl: float | None = None,
|
|
158
|
+
min_profit_factor: float | None = None,
|
|
159
|
+
max_avg_entry: float | None = None,
|
|
160
|
+
max_sports_pct: float | None = None,
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Smart-whale leaderboard.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
sort: ``pnl`` (default), ``win_rate``, ``volume`` or ``trades``.
|
|
166
|
+
category: Market category filter (e.g. ``Crypto``, ``Sports``).
|
|
167
|
+
filter: ``smart`` (curated whales, default) or ``all``.
|
|
168
|
+
limit / offset: Pagination.
|
|
169
|
+
min_markets, min_wr, min_pnl, min_profit_factor, max_avg_entry,
|
|
170
|
+
max_sports_pct: Optional numeric screens, applied server-side.
|
|
171
|
+
"""
|
|
172
|
+
return self._get(
|
|
173
|
+
"whales/leaderboard",
|
|
174
|
+
{
|
|
175
|
+
"sort": sort,
|
|
176
|
+
"category": category,
|
|
177
|
+
"filter": filter,
|
|
178
|
+
"limit": limit,
|
|
179
|
+
"offset": offset,
|
|
180
|
+
"min_markets": min_markets,
|
|
181
|
+
"min_wr": min_wr,
|
|
182
|
+
"min_pnl": min_pnl,
|
|
183
|
+
"min_profit_factor": min_profit_factor,
|
|
184
|
+
"max_avg_entry": max_avg_entry,
|
|
185
|
+
"max_sports_pct": max_sports_pct,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def wallet_overview(self, address: str) -> dict:
|
|
190
|
+
"""Wallet profile and trading stats.
|
|
191
|
+
|
|
192
|
+
``address`` is a 0x wallet address or an OrcaLayer nickname.
|
|
193
|
+
The response carries ``as_of`` (data timestamp, epoch seconds) and
|
|
194
|
+
``degraded`` (True when heavy side-stats timed out; core stats are
|
|
195
|
+
still present). A cold heavy wallet answers HTTP 202 while its stats
|
|
196
|
+
are computed: the client retries once automatically after the
|
|
197
|
+
server's ``Retry-After`` interval and raises ``WalletComputingError``
|
|
198
|
+
if the wallet is still not ready.
|
|
199
|
+
"""
|
|
200
|
+
return self._get(f"wallet/{address}/overview")
|
|
201
|
+
|
|
202
|
+
def wallet_positions(self, address: str, *, limit: int = 200, offset: int = 0) -> dict:
|
|
203
|
+
"""Open positions for a wallet (sorted by current value, cap 500/page)."""
|
|
204
|
+
return self._get(
|
|
205
|
+
f"wallet/{address}/positions", {"limit": limit, "offset": offset}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def markets(
|
|
209
|
+
self,
|
|
210
|
+
q: str = "",
|
|
211
|
+
*,
|
|
212
|
+
category: str | None = None,
|
|
213
|
+
limit: int = 50,
|
|
214
|
+
offset: int = 0,
|
|
215
|
+
min_volume: float | None = None,
|
|
216
|
+
min_whales: int | None = None,
|
|
217
|
+
price_min: int | None = None,
|
|
218
|
+
price_max: int | None = None,
|
|
219
|
+
) -> dict:
|
|
220
|
+
"""Search or browse markets.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
q: Free-text query; also accepts a Polymarket URL or slug.
|
|
224
|
+
category: ``Crypto`` | ``Geopolitics`` | ``Sports`` | ``Politics``
|
|
225
|
+
| ``Economics`` | ``Tech/AI``.
|
|
226
|
+
min_volume: Minimum market volume in USD.
|
|
227
|
+
min_whales: Minimum count of smart whales in the market.
|
|
228
|
+
price_min / price_max: YES price band in cents (0-100).
|
|
229
|
+
"""
|
|
230
|
+
return self._get(
|
|
231
|
+
"markets/search",
|
|
232
|
+
{
|
|
233
|
+
"q": q,
|
|
234
|
+
"category": category,
|
|
235
|
+
"limit": limit,
|
|
236
|
+
"offset": offset,
|
|
237
|
+
"min_volume": min_volume,
|
|
238
|
+
"min_whales": min_whales,
|
|
239
|
+
"price_min": price_min,
|
|
240
|
+
"price_max": price_max,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# ── Premium endpoints (API key required) ─────────────────────────────
|
|
245
|
+
|
|
246
|
+
def whale_alerts(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
minutes: int = 10,
|
|
250
|
+
wallet: str | None = None,
|
|
251
|
+
market_id: str | None = None,
|
|
252
|
+
min_usd: float = 10,
|
|
253
|
+
smart_only: bool = True,
|
|
254
|
+
limit: int = 50,
|
|
255
|
+
category: str | None = None,
|
|
256
|
+
) -> dict:
|
|
257
|
+
"""Recent smart-whale trades (Premium).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
minutes: Lookback window, max 1440 (24h).
|
|
261
|
+
wallet: Filter to one wallet address.
|
|
262
|
+
market_id: Filter to one market.
|
|
263
|
+
min_usd: Minimum trade size in USD.
|
|
264
|
+
smart_only: Only curated smart whales (default True).
|
|
265
|
+
"""
|
|
266
|
+
return self._get(
|
|
267
|
+
"whale-alerts",
|
|
268
|
+
{
|
|
269
|
+
"minutes": minutes,
|
|
270
|
+
"wallet": wallet,
|
|
271
|
+
"market_id": market_id,
|
|
272
|
+
"min_usd": min_usd,
|
|
273
|
+
"smart_only": smart_only,
|
|
274
|
+
"limit": limit,
|
|
275
|
+
"category": category,
|
|
276
|
+
},
|
|
277
|
+
premium_only=True,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# ── Lifecycle ────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
def close(self) -> None:
|
|
283
|
+
self._client.close()
|
|
284
|
+
|
|
285
|
+
def __enter__(self) -> "OrcaLayer":
|
|
286
|
+
return self
|
|
287
|
+
|
|
288
|
+
def __exit__(self, *exc) -> None:
|
|
289
|
+
self.close()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _retry_after_seconds(resp: httpx.Response) -> float:
|
|
293
|
+
try:
|
|
294
|
+
return max(1.0, float(resp.headers.get("Retry-After", "60")))
|
|
295
|
+
except ValueError:
|
|
296
|
+
return 60.0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _detail(resp: httpx.Response) -> str:
|
|
300
|
+
try:
|
|
301
|
+
return str(resp.json().get("error", ""))
|
|
302
|
+
except Exception:
|
|
303
|
+
return ""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Exception types raised by the OrcaLayer client."""
|
|
2
|
+
|
|
3
|
+
PRICING_URL = "https://orcalayer.com/pricing"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OrcaLayerError(Exception):
|
|
7
|
+
"""Base class for all OrcaLayer client errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PremiumRequiredError(OrcaLayerError):
|
|
11
|
+
"""A Premium endpoint was called without an API key."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, endpoint: str):
|
|
14
|
+
super().__init__(
|
|
15
|
+
f"'{endpoint}' is a Premium endpoint and requires an API key. "
|
|
16
|
+
f"Create one at {PRICING_URL} and pass it as "
|
|
17
|
+
f"OrcaLayer(api_key=\"...\")."
|
|
18
|
+
)
|
|
19
|
+
self.endpoint = endpoint
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthenticationError(OrcaLayerError):
|
|
23
|
+
"""The API key was rejected (HTTP 401/403)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, status_code: int, detail: str = ""):
|
|
26
|
+
msg = f"API key rejected (HTTP {status_code})."
|
|
27
|
+
if detail:
|
|
28
|
+
msg += f" Server said: {detail}"
|
|
29
|
+
msg += f" Check your key at https://orcalayer.com/settings or get one at {PRICING_URL}."
|
|
30
|
+
super().__init__(msg)
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WalletComputingError(OrcaLayerError):
|
|
35
|
+
"""The wallet's stats are still being computed server-side (HTTP 202).
|
|
36
|
+
|
|
37
|
+
Raised after one automatic retry. Poll again after ``retry_after`` seconds.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, retry_after: float):
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"Wallet stats are still computing server-side; "
|
|
43
|
+
f"retry in {retry_after:.0f}s."
|
|
44
|
+
)
|
|
45
|
+
self.retry_after = retry_after
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitError(OrcaLayerError):
|
|
49
|
+
"""Rate limit still exceeded after all retries (HTTP 429)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, retry_after: float, detail: str = ""):
|
|
52
|
+
msg = f"Rate limit exceeded; retry after {retry_after:.0f}s."
|
|
53
|
+
if detail:
|
|
54
|
+
msg += f" Server said: {detail}"
|
|
55
|
+
super().__init__(msg)
|
|
56
|
+
self.retry_after = retry_after
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ServerError(OrcaLayerError):
|
|
60
|
+
"""The API returned an unexpected 5xx response."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, status_code: int, detail: str = ""):
|
|
63
|
+
msg = f"OrcaLayer API server error (HTTP {status_code})."
|
|
64
|
+
if detail:
|
|
65
|
+
msg += f" Body: {detail[:200]}"
|
|
66
|
+
super().__init__(msg)
|
|
67
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Offline tests for retry semantics (202 / 429 / 502) via httpx.MockTransport."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from orcalayer import OrcaLayer, RateLimitError, WalletComputingError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_client(responses: list[httpx.Response]) -> OrcaLayer:
|
|
10
|
+
"""OrcaLayer whose HTTP layer replays canned responses in order."""
|
|
11
|
+
queue = list(responses)
|
|
12
|
+
|
|
13
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
14
|
+
return queue.pop(0)
|
|
15
|
+
|
|
16
|
+
ol = OrcaLayer()
|
|
17
|
+
ol._client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
18
|
+
return ol
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_202_retries_once_then_returns_data():
|
|
22
|
+
ol = make_client([
|
|
23
|
+
httpx.Response(202, headers={"Retry-After": "1"}, json={"detail": "computing"}),
|
|
24
|
+
httpx.Response(200, json={"wallet": "0xabc", "total_pnl": 1.0}),
|
|
25
|
+
])
|
|
26
|
+
data = ol.wallet_overview("0xabc")
|
|
27
|
+
assert data["total_pnl"] == 1.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_202_twice_raises_computing_error():
|
|
31
|
+
ol = make_client([
|
|
32
|
+
httpx.Response(202, headers={"Retry-After": "1"}, json={"detail": "computing"}),
|
|
33
|
+
httpx.Response(202, headers={"Retry-After": "30"}, json={"detail": "computing"}),
|
|
34
|
+
])
|
|
35
|
+
with pytest.raises(WalletComputingError) as exc:
|
|
36
|
+
ol.wallet_overview("0xabc")
|
|
37
|
+
assert exc.value.retry_after == 30
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_429_long_retry_after_raises_immediately():
|
|
41
|
+
# Retry-After beyond 300s = daily quota; must not sleep/retry.
|
|
42
|
+
ol = make_client([
|
|
43
|
+
httpx.Response(429, headers={"Retry-After": "3600"}, json={"error": "daily"}),
|
|
44
|
+
])
|
|
45
|
+
with pytest.raises(RateLimitError) as exc:
|
|
46
|
+
ol.wallet_overview("0xabc")
|
|
47
|
+
assert exc.value.retry_after == 3600
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_429_short_then_ok_retries():
|
|
51
|
+
ol = make_client([
|
|
52
|
+
httpx.Response(429, headers={"Retry-After": "1"}, json={"error": "burst"}),
|
|
53
|
+
httpx.Response(200, json={"ok": True}),
|
|
54
|
+
])
|
|
55
|
+
assert ol.wallet_overview("0xabc") == {"ok": True}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_502_single_retry():
|
|
59
|
+
ol = make_client([
|
|
60
|
+
httpx.Response(502, text="bad gateway"),
|
|
61
|
+
httpx.Response(200, json={"ok": True}),
|
|
62
|
+
])
|
|
63
|
+
assert ol.markets(limit=1) == {"ok": True}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Smoke tests against the live API.
|
|
2
|
+
|
|
3
|
+
Anonymous tests always run. Premium tests need ORCALAYER_TEST_API_KEY in the
|
|
4
|
+
environment and are skipped otherwise. Run: python -m pytest tests/ -v
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from orcalayer import OrcaLayer, PremiumRequiredError
|
|
12
|
+
|
|
13
|
+
PREMIUM_KEY = os.environ.get("ORCALAYER_TEST_API_KEY", "")
|
|
14
|
+
# Known active wallet for read-only smoke checks (public leaderboard member).
|
|
15
|
+
TEST_WALLET = os.environ.get("ORCALAYER_TEST_WALLET", "")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(scope="module")
|
|
19
|
+
def anon():
|
|
20
|
+
with OrcaLayer() as client:
|
|
21
|
+
yield client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_leaderboard_anon(anon):
|
|
25
|
+
data = anon.leaderboard(limit=5)
|
|
26
|
+
assert isinstance(data, dict)
|
|
27
|
+
whales = data.get("whales") or data.get("leaderboard") or data.get("results")
|
|
28
|
+
assert whales, f"unexpected leaderboard shape: {list(data)}"
|
|
29
|
+
assert len(whales) <= 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_markets_anon(anon):
|
|
33
|
+
data = anon.markets(limit=5)
|
|
34
|
+
assert isinstance(data, dict)
|
|
35
|
+
assert data.get("markets") or data.get("results"), f"unexpected shape: {list(data)}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_wallet_overview_anon(anon):
|
|
39
|
+
address = TEST_WALLET or _first_leaderboard_wallet(anon)
|
|
40
|
+
data = anon.wallet_overview(address)
|
|
41
|
+
assert isinstance(data, dict)
|
|
42
|
+
assert "error" not in data, data.get("error")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_wallet_positions_anon(anon):
|
|
46
|
+
address = TEST_WALLET or _first_leaderboard_wallet(anon)
|
|
47
|
+
data = anon.wallet_positions(address, limit=10)
|
|
48
|
+
assert isinstance(data, dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_premium_without_key_raises(anon):
|
|
52
|
+
with pytest.raises(PremiumRequiredError) as exc:
|
|
53
|
+
anon.whale_alerts()
|
|
54
|
+
assert "pricing" in str(exc.value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.skipif(not PREMIUM_KEY, reason="ORCALAYER_TEST_API_KEY not set")
|
|
58
|
+
def test_whale_alerts_premium():
|
|
59
|
+
with OrcaLayer(api_key=PREMIUM_KEY) as client:
|
|
60
|
+
data = client.whale_alerts(minutes=60, limit=5)
|
|
61
|
+
assert isinstance(data, dict)
|
|
62
|
+
assert "error" not in data, data.get("error")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.skipif(not PREMIUM_KEY, reason="ORCALAYER_TEST_API_KEY not set")
|
|
66
|
+
def test_overview_via_premium_surface():
|
|
67
|
+
with OrcaLayer(api_key=PREMIUM_KEY) as client:
|
|
68
|
+
address = TEST_WALLET or _first_leaderboard_wallet(client)
|
|
69
|
+
data = client.wallet_overview(address)
|
|
70
|
+
assert isinstance(data, dict)
|
|
71
|
+
assert "error" not in data, data.get("error")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _first_leaderboard_wallet(client):
|
|
75
|
+
data = client.leaderboard(limit=1)
|
|
76
|
+
whales = data.get("whales") or data.get("leaderboard") or data.get("results")
|
|
77
|
+
entry = whales[0]
|
|
78
|
+
return entry.get("wallet") or entry.get("address") or entry.get("proxy_wallet")
|