ccxt 4.3.51__py2.py3-none-any.whl → 4.3.53__py2.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.

Potentially problematic release.


This version of ccxt might be problematic. Click here for more details.

ccxt/pro/vertex.py ADDED
@@ -0,0 +1,916 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
4
+ # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
5
+
6
+ import ccxt.async_support
7
+ from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide
8
+ from ccxt.base.types import Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Trade
9
+ from ccxt.async_support.base.ws.client import Client
10
+ from typing import List
11
+ from ccxt.base.errors import AuthenticationError
12
+ from ccxt.base.errors import ArgumentsRequired
13
+ from ccxt.base.errors import NotSupported
14
+ from ccxt.base.precise import Precise
15
+
16
+
17
+ class vertex(ccxt.async_support.vertex):
18
+
19
+ def describe(self):
20
+ return self.deep_extend(super(vertex, self).describe(), {
21
+ 'has': {
22
+ 'ws': True,
23
+ 'watchBalance': False,
24
+ 'watchMyTrades': True,
25
+ 'watchOHLCV': False,
26
+ 'watchOrderBook': True,
27
+ 'watchOrders': True,
28
+ 'watchTicker': True,
29
+ 'watchTickers': False,
30
+ 'watchTrades': True,
31
+ 'watchPositions': True,
32
+ },
33
+ 'urls': {
34
+ 'api': {
35
+ 'ws': 'wss://gateway.prod.vertexprotocol.com/v1/subscribe',
36
+ },
37
+ 'test': {
38
+ 'ws': 'wss://gateway.sepolia-test.vertexprotocol.com/v1/subscribe',
39
+ },
40
+ },
41
+ 'requiredCredentials': {
42
+ 'apiKey': False,
43
+ 'secret': False,
44
+ 'walletAddress': True,
45
+ 'privateKey': True,
46
+ },
47
+ 'options': {
48
+ 'tradesLimit': 1000,
49
+ 'ordersLimit': 1000,
50
+ 'requestId': {},
51
+ 'watchPositions': {
52
+ 'fetchPositionsSnapshot': True, # or False
53
+ 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates
54
+ },
55
+ },
56
+ 'streaming': {
57
+ # 'ping': self.ping,
58
+ 'keepAlive': 30000,
59
+ },
60
+ 'exceptions': {
61
+ 'ws': {
62
+ 'exact': {
63
+ 'Auth is needed.': AuthenticationError,
64
+ },
65
+ },
66
+ },
67
+ })
68
+
69
+ def request_id(self, url):
70
+ options = self.safe_dict(self.options, 'requestId', {})
71
+ previousValue = self.safe_integer(options, url, 0)
72
+ newValue = self.sum(previousValue, 1)
73
+ self.options['requestId'][url] = newValue
74
+ return newValue
75
+
76
+ async def watch_public(self, messageHash, message):
77
+ url = self.urls['api']['ws']
78
+ requestId = self.request_id(url)
79
+ subscribe = {
80
+ 'id': requestId,
81
+ }
82
+ request = self.extend(subscribe, message)
83
+ return await self.watch(url, messageHash, request, messageHash, subscribe)
84
+
85
+ async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
86
+ """
87
+ watches information on multiple trades made in a market
88
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
89
+ :param str symbol: unified market symbol of the market trades were made in
90
+ :param int [since]: the earliest time in ms to fetch trades for
91
+ :param int [limit]: the maximum number of trade structures to retrieve
92
+ :param dict [params]: extra parameters specific to the exchange API endpoint
93
+ :returns dict[]: a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure
94
+ """
95
+ await self.load_markets()
96
+ market = self.market(symbol)
97
+ name = 'trade'
98
+ topic = market['id'] + '@' + name
99
+ request = {
100
+ 'method': 'subscribe',
101
+ 'stream': {
102
+ 'type': name,
103
+ 'product_id': self.parse_to_numeric(market['id']),
104
+ },
105
+ }
106
+ message = self.extend(request, params)
107
+ trades = await self.watch_public(topic, message)
108
+ if self.newUpdates:
109
+ limit = trades.getLimit(market['symbol'], limit)
110
+ return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
111
+
112
+ def handle_trade(self, client: Client, message):
113
+ #
114
+ # {
115
+ # "type": "trade",
116
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
117
+ # "product_id": 1,
118
+ # "price": "1000", # price the trade happened at, multiplied by 1e18
119
+ # # both taker_qty and maker_qty have the same value
120
+ # # set to filled amount(min amount of taker and maker) when matching against book
121
+ # # set to matched amm base amount when matching against amm
122
+ # "taker_qty": "1000",
123
+ # "maker_qty": "1000",
124
+ # "is_taker_buyer": True,
125
+ # "is_maker_amm": True # True when maker is amm
126
+ # }
127
+ #
128
+ topic = self.safe_string(message, 'type')
129
+ marketId = self.safe_string(message, 'product_id')
130
+ trade = self.parse_ws_trade(message)
131
+ symbol = trade['symbol']
132
+ if not (symbol in self.trades):
133
+ limit = self.safe_integer(self.options, 'tradesLimit', 1000)
134
+ stored = ArrayCache(limit)
135
+ self.trades[symbol] = stored
136
+ trades = self.trades[symbol]
137
+ trades.append(trade)
138
+ self.trades[symbol] = trades
139
+ client.resolve(trades, marketId + '@' + topic)
140
+
141
+ async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
142
+ """
143
+ watches information on multiple trades made by the user
144
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
145
+ :param str symbol: unified market symbol of the market orders were made in
146
+ :param int [since]: the earliest time in ms to fetch orders for
147
+ :param int [limit]: the maximum number of order structures to retrieve
148
+ :param dict [params]: extra parameters specific to the exchange API endpoint
149
+ :param str [params.user]: user address, will default to self.walletAddress if not provided
150
+ :returns dict[]: a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure
151
+ """
152
+ if symbol is None:
153
+ raise ArgumentsRequired(self.id + ' watchMyTrades requires a symbol.')
154
+ await self.load_markets()
155
+ userAddress = None
156
+ userAddress, params = self.handlePublicAddress('watchMyTrades', params)
157
+ market = self.market(symbol)
158
+ name = 'fill'
159
+ topic = market['id'] + '@' + name
160
+ request = {
161
+ 'method': 'subscribe',
162
+ 'stream': {
163
+ 'type': name,
164
+ 'product_id': self.parse_to_numeric(market['id']),
165
+ 'subaccount': self.convertAddressToSender(userAddress),
166
+ },
167
+ }
168
+ message = self.extend(request, params)
169
+ trades = await self.watch_public(topic, message)
170
+ if self.newUpdates:
171
+ limit = trades.getLimit(symbol, limit)
172
+ return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
173
+
174
+ def handle_my_trades(self, client: Client, message):
175
+ #
176
+ # {
177
+ # "type": "fill",
178
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
179
+ # "product_id": 1,
180
+ # # the subaccount that placed self order
181
+ # "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
182
+ # # hash of the order that uniquely identifies it
183
+ # "order_digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff",
184
+ # # the amount filled, multiplied by 1e18
185
+ # "filled_qty": "1000",
186
+ # # the amount outstanding unfilled, multiplied by 1e18
187
+ # "remaining_qty": "2000",
188
+ # # the original order amount, multiplied by 1e18
189
+ # "original_qty": "3000",
190
+ # # fill price
191
+ # "price": "24991000000000000000000",
192
+ # # True for `taker`, False for `maker`
193
+ # "is_taker": True,
194
+ # "is_bid": True,
195
+ # # True when matching against amm
196
+ # "is_against_amm": True,
197
+ # # an optional `order id` that can be provided when placing an order
198
+ # "id": 100
199
+ # }
200
+ #
201
+ topic = self.safe_string(message, 'type')
202
+ marketId = self.safe_string(message, 'product_id')
203
+ if self.myTrades is None:
204
+ limit = self.safe_integer(self.options, 'tradesLimit', 1000)
205
+ self.myTrades = ArrayCacheBySymbolById(limit)
206
+ trades = self.myTrades
207
+ parsed = self.parse_ws_trade(message)
208
+ trades.append(parsed)
209
+ client.resolve(trades, marketId + '@' + topic)
210
+
211
+ def parse_ws_trade(self, trade, market=None):
212
+ #
213
+ # watchTrades
214
+ # {
215
+ # "type": "trade",
216
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
217
+ # "product_id": 1,
218
+ # "price": "1000", # price the trade happened at, multiplied by 1e18
219
+ # # both taker_qty and maker_qty have the same value
220
+ # # set to filled amount(min amount of taker and maker) when matching against book
221
+ # # set to matched amm base amount when matching against amm
222
+ # "taker_qty": "1000",
223
+ # "maker_qty": "1000",
224
+ # "is_taker_buyer": True,
225
+ # "is_maker_amm": True # True when maker is amm
226
+ # }
227
+ # watchMyTrades
228
+ # {
229
+ # "type": "fill",
230
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
231
+ # "product_id": 1,
232
+ # # the subaccount that placed self order
233
+ # "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000",
234
+ # # hash of the order that uniquely identifies it
235
+ # "order_digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff",
236
+ # # the amount filled, multiplied by 1e18
237
+ # "filled_qty": "1000",
238
+ # # the amount outstanding unfilled, multiplied by 1e18
239
+ # "remaining_qty": "2000",
240
+ # # the original order amount, multiplied by 1e18
241
+ # "original_qty": "3000",
242
+ # # fill price
243
+ # "price": "24991000000000000000000",
244
+ # # True for `taker`, False for `maker`
245
+ # "is_taker": True,
246
+ # "is_bid": True,
247
+ # # True when matching against amm
248
+ # "is_against_amm": True,
249
+ # # an optional `order id` that can be provided when placing an order
250
+ # "id": 100
251
+ # }
252
+ #
253
+ marketId = self.safe_string(trade, 'product_id')
254
+ market = self.safe_market(marketId, market)
255
+ symbol = market['symbol']
256
+ price = self.convertFromX18(self.safe_string(trade, 'price'))
257
+ amount = self.convertFromX18(self.safe_string_2(trade, 'taker_qty', 'filled_qty'))
258
+ cost = Precise.string_mul(price, amount)
259
+ timestamp = Precise.string_div(self.safe_string(trade, 'timestamp'), '1000000')
260
+ takerOrMaker = None
261
+ isTaker = self.safe_bool(trade, 'is_taker')
262
+ if isTaker is not None:
263
+ takerOrMaker = 'taker' if (isTaker) else 'maker'
264
+ side = None
265
+ isBid = self.safe_bool(trade, 'is_bid')
266
+ if isBid is not None:
267
+ side = 'buy' if (isBid) else 'sell'
268
+ return self.safe_trade({
269
+ 'id': None,
270
+ 'timestamp': timestamp,
271
+ 'datetime': self.iso8601(timestamp),
272
+ 'symbol': symbol,
273
+ 'side': side,
274
+ 'price': price,
275
+ 'amount': amount,
276
+ 'cost': cost,
277
+ 'order': self.safe_string_2(trade, 'digest', 'id'),
278
+ 'takerOrMaker': takerOrMaker,
279
+ 'type': None,
280
+ 'fee': None,
281
+ 'info': trade,
282
+ }, market)
283
+
284
+ async def watch_ticker(self, symbol: str, params={}) -> Ticker:
285
+ """
286
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
287
+ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
288
+ :param str symbol: unified symbol of the market to fetch the ticker for
289
+ :param dict [params]: extra parameters specific to the exchange API endpoint
290
+ :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
291
+ """
292
+ await self.load_markets()
293
+ name = 'best_bid_offer'
294
+ market = self.market(symbol)
295
+ topic = market['id'] + '@' + name
296
+ request = {
297
+ 'method': 'subscribe',
298
+ 'stream': {
299
+ 'type': name,
300
+ 'product_id': self.parse_to_numeric(market['id']),
301
+ },
302
+ }
303
+ message = self.extend(request, params)
304
+ return await self.watch_public(topic, message)
305
+
306
+ def parse_ws_ticker(self, ticker, market=None):
307
+ #
308
+ # {
309
+ # "type": "best_bid_offer",
310
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
311
+ # "product_id": 1,
312
+ # "bid_price": "1000", # the highest bid price, multiplied by 1e18
313
+ # "bid_qty": "1000", # quantity at the huighest bid, multiplied by 1e18.
314
+ # # i.e. if self is USDC with 6 decimals, one USDC
315
+ # # would be 1e12
316
+ # "ask_price": "1000", # lowest ask price
317
+ # "ask_qty": "1000" # quantity at the lowest ask
318
+ # }
319
+ #
320
+ timestamp = Precise.string_div(self.safe_string(ticker, 'timestamp'), '1000000')
321
+ return self.safe_ticker({
322
+ 'symbol': self.safe_symbol(None, market),
323
+ 'timestamp': timestamp,
324
+ 'datetime': self.iso8601(timestamp),
325
+ 'high': self.safe_string(ticker, 'high'),
326
+ 'low': self.safe_string(ticker, 'low'),
327
+ 'bid': self.convertFromX18(self.safe_string(ticker, 'bid_price')),
328
+ 'bidVolume': self.convertFromX18(self.safe_string(ticker, 'bid_qty')),
329
+ 'ask': self.convertFromX18(self.safe_string(ticker, 'ask_price')),
330
+ 'askVolume': self.convertFromX18(self.safe_string(ticker, 'ask_qty')),
331
+ 'vwap': None,
332
+ 'open': None,
333
+ 'close': None,
334
+ 'last': None,
335
+ 'previousClose': None,
336
+ 'change': None,
337
+ 'percentage': None,
338
+ 'average': None,
339
+ 'baseVolume': None,
340
+ 'quoteVolume': None,
341
+ 'info': ticker,
342
+ }, market)
343
+
344
+ def handle_ticker(self, client: Client, message):
345
+ #
346
+ # {
347
+ # "type": "best_bid_offer",
348
+ # "timestamp": "1676151190656903000", # timestamp of the event in nanoseconds
349
+ # "product_id": 1,
350
+ # "bid_price": "1000", # the highest bid price, multiplied by 1e18
351
+ # "bid_qty": "1000", # quantity at the huighest bid, multiplied by 1e18.
352
+ # # i.e. if self is USDC with 6 decimals, one USDC
353
+ # # would be 1e12
354
+ # "ask_price": "1000", # lowest ask price
355
+ # "ask_qty": "1000" # quantity at the lowest ask
356
+ # }
357
+ #
358
+ marketId = self.safe_string(message, 'product_id')
359
+ market = self.safe_market(marketId)
360
+ ticker = self.parse_ws_ticker(message, market)
361
+ ticker['symbol'] = market['symbol']
362
+ self.tickers[market['symbol']] = ticker
363
+ client.resolve(ticker, marketId + '@best_bid_offer')
364
+ return message
365
+
366
+ async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
367
+ """
368
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
369
+ watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
370
+ :param str symbol: unified symbol of the market to fetch the order book for
371
+ :param int [limit]: the maximum amount of order book entries to return.
372
+ :param dict [params]: extra parameters specific to the exchange API endpoint
373
+ :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
374
+ """
375
+ await self.load_markets()
376
+ name = 'book_depth'
377
+ market = self.market(symbol)
378
+ messageHash = market['id'] + '@' + name
379
+ url = self.urls['api']['ws']
380
+ requestId = self.request_id(url)
381
+ request: dict = {
382
+ 'id': requestId,
383
+ 'method': 'subscribe',
384
+ 'stream': {
385
+ 'type': name,
386
+ 'product_id': self.parse_to_numeric(market['id']),
387
+ },
388
+ }
389
+ subscription: dict = {
390
+ 'id': str(requestId),
391
+ 'name': name,
392
+ 'symbol': symbol,
393
+ 'method': self.handle_order_book_subscription,
394
+ 'limit': limit,
395
+ 'params': params,
396
+ }
397
+ message = self.extend(request, params)
398
+ orderbook = await self.watch(url, messageHash, message, messageHash, subscription)
399
+ return orderbook.limit()
400
+
401
+ def handle_order_book_subscription(self, client: Client, message, subscription):
402
+ defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
403
+ limit = self.safe_integer(subscription, 'limit', defaultLimit)
404
+ symbol = self.safe_string(subscription, 'symbol') # watchOrderBook
405
+ if symbol in self.orderbooks:
406
+ del self.orderbooks[symbol]
407
+ self.orderbooks[symbol] = self.order_book({}, limit)
408
+ self.spawn(self.fetch_order_book_snapshot, client, message, subscription)
409
+
410
+ async def fetch_order_book_snapshot(self, client, message, subscription):
411
+ symbol = self.safe_string(subscription, 'symbol')
412
+ market = self.market(symbol)
413
+ messageHash = market['id'] + '@book_depth'
414
+ try:
415
+ defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
416
+ limit = self.safe_integer(subscription, 'limit', defaultLimit)
417
+ params = self.safe_value(subscription, 'params')
418
+ snapshot = await self.fetch_rest_order_book_safe(symbol, limit, params)
419
+ if self.safe_value(self.orderbooks, symbol) is None:
420
+ # if the orderbook is dropped before the snapshot is received
421
+ return
422
+ orderbook = self.orderbooks[symbol]
423
+ orderbook.reset(snapshot)
424
+ messages = orderbook.cache
425
+ for i in range(0, len(messages)):
426
+ messageItem = messages[i]
427
+ lastTimestamp = self.parse_to_int(Precise.string_div(self.safe_string(messageItem, 'last_max_timestamp'), '1000000'))
428
+ if lastTimestamp < orderbook['timestamp']:
429
+ continue
430
+ else:
431
+ self.handle_order_book_message(client, messageItem, orderbook)
432
+ self.orderbooks[symbol] = orderbook
433
+ client.resolve(orderbook, messageHash)
434
+ except Exception as e:
435
+ del client.subscriptions[messageHash]
436
+ client.reject(e, messageHash)
437
+
438
+ def handle_order_book(self, client: Client, message):
439
+ #
440
+ #
441
+ # the feed does not include a snapshot, just the deltas
442
+ #
443
+ # {
444
+ # "type":"book_depth",
445
+ # # book depth aggregates a number of events once every 50ms
446
+ # # these are the minimum and maximum timestamps from
447
+ # # events that contributed to self response
448
+ # "min_timestamp": "1683805381879572835",
449
+ # "max_timestamp": "1683805381879572835",
450
+ # # the max_timestamp of the last book_depth event for self product
451
+ # "last_max_timestamp": "1683805381771464799",
452
+ # "product_id":1,
453
+ # # changes to the bid side of the book in the form of [[price, new_qty]]
454
+ # "bids":[["21594490000000000000000","51007390115411548"]],
455
+ # # changes to the ask side of the book in the form of [[price, new_qty]]
456
+ # "asks":[["21694490000000000000000","0"],["21695050000000000000000","0"]]
457
+ # }
458
+ #
459
+ marketId = self.safe_string(message, 'product_id')
460
+ market = self.safe_market(marketId)
461
+ symbol = market['symbol']
462
+ if not (symbol in self.orderbooks):
463
+ self.orderbooks[symbol] = self.order_book()
464
+ orderbook = self.orderbooks[symbol]
465
+ timestamp = self.safe_integer(orderbook, 'timestamp')
466
+ if timestamp is None:
467
+ # Buffer the events you receive from the stream.
468
+ orderbook.cache.append(message)
469
+ else:
470
+ lastTimestamp = self.parse_to_int(Precise.string_div(self.safe_string(message, 'last_max_timestamp'), '1000000'))
471
+ if lastTimestamp > timestamp:
472
+ self.handle_order_book_message(client, message, orderbook)
473
+ client.resolve(orderbook, marketId + '@book_depth')
474
+
475
+ def handle_order_book_message(self, client: Client, message, orderbook):
476
+ timestamp = self.parse_to_int(Precise.string_div(self.safe_string(message, 'last_max_timestamp'), '1000000'))
477
+ # convert from X18
478
+ data = {
479
+ 'bids': [],
480
+ 'asks': [],
481
+ }
482
+ bids = self.safe_list(message, 'bids', [])
483
+ for i in range(0, len(bids)):
484
+ bid = bids[i]
485
+ data['bids'].append([
486
+ self.convertFromX18(bid[0]),
487
+ self.convertFromX18(bid[1]),
488
+ ])
489
+ asks = self.safe_list(message, 'asks', [])
490
+ for i in range(0, len(asks)):
491
+ ask = asks[i]
492
+ data['asks'].append([
493
+ self.convertFromX18(ask[0]),
494
+ self.convertFromX18(ask[1]),
495
+ ])
496
+ self.handle_deltas(orderbook['asks'], self.safe_list(data, 'asks', []))
497
+ self.handle_deltas(orderbook['bids'], self.safe_list(data, 'bids', []))
498
+ orderbook['timestamp'] = timestamp
499
+ orderbook['datetime'] = self.iso8601(timestamp)
500
+ return orderbook
501
+
502
+ def handle_delta(self, bookside, delta):
503
+ price = self.safe_float(delta, 0)
504
+ amount = self.safe_float(delta, 1)
505
+ bookside.store(price, amount)
506
+
507
+ def handle_deltas(self, bookside, deltas):
508
+ for i in range(0, len(deltas)):
509
+ self.handle_delta(bookside, deltas[i])
510
+
511
+ def handle_subscription_status(self, client: Client, message):
512
+ #
513
+ # {
514
+ # "result": null,
515
+ # "id": 1574649734450
516
+ # }
517
+ #
518
+ id = self.safe_string(message, 'id')
519
+ subscriptionsById = self.index_by(client.subscriptions, 'id')
520
+ subscription = self.safe_value(subscriptionsById, id, {})
521
+ method = self.safe_value(subscription, 'method')
522
+ if method is not None:
523
+ method(client, message, subscription)
524
+ return message
525
+
526
+ async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
527
+ """
528
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
529
+ watch all open positions
530
+ :param str[]|None symbols: list of unified market symbols
531
+ :param dict params: extra parameters specific to the exchange API endpoint
532
+ :param str [params.user]: user address, will default to self.walletAddress if not provided
533
+ :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
534
+ """
535
+ await self.load_markets()
536
+ symbols = self.market_symbols(symbols)
537
+ if not self.is_empty(symbols):
538
+ if len(symbols) > 1:
539
+ raise NotSupported(self.id + ' watchPositions require only one symbol.')
540
+ else:
541
+ raise ArgumentsRequired(self.id + ' watchPositions require one symbol.')
542
+ userAddress = None
543
+ userAddress, params = self.handlePublicAddress('watchPositions', params)
544
+ url = self.urls['api']['ws']
545
+ client = self.client(url)
546
+ self.set_positions_cache(client, symbols, params)
547
+ fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True)
548
+ awaitPositionsSnapshot = self.safe_bool('watchPositions', 'awaitPositionsSnapshot', True)
549
+ if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None:
550
+ snapshot = await client.future('fetchPositionsSnapshot')
551
+ return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True)
552
+ name = 'position_change'
553
+ market = self.market(symbols[0])
554
+ topic = market['id'] + '@' + name
555
+ request = {
556
+ 'method': 'subscribe',
557
+ 'stream': {
558
+ 'type': name,
559
+ 'product_id': self.parse_to_numeric(market['id']),
560
+ 'subaccount': self.convertAddressToSender(userAddress),
561
+ },
562
+ }
563
+ message = self.extend(request, params)
564
+ newPositions = await self.watch_public(topic, message)
565
+ if self.newUpdates:
566
+ limit = newPositions.getLimit(symbols[0], limit)
567
+ return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)
568
+
569
+ def set_positions_cache(self, client: Client, symbols: Strings = None, params={}):
570
+ fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False)
571
+ if fetchPositionsSnapshot:
572
+ messageHash = 'fetchPositionsSnapshot'
573
+ if not (messageHash in client.futures):
574
+ client.future(messageHash)
575
+ self.spawn(self.load_positions_snapshot, client, messageHash, symbols, params)
576
+ else:
577
+ self.positions = ArrayCacheBySymbolBySide()
578
+
579
+ async def load_positions_snapshot(self, client, messageHash, symbols, params):
580
+ positions = await self.fetch_positions(symbols, params)
581
+ self.positions = ArrayCacheBySymbolBySide()
582
+ cache = self.positions
583
+ for i in range(0, len(positions)):
584
+ position = positions[i]
585
+ cache.append(position)
586
+ # don't remove the future from the .futures cache
587
+ future = client.futures[messageHash]
588
+ future.resolve(cache)
589
+ client.resolve(cache, 'positions')
590
+
591
+ def handle_positions(self, client, message):
592
+ #
593
+ # {
594
+ # "type":"position_change",
595
+ # "timestamp": "1676151190656903000", # timestamp of event in nanoseconds
596
+ # "product_id":1,
597
+ # # whether self is a position change for the LP token for self product
598
+ # "is_lp":false,
599
+ # # subaccount who's position changed
600
+ # "subaccount":"0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43706d00000000000000000000",
601
+ # # new amount for self product
602
+ # "amount":"51007390115411548",
603
+ # # new quote balance for self product; zero for everything except non lp perps
604
+ # # the negative of the entry cost of the perp
605
+ # "v_quote_amount":"0"
606
+ # }
607
+ #
608
+ if self.positions is None:
609
+ self.positions = ArrayCacheBySymbolBySide()
610
+ cache = self.positions
611
+ topic = self.safe_string(message, 'type')
612
+ marketId = self.safe_string(message, 'product_id')
613
+ market = self.safe_market(marketId)
614
+ position = self.parse_ws_position(message, market)
615
+ cache.append(position)
616
+ client.resolve(position, marketId + '@' + topic)
617
+
618
+ def parse_ws_position(self, position, market=None):
619
+ #
620
+ # {
621
+ # "type":"position_change",
622
+ # "timestamp": "1676151190656903000", # timestamp of event in nanoseconds
623
+ # "product_id":1,
624
+ # # whether self is a position change for the LP token for self product
625
+ # "is_lp":false,
626
+ # # subaccount who's position changed
627
+ # "subaccount":"0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43706d00000000000000000000",
628
+ # # new amount for self product
629
+ # "amount":"51007390115411548",
630
+ # # new quote balance for self product; zero for everything except non lp perps
631
+ # # the negative of the entry cost of the perp
632
+ # "v_quote_amount":"0"
633
+ # }
634
+ #
635
+ marketId = self.safe_string(position, 'product_id')
636
+ market = self.safe_market(marketId)
637
+ contractSize = self.convertFromX18(self.safe_string(position, 'amount'))
638
+ side = 'buy'
639
+ if Precise.string_lt(contractSize, '1'):
640
+ side = 'sell'
641
+ timestamp = self.parse_to_int(Precise.string_div(self.safe_string(position, 'timestamp'), '1000000'))
642
+ return self.safe_position({
643
+ 'info': position,
644
+ 'id': None,
645
+ 'symbol': self.safe_string(market, 'symbol'),
646
+ 'timestamp': timestamp,
647
+ 'datetime': self.iso8601(timestamp),
648
+ 'lastUpdateTimestamp': None,
649
+ 'initialMargin': None,
650
+ 'initialMarginPercentage': None,
651
+ 'maintenanceMargin': None,
652
+ 'maintenanceMarginPercentage': None,
653
+ 'entryPrice': None,
654
+ 'notional': None,
655
+ 'leverage': None,
656
+ 'unrealizedPnl': None,
657
+ 'contracts': None,
658
+ 'contractSize': self.parse_number(contractSize),
659
+ 'marginRatio': None,
660
+ 'liquidationPrice': None,
661
+ 'markPrice': None,
662
+ 'lastPrice': None,
663
+ 'collateral': None,
664
+ 'marginMode': 'cross',
665
+ 'marginType': None,
666
+ 'side': side,
667
+ 'percentage': None,
668
+ 'hedged': None,
669
+ 'stopLossPrice': None,
670
+ 'takeProfitPrice': None,
671
+ })
672
+
673
+ def handle_auth(self, client: Client, message):
674
+ #
675
+ # {result: null, id: 1}
676
+ #
677
+ messageHash = 'authenticated'
678
+ error = self.safe_string(message, 'error')
679
+ if error is None:
680
+ # client.resolve(message, messageHash)
681
+ future = self.safe_value(client.futures, 'authenticated')
682
+ future.resolve(True)
683
+ else:
684
+ authError = AuthenticationError(self.json(message))
685
+ client.reject(authError, messageHash)
686
+ # allows further authentication attempts
687
+ if messageHash in client.subscriptions:
688
+ del client.subscriptions['authenticated']
689
+
690
+ def build_ws_authentication_sig(self, message, chainId, verifyingContractAddress):
691
+ messageTypes = {
692
+ 'StreamAuthentication': [
693
+ {'name': 'sender', 'type': 'bytes32'},
694
+ {'name': 'expiration', 'type': 'uint64'},
695
+ ],
696
+ }
697
+ return self.buildSig(chainId, messageTypes, message, verifyingContractAddress)
698
+
699
+ async def authenticate(self, params={}):
700
+ self.check_required_credentials()
701
+ url = self.urls['api']['ws']
702
+ client = self.client(url)
703
+ messageHash = 'authenticated'
704
+ future = client.future(messageHash)
705
+ authenticated = self.safe_value(client.subscriptions, messageHash)
706
+ if authenticated is None:
707
+ requestId = self.request_id(url)
708
+ contracts = await self.queryContracts()
709
+ chainId = self.safe_string(contracts, 'chain_id')
710
+ verifyingContractAddress = self.safe_string(contracts, 'endpoint_addr')
711
+ now = self.nonce()
712
+ nonce = now + 90000
713
+ authentication = {
714
+ 'sender': self.convertAddressToSender(self.walletAddress),
715
+ 'expiration': nonce,
716
+ }
717
+ request = {
718
+ 'id': requestId,
719
+ 'method': 'authenticate',
720
+ 'tx': {
721
+ 'sender': authentication['sender'],
722
+ 'expiration': self.number_to_string(authentication['expiration']),
723
+ },
724
+ 'signature': self.build_ws_authentication_sig(authentication, chainId, verifyingContractAddress),
725
+ }
726
+ message = self.extend(request, params)
727
+ self.watch(url, messageHash, message, messageHash)
728
+ return await future
729
+
730
+ async def watch_private(self, messageHash, message, params={}):
731
+ await self.authenticate(params)
732
+ url = self.urls['api']['ws']
733
+ requestId = self.request_id(url)
734
+ subscribe = {
735
+ 'id': requestId,
736
+ }
737
+ request = self.extend(subscribe, message)
738
+ return await self.watch(url, messageHash, request, messageHash, subscribe)
739
+
740
+ async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
741
+ """
742
+ watches information on multiple orders made by the user
743
+ :see: https://docs.vertexprotocol.com/developer-resources/api/subscriptions/streams
744
+ :param str symbol: unified market symbol of the market orders were made in
745
+ :param int [since]: the earliest time in ms to fetch orders for
746
+ :param int [limit]: the maximum number of order structures to retrieve
747
+ :param dict [params]: extra parameters specific to the exchange API endpoint
748
+ :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
749
+ """
750
+ if symbol is None:
751
+ raise ArgumentsRequired(self.id + ' watchOrders requires a symbol.')
752
+ self.check_required_credentials()
753
+ await self.load_markets()
754
+ name = 'order_update'
755
+ market = self.market(symbol)
756
+ topic = market['id'] + '@' + name
757
+ request = {
758
+ 'method': 'subscribe',
759
+ 'stream': {
760
+ 'type': name,
761
+ 'subaccount': self.convertAddressToSender(self.walletAddress),
762
+ 'product_id': self.parse_to_numeric(market['id']),
763
+ },
764
+ }
765
+ message = self.extend(request, params)
766
+ orders = await self.watch_private(topic, message)
767
+ if self.newUpdates:
768
+ limit = orders.getLimit(symbol, limit)
769
+ return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
770
+
771
+ def parse_ws_order_status(self, status):
772
+ if status is not None:
773
+ statuses = {
774
+ 'filled': 'open',
775
+ 'placed': 'open',
776
+ 'cancelled': 'canceled',
777
+ }
778
+ return self.safe_string(statuses, status, status)
779
+ return status
780
+
781
+ def parse_ws_order(self, order, market: Market = None) -> Order:
782
+ #
783
+ # {
784
+ # "type": "order_update",
785
+ # # timestamp of the event in nanoseconds
786
+ # "timestamp": "1695081920633151000",
787
+ # "product_id": 1,
788
+ # # order digest
789
+ # "digest": "0xf7712b63ccf70358db8f201e9bf33977423e7a63f6a16f6dab180bdd580f7c6c",
790
+ # # remaining amount to be filled.
791
+ # # will be `0` if the order is either fully filled or cancelled.
792
+ # "amount": "82000000000000000",
793
+ # # any of: "filled", "cancelled", "placed"
794
+ # "reason": "filled"
795
+ # # an optional `order id` that can be provided when placing an order
796
+ # "id": 100
797
+ # }
798
+ #
799
+ marketId = self.safe_string(order, 'product_id')
800
+ timestamp = self.parse_to_int(Precise.string_div(self.safe_string(order, 'timestamp'), '1000000'))
801
+ remaining = self.parse_to_numeric(self.convertFromX18(self.safe_string(order, 'amount')))
802
+ status = self.parse_ws_order_status(self.safe_string(order, 'reason'))
803
+ if remaining == 0 and status == 'open':
804
+ status = 'closed'
805
+ market = self.safe_market(marketId, market)
806
+ symbol = market['symbol']
807
+ return self.safe_order({
808
+ 'info': order,
809
+ 'id': self.safe_string_2(order, 'digest', 'id'),
810
+ 'clientOrderId': None,
811
+ 'timestamp': timestamp,
812
+ 'datetime': self.iso8601(timestamp),
813
+ 'lastTradeTimestamp': None,
814
+ 'lastUpdateTimestamp': None,
815
+ 'symbol': symbol,
816
+ 'type': None,
817
+ 'timeInForce': None,
818
+ 'postOnly': None,
819
+ 'reduceOnly': None,
820
+ 'side': None,
821
+ 'price': None,
822
+ 'triggerPrice': None,
823
+ 'amount': None,
824
+ 'cost': None,
825
+ 'average': None,
826
+ 'filled': None,
827
+ 'remaining': remaining,
828
+ 'status': status,
829
+ 'fee': None,
830
+ 'trades': None,
831
+ }, market)
832
+
833
+ def handle_order_update(self, client: Client, message):
834
+ #
835
+ # {
836
+ # "type": "order_update",
837
+ # # timestamp of the event in nanoseconds
838
+ # "timestamp": "1695081920633151000",
839
+ # "product_id": 1,
840
+ # # order digest
841
+ # "digest": "0xf7712b63ccf70358db8f201e9bf33977423e7a63f6a16f6dab180bdd580f7c6c",
842
+ # # remaining amount to be filled.
843
+ # # will be `0` if the order is either fully filled or cancelled.
844
+ # "amount": "82000000000000000",
845
+ # # any of: "filled", "cancelled", "placed"
846
+ # "reason": "filled"
847
+ # # an optional `order id` that can be provided when placing an order
848
+ # "id": 100
849
+ # }
850
+ #
851
+ topic = self.safe_string(message, 'type')
852
+ marketId = self.safe_string(message, 'product_id')
853
+ parsed = self.parse_ws_order(message)
854
+ symbol = self.safe_string(parsed, 'symbol')
855
+ orderId = self.safe_string(parsed, 'id')
856
+ if symbol is not None:
857
+ if self.orders is None:
858
+ limit = self.safe_integer(self.options, 'ordersLimit', 1000)
859
+ self.orders = ArrayCacheBySymbolById(limit)
860
+ cachedOrders = self.orders
861
+ orders = self.safe_dict(cachedOrders.hashmap, symbol, {})
862
+ order = self.safe_dict(orders, orderId)
863
+ if order is not None:
864
+ parsed['timestamp'] = self.safe_integer(order, 'timestamp')
865
+ parsed['datetime'] = self.safe_string(order, 'datetime')
866
+ cachedOrders.append(parsed)
867
+ client.resolve(self.orders, marketId + '@' + topic)
868
+
869
+ def handle_error_message(self, client: Client, message):
870
+ #
871
+ # {
872
+ # result: null,
873
+ # error: 'error parsing request: missing field `expiration`',
874
+ # id: 0
875
+ # }
876
+ #
877
+ errorMessage = self.safe_string(message, 'error')
878
+ try:
879
+ if errorMessage is not None:
880
+ feedback = self.id + ' ' + self.json(message)
881
+ self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback)
882
+ return False
883
+ except Exception as error:
884
+ if isinstance(error, AuthenticationError):
885
+ messageHash = 'authenticated'
886
+ client.reject(error, messageHash)
887
+ if messageHash in client.subscriptions:
888
+ del client.subscriptions[messageHash]
889
+ else:
890
+ client.reject(error)
891
+ return True
892
+
893
+ def handle_message(self, client: Client, message):
894
+ if self.handle_error_message(client, message):
895
+ return
896
+ methods = {
897
+ 'trade': self.handle_trade,
898
+ 'best_bid_offer': self.handle_ticker,
899
+ 'book_depth': self.handle_order_book,
900
+ 'fill': self.handle_my_trades,
901
+ 'position_change': self.handle_positions,
902
+ 'order_update': self.handle_order_update,
903
+ }
904
+ event = self.safe_string(message, 'type')
905
+ method = self.safe_value(methods, event)
906
+ if method is not None:
907
+ method(client, message)
908
+ return
909
+ requestId = self.safe_string(message, 'id')
910
+ if requestId is not None:
911
+ self.handle_subscription_status(client, message)
912
+ return
913
+ # check whether it's authentication
914
+ auth = self.safe_value(client.futures, 'authenticated')
915
+ if auth is not None:
916
+ self.handle_auth(client, message)