avantis-trader-sdk 0.8.12__py3-none-any.whl → 0.8.14__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.
@@ -1,4 +1,5 @@
1
1
  from ..feed.feed_client import FeedClient
2
+ from ..config import AVANTIS_CORE_API_BASE_URL
2
3
  from ..types import (
3
4
  TradeInput,
4
5
  TradeInputOrderType,
@@ -7,9 +8,12 @@ from ..types import (
7
8
  TradeInfo,
8
9
  PendingLimitOrderExtendedResponse,
9
10
  MarginUpdateType,
11
+ PriceSourcing,
10
12
  )
11
- from typing import Optional
13
+ from typing import Optional, List, Tuple
12
14
  import math
15
+ import asyncio
16
+ import requests
13
17
 
14
18
 
15
19
  class TradeRPC:
@@ -17,16 +21,23 @@ class TradeRPC:
17
21
  The TradeRPC class contains methods for retrieving trading parameters from the Avantis Protocol.
18
22
  """
19
23
 
20
- def __init__(self, client, feed_client: FeedClient):
24
+ def __init__(
25
+ self,
26
+ client,
27
+ feed_client: FeedClient,
28
+ core_api_base_url: Optional[str] = None,
29
+ ):
21
30
  """
22
31
  Constructor for the TradeRPC class.
23
32
 
24
33
  Args:
25
34
  client: The TraderClient object.
26
35
  feed_client: The FeedClient object.
36
+ core_api_base_url: Optional override for the API base URL.
27
37
  """
28
38
  self.client = client
29
39
  self.feed_client = feed_client
40
+ self.core_api_base_url = core_api_base_url or AVANTIS_CORE_API_BASE_URL
30
41
 
31
42
  async def build_trade_open_tx(
32
43
  self,
@@ -193,28 +204,143 @@ class TradeRPC:
193
204
  print("Error getting correct trade execution fee. Using fallback: ", e)
194
205
  return execution_fee_wei
195
206
 
196
- async def get_trades(self, trader: Optional[str] = None):
207
+ async def get_trades(
208
+ self,
209
+ trader: Optional[str] = None,
210
+ use_api: bool = True,
211
+ ) -> Tuple[List[TradeExtendedResponse], List[PendingLimitOrderExtendedResponse]]:
197
212
  """
198
- Gets the trades.
213
+ Gets the trades and pending limit orders for a trader.
214
+
215
+ Attempts to fetch from API first for better performance. Falls back to
216
+ paginated smart contract calls if API is unavailable or disabled.
199
217
 
200
218
  Args:
201
219
  trader: The trader's wallet address.
220
+ use_api: Whether to attempt API fetch first. Defaults to True.
202
221
 
203
222
  Returns:
204
- The trades.
223
+ A tuple of (trades, pending_limit_orders).
205
224
  """
206
225
  if trader is None:
207
226
  trader = self.client.get_signer().get_ethereum_address()
208
227
 
228
+ if use_api:
229
+ api_enabled = await self._check_api_enabled(trader)
230
+ if api_enabled:
231
+ try:
232
+ return await self._fetch_trades_from_api(trader)
233
+ except Exception:
234
+ pass
235
+
236
+ return await self._fetch_trades_from_contracts(trader)
237
+
238
+ async def _check_api_enabled(self, trader: str) -> bool:
239
+ """Checks if the API is enabled for the given trader."""
240
+ try:
241
+ response = requests.get(
242
+ f"{self.core_api_base_url}/user-data/config",
243
+ params={"wallet": trader},
244
+ timeout=5,
245
+ )
246
+ response.raise_for_status()
247
+ data = response.json()
248
+ return data.get("globallyEnabled", False) or data.get(
249
+ "enabledForWallet", False
250
+ )
251
+ except Exception:
252
+ return False
253
+
254
+ async def _fetch_trades_from_api(
255
+ self, trader: str
256
+ ) -> Tuple[List[TradeExtendedResponse], List[PendingLimitOrderExtendedResponse]]:
257
+ """Fetches trades from the API."""
258
+ response = requests.get(
259
+ f"{self.core_api_base_url}/user-data",
260
+ params={"trader": trader},
261
+ timeout=10,
262
+ )
263
+ response.raise_for_status()
264
+ data = response.json()
265
+
266
+ trades = []
267
+ for position in data.get("positions", []):
268
+ loss_protection_tier = int(position.get("lossProtection", 0))
269
+ pair_index = int(position.get("pairIndex", 0))
270
+ loss_protection_pct = await self.client.trading_parameters.get_loss_protection_percentage_by_tier(
271
+ loss_protection_tier, pair_index
272
+ )
273
+ position["lossProtectionPercentage"] = loss_protection_pct
274
+ trades.append(TradeExtendedResponse(**position))
275
+
276
+ limit_orders = [
277
+ PendingLimitOrderExtendedResponse(**order)
278
+ for order in data.get("limitOrders", [])
279
+ ]
280
+
281
+ return trades, limit_orders
282
+
283
+ async def _fetch_trades_from_contracts(
284
+ self,
285
+ trader: str,
286
+ max_pairs_per_call: int = 12,
287
+ ) -> Tuple[List[TradeExtendedResponse], List[PendingLimitOrderExtendedResponse]]:
288
+ """Fetches trades from smart contracts with paginated calls."""
289
+ socket_info = await self.client.pairs_cache.get_info_from_socket()
290
+ max_trades_per_pair = socket_info.get("maxTradesPerPair", 40)
291
+
292
+ pairs_count = await self.client.pairs_cache.get_pairs_count()
293
+ pair_ranges = self._build_pair_ranges(pairs_count, max_pairs_per_call)
294
+
295
+ tasks = [
296
+ self._fetch_positions_for_range(trader, start, end, max_trades_per_pair)
297
+ for start, end in pair_ranges
298
+ ]
299
+ results = await asyncio.gather(*tasks)
300
+
301
+ raw_trades = []
302
+ raw_orders = []
303
+ for trades_batch, orders_batch in results:
304
+ raw_trades.extend(trades_batch)
305
+ raw_orders.extend(orders_batch)
306
+
307
+ trades = await self._parse_raw_trades(raw_trades)
308
+ limit_orders = self._parse_raw_limit_orders(raw_orders)
309
+
310
+ return trades, limit_orders
311
+
312
+ def _build_pair_ranges(
313
+ self, pairs_count: int, max_pairs_per_call: int
314
+ ) -> List[Tuple[int, int]]:
315
+ """Builds pair index ranges for paginated fetching."""
316
+ ranges = []
317
+ for i in range(0, pairs_count, max_pairs_per_call):
318
+ start = i
319
+ end = min(i + max_pairs_per_call, pairs_count)
320
+ ranges.append((start, end))
321
+ return ranges
322
+
323
+ async def _fetch_positions_for_range(
324
+ self,
325
+ trader: str,
326
+ start_pair: int,
327
+ end_pair: int,
328
+ max_trades_per_pair: int,
329
+ ) -> Tuple[list, list]:
330
+ """Fetches positions for a range of pair indexes."""
209
331
  result = (
210
332
  await self.client.contracts.get("Multicall")
211
- .functions.getPositions(trader)
333
+ .functions.getPositionsForPairIndexes(
334
+ trader, start_pair, end_pair, max_trades_per_pair
335
+ )
212
336
  .call()
213
337
  )
214
- trades = []
215
- pendingOpenLimitOrders = []
338
+ return result[0], result[1]
216
339
 
217
- for aggregated_trade in result[0]: # Access the list of aggregated trades
340
+ async def _parse_raw_trades(self, raw_trades: list) -> List[TradeExtendedResponse]:
341
+ """Parses raw contract trade data into TradeExtendedResponse objects."""
342
+ trades = []
343
+ for aggregated_trade in raw_trades:
218
344
  (trade, trade_info, margin_fee, liquidation_price, is_zfp) = (
219
345
  aggregated_trade
220
346
  )
@@ -222,71 +348,64 @@ class TradeRPC:
222
348
  if trade[7] <= 0:
223
349
  continue
224
350
 
225
- # Extract and format the trade data
226
- trade_details = {
227
- "trade": {
228
- "trader": trade[0],
229
- "pairIndex": trade[1],
230
- "index": trade[2],
231
- "initialPosToken": trade[3],
232
- "positionSizeUSDC": trade[4],
233
- "openPrice": trade[5],
234
- "buy": trade[6],
235
- "leverage": trade[7],
236
- "tp": trade[8],
237
- "sl": trade[9],
238
- "timestamp": trade[10],
239
- },
240
- "additional_info": {
241
- "openInterestUSDC": trade_info[0],
242
- "tpLastUpdated": trade_info[1],
243
- "slLastUpdated": trade_info[2],
244
- "beingMarketClosed": trade_info[3],
245
- "lossProtectionPercentage": await self.client.trading_parameters.get_loss_protection_percentage_by_tier(
246
- trade_info[4], trade[1]
247
- ),
248
- },
249
- "margin_fee": margin_fee,
250
- "liquidationPrice": liquidation_price,
251
- "is_zfp": is_zfp,
252
- }
351
+ loss_protection = await self.client.trading_parameters.get_loss_protection_percentage_by_tier(
352
+ trade_info[4], trade[1]
353
+ )
354
+
253
355
  trades.append(
254
356
  TradeExtendedResponse(
255
- trade=TradeResponse(**trade_details["trade"]),
256
- additional_info=TradeInfo(**trade_details["additional_info"]),
257
- margin_fee=trade_details["margin_fee"],
258
- liquidation_price=trade_details["liquidationPrice"],
259
- is_zfp=trade_details["is_zfp"],
357
+ trade=TradeResponse(
358
+ trader=trade[0],
359
+ pairIndex=trade[1],
360
+ index=trade[2],
361
+ initialPosToken=trade[3],
362
+ positionSizeUSDC=trade[3],
363
+ openPrice=trade[5],
364
+ buy=trade[6],
365
+ leverage=trade[7],
366
+ tp=trade[8],
367
+ sl=trade[9],
368
+ timestamp=trade[10],
369
+ ),
370
+ additional_info=TradeInfo(
371
+ lossProtectionPercentage=loss_protection,
372
+ ),
373
+ margin_fee=margin_fee,
374
+ liquidation_price=liquidation_price,
375
+ is_zfp=is_zfp,
260
376
  )
261
377
  )
262
-
263
- for aggregated_order in result[1]: # Access the list of aggregated orders
378
+ return trades
379
+
380
+ def _parse_raw_limit_orders(
381
+ self, raw_orders: list
382
+ ) -> List[PendingLimitOrderExtendedResponse]:
383
+ """Parses raw contract order data into PendingLimitOrderExtendedResponse objects."""
384
+ orders = []
385
+ for aggregated_order in raw_orders:
264
386
  (order, liquidation_price) = aggregated_order
265
387
 
266
388
  if order[5] <= 0:
267
389
  continue
268
390
 
269
- # Extract and format the order data
270
- order_details = {
271
- "trader": order[0],
272
- "pairIndex": order[1],
273
- "index": order[2],
274
- "positionSize": order[3],
275
- "buy": order[4],
276
- "leverage": order[5],
277
- "tp": order[6],
278
- "sl": order[7],
279
- "price": order[8],
280
- "slippageP": order[9],
281
- "block": order[10],
282
- # 'executionFee': order[11],
283
- "liquidation_price": liquidation_price,
284
- }
285
- pendingOpenLimitOrders.append(
286
- PendingLimitOrderExtendedResponse(**order_details)
391
+ orders.append(
392
+ PendingLimitOrderExtendedResponse(
393
+ trader=order[0],
394
+ pairIndex=order[1],
395
+ index=order[2],
396
+ positionSize=order[3],
397
+ buy=order[4],
398
+ leverage=order[5],
399
+ tp=order[6],
400
+ sl=order[7],
401
+ price=order[8],
402
+ slippageP=order[9],
403
+ block=order[10],
404
+ executionFee=order[11],
405
+ liquidation_price=liquidation_price,
406
+ )
287
407
  )
288
-
289
- return trades, pendingOpenLimitOrders
408
+ return orders
290
409
 
291
410
  async def build_trade_close_tx(
292
411
  self,
@@ -477,6 +596,7 @@ class TradeRPC:
477
596
  margin_update_type: MarginUpdateType,
478
597
  collateral_change: float,
479
598
  trader: Optional[str] = None,
599
+ price_sourcing: PriceSourcing = PriceSourcing.PRO,
480
600
  ):
481
601
  """
482
602
  Builds a transaction to update the margin of a trade.
@@ -487,6 +607,7 @@ class TradeRPC:
487
607
  margin_update_type: The margin update type.
488
608
  collateral_change: The collateral change.
489
609
  trader (optional): The trader's wallet address.
610
+ price_sourcing: The price sourcing to use. Defaults to PriceSourcing.PRO (Pyth Pro/Lazer).
490
611
  Returns:
491
612
  A transaction object.
492
613
  """
@@ -497,11 +618,15 @@ class TradeRPC:
497
618
 
498
619
  collateral_change = int(collateral_change * 10**6)
499
620
 
500
- pair_name = await self.client.pairs_cache.get_pair_name_from_index(pair_index)
501
-
502
- price_data = await self.feed_client.get_latest_price_updates([pair_name])
503
-
504
- price_update_data = "0x" + price_data.binary.data[0]
621
+ if price_sourcing == PriceSourcing.PRO:
622
+ price_data = await self.feed_client.get_price_update_data(pair_index)
623
+ price_update_data = price_data.pro.price_update_data
624
+ else:
625
+ pair_name = await self.client.pairs_cache.get_pair_name_from_index(
626
+ pair_index
627
+ )
628
+ price_data = await self.feed_client.get_latest_price_updates([pair_name])
629
+ price_update_data = "0x" + price_data.binary.data[0]
505
630
 
506
631
  transaction = await Trading.functions.updateMargin(
507
632
  pair_index,
@@ -509,6 +634,7 @@ class TradeRPC:
509
634
  margin_update_type.value,
510
635
  collateral_change,
511
636
  [price_update_data],
637
+ price_sourcing.value,
512
638
  ).build_transaction(
513
639
  {
514
640
  "from": trader,
@@ -527,6 +653,7 @@ class TradeRPC:
527
653
  margin_update_type: MarginUpdateType,
528
654
  collateral_change: float,
529
655
  trader: Optional[str] = None,
656
+ price_sourcing: PriceSourcing = PriceSourcing.PRO,
530
657
  ):
531
658
  """
532
659
  Builds a transaction to update the margin of a trade.
@@ -537,6 +664,7 @@ class TradeRPC:
537
664
  margin_update_type: The margin update type.
538
665
  collateral_change: The collateral change.
539
666
  trader (optional): The trader's wallet address.
667
+ price_sourcing: The price sourcing to use. Defaults to PriceSourcing.PRO (Pyth Pro/Lazer).
540
668
  Returns:
541
669
  A transaction object.
542
670
  """
@@ -547,11 +675,15 @@ class TradeRPC:
547
675
 
548
676
  collateral_change = int(collateral_change * 10**6)
549
677
 
550
- pair_name = await self.client.pairs_cache.get_pair_name_from_index(pair_index)
551
-
552
- price_data = await self.feed_client.get_latest_price_updates([pair_name])
553
-
554
- price_update_data = "0x" + price_data.binary.data[0]
678
+ if price_sourcing == PriceSourcing.PRO:
679
+ price_data = await self.feed_client.get_price_update_data(pair_index)
680
+ price_update_data = price_data.pro.price_update_data
681
+ else:
682
+ pair_name = await self.client.pairs_cache.get_pair_name_from_index(
683
+ pair_index
684
+ )
685
+ price_data = await self.feed_client.get_latest_price_updates([pair_name])
686
+ price_update_data = "0x" + price_data.binary.data[0]
555
687
 
556
688
  transaction = await Trading.functions.updateMargin(
557
689
  pair_index,
@@ -559,6 +691,7 @@ class TradeRPC:
559
691
  margin_update_type.value,
560
692
  collateral_change,
561
693
  [price_update_data],
694
+ price_sourcing.value,
562
695
  ).build_transaction(
563
696
  {
564
697
  "from": trader,
@@ -590,6 +723,7 @@ class TradeRPC:
590
723
  take_profit_price: float,
591
724
  stop_loss_price: float,
592
725
  trader: str = None,
726
+ price_sourcing: PriceSourcing = PriceSourcing.PRO,
593
727
  ):
594
728
  """
595
729
  Builds a transaction to update the stop loss and take profit of a trade.
@@ -600,6 +734,7 @@ class TradeRPC:
600
734
  take_profit_price: The take profit price.
601
735
  stop_loss_price: The stop loss price. Pass 0 if you want to remove the stop loss.
602
736
  trader (optional): The trader's wallet address.
737
+ price_sourcing: The price sourcing to use. Defaults to PriceSourcing.PRO (Pyth Pro/Lazer).
603
738
  Returns:
604
739
  A transaction object.
605
740
  """
@@ -611,11 +746,15 @@ class TradeRPC:
611
746
  if trader is None:
612
747
  trader = self.client.get_signer().get_ethereum_address()
613
748
 
614
- pair_name = await self.client.pairs_cache.get_pair_name_from_index(pair_index)
615
-
616
- price_data = await self.feed_client.get_latest_price_updates([pair_name])
617
-
618
- price_update_data = "0x" + price_data.binary.data[0]
749
+ if price_sourcing == PriceSourcing.PRO:
750
+ price_data = await self.feed_client.get_price_update_data(pair_index)
751
+ price_update_data = price_data.pro.price_update_data
752
+ else:
753
+ pair_name = await self.client.pairs_cache.get_pair_name_from_index(
754
+ pair_index
755
+ )
756
+ price_data = await self.feed_client.get_latest_price_updates([pair_name])
757
+ price_update_data = "0x" + price_data.binary.data[0]
619
758
 
620
759
  take_profit_price = int(take_profit_price * 10**10)
621
760
  stop_loss_price = int(stop_loss_price * 10**10)
@@ -626,6 +765,7 @@ class TradeRPC:
626
765
  stop_loss_price,
627
766
  take_profit_price,
628
767
  [price_update_data],
768
+ price_sourcing.value,
629
769
  ).build_transaction(
630
770
  {
631
771
  "from": trader,
@@ -645,6 +785,7 @@ class TradeRPC:
645
785
  take_profit_price: float,
646
786
  stop_loss_price: float,
647
787
  trader: str = None,
788
+ price_sourcing: PriceSourcing = PriceSourcing.PRO,
648
789
  ):
649
790
  """
650
791
  Builds a transaction to update the stop loss and take profit of a trade.
@@ -655,6 +796,7 @@ class TradeRPC:
655
796
  take_profit_price: The take profit price.
656
797
  stop_loss_price: The stop loss price. Pass 0 if you want to remove the stop loss.
657
798
  trader (optional): The trader's wallet address.
799
+ price_sourcing: The price sourcing to use. Defaults to PriceSourcing.PRO (Pyth Pro/Lazer).
658
800
  Returns:
659
801
  A transaction object.
660
802
  """
@@ -666,13 +808,15 @@ class TradeRPC:
666
808
  if trader is None:
667
809
  trader = self.client.get_signer().get_ethereum_address()
668
810
 
669
- feed_client = self.FeedClient()
670
-
671
- pair_name = await self.client.pairs_cache.get_pair_name_from_index(pair_index)
672
-
673
- price_data = await self.feed_client.get_latest_price_updates([pair_name])
674
-
675
- price_update_data = "0x" + price_data.binary.data[0]
811
+ if price_sourcing == PriceSourcing.PRO:
812
+ price_data = await self.feed_client.get_price_update_data(pair_index)
813
+ price_update_data = price_data.pro.price_update_data
814
+ else:
815
+ pair_name = await self.client.pairs_cache.get_pair_name_from_index(
816
+ pair_index
817
+ )
818
+ price_data = await self.feed_client.get_latest_price_updates([pair_name])
819
+ price_update_data = "0x" + price_data.binary.data[0]
676
820
 
677
821
  take_profit_price = int(take_profit_price * 10**10)
678
822
  stop_loss_price = int(stop_loss_price * 10**10)
@@ -683,6 +827,7 @@ class TradeRPC:
683
827
  stop_loss_price,
684
828
  take_profit_price,
685
829
  [price_update_data],
830
+ price_sourcing.value,
686
831
  ).build_transaction(
687
832
  {
688
833
  "from": trader,
@@ -707,3 +852,79 @@ class TradeRPC:
707
852
  }
708
853
  )
709
854
  return delegate_transaction
855
+
856
+ async def get_delegate(self, trader: Optional[str] = None) -> str:
857
+ """
858
+ Gets the delegate address for a trader.
859
+
860
+ Args:
861
+ trader: The trader's wallet address. Defaults to signer's address.
862
+
863
+ Returns:
864
+ The delegate address, or zero address if no delegate is set.
865
+ """
866
+ Trading = self.client.contracts.get("Trading")
867
+
868
+ if trader is None:
869
+ trader = self.client.get_signer().get_ethereum_address()
870
+
871
+ delegate = await Trading.functions.delegations(trader).call()
872
+ return delegate
873
+
874
+ async def build_set_delegate_tx(
875
+ self,
876
+ delegate: str,
877
+ trader: Optional[str] = None,
878
+ ):
879
+ """
880
+ Builds a transaction to set a delegate for trading.
881
+
882
+ The delegate can perform all trade-related actions on behalf of the trader.
883
+ Each wallet can have at most one delegate.
884
+
885
+ Args:
886
+ delegate: The delegate's wallet address.
887
+ trader: The trader's wallet address. Defaults to signer's address.
888
+
889
+ Returns:
890
+ A transaction object.
891
+ """
892
+ Trading = self.client.contracts.get("Trading")
893
+
894
+ if trader is None:
895
+ trader = self.client.get_signer().get_ethereum_address()
896
+
897
+ transaction = await Trading.functions.setDelegate(delegate).build_transaction(
898
+ {
899
+ "from": trader,
900
+ "chainId": self.client.chain_id,
901
+ "nonce": await self.client.get_transaction_count(trader),
902
+ }
903
+ )
904
+
905
+ return transaction
906
+
907
+ async def build_remove_delegate_tx(self, trader: Optional[str] = None):
908
+ """
909
+ Builds a transaction to remove the current delegate.
910
+
911
+ Args:
912
+ trader: The trader's wallet address. Defaults to signer's address.
913
+
914
+ Returns:
915
+ A transaction object.
916
+ """
917
+ Trading = self.client.contracts.get("Trading")
918
+
919
+ if trader is None:
920
+ trader = self.client.get_signer().get_ethereum_address()
921
+
922
+ transaction = await Trading.functions.removeDelegate().build_transaction(
923
+ {
924
+ "from": trader,
925
+ "chainId": self.client.chain_id,
926
+ "nonce": await self.client.get_transaction_count(trader),
927
+ }
928
+ )
929
+
930
+ return transaction
@@ -57,7 +57,9 @@ class TradingParametersRPC:
57
57
  Returns:
58
58
  The loss protection percentage.
59
59
  """
60
- pair_info = await self.client.pairs_cache.get_pair_info_from_socket(pair_index)
60
+ pair_info = await self.client.pairs_cache.get_pair_info_from_socket(
61
+ pair_index, use_cache=True
62
+ )
61
63
  return pair_info["lossProtectionMultiplier"][str(tier)]
62
64
 
63
65
  async def get_loss_protection_percentage(self, trade: TradeInput):