afp-sdk 0.5.1__tar.gz → 0.5.2__tar.gz
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_sdk-0.5.1 → afp_sdk-0.5.2}/CHANGELOG.md +20 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/PKG-INFO +1 -1
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/trading.py +123 -23
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/constants.py +5 -2
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/enums.py +1 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/exchange.py +24 -10
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/schemas.py +55 -6
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/pyproject.toml +1 -1
- afp_sdk-0.5.2/tests/test_schemas.py +31 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_validators.py +9 -13
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/uv.lock +1 -1
- afp_sdk-0.5.1/tests/test_schemas.py +0 -17
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/.env.template +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/.envrc +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/LICENSE +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/README.md +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/__init__.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/afp.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/__init__.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/admin.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/base.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/margin_account.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/api/product.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/auth.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/__init__.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/auctioneer_facet.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/bankruptcy_facet.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/clearing_facet.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/erc20.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/facade.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/final_settlement_facet.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/margin_account.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/margin_account_registry.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/mark_price_tracker_facet.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/oracle_provider.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/product_registry.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/system_viewer.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/bindings/trading_protocol.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/config.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/decorators.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/exceptions.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/hashing.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/py.typed +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/afp/validators.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/devenv.lock +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/devenv.nix +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/devenv.yaml +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/examples/cancel_order.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/examples/create_product.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/examples/execute_trade.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/__init__.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/assets/test.key +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_afp.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_base_api.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_decorators.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_exchange_client.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_hashing.py +0 -0
- {afp_sdk-0.5.1 → afp_sdk-0.5.2}/tests/test_signing.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [v0.5.2] - 2025-10-04
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- Add `Trading.orders()` method for filtering orders based on multiple criteria ([#26](https://github.com/autonity/afp-sdk/pull/26))
|
|
6
|
+
- Add `trade_states` argument to `Trading.order_fills()` ([#26](https://github.com/autonity/afp-sdk/pull/26))
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Change `Trading.products()`, `Trading.orders()` and `Trading.order_fills()` to retrieve items in batches ([#31](https://github.com/autonity/afp-sdk/pull/31))
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Raise `afp.exceptions.ExchangeError` for AutEx internal server errors instead of `afp.exceptions.ValidationError` ([#32](https://github.com/autonity/afp-sdk/pull/32))
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- Flag `Trading.open_orders()` method for deprecation. ([#26](https://github.com/autonity/afp-sdk/pull/26))
|
|
19
|
+
|
|
1
20
|
## [v0.5.1] - 2025-09-23
|
|
2
21
|
|
|
3
22
|
### Added
|
|
@@ -38,6 +57,7 @@
|
|
|
38
57
|
|
|
39
58
|
_First public release for Forecastathon._
|
|
40
59
|
|
|
60
|
+
[v0.5.2]: https://github.com/autonity/afp-sdk/releases/tag/v0.5.2
|
|
41
61
|
[v0.5.1]: https://github.com/autonity/afp-sdk/releases/tag/v0.5.1
|
|
42
62
|
[v0.5.0]: https://github.com/autonity/afp-sdk/releases/tag/v0.5.0
|
|
43
63
|
[v0.4.0]: https://github.com/autonity/afp-sdk/releases/tag/v0.4.0
|
|
@@ -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=
|
|
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(
|
|
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
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
275
|
+
list of afp.schemas.OrderFill
|
|
225
276
|
"""
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
276
|
-
exchange.
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
15
|
+
# Clearing System constants
|
|
13
16
|
RATE_MULTIPLIER = 10**4
|
|
14
17
|
FEE_RATE_MULTIPLIER = 10**6
|
|
15
18
|
FULL_PRECISION_MULTIPLIER = 10**18
|
|
@@ -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(
|
|
55
|
-
|
|
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
|
|
95
|
+
def get_orders(self, filter: OrderFilter) -> list[Order]:
|
|
90
96
|
response = self._send_request(
|
|
91
|
-
"GET", "/orders", params=(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
@@ -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(
|
|
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
|
-
|
|
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):
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from afp.schemas import Model, PaginationFilter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Person(Model):
|
|
5
|
+
first_name: str
|
|
6
|
+
last_name: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_schema_aliasing__from_json():
|
|
10
|
+
person = Person.model_validate_json('{"firstName":"Foo","lastName":"Bar"}')
|
|
11
|
+
assert person.first_name == "Foo"
|
|
12
|
+
assert person.last_name == "Bar"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_schema_aliasing__to_json():
|
|
16
|
+
person = Person(first_name="Foo", last_name="Bar")
|
|
17
|
+
assert person.model_dump_json() == '{"firstName":"Foo","lastName":"Bar"}'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_pagination_parameters__conversion():
|
|
21
|
+
filter = PaginationFilter(batch=2, batch_size=20, newest_first=False)
|
|
22
|
+
assert filter.model_dump() == {
|
|
23
|
+
"page": 2,
|
|
24
|
+
"page_size": 20,
|
|
25
|
+
"sort": "ASC",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pagination_parameters__null_values():
|
|
30
|
+
filter = PaginationFilter(batch=None, batch_size=None, newest_first=None)
|
|
31
|
+
assert filter.model_dump(exclude_none=True) == {}
|
|
@@ -5,7 +5,11 @@ from decimal import Decimal
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from afp import validators
|
|
8
|
-
from afp.schemas import
|
|
8
|
+
from afp.schemas import Model, Timestamp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DummyModel(Model):
|
|
12
|
+
timestamp: Timestamp
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
def test_timestamp_conversion():
|
|
@@ -13,20 +17,12 @@ def test_timestamp_conversion():
|
|
|
13
17
|
ts_time = 1893499200
|
|
14
18
|
|
|
15
19
|
# timestamp -> datetime
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
margin_account_id=None,
|
|
20
|
-
intent_hash=None,
|
|
21
|
-
start=None,
|
|
22
|
-
end=ts_time, # type: ignore
|
|
23
|
-
trade_state=None,
|
|
24
|
-
)
|
|
25
|
-
assert filter.end is not None
|
|
26
|
-
assert filter.end.astimezone(UTC) == dt_time
|
|
20
|
+
instance = DummyModel(timestamp=ts_time) # type: ignore
|
|
21
|
+
assert instance.timestamp is not None
|
|
22
|
+
assert instance.timestamp.astimezone(UTC) == dt_time
|
|
27
23
|
|
|
28
24
|
# datetime -> timestamp
|
|
29
|
-
assert '"
|
|
25
|
+
assert '"timestamp":%d' % ts_time in instance.model_dump_json()
|
|
30
26
|
|
|
31
27
|
|
|
32
28
|
def test_validate_hexstr32__pass():
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from afp.schemas import Model
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Person(Model):
|
|
5
|
-
first_name: str
|
|
6
|
-
last_name: str
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_schema_aliasing__from_json():
|
|
10
|
-
person = Person.model_validate_json('{"firstName":"Foo","lastName":"Bar"}')
|
|
11
|
-
assert person.first_name == "Foo"
|
|
12
|
-
assert person.last_name == "Bar"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_schema_aliasing__to_json():
|
|
16
|
-
person = Person(first_name="Foo", last_name="Bar")
|
|
17
|
-
assert person.model_dump_json() == '{"firstName":"Foo","lastName":"Bar"}'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|