coinex-mcp-server 0.1.0a1__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.
- coinex_mcp_server/__init__.py +13 -0
- coinex_mcp_server/coinex_client.py +393 -0
- coinex_mcp_server/main.py +637 -0
- coinex_mcp_server-0.1.0a1.dist-info/METADATA +237 -0
- coinex_mcp_server-0.1.0a1.dist-info/RECORD +8 -0
- coinex_mcp_server-0.1.0a1.dist-info/WHEEL +5 -0
- coinex_mcp_server-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- coinex_mcp_server-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""CoinEx MCP Server - A Model Context Protocol server for CoinEx cryptocurrency exchange.
|
|
2
|
+
|
|
3
|
+
This package enables AI agents to interact with the CoinEx exchange API through
|
|
4
|
+
the Model Context Protocol (MCP), supporting both spot and futures markets.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0a1"
|
|
8
|
+
__author__ = "CoinEx MCP Contributors"
|
|
9
|
+
__license__ = "Apache-2.0"
|
|
10
|
+
|
|
11
|
+
from .coinex_client import CoinExClient
|
|
12
|
+
|
|
13
|
+
__all__ = ["CoinExClient", "__version__"]
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CoinEx API Client
|
|
3
|
+
Implements authentication, signing and request functionality for CoinEx API v2
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import hmac
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Dict, Optional, List
|
|
13
|
+
from urllib.parse import urlencode
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoinExClient:
|
|
18
|
+
"""CoinEx API Client"""
|
|
19
|
+
class MarketType(Enum):
|
|
20
|
+
SPOT = "spot"
|
|
21
|
+
MARGIN = "margin"
|
|
22
|
+
FUTURES = "futures"
|
|
23
|
+
|
|
24
|
+
class OrderType(Enum):
|
|
25
|
+
LIMIT = "limit"
|
|
26
|
+
MARKET = "market"
|
|
27
|
+
|
|
28
|
+
class OrderSide(Enum):
|
|
29
|
+
BUY = "buy"
|
|
30
|
+
SELL = "sell"
|
|
31
|
+
|
|
32
|
+
class OrderStatus(Enum):
|
|
33
|
+
PENDING = "pending"
|
|
34
|
+
FINISHED = "finished"
|
|
35
|
+
|
|
36
|
+
def __init__(self, access_id: str = None, secret_key: str = None, *, enable_env_credentials: bool = True):
|
|
37
|
+
"""Initialize CoinEx client
|
|
38
|
+
:param access_id: API access ID
|
|
39
|
+
:param secret_key: API secret key
|
|
40
|
+
:param enable_env_credentials: Whether to allow fallback reading from environment variables COINEX_ACCESS_ID/COINEX_SECRET_KEY
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if enable_env_credentials:
|
|
44
|
+
self.access_id = access_id or os.getenv('COINEX_ACCESS_ID')
|
|
45
|
+
self.secret_key = secret_key or os.getenv('COINEX_SECRET_KEY')
|
|
46
|
+
else:
|
|
47
|
+
# Strictly use passed parameters, do not take over from environment
|
|
48
|
+
self.access_id = access_id
|
|
49
|
+
self.secret_key = secret_key
|
|
50
|
+
self.base_url = "https://api.coinex.com" # CoinEx doesn't have a dedicated testnet, use mainnet
|
|
51
|
+
self.timeout = 30
|
|
52
|
+
|
|
53
|
+
def _generate_signature(self, method: str, path: str, params: Dict = None, body: str = "") -> tuple[str, str]:
|
|
54
|
+
"""Generate API signature"""
|
|
55
|
+
if not self.secret_key:
|
|
56
|
+
raise ValueError("secret_key is required to generate signature")
|
|
57
|
+
|
|
58
|
+
timestamp = str(int(time.time() * 1000))
|
|
59
|
+
|
|
60
|
+
# Build string to be signed
|
|
61
|
+
if params:
|
|
62
|
+
query_string = urlencode(params)
|
|
63
|
+
prepared_str = f"{method}{path}?{query_string}{body}{timestamp}"
|
|
64
|
+
else:
|
|
65
|
+
prepared_str = f"{method}{path}{body}{timestamp}"
|
|
66
|
+
|
|
67
|
+
# Generate signature using HMAC-SHA256
|
|
68
|
+
signature = hmac.new(
|
|
69
|
+
self.secret_key.encode('utf-8'),
|
|
70
|
+
prepared_str.encode('utf-8'),
|
|
71
|
+
hashlib.sha256
|
|
72
|
+
).hexdigest()
|
|
73
|
+
|
|
74
|
+
return signature, timestamp
|
|
75
|
+
|
|
76
|
+
def _get_headers(self, method: str, path: str, params: Dict = None, body: str = "") -> Dict[str, str]:
|
|
77
|
+
"""Get request headers"""
|
|
78
|
+
headers = {
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"User-Agent": "coinex-mcp-server/1.0"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# If authentication information is available, add signature headers
|
|
84
|
+
if self.access_id and self.secret_key:
|
|
85
|
+
signature, timestamp = self._generate_signature(method, path, params, body)
|
|
86
|
+
headers.update({
|
|
87
|
+
"X-COINEX-KEY": self.access_id,
|
|
88
|
+
"X-COINEX-SIGN": signature,
|
|
89
|
+
"X-COINEX-TIMESTAMP": timestamp
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return headers
|
|
93
|
+
|
|
94
|
+
# --------------------
|
|
95
|
+
# Generic public market query helpers
|
|
96
|
+
# --------------------
|
|
97
|
+
@classmethod
|
|
98
|
+
def _market_type_str_in_path(cls, market_type: MarketType) -> str:
|
|
99
|
+
return market_type.value if market_type != cls.MarketType.MARGIN else cls.MarketType.SPOT.value
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def _build_market_path(cls, market_type: MarketType, endpoint: str) -> str:
|
|
103
|
+
"""Assemble generic path, e.g. endpoint='market' -> '/v2/spot/market'."""
|
|
104
|
+
mt = cls._market_type_str_in_path(market_type)
|
|
105
|
+
endpoint = endpoint.lstrip('/')
|
|
106
|
+
return f"/v2/{mt}/{endpoint}"
|
|
107
|
+
|
|
108
|
+
# Unified: public market queries (spot/futures routing)
|
|
109
|
+
async def _market_request(self, endpoint: str, method: str = 'GET', base_currency: Optional[str] = None,
|
|
110
|
+
quote_currency: Optional[str] = None, market_type: MarketType = MarketType.SPOT,
|
|
111
|
+
extra_params: Optional[Dict[str, Any]] = None):
|
|
112
|
+
"""Unified market request with market type routing and symbol normalization.
|
|
113
|
+
- market_type: 'spot' | 'futures'
|
|
114
|
+
- endpoint: e.g. 'market', 'ticker', 'order', 'cancel-order', 'deals', 'depth', 'kline'
|
|
115
|
+
- method: HTTP method 'GET' | 'POST' | 'DELETE', default 'GET'
|
|
116
|
+
- markets: str or [str], market symbol(s), maybe needs to be normalized
|
|
117
|
+
- extra_params: additional query parameters, such as limit/interval/period
|
|
118
|
+
"""
|
|
119
|
+
path = self._build_market_path(market_type, endpoint)
|
|
120
|
+
|
|
121
|
+
data: Dict[str, Any] = {}
|
|
122
|
+
|
|
123
|
+
if base_currency and not quote_currency or quote_currency and not base_currency:
|
|
124
|
+
raise ValueError(f"Base currency is '{base_currency}' and quote_currency is {quote_currency}, this is meaningless!")
|
|
125
|
+
|
|
126
|
+
if base_currency and quote_currency:
|
|
127
|
+
market = base_currency + quote_currency
|
|
128
|
+
data['market'] = market
|
|
129
|
+
|
|
130
|
+
if extra_params:
|
|
131
|
+
data.update(extra_params)
|
|
132
|
+
|
|
133
|
+
return await self._request(method, path, data=data)
|
|
134
|
+
|
|
135
|
+
async def _request(self, method: str, path: str, data: Dict = None) -> Dict[str, Any]:
|
|
136
|
+
"""Send HTTP request
|
|
137
|
+
|
|
138
|
+
:param method: HTTP method (GET/POST/DELETE)
|
|
139
|
+
:param path: API path
|
|
140
|
+
:param data: Request data - for GET requests becomes URL params, for POST/DELETE becomes request body
|
|
141
|
+
"""
|
|
142
|
+
url = f"{self.base_url}{path}"
|
|
143
|
+
|
|
144
|
+
# Determine params and body based on HTTP method
|
|
145
|
+
params = None
|
|
146
|
+
request_body = ""
|
|
147
|
+
|
|
148
|
+
if method.upper() == "GET":
|
|
149
|
+
# GET requests use URL parameters
|
|
150
|
+
params = data
|
|
151
|
+
else:
|
|
152
|
+
# POST/DELETE requests use request body
|
|
153
|
+
if data:
|
|
154
|
+
request_body = json.dumps(data, separators=(',', ':'))
|
|
155
|
+
|
|
156
|
+
# Get request headers
|
|
157
|
+
headers = self._get_headers(method, path, params, request_body)
|
|
158
|
+
|
|
159
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
160
|
+
"""Send HTTP request"""
|
|
161
|
+
try:
|
|
162
|
+
if method.upper() == "GET":
|
|
163
|
+
response = await client.get(url, params=params, headers=headers)
|
|
164
|
+
elif method.upper() == "POST":
|
|
165
|
+
response = await client.post(url, headers=headers, content=request_body)
|
|
166
|
+
else:
|
|
167
|
+
# coinex api doesn't have DELETE/PUT requests
|
|
168
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
169
|
+
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
return response.json()
|
|
172
|
+
|
|
173
|
+
except httpx.TimeoutException:
|
|
174
|
+
raise Exception("Request timeout")
|
|
175
|
+
except httpx.HTTPStatusError as e:
|
|
176
|
+
error_msg = f"HTTP error {e.response.status_code}"
|
|
177
|
+
try:
|
|
178
|
+
error_data = e.response.json()
|
|
179
|
+
if 'message' in error_data:
|
|
180
|
+
error_msg += f": {error_data['message']}"
|
|
181
|
+
except (ValueError, KeyError, AttributeError):
|
|
182
|
+
pass
|
|
183
|
+
raise Exception(error_msg)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise Exception(f"Request failed: {str(e)}")
|
|
186
|
+
|
|
187
|
+
# =====================
|
|
188
|
+
# Unified public market queries
|
|
189
|
+
# =====================
|
|
190
|
+
async def get_market_info(self, base: str | None = None, quote: str | None = None, market_type: MarketType | None = None) -> Dict[str, Any]:
|
|
191
|
+
return await self._market_request('market', base_currency=base, quote_currency=quote, market_type=market_type)
|
|
192
|
+
|
|
193
|
+
async def get_tickers(self, base: str | None = None, quote: str | None = None, market_type: MarketType | None = None) -> Dict[str, Any]:
|
|
194
|
+
return await self._market_request('ticker', base_currency=base, quote_currency=quote, market_type=market_type)
|
|
195
|
+
|
|
196
|
+
async def get_depth(self, base: str, quote: str = 'USDT', market_type: MarketType | None = None, limit: int = 20, interval: str = "0") -> Dict[str, Any]:
|
|
197
|
+
extra = {"limit": limit, "interval": interval}
|
|
198
|
+
return await self._market_request('depth', base_currency=base, quote_currency=quote, market_type=market_type,
|
|
199
|
+
extra_params=extra)
|
|
200
|
+
|
|
201
|
+
async def get_kline(self, period: str, base: str, quote: str = 'USDT', market_type: MarketType | None = None, limit: int = 100) -> Dict[str, Any]:
|
|
202
|
+
extra = {"period": period, "limit": limit}
|
|
203
|
+
return await self._market_request('kline', base_currency=base, quote_currency=quote, market_type=market_type,
|
|
204
|
+
extra_params=extra)
|
|
205
|
+
|
|
206
|
+
async def get_deal(self, base: str, quote: str = 'USDT', market_type: MarketType | None = None, limit: int = 100) -> Dict[str, Any]:
|
|
207
|
+
return await self._market_request('deals', base_currency=base, quote_currency=quote, market_type=market_type,
|
|
208
|
+
extra_params={"limit": limit})
|
|
209
|
+
|
|
210
|
+
async def get_index_price(self, base: str, quote: str = 'USDT', market_type: MarketType | None = None) -> Dict[str, Any]:
|
|
211
|
+
return await self._market_request('index', base_currency=base, quote_currency=quote, market_type=market_type)
|
|
212
|
+
|
|
213
|
+
# =====================
|
|
214
|
+
# Futures public market queries
|
|
215
|
+
# =====================
|
|
216
|
+
async def futures_get_funding_rate(self, base: str, quote: str = 'USDT'):
|
|
217
|
+
"""Get current funding rate (futures)"""
|
|
218
|
+
return await self._market_request("funding-rate", base_currency=base, quote_currency=quote, market_type=self.MarketType.FUTURES)
|
|
219
|
+
|
|
220
|
+
async def futures_get_funding_rate_history(self, base: str, quote: str = 'USDT',
|
|
221
|
+
start_time: Optional[int] = None,
|
|
222
|
+
end_time: Optional[int] = None, page: int = 1, limit: int = 10) -> Dict[str, Any]:
|
|
223
|
+
"""Get funding rate history (futures)"""
|
|
224
|
+
extra_params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
225
|
+
if start_time is not None:
|
|
226
|
+
extra_params["start_time"] = start_time
|
|
227
|
+
if end_time is not None:
|
|
228
|
+
extra_params["end_time"] = end_time
|
|
229
|
+
return await self._market_request("funding-rate-history",
|
|
230
|
+
base_currency=base, quote_currency=quote, market_type=self.MarketType.FUTURES,
|
|
231
|
+
extra_params=extra_params)
|
|
232
|
+
|
|
233
|
+
async def futures_get_premium_history(self, base: str, quote: str = 'USDT',
|
|
234
|
+
start_time: Optional[int] = None, end_time: Optional[int] = None,
|
|
235
|
+
page: int = 1, limit: int = 10) -> Dict[str, Any]:
|
|
236
|
+
"""Get premium index history (futures)"""
|
|
237
|
+
extra_params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
238
|
+
if start_time is not None:
|
|
239
|
+
extra_params["start_time"] = start_time
|
|
240
|
+
if end_time is not None:
|
|
241
|
+
extra_params["end_time"] = end_time
|
|
242
|
+
return await self._market_request("premium-index-history",
|
|
243
|
+
base_currency=base, quote_currency=quote, market_type=self.MarketType.FUTURES,
|
|
244
|
+
extra_params=extra_params)
|
|
245
|
+
|
|
246
|
+
async def futures_get_position_level(self, base: str, quote: str = 'USDT') -> Dict[str, Any]:
|
|
247
|
+
"""Get position levels (futures)"""
|
|
248
|
+
return await self._market_request("position-level", base_currency=base, quote_currency=quote,
|
|
249
|
+
market_type=self.MarketType.FUTURES)
|
|
250
|
+
|
|
251
|
+
async def futures_premium_index_history(self, base: str, quote: str = 'USDT',
|
|
252
|
+
start_time: Optional[int] = None, end_time: Optional[int] = None,
|
|
253
|
+
page: int = 1, limit: int = 10) -> Dict[str, Any]:
|
|
254
|
+
"""Get premium index history (futures)"""
|
|
255
|
+
extra_params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
256
|
+
if start_time is not None:
|
|
257
|
+
extra_params["start_time"] = start_time
|
|
258
|
+
if end_time is not None:
|
|
259
|
+
extra_params["end_time"] = end_time
|
|
260
|
+
return await self._market_request("premium-index-history",
|
|
261
|
+
base_currency=base, quote_currency=quote, market_type=self.MarketType.FUTURES,
|
|
262
|
+
extra_params=extra_params)
|
|
263
|
+
|
|
264
|
+
async def futures_basis_index_history(self, base: str, quote: str = 'USDT',
|
|
265
|
+
start_time: Optional[int] = None, end_time: Optional[int] = None,
|
|
266
|
+
page: int = 1, limit: int = 10) -> Dict[str, Any]:
|
|
267
|
+
extra_params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
268
|
+
if start_time is not None:
|
|
269
|
+
extra_params["start_time"] = start_time
|
|
270
|
+
if end_time is not None:
|
|
271
|
+
extra_params["end_time"] = end_time
|
|
272
|
+
return await self._market_request("basis-history",
|
|
273
|
+
base_currency=base, quote_currency=quote, market_type=self.MarketType.FUTURES,
|
|
274
|
+
extra_params=extra_params)
|
|
275
|
+
|
|
276
|
+
# =====================
|
|
277
|
+
# personal account info( require authentication)
|
|
278
|
+
# =====================
|
|
279
|
+
# Account interfaces
|
|
280
|
+
async def get_balances(self, market_type: MarketType = MarketType.SPOT) -> Dict[str, Any]:
|
|
281
|
+
"""Get account balance"""
|
|
282
|
+
if not self.access_id or not self.secret_key:
|
|
283
|
+
raise ValueError("Account interface requires access_id and secret_key")
|
|
284
|
+
|
|
285
|
+
path = f"/v2/assets/{self._market_type_str_in_path(market_type)}/balance"
|
|
286
|
+
return await self._request("GET", path, data=None)
|
|
287
|
+
|
|
288
|
+
# Trading interfaces (authentication required)
|
|
289
|
+
async def place_order(self, side: OrderSide, base: str, quote: str, amount: str,
|
|
290
|
+
market_type: MarketType = MarketType.SPOT,
|
|
291
|
+
price: str = None,
|
|
292
|
+
is_hide: bool = None, client_id: str = None,
|
|
293
|
+
trigger_price: str = None, stp_mode: str = None) -> Dict[str, Any]:
|
|
294
|
+
"""Place order.
|
|
295
|
+
This is an important interface that involves fund operations, so both the base and quote parameters must be explicitly specified by the user.
|
|
296
|
+
Parameters:
|
|
297
|
+
side: buy/sell
|
|
298
|
+
base: base currency
|
|
299
|
+
quote: quote currency
|
|
300
|
+
amount: order quantity (string), must meet precision and minimum volume requirements
|
|
301
|
+
market_type: spot/futures/margin
|
|
302
|
+
price: optional, if provided, creates a limit order; otherwise, creates a market order
|
|
303
|
+
is_hide: optional, if True, places a hidden order
|
|
304
|
+
trigger_price: optional, if provided, creates a stop order
|
|
305
|
+
stp_mode: optional, self-trade prevention mode
|
|
306
|
+
client_id: optional, custom order ID
|
|
307
|
+
"""
|
|
308
|
+
if not self.access_id or not self.secret_key:
|
|
309
|
+
raise ValueError("Trading interface requires access_id and secret_key")
|
|
310
|
+
|
|
311
|
+
params = {
|
|
312
|
+
"market_type": market_type.name,
|
|
313
|
+
"side": side.value,
|
|
314
|
+
"amount": amount,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if price:
|
|
318
|
+
params["price"] = price
|
|
319
|
+
params["type"] = self.OrderType.LIMIT.value
|
|
320
|
+
else:
|
|
321
|
+
params["type"] = self.OrderType.MARKET.value
|
|
322
|
+
|
|
323
|
+
if client_id:
|
|
324
|
+
params["client_id"] = client_id
|
|
325
|
+
if is_hide:
|
|
326
|
+
params["is_hide"] = 'true'
|
|
327
|
+
|
|
328
|
+
if trigger_price:
|
|
329
|
+
params["trigger_price"] = trigger_price
|
|
330
|
+
if stp_mode:
|
|
331
|
+
params["stp_mode"] = stp_mode
|
|
332
|
+
|
|
333
|
+
endpoint = 'stop-order' if trigger_price else "order"
|
|
334
|
+
|
|
335
|
+
return await self._market_request(endpoint, 'POST', base, quote,
|
|
336
|
+
market_type=market_type, extra_params=params)
|
|
337
|
+
|
|
338
|
+
# Trading interfaces (authentication required)
|
|
339
|
+
async def cancel_order(self, base: str, quote: str, market_type: MarketType = MarketType.SPOT,
|
|
340
|
+
order_id: int | None = None) -> Dict[str, Any]:
|
|
341
|
+
"""Cancel an order or all orders in a market.
|
|
342
|
+
This is an important interface that involves fund operations, so both the base and quote parameters must be explicitly specified by the user.
|
|
343
|
+
Parameters:
|
|
344
|
+
base: base currency
|
|
345
|
+
quote: quote currency
|
|
346
|
+
market_type: spot/futures/margin
|
|
347
|
+
order_id: optional, if provided, cancels the specific order; otherwise, cancels all orders in the market
|
|
348
|
+
"""
|
|
349
|
+
if not self.access_id or not self.secret_key:
|
|
350
|
+
raise ValueError("Trading interface requires access_id and secret_key")
|
|
351
|
+
endpoint = "cancel-order" if order_id else "cancel-all-orders"
|
|
352
|
+
params: Dict[str, Any] = {"market_type": market_type.name}
|
|
353
|
+
if order_id:
|
|
354
|
+
params["order_id"] = order_id
|
|
355
|
+
return await self._market_request(endpoint, 'POST', base, quote,
|
|
356
|
+
market_type=market_type, extra_params=params)
|
|
357
|
+
|
|
358
|
+
# Trading interfaces (authentication required)
|
|
359
|
+
async def get_orders(self, base: str = None, quote: str = None,
|
|
360
|
+
market_type: MarketType = MarketType.SPOT,
|
|
361
|
+
side: OrderSide = None,
|
|
362
|
+
status: OrderStatus = OrderStatus.FINISHED,
|
|
363
|
+
is_stop=False,
|
|
364
|
+
page: int = 1, limit: int = 100) -> Dict[str, Any]:
|
|
365
|
+
if not self.access_id or not self.secret_key:
|
|
366
|
+
raise ValueError("Account interface requires access_id and secret_key")
|
|
367
|
+
|
|
368
|
+
extra_params = {
|
|
369
|
+
"market_type": market_type.value,
|
|
370
|
+
"page": page,
|
|
371
|
+
"limit": limit
|
|
372
|
+
}
|
|
373
|
+
if side:
|
|
374
|
+
extra_params["side"] = side.value
|
|
375
|
+
|
|
376
|
+
stop_seg = "stop-" if is_stop else ""
|
|
377
|
+
endpoint = f"{status.value}-{stop_seg}order"
|
|
378
|
+
return await self._market_request(endpoint, 'GET', base, quote,
|
|
379
|
+
market_type=market_type, extra_params=extra_params)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def validate_environment():
|
|
383
|
+
"""Validate environment variable configuration"""
|
|
384
|
+
access_id = os.getenv('COINEX_ACCESS_ID')
|
|
385
|
+
secret_key = os.getenv('COINEX_SECRET_KEY')
|
|
386
|
+
|
|
387
|
+
if not access_id or not secret_key:
|
|
388
|
+
print("Warning: COINEX_ACCESS_ID and COINEX_SECRET_KEY environment variables not set")
|
|
389
|
+
print("Some features (account info, trading) will be unavailable")
|
|
390
|
+
print("Market data features can still be used normally")
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
return True
|