afp-sdk 0.5.1__py3-none-any.whl → 0.5.2__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.
afp/api/trading.py CHANGED
@@ -1,25 +1,27 @@
1
1
  import secrets
2
+ import warnings
2
3
  from datetime import datetime
3
4
  from decimal import Decimal
4
- from typing import Generator
5
+ from typing import Generator, Iterable
5
6
 
6
7
  from web3 import Web3
7
8
 
8
9
  from .. import hashing, validators
10
+ from ..constants import DEFAULT_BATCH_SIZE
9
11
  from ..decorators import refresh_token_on_expiry
10
- from ..enums import OrderType
12
+ from ..enums import OrderSide, OrderState, OrderType, TradeState
11
13
  from ..schemas import (
12
14
  ExchangeProduct,
15
+ ExchangeProductFilter,
13
16
  Intent,
14
17
  IntentData,
15
18
  MarketDepthData,
16
19
  Order,
20
+ OrderFilter,
17
21
  OrderCancellationData,
18
22
  OrderFill,
19
23
  OrderFillFilter,
20
- OrderSide,
21
24
  OrderSubmission,
22
- TradeState,
23
25
  )
24
26
  from .base import ExchangeAPI
25
27
 
@@ -61,6 +63,7 @@ class Trading(ExchangeAPI):
61
63
  ----------
62
64
  product : afp.schemas.ExchangeProduct
63
65
  side : str
66
+ One of `BID` and `ASK`.
64
67
  limit_price : decimal.Decimal
65
68
  quantity : decimal.Decimal
66
69
  max_trading_fee_rate : decimal.Decimal
@@ -87,7 +90,7 @@ class Trading(ExchangeAPI):
87
90
  ),
88
91
  quantity=quantity,
89
92
  max_trading_fee_rate=max_trading_fee_rate,
90
- side=getattr(OrderSide, side.upper()),
93
+ side=OrderSide(side.upper()),
91
94
  good_until_time=good_until_time,
92
95
  nonce=self._generate_nonce(),
93
96
  )
@@ -161,14 +164,34 @@ class Trading(ExchangeAPI):
161
164
  )
162
165
  return self._exchange.submit_order(submission)
163
166
 
164
- def products(self) -> list[ExchangeProduct]:
167
+ def products(
168
+ self,
169
+ batch: int = 1,
170
+ batch_size: int = DEFAULT_BATCH_SIZE,
171
+ newest_first: bool = True,
172
+ ) -> list[ExchangeProduct]:
165
173
  """Retrieves the products approved for trading on the exchange.
166
174
 
175
+ If there are more than `batch_size` number of products then they can be queried
176
+ in batches.
177
+
178
+ Parameters
179
+ ----------
180
+ batch : int, optional
181
+ 1-based index of the batch of products.
182
+ batch_size : int, optional
183
+ The maximum number of products in one batch.
184
+ newest_first : bool, optional
185
+ Whether to sort products in descending or ascending order by creation time.
186
+
167
187
  Returns
168
188
  -------
169
189
  list of afp.schemas.ExchangeProduct
170
190
  """
171
- return self._exchange.get_approved_products()
191
+ filter = ExchangeProductFilter(
192
+ batch=batch, batch_size=batch_size, newest_first=newest_first
193
+ )
194
+ return self._exchange.get_approved_products(filter)
172
195
 
173
196
  def product(self, product_id: str) -> ExchangeProduct:
174
197
  """Retrieves a product for trading by its ID.
@@ -211,40 +234,106 @@ class Trading(ExchangeAPI):
211
234
  return self._exchange.get_order_by_id(value)
212
235
 
213
236
  @refresh_token_on_expiry
214
- def open_orders(self, product_id: str | None = None) -> list[Order]:
215
- """Retrieves all open and partially filled limit orders that have been submitted
216
- by the authenticated account.
237
+ def orders(
238
+ self,
239
+ *,
240
+ product_id: str | None = None,
241
+ type_: str | None = None,
242
+ states: Iterable[str] = (),
243
+ side: str | None = None,
244
+ start: datetime | None = None,
245
+ end: datetime | None = None,
246
+ batch: int = 1,
247
+ batch_size: int = DEFAULT_BATCH_SIZE,
248
+ newest_first: bool = True,
249
+ ) -> list[Order]:
250
+ """Retrieves the authenticated account's orders that match the given parameters.
251
+
252
+ If there are more than `batch_size` number of orders then they can be queried
253
+ in batches.
217
254
 
218
255
  Parameters
219
256
  ----------
220
257
  product_id : str, optional
258
+ type_ : str, optional
259
+ One of `LIMIT_ORDER` and `CANCEL_ORDER`.
260
+ states : iterable of str
261
+ Any of `RECEIVED`, `PENDING`, `OPEN`, `COMPLETED` and `REJECTED`.
262
+ side : str, optional
263
+ One of `BID` and `ASK`.
264
+ start : datetime.datetime, optional
265
+ end : datetime.datetime, optional
266
+ batch : int, optional
267
+ 1-based index of the batch of orders.
268
+ batch_size : int, optional
269
+ The maximum number of orders in one batch.
270
+ newest_first : bool, optional
271
+ Whether to sort orders in descending or ascending order by creation time.
221
272
 
222
273
  Returns
223
274
  -------
224
- list of afp.schemas.Order
275
+ list of afp.schemas.OrderFill
225
276
  """
226
- return self._exchange.get_open_orders(product_id)
277
+ filter = OrderFilter(
278
+ intent_account_id=self._authenticator.address,
279
+ product_id=product_id,
280
+ type=None if type_ is None else OrderType(type_.upper()),
281
+ states=[OrderState(state.upper()) for state in states],
282
+ side=None if side is None else OrderSide(side.upper()),
283
+ start=start,
284
+ end=end,
285
+ batch=batch,
286
+ batch_size=batch_size,
287
+ newest_first=newest_first,
288
+ )
289
+ return self._exchange.get_orders(filter)
290
+
291
+ @refresh_token_on_expiry
292
+ def open_orders(self, product_id: str | None = None) -> list[Order]:
293
+ """Deprecated alias of Trading.orders(type_="LIMIT_ORDER", states=("OPEN", "PARTIAL"))."""
294
+ warnings.warn(
295
+ "Trading.open_orders() is deprecated. Use "
296
+ 'Trading.orders(type_="LIMIT_ORDER", states=("OPEN", "PARTIAL")) instead.',
297
+ DeprecationWarning,
298
+ stacklevel=2,
299
+ )
300
+ return self.orders(
301
+ type_="limit_order", states=("open", "partial"), product_id=product_id
302
+ )
227
303
 
228
304
  @refresh_token_on_expiry
229
305
  def order_fills(
230
306
  self,
231
307
  *,
232
308
  product_id: str | None = None,
233
- margin_account_id: str | None = None,
234
309
  intent_hash: str | None = None,
235
310
  start: datetime | None = None,
236
311
  end: datetime | None = None,
312
+ trade_states: Iterable[str] = (),
313
+ batch: int = 1,
314
+ batch_size: int = DEFAULT_BATCH_SIZE,
315
+ newest_first: bool = True,
237
316
  ) -> list[OrderFill]:
238
317
  """Retrieves the authenticated account's order fills that match the given
239
318
  parameters.
240
319
 
320
+ If there are more than `batch_size` number of order fills then they can be
321
+ queried in batches.
322
+
241
323
  Parameters
242
324
  ----------
243
325
  product_id : str, optional
244
- margin_account_id : str, optional
245
326
  intent_hash : str, optional
246
327
  start : datetime.datetime, optional
247
328
  end : datetime.datetime, optional
329
+ trade_states : iterable of str
330
+ Any of `PENDING`, `CLEARED` and `REJECTED`.
331
+ batch : int, optional
332
+ 1-based index of the batch of order fills.
333
+ batch_size : int, optional
334
+ The maximum number of order fills in one batch.
335
+ newest_first : bool, optional
336
+ Whether to sort order fills in descending or ascending order by creation time.
248
337
 
249
338
  Returns
250
339
  -------
@@ -253,11 +342,13 @@ class Trading(ExchangeAPI):
253
342
  filter = OrderFillFilter(
254
343
  intent_account_id=self._authenticator.address,
255
344
  product_id=product_id,
256
- margin_account_id=margin_account_id,
257
345
  intent_hash=intent_hash,
258
346
  start=start,
259
347
  end=end,
260
- trade_state=None,
348
+ trade_states=[TradeState(state.upper()) for state in trade_states],
349
+ batch=batch,
350
+ batch_size=batch_size,
351
+ newest_first=newest_first,
261
352
  )
262
353
  return self._exchange.get_order_fills(filter)
263
354
 
@@ -266,21 +357,28 @@ class Trading(ExchangeAPI):
266
357
  self,
267
358
  *,
268
359
  product_id: str | None = None,
269
- margin_account_id: str | None = None,
270
360
  intent_hash: str | None = None,
361
+ trade_states: Iterable[str] = ("PENDING",),
271
362
  ) -> Generator[OrderFill, None, None]:
272
363
  """Subscribes to the authenticated account's new order fills that match the
273
364
  given parameters.
274
365
 
275
- Returns a generator that yields new order fills as they are published by the
276
- exchange. A new order fill gets publised as soon as there is a match in the
277
- order book, before the trade is submitted to clearing.
366
+ Returns a generator that yields order fills as they are published by the
367
+ exchange.
368
+
369
+ If `trade_states` includes `PENDING` (the default value) then a new order fill
370
+ is yielded as soon as there is a match in the order book, before the trade is
371
+ submitted to clearing.
372
+
373
+ If `trade_states` is empty or more than one trade state is specified then
374
+ updates to order fills are yielded at every state transition.
278
375
 
279
376
  Parameters
280
377
  ----------
281
378
  product_id : str, optional
282
- margin_account_id : str, optional
283
379
  intent_hash : str, optional
380
+ trade_states: iterable of str
381
+ Any of `PENDING`, `CLEARED` and `REJECTED`.
284
382
 
285
383
  Yields
286
384
  -------
@@ -289,11 +387,13 @@ class Trading(ExchangeAPI):
289
387
  filter = OrderFillFilter(
290
388
  intent_account_id=self._authenticator.address,
291
389
  product_id=product_id,
292
- margin_account_id=margin_account_id,
293
390
  intent_hash=intent_hash,
294
391
  start=None,
295
392
  end=None,
296
- trade_state=TradeState.PENDING,
393
+ trade_states=[TradeState(state.upper()) for state in trade_states],
394
+ batch=None,
395
+ batch_size=None,
396
+ newest_first=None,
297
397
  )
298
398
  yield from self._exchange.iter_order_fills(filter)
299
399
 
afp/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from importlib import metadata
2
3
  from types import SimpleNamespace
3
4
 
4
5
 
@@ -6,10 +7,12 @@ def _int_or_none(value: str | None) -> int | None:
6
7
  return int(value) if value is not None else None
7
8
 
8
9
 
9
- USER_AGENT = "afp-sdk"
10
+ # Venue API constants
11
+ USER_AGENT = "afp-sdk/{}".format(metadata.version("afp-sdk"))
12
+ DEFAULT_BATCH_SIZE = 50
10
13
  DEFAULT_EXCHANGE_API_VERSION = 1
11
14
 
12
- # Constants from clearing/contracts/lib/constants.sol
15
+ # Clearing System constants
13
16
  RATE_MULTIPLIER = 10**4
14
17
  FEE_RATE_MULTIPLIER = 10**6
15
18
  FULL_PRECISION_MULTIPLIER = 10**18
afp/enums.py CHANGED
@@ -4,6 +4,7 @@ from enum import StrEnum
4
4
  class ListingState(StrEnum):
5
5
  PRIVATE = "PRIVATE"
6
6
  PUBLIC = "PUBLIC"
7
+ READ_ONLY = "READ_ONLY"
7
8
  DELISTED = "DELISTED"
8
9
 
9
10
 
afp/exchange.py CHANGED
@@ -16,11 +16,13 @@ from .exceptions import (
16
16
  from .schemas import (
17
17
  ExchangeParameters,
18
18
  ExchangeProduct,
19
+ ExchangeProductFilter,
19
20
  ExchangeProductListingSubmission,
20
21
  ExchangeProductUpdateSubmission,
21
22
  LoginSubmission,
22
23
  MarketDepthData,
23
24
  Order,
25
+ OrderFilter,
24
26
  OrderFill,
25
27
  OrderFillFilter,
26
28
  OrderSubmission,
@@ -51,8 +53,12 @@ class ExchangeClient:
51
53
  return ExchangeParameters(**response.json())
52
54
 
53
55
  # GET /products
54
- def get_approved_products(self) -> list[ExchangeProduct]:
55
- response = self._send_request("GET", "/products")
56
+ def get_approved_products(
57
+ self, filter: ExchangeProductFilter
58
+ ) -> list[ExchangeProduct]:
59
+ response = self._send_request(
60
+ "GET", "/products", params=filter.model_dump(exclude_none=True)
61
+ )
56
62
  return [ExchangeProduct(**item) for item in response.json()["products"]]
57
63
 
58
64
  # GET /products/{product_id}
@@ -86,9 +92,9 @@ class ExchangeClient:
86
92
  return Order(**response.json())
87
93
 
88
94
  # GET /orders
89
- def get_open_orders(self, product_id: str | None = None) -> list[Order]:
95
+ def get_orders(self, filter: OrderFilter) -> list[Order]:
90
96
  response = self._send_request(
91
- "GET", "/orders", params=({"product_id": product_id} if product_id else {})
97
+ "GET", "/orders", params=filter.model_dump(exclude_none=True)
92
98
  )
93
99
  return [Order(**item) for item in response.json()["orders"]]
94
100
 
@@ -167,11 +173,19 @@ class ExchangeClient:
167
173
  raise AuthorizationError(http_error) from http_error
168
174
  if http_error.response.status_code == requests.codes.NOT_FOUND:
169
175
  raise NotFoundError(http_error) from http_error
170
-
171
- try:
172
- reason = response.json()["detail"]
173
- except (json.JSONDecodeError, KeyError):
174
- reason = response.text
175
- raise ValidationError(reason) from http_error
176
+ if http_error.response.status_code == requests.codes.BAD_REQUEST:
177
+ try:
178
+ reason = response.json()["detail"]
179
+ except (json.JSONDecodeError, KeyError):
180
+ reason = http_error
181
+ raise ValidationError(reason) from http_error
182
+ if http_error.response.status_code == requests.codes.UNPROCESSABLE:
183
+ try:
184
+ reason = ", ".join(err["msg"] for err in response.json()["detail"])
185
+ except (json.JSONDecodeError, KeyError, TypeError):
186
+ reason = http_error
187
+ raise ValidationError(reason) from http_error
188
+
189
+ raise ExchangeError(http_error) from http_error
176
190
 
177
191
  return response
afp/schemas.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from decimal import Decimal
3
3
  from functools import partial
4
- from typing import Annotated, Any
4
+ from typing import Annotated, Any, Literal
5
5
 
6
6
  import inflection
7
7
  from pydantic import (
@@ -12,6 +12,7 @@ from pydantic import (
12
12
  ConfigDict,
13
13
  Field,
14
14
  PlainSerializer,
15
+ computed_field,
15
16
  )
16
17
 
17
18
  from . import validators
@@ -40,6 +41,33 @@ class Model(BaseModel):
40
41
  return super().model_dump_json(by_alias=by_alias, **kwargs)
41
42
 
42
43
 
44
+ class PaginationFilter(Model):
45
+ batch: Annotated[None | int, Field(gt=0, exclude=True)]
46
+ batch_size: Annotated[None | int, Field(gt=0, exclude=True)]
47
+ newest_first: Annotated[None | bool, Field(exclude=True)]
48
+
49
+ @computed_field
50
+ @property
51
+ def page(self) -> None | int:
52
+ return self.batch
53
+
54
+ @computed_field
55
+ @property
56
+ def page_size(self) -> None | int:
57
+ return self.batch_size
58
+
59
+ @computed_field
60
+ @property
61
+ def sort(self) -> None | Literal["ASC", "DESC"]:
62
+ match self.newest_first:
63
+ case None:
64
+ return None
65
+ case True:
66
+ return "DESC"
67
+ case False:
68
+ return "ASC"
69
+
70
+
43
71
  # Authentication
44
72
 
45
73
 
@@ -78,6 +106,10 @@ class ExchangeProduct(Model):
78
106
  return self.id
79
107
 
80
108
 
109
+ class ExchangeProductFilter(PaginationFilter):
110
+ pass
111
+
112
+
81
113
  class IntentData(Model):
82
114
  trading_protocol_id: str
83
115
  product_id: str
@@ -106,6 +138,21 @@ class Order(Model):
106
138
  intent: Intent
107
139
 
108
140
 
141
+ class OrderFilter(PaginationFilter):
142
+ intent_account_id: str
143
+ product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
144
+ type: None | OrderType
145
+ states: Annotated[list[OrderState], Field(exclude=True)]
146
+ side: None | OrderSide
147
+ start: None | Timestamp
148
+ end: None | Timestamp
149
+
150
+ @computed_field
151
+ @property
152
+ def state(self) -> str | None:
153
+ return ",".join(self.states) if self.states else None
154
+
155
+
109
156
  class OrderCancellationData(Model):
110
157
  intent_hash: Annotated[str, AfterValidator(validators.validate_hexstr32)]
111
158
  nonce: int
@@ -137,16 +184,18 @@ class OrderFill(Model):
137
184
  price: Decimal
138
185
 
139
186
 
140
- class OrderFillFilter(Model):
187
+ class OrderFillFilter(PaginationFilter):
141
188
  intent_account_id: str
142
189
  product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
143
- margin_account_id: (
144
- None | Annotated[str, AfterValidator(validators.validate_address)]
145
- )
146
190
  intent_hash: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
147
191
  start: None | Timestamp
148
192
  end: None | Timestamp
149
- trade_state: None | TradeState
193
+ trade_states: Annotated[list[TradeState], Field(exclude=True)]
194
+
195
+ @computed_field
196
+ @property
197
+ def trade_state(self) -> str | None:
198
+ return ",".join(self.trade_states) if self.trade_states else None
150
199
 
151
200
 
152
201
  class MarketDepthItem(Model):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afp-sdk
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Autonomous Futures Protocol Python SDK
5
5
  Keywords: autonity,web3,trading,crypto,prediction,forecast,markets
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ afp/api/admin.py,sha256=6TiWCo0SB41CtXYTkTYYGQY7MrdB6OMTyU3-0J0V_7o,2170
5
5
  afp/api/base.py,sha256=5X1joEwFX4gxYUDCSTvp_hSFCfLZCktnW-UtZFxDquk,4471
6
6
  afp/api/margin_account.py,sha256=sC1DF7J3QTHR7cXcRjTG33oZswVy6qr9xnevQbrQ-ew,15256
7
7
  afp/api/product.py,sha256=2N4bPBahuw19GcBbBL20oQ5RFRuUxa5LOZzmN7hs39U,10156
8
- afp/api/trading.py,sha256=exQQ3LJA6XCbRAUBuYVjSJ5PHFi4COyqDORxC37LLfQ,10379
8
+ afp/api/trading.py,sha256=RdBFcY4U6A1T24vZUqR1J8MwdhaouMYZfFqBrdbMcA0,14309
9
9
  afp/auth.py,sha256=sV_9E6CgRWV1xYoppc4IdrnqNo5ZNDBIp6QF3fQbMWE,2055
10
10
  afp/bindings/__init__.py,sha256=_n9xoogYi8AAlSx_PE-wjnwP1ujVyDUwoRM0BSm243U,1271
11
11
  afp/bindings/auctioneer_facet.py,sha256=4p906zdU2lUsqpWlsiLE3dlxTPrlNpqk8DtjiQUWJ8M,23919
@@ -22,16 +22,16 @@ afp/bindings/product_registry.py,sha256=-_h786jzMCsaTqqnoxpmVgBkGf45eCUMthp_Pkqr
22
22
  afp/bindings/system_viewer.py,sha256=0FivdhpfXMrBesXcHkfO9uELyr7GiRmGe36xS5sURGE,41094
23
23
  afp/bindings/trading_protocol.py,sha256=ZloF3REbjFq9v0UGVsM0_Lk0EhfWJKdeJ0PzVEnyZo0,39573
24
24
  afp/config.py,sha256=_WKywiuty8poE1A0v46uBe1JGpfCzRlxCPamKennfpE,699
25
- afp/constants.py,sha256=XIph4R0Tx-BPw_NZJgWUtb7NdS9sYzEciFRSStm3VMs,1840
25
+ afp/constants.py,sha256=EvDhLpKBOsc8OHGm1paiUAdAetPGD4nyi13coB8rd14,1930
26
26
  afp/decorators.py,sha256=SEUQtbgPGc4iVPtBQV2eiCejcDAVImmXcI0uPXFhtJA,2774
27
- afp/enums.py,sha256=U5b_3RteMiVJozp4WYAKzUfPXI1RdJLEK19jAg-LN8Q,588
27
+ afp/enums.py,sha256=HQ9EqLEvg9EHBA_UDSyn0Lma72fYoefjHfzOcIVKyh0,616
28
28
  afp/exceptions.py,sha256=frdS-EH84K0fOf92RgRgNkTe3VII2m36XNCS8eyXLLM,390
29
- afp/exchange.py,sha256=MXQ-RhcI76l6bA9_IDTUq0RQGfwxBisrRhl6eC_yH4I,5983
29
+ afp/exchange.py,sha256=-QE44UZ-3Q0gsyWip4o19V4BYy283JpaKuQh7_-uS5g,6624
30
30
  afp/hashing.py,sha256=gBCWN93-ydRPlgnnorSvDQlylcnglrAypRDb-1K-20I,1949
31
31
  afp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- afp/schemas.py,sha256=M6qr0GhCeT7KevzOso5dIhydYzaYx78Rs66IWdadJO8,5284
32
+ afp/schemas.py,sha256=PCHoEpaU-O-C50UTrru3eRNalCLccWsTRE1AWITiIP0,6607
33
33
  afp/validators.py,sha256=zQvPu3HDu6ADnEE72EJlS5hc1-xfru78Mzd74s1u_EM,1841
34
- afp_sdk-0.5.1.dist-info/licenses/LICENSE,sha256=ZdaKItgc2ppfqta2OJV0oHpSJiK87PUxmUkUo-_0SB8,1065
35
- afp_sdk-0.5.1.dist-info/WHEEL,sha256=lh7MMMfiuFQLQaR9J7pNBODdWf-aa5UOeuuDAol3xps,79
36
- afp_sdk-0.5.1.dist-info/METADATA,sha256=RM6EyQq2W_ZlrkRtYcGf85pZg7S15poSNJq7CUEHz0w,6354
37
- afp_sdk-0.5.1.dist-info/RECORD,,
34
+ afp_sdk-0.5.2.dist-info/licenses/LICENSE,sha256=ZdaKItgc2ppfqta2OJV0oHpSJiK87PUxmUkUo-_0SB8,1065
35
+ afp_sdk-0.5.2.dist-info/WHEEL,sha256=n2u5OFBbdZvCiUKAmfnY1Po2j3FB_NWfuUlt5WiAjrk,79
36
+ afp_sdk-0.5.2.dist-info/METADATA,sha256=SBHaznfY-kCneVxC6gFXrqL0YE28Wxk1LlxWsQ1-hrg,6354
37
+ afp_sdk-0.5.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.20
2
+ Generator: uv 0.8.23
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any