liquidtrading-python 0.1.0__py3-none-any.whl
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.
- liquid/__init__.py +23 -0
- liquidtrading/__init__.py +92 -0
- liquidtrading/auth.py +182 -0
- liquidtrading/client.py +476 -0
- liquidtrading/constants.py +15 -0
- liquidtrading/demo.py +91 -0
- liquidtrading/errors.py +208 -0
- liquidtrading/http.py +237 -0
- liquidtrading/models.py +296 -0
- liquidtrading_python-0.1.0.dist-info/METADATA +180 -0
- liquidtrading_python-0.1.0.dist-info/RECORD +13 -0
- liquidtrading_python-0.1.0.dist-info/WHEEL +4 -0
- liquidtrading_python-0.1.0.dist-info/entry_points.txt +3 -0
liquid/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Compatibility package that mirrors the upstream ``liquid`` import path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import liquidtrading.auth as _auth
|
|
8
|
+
import liquidtrading.client as _client
|
|
9
|
+
import liquidtrading.constants as _constants
|
|
10
|
+
import liquidtrading.errors as _errors
|
|
11
|
+
import liquidtrading.http as _http
|
|
12
|
+
import liquidtrading.models as _models
|
|
13
|
+
import liquidtrading as _liquidtrading
|
|
14
|
+
from liquidtrading import * # noqa: F403
|
|
15
|
+
|
|
16
|
+
sys.modules[__name__ + ".auth"] = _auth
|
|
17
|
+
sys.modules[__name__ + ".client"] = _client
|
|
18
|
+
sys.modules[__name__ + ".constants"] = _constants
|
|
19
|
+
sys.modules[__name__ + ".errors"] = _errors
|
|
20
|
+
sys.modules[__name__ + ".http"] = _http
|
|
21
|
+
sys.modules[__name__ + ".models"] = _models
|
|
22
|
+
|
|
23
|
+
__all__ = list(getattr(_liquidtrading, "__all__", ()))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Liquid Trading SDK — ``from liquidtrading import LiquidClient``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .client import Client, LiquidClient
|
|
8
|
+
from .constants import SDK_VERSION as __version__
|
|
9
|
+
from .errors import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
ConnectionError,
|
|
12
|
+
ExchangeError,
|
|
13
|
+
ExpiredTimestampError,
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
GatewayTimeoutError,
|
|
16
|
+
InsufficientBalanceError,
|
|
17
|
+
InsufficientScopeError,
|
|
18
|
+
InvalidApiKeyError,
|
|
19
|
+
InvalidSignatureError,
|
|
20
|
+
IpForbiddenError,
|
|
21
|
+
LiquidError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
OrderRejectedError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ReplayedNonceError,
|
|
26
|
+
ServiceUnavailableError,
|
|
27
|
+
ServerError,
|
|
28
|
+
SymbolNotFoundError,
|
|
29
|
+
TimeoutError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
)
|
|
32
|
+
from .models import (
|
|
33
|
+
Account,
|
|
34
|
+
Balance,
|
|
35
|
+
Candle,
|
|
36
|
+
CloseResult,
|
|
37
|
+
LeverageResult,
|
|
38
|
+
MarginResult,
|
|
39
|
+
Market,
|
|
40
|
+
OpenOrder,
|
|
41
|
+
Order,
|
|
42
|
+
Orderbook,
|
|
43
|
+
OrderbookLevel,
|
|
44
|
+
Position,
|
|
45
|
+
Ticker,
|
|
46
|
+
TpSlResult,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logging.getLogger("liquidtrading").addHandler(logging.NullHandler())
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"__version__",
|
|
53
|
+
"LiquidClient",
|
|
54
|
+
"Client",
|
|
55
|
+
# Errors
|
|
56
|
+
"LiquidError",
|
|
57
|
+
"AuthenticationError",
|
|
58
|
+
"InvalidApiKeyError",
|
|
59
|
+
"InvalidSignatureError",
|
|
60
|
+
"ExpiredTimestampError",
|
|
61
|
+
"ReplayedNonceError",
|
|
62
|
+
"ForbiddenError",
|
|
63
|
+
"IpForbiddenError",
|
|
64
|
+
"InsufficientScopeError",
|
|
65
|
+
"NotFoundError",
|
|
66
|
+
"SymbolNotFoundError",
|
|
67
|
+
"ValidationError",
|
|
68
|
+
"InsufficientBalanceError",
|
|
69
|
+
"OrderRejectedError",
|
|
70
|
+
"RateLimitError",
|
|
71
|
+
"ServerError",
|
|
72
|
+
"ExchangeError",
|
|
73
|
+
"GatewayTimeoutError",
|
|
74
|
+
"ServiceUnavailableError",
|
|
75
|
+
"TimeoutError",
|
|
76
|
+
"ConnectionError",
|
|
77
|
+
# Models
|
|
78
|
+
"Market",
|
|
79
|
+
"Account",
|
|
80
|
+
"Balance",
|
|
81
|
+
"Candle",
|
|
82
|
+
"CloseResult",
|
|
83
|
+
"LeverageResult",
|
|
84
|
+
"MarginResult",
|
|
85
|
+
"OpenOrder",
|
|
86
|
+
"Order",
|
|
87
|
+
"Orderbook",
|
|
88
|
+
"OrderbookLevel",
|
|
89
|
+
"Position",
|
|
90
|
+
"Ticker",
|
|
91
|
+
"TpSlResult",
|
|
92
|
+
]
|
liquidtrading/auth.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""HMAC-SHA256 request signing — mirrors liquidserver/public_api/middleware/auth.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import secrets
|
|
9
|
+
import time
|
|
10
|
+
from urllib.parse import parse_qsl, quote, urlparse
|
|
11
|
+
|
|
12
|
+
from .constants import (
|
|
13
|
+
HEADER_NONCE,
|
|
14
|
+
HEADER_SIGNATURE,
|
|
15
|
+
HEADER_TIMESTAMP,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def canonicalize_path(path: str) -> str:
|
|
20
|
+
"""Normalize a URL path for HMAC signing.
|
|
21
|
+
|
|
22
|
+
Strips trailing slashes, lowercases the path, and removes any query
|
|
23
|
+
string component. Returns ``"/"`` for empty paths.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
path: The raw request path, e.g. ``"/v1/Orders/?foo=bar"``.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Canonical path string, e.g. ``"/v1/orders"``.
|
|
30
|
+
"""
|
|
31
|
+
parsed = urlparse(path)
|
|
32
|
+
clean = parsed.path.rstrip("/").lower()
|
|
33
|
+
return clean if clean else "/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def canonicalize_query(query_string: str) -> str:
|
|
37
|
+
"""Sort query parameters alphabetically for deterministic signing.
|
|
38
|
+
|
|
39
|
+
Parses the query string, sorts by ``(key, value)`` tuples, and
|
|
40
|
+
re-encodes as ``key=value&...``. Returns ``""`` for empty input.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query_string: Raw query string without leading ``?``.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Sorted, canonicalized query string.
|
|
47
|
+
"""
|
|
48
|
+
if not query_string:
|
|
49
|
+
return ""
|
|
50
|
+
params = parse_qsl(query_string, keep_blank_values=True)
|
|
51
|
+
sorted_params = sorted(params, key=lambda x: (x[0], x[1]))
|
|
52
|
+
return "&".join(
|
|
53
|
+
f"{quote(k, safe='-_.~')}={quote(v, safe='-_.~')}"
|
|
54
|
+
for k, v in sorted_params
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_body_hash(body: bytes | None) -> str:
|
|
59
|
+
"""Compute SHA-256 hash of the request body for HMAC signing.
|
|
60
|
+
|
|
61
|
+
If the body is valid JSON, it is first canonicalized (sorted keys,
|
|
62
|
+
compact separators) to ensure deterministic hashing regardless of
|
|
63
|
+
key ordering. Non-JSON bodies are hashed as-is. ``None`` or empty
|
|
64
|
+
bodies hash the empty byte string.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
body: Raw request body bytes, or ``None``.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Hex-encoded SHA-256 digest.
|
|
71
|
+
"""
|
|
72
|
+
if body is None or len(body) == 0:
|
|
73
|
+
return hashlib.sha256(b"").hexdigest()
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
parsed = json.loads(body)
|
|
77
|
+
canonical = json.dumps(parsed, separators=(",", ":"), sort_keys=True).encode()
|
|
78
|
+
return hashlib.sha256(canonical).hexdigest()
|
|
79
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
80
|
+
return hashlib.sha256(body).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_signature(
|
|
84
|
+
secret: str,
|
|
85
|
+
timestamp: str,
|
|
86
|
+
nonce: str,
|
|
87
|
+
method: str,
|
|
88
|
+
path: str,
|
|
89
|
+
query: str,
|
|
90
|
+
body_hash: str,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Compute the HMAC-SHA256 signature for a request.
|
|
93
|
+
|
|
94
|
+
Constructs a newline-delimited message from the request components
|
|
95
|
+
and signs it with the API secret. The message format is::
|
|
96
|
+
|
|
97
|
+
{timestamp}\\n{nonce}\\n{METHOD}\\n{path}\\n{query}\\n{body_hash}
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
secret: The API secret (``sk_...``).
|
|
101
|
+
timestamp: Millisecond Unix timestamp string.
|
|
102
|
+
nonce: Unique request nonce.
|
|
103
|
+
method: HTTP method (uppercased).
|
|
104
|
+
path: Canonicalized request path.
|
|
105
|
+
query: Canonicalized query string.
|
|
106
|
+
body_hash: SHA-256 hex digest of the canonical body.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Hex-encoded HMAC-SHA256 signature.
|
|
110
|
+
"""
|
|
111
|
+
message = f"{timestamp}\n{nonce}\n{method.upper()}\n{path}\n{query}\n{body_hash}"
|
|
112
|
+
return hmac.new(
|
|
113
|
+
secret.encode("utf-8"),
|
|
114
|
+
message.encode("utf-8"),
|
|
115
|
+
hashlib.sha256,
|
|
116
|
+
).hexdigest()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def generate_nonce() -> str:
|
|
120
|
+
"""Generate a cryptographically random 32-character hex nonce.
|
|
121
|
+
|
|
122
|
+
Satisfies the server's nonce pattern ``^[a-zA-Z0-9_-]{8,64}$``.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A 32-character hex string.
|
|
126
|
+
"""
|
|
127
|
+
return secrets.token_hex(16)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def generate_timestamp() -> str:
|
|
131
|
+
"""Return the current UTC time as a millisecond Unix timestamp string.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Millisecond timestamp, e.g. ``"1709654400000"``.
|
|
135
|
+
"""
|
|
136
|
+
return str(int(time.time() * 1000))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def sign_request(
|
|
140
|
+
api_secret: str,
|
|
141
|
+
method: str,
|
|
142
|
+
path: str,
|
|
143
|
+
query_string: str,
|
|
144
|
+
body: bytes | None,
|
|
145
|
+
) -> dict[str, str]:
|
|
146
|
+
"""Sign an HTTP request and return the authentication headers.
|
|
147
|
+
|
|
148
|
+
Generates a timestamp and nonce, canonicalizes the path/query/body,
|
|
149
|
+
computes the HMAC-SHA256 signature, and returns a dict containing
|
|
150
|
+
``X-Liquid-Timestamp``, ``X-Liquid-Nonce``, and ``X-Liquid-Signature``.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
api_secret: The API secret (``sk_...``).
|
|
154
|
+
method: HTTP method (e.g. ``"GET"``, ``"POST"``).
|
|
155
|
+
path: Request path (e.g. ``"/v1/orders"``).
|
|
156
|
+
query_string: Raw query string without leading ``?``.
|
|
157
|
+
body: Raw request body bytes, or ``None``.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dict with the three HMAC auth headers.
|
|
161
|
+
"""
|
|
162
|
+
timestamp = generate_timestamp()
|
|
163
|
+
nonce = generate_nonce()
|
|
164
|
+
canonical_path = canonicalize_path(path)
|
|
165
|
+
canonical_query = canonicalize_query(query_string)
|
|
166
|
+
body_hash = compute_body_hash(body)
|
|
167
|
+
|
|
168
|
+
signature = compute_signature(
|
|
169
|
+
secret=api_secret,
|
|
170
|
+
timestamp=timestamp,
|
|
171
|
+
nonce=nonce,
|
|
172
|
+
method=method.upper(),
|
|
173
|
+
path=canonical_path,
|
|
174
|
+
query=canonical_query,
|
|
175
|
+
body_hash=body_hash,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
HEADER_TIMESTAMP: timestamp,
|
|
180
|
+
HEADER_NONCE: nonce,
|
|
181
|
+
HEADER_SIGNATURE: signature,
|
|
182
|
+
}
|