afp-sdk 0.5.1__py3-none-any.whl → 0.5.3__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 +213 -26
- afp/constants.py +5 -2
- afp/enums.py +2 -0
- afp/exchange.py +49 -10
- afp/schemas.py +64 -6
- {afp_sdk-0.5.1.dist-info → afp_sdk-0.5.3.dist-info}/METADATA +1 -1
- {afp_sdk-0.5.1.dist-info → afp_sdk-0.5.3.dist-info}/RECORD +9 -9
- {afp_sdk-0.5.1.dist-info → afp_sdk-0.5.3.dist-info}/WHEEL +1 -1
- {afp_sdk-0.5.1.dist-info → afp_sdk-0.5.3.dist-info}/licenses/LICENSE +0 -0
afp/api/trading.py
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import secrets
|
|
2
|
-
|
|
2
|
+
import warnings
|
|
3
|
+
from datetime import datetime, timedelta
|
|
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,
|
|
19
|
+
OHLCVItem,
|
|
16
20
|
Order,
|
|
21
|
+
OrderFilter,
|
|
17
22
|
OrderCancellationData,
|
|
18
23
|
OrderFill,
|
|
19
24
|
OrderFillFilter,
|
|
20
|
-
OrderSide,
|
|
21
25
|
OrderSubmission,
|
|
22
|
-
TradeState,
|
|
23
26
|
)
|
|
24
27
|
from .base import ExchangeAPI
|
|
25
28
|
|
|
@@ -61,6 +64,7 @@ class Trading(ExchangeAPI):
|
|
|
61
64
|
----------
|
|
62
65
|
product : afp.schemas.ExchangeProduct
|
|
63
66
|
side : str
|
|
67
|
+
One of `BID` and `ASK`.
|
|
64
68
|
limit_price : decimal.Decimal
|
|
65
69
|
quantity : decimal.Decimal
|
|
66
70
|
max_trading_fee_rate : decimal.Decimal
|
|
@@ -87,7 +91,7 @@ class Trading(ExchangeAPI):
|
|
|
87
91
|
),
|
|
88
92
|
quantity=quantity,
|
|
89
93
|
max_trading_fee_rate=max_trading_fee_rate,
|
|
90
|
-
side=
|
|
94
|
+
side=OrderSide(side.upper()),
|
|
91
95
|
good_until_time=good_until_time,
|
|
92
96
|
nonce=self._generate_nonce(),
|
|
93
97
|
)
|
|
@@ -161,14 +165,34 @@ class Trading(ExchangeAPI):
|
|
|
161
165
|
)
|
|
162
166
|
return self._exchange.submit_order(submission)
|
|
163
167
|
|
|
164
|
-
def products(
|
|
168
|
+
def products(
|
|
169
|
+
self,
|
|
170
|
+
batch: int = 1,
|
|
171
|
+
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
172
|
+
newest_first: bool = True,
|
|
173
|
+
) -> list[ExchangeProduct]:
|
|
165
174
|
"""Retrieves the products approved for trading on the exchange.
|
|
166
175
|
|
|
176
|
+
If there are more than `batch_size` number of products then they can be queried
|
|
177
|
+
in batches.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
batch : int, optional
|
|
182
|
+
1-based index of the batch of products.
|
|
183
|
+
batch_size : int, optional
|
|
184
|
+
The maximum number of products in one batch.
|
|
185
|
+
newest_first : bool, optional
|
|
186
|
+
Whether to sort products in descending or ascending order by creation time.
|
|
187
|
+
|
|
167
188
|
Returns
|
|
168
189
|
-------
|
|
169
190
|
list of afp.schemas.ExchangeProduct
|
|
170
191
|
"""
|
|
171
|
-
|
|
192
|
+
filter = ExchangeProductFilter(
|
|
193
|
+
batch=batch, batch_size=batch_size, newest_first=newest_first
|
|
194
|
+
)
|
|
195
|
+
return self._exchange.get_approved_products(filter)
|
|
172
196
|
|
|
173
197
|
def product(self, product_id: str) -> ExchangeProduct:
|
|
174
198
|
"""Retrieves a product for trading by its ID.
|
|
@@ -211,53 +235,133 @@ class Trading(ExchangeAPI):
|
|
|
211
235
|
return self._exchange.get_order_by_id(value)
|
|
212
236
|
|
|
213
237
|
@refresh_token_on_expiry
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
238
|
+
def orders(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
product_id: str | None = None,
|
|
242
|
+
intent_account_id: str | None = None,
|
|
243
|
+
type_: str | None = None,
|
|
244
|
+
states: Iterable[str] = (),
|
|
245
|
+
side: str | None = None,
|
|
246
|
+
start: datetime | None = None,
|
|
247
|
+
end: datetime | None = None,
|
|
248
|
+
batch: int = 1,
|
|
249
|
+
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
250
|
+
newest_first: bool = True,
|
|
251
|
+
) -> list[Order]:
|
|
252
|
+
"""Retrieves the authenticated account's orders that match the given parameters.
|
|
253
|
+
|
|
254
|
+
If there are more than `batch_size` number of orders then they can be queried
|
|
255
|
+
in batches.
|
|
217
256
|
|
|
218
257
|
Parameters
|
|
219
258
|
----------
|
|
220
259
|
product_id : str, optional
|
|
260
|
+
intent_account_id : str, optional
|
|
261
|
+
Defaults to the address of the authenticated account.
|
|
262
|
+
type_ : str, optional
|
|
263
|
+
One of `LIMIT_ORDER` and `CANCEL_ORDER`.
|
|
264
|
+
states : iterable of str
|
|
265
|
+
Any of `RECEIVED`, `PENDING`, `OPEN`, `COMPLETED` and `REJECTED`.
|
|
266
|
+
side : str, optional
|
|
267
|
+
One of `BID` and `ASK`.
|
|
268
|
+
start : datetime.datetime, optional
|
|
269
|
+
end : datetime.datetime, optional
|
|
270
|
+
batch : int, optional
|
|
271
|
+
1-based index of the batch of orders.
|
|
272
|
+
batch_size : int, optional
|
|
273
|
+
The maximum number of orders in one batch.
|
|
274
|
+
newest_first : bool, optional
|
|
275
|
+
Whether to sort orders in descending or ascending order by creation time.
|
|
221
276
|
|
|
222
277
|
Returns
|
|
223
278
|
-------
|
|
224
|
-
list of afp.schemas.
|
|
279
|
+
list of afp.schemas.OrderFill
|
|
225
280
|
"""
|
|
226
|
-
|
|
281
|
+
if intent_account_id is None:
|
|
282
|
+
intent_account_id = self._authenticator.address
|
|
283
|
+
|
|
284
|
+
filter = OrderFilter(
|
|
285
|
+
intent_account_id=intent_account_id,
|
|
286
|
+
product_id=product_id,
|
|
287
|
+
type=None if type_ is None else OrderType(type_.upper()),
|
|
288
|
+
states=[OrderState(state.upper()) for state in states],
|
|
289
|
+
side=None if side is None else OrderSide(side.upper()),
|
|
290
|
+
start=start,
|
|
291
|
+
end=end,
|
|
292
|
+
batch=batch,
|
|
293
|
+
batch_size=batch_size,
|
|
294
|
+
newest_first=newest_first,
|
|
295
|
+
)
|
|
296
|
+
return self._exchange.get_orders(filter)
|
|
297
|
+
|
|
298
|
+
@refresh_token_on_expiry
|
|
299
|
+
def open_orders(self, product_id: str | None = None) -> list[Order]:
|
|
300
|
+
"""Deprecated alias of Trading.orders(type_="LIMIT_ORDER", states=("OPEN", "PARTIAL"))."""
|
|
301
|
+
warnings.warn(
|
|
302
|
+
"Trading.open_orders() is deprecated. Use "
|
|
303
|
+
'Trading.orders(type_="LIMIT_ORDER", states=("OPEN", "PARTIAL")) instead.',
|
|
304
|
+
DeprecationWarning,
|
|
305
|
+
stacklevel=2,
|
|
306
|
+
)
|
|
307
|
+
return self.orders(
|
|
308
|
+
type_="limit_order", states=("open", "partial"), product_id=product_id
|
|
309
|
+
)
|
|
227
310
|
|
|
228
311
|
@refresh_token_on_expiry
|
|
229
312
|
def order_fills(
|
|
230
313
|
self,
|
|
231
314
|
*,
|
|
232
315
|
product_id: str | None = None,
|
|
233
|
-
|
|
316
|
+
intent_account_id: str | None = None,
|
|
234
317
|
intent_hash: str | None = None,
|
|
235
318
|
start: datetime | None = None,
|
|
236
319
|
end: datetime | None = None,
|
|
320
|
+
trade_states: Iterable[str] = (),
|
|
321
|
+
batch: int = 1,
|
|
322
|
+
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
323
|
+
newest_first: bool = True,
|
|
237
324
|
) -> list[OrderFill]:
|
|
238
325
|
"""Retrieves the authenticated account's order fills that match the given
|
|
239
326
|
parameters.
|
|
240
327
|
|
|
328
|
+
If there are more than `batch_size` number of order fills then they can be
|
|
329
|
+
queried in batches.
|
|
330
|
+
|
|
241
331
|
Parameters
|
|
242
332
|
----------
|
|
243
333
|
product_id : str, optional
|
|
244
|
-
|
|
334
|
+
intent_account_id : str, optional
|
|
335
|
+
Defaults to the address of the authenticated account.
|
|
245
336
|
intent_hash : str, optional
|
|
246
337
|
start : datetime.datetime, optional
|
|
247
338
|
end : datetime.datetime, optional
|
|
339
|
+
trade_states : iterable of str
|
|
340
|
+
Any of `PENDING`, `CLEARED` and `REJECTED`.
|
|
341
|
+
batch : int, optional
|
|
342
|
+
1-based index of the batch of order fills.
|
|
343
|
+
batch_size : int, optional
|
|
344
|
+
The maximum number of order fills in one batch.
|
|
345
|
+
newest_first : bool, optional
|
|
346
|
+
Whether to sort order fills in descending or ascending order by creation time.
|
|
248
347
|
|
|
249
348
|
Returns
|
|
250
349
|
-------
|
|
251
350
|
list of afp.schemas.OrderFill
|
|
252
351
|
"""
|
|
352
|
+
if intent_account_id is None:
|
|
353
|
+
intent_account_id = self._authenticator.address
|
|
354
|
+
|
|
253
355
|
filter = OrderFillFilter(
|
|
254
|
-
intent_account_id=
|
|
356
|
+
intent_account_id=intent_account_id,
|
|
255
357
|
product_id=product_id,
|
|
256
|
-
margin_account_id=margin_account_id,
|
|
257
358
|
intent_hash=intent_hash,
|
|
258
359
|
start=start,
|
|
259
360
|
end=end,
|
|
260
|
-
|
|
361
|
+
trade_states=[TradeState(state.upper()) for state in trade_states],
|
|
362
|
+
batch=batch,
|
|
363
|
+
batch_size=batch_size,
|
|
364
|
+
newest_first=newest_first,
|
|
261
365
|
)
|
|
262
366
|
return self._exchange.get_order_fills(filter)
|
|
263
367
|
|
|
@@ -266,34 +370,49 @@ class Trading(ExchangeAPI):
|
|
|
266
370
|
self,
|
|
267
371
|
*,
|
|
268
372
|
product_id: str | None = None,
|
|
269
|
-
|
|
373
|
+
intent_account_id: str | None = None,
|
|
270
374
|
intent_hash: str | None = None,
|
|
375
|
+
trade_states: Iterable[str] = ("PENDING",),
|
|
271
376
|
) -> Generator[OrderFill, None, None]:
|
|
272
377
|
"""Subscribes to the authenticated account's new order fills that match the
|
|
273
378
|
given parameters.
|
|
274
379
|
|
|
275
|
-
Returns a generator that yields
|
|
276
|
-
exchange.
|
|
277
|
-
|
|
380
|
+
Returns a generator that yields order fills as they are published by the
|
|
381
|
+
exchange.
|
|
382
|
+
|
|
383
|
+
If `trade_states` includes `PENDING` (the default value) then a new order fill
|
|
384
|
+
is yielded as soon as there is a match in the order book, before the trade is
|
|
385
|
+
submitted to clearing.
|
|
386
|
+
|
|
387
|
+
If `trade_states` is empty or more than one trade state is specified then
|
|
388
|
+
updates to order fills are yielded at every state transition.
|
|
278
389
|
|
|
279
390
|
Parameters
|
|
280
391
|
----------
|
|
281
392
|
product_id : str, optional
|
|
282
|
-
|
|
393
|
+
intent_account_id : str, optional
|
|
394
|
+
Defaults to the address of the authenticated account.
|
|
283
395
|
intent_hash : str, optional
|
|
396
|
+
trade_states: iterable of str
|
|
397
|
+
Any of `PENDING`, `CLEARED` and `REJECTED`.
|
|
284
398
|
|
|
285
399
|
Yields
|
|
286
400
|
-------
|
|
287
401
|
afp.schemas.OrderFill
|
|
288
402
|
"""
|
|
403
|
+
if intent_account_id is None:
|
|
404
|
+
intent_account_id = self._authenticator.address
|
|
405
|
+
|
|
289
406
|
filter = OrderFillFilter(
|
|
290
|
-
intent_account_id=
|
|
407
|
+
intent_account_id=intent_account_id,
|
|
291
408
|
product_id=product_id,
|
|
292
|
-
margin_account_id=margin_account_id,
|
|
293
409
|
intent_hash=intent_hash,
|
|
294
410
|
start=None,
|
|
295
411
|
end=None,
|
|
296
|
-
|
|
412
|
+
trade_states=[TradeState(state.upper()) for state in trade_states],
|
|
413
|
+
batch=None,
|
|
414
|
+
batch_size=None,
|
|
415
|
+
newest_first=None,
|
|
297
416
|
)
|
|
298
417
|
yield from self._exchange.iter_order_fills(filter)
|
|
299
418
|
|
|
@@ -339,3 +458,71 @@ class Trading(ExchangeAPI):
|
|
|
339
458
|
"""
|
|
340
459
|
value = validators.validate_hexstr32(product_id)
|
|
341
460
|
yield from self._exchange.iter_market_depth_data(value)
|
|
461
|
+
|
|
462
|
+
def ohlcv(
|
|
463
|
+
self,
|
|
464
|
+
product_id: str,
|
|
465
|
+
start: datetime | None = None,
|
|
466
|
+
interval: timedelta = timedelta(minutes=5),
|
|
467
|
+
) -> list[OHLCVItem]:
|
|
468
|
+
"""Retrieves Open-High-Low-Close-Volume time series data for the given product.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
product_id : str
|
|
473
|
+
start : datetime
|
|
474
|
+
Defaults to 1 day ago.
|
|
475
|
+
interval : timedelta
|
|
476
|
+
The distance between 2 data points. Gets rounded to a multiple of 5 seconds.
|
|
477
|
+
Defaults to 5 minutes.
|
|
478
|
+
|
|
479
|
+
Returns
|
|
480
|
+
-------
|
|
481
|
+
list of afp.schemas.OHLCVItem
|
|
482
|
+
|
|
483
|
+
Raises
|
|
484
|
+
------
|
|
485
|
+
afp.exceptions.NotFoundError
|
|
486
|
+
If no such product exists.
|
|
487
|
+
"""
|
|
488
|
+
if start is None:
|
|
489
|
+
start = datetime.now() - timedelta(days=1)
|
|
490
|
+
|
|
491
|
+
product_id = validators.validate_hexstr32(product_id)
|
|
492
|
+
start_timestamp = int(start.timestamp())
|
|
493
|
+
interval_secs = int(validators.validate_timedelta(interval).total_seconds())
|
|
494
|
+
return self._exchange.get_time_series_data(
|
|
495
|
+
product_id, start_timestamp, interval_secs
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def iter_ohlcv(
|
|
499
|
+
self, product_id: str, interval: timedelta = timedelta(seconds=5)
|
|
500
|
+
) -> Generator[OHLCVItem, None, None]:
|
|
501
|
+
"""Subscribes to Open-High-Low-Close-Volume time series data updates for the
|
|
502
|
+
given product.
|
|
503
|
+
|
|
504
|
+
Returns a generator that yields OHLCV data points as they are published
|
|
505
|
+
by the exhange.
|
|
506
|
+
|
|
507
|
+
Parameters
|
|
508
|
+
----------
|
|
509
|
+
product_id : str
|
|
510
|
+
interval : timedelta
|
|
511
|
+
The distance between 2 data points. Gets rounded to a multiple of 5 seconds.
|
|
512
|
+
Defaults to 5 seconds.
|
|
513
|
+
|
|
514
|
+
Yields
|
|
515
|
+
-------
|
|
516
|
+
afp.schemas.OHLCVItem
|
|
517
|
+
|
|
518
|
+
Raises
|
|
519
|
+
------
|
|
520
|
+
afp.exceptions.NotFoundError
|
|
521
|
+
If no such product exists.
|
|
522
|
+
"""
|
|
523
|
+
product_id = validators.validate_hexstr32(product_id)
|
|
524
|
+
start_timestamp = int(datetime.now().timestamp())
|
|
525
|
+
interval_secs = int(validators.validate_timedelta(interval).total_seconds())
|
|
526
|
+
yield from self._exchange.iter_time_series_data(
|
|
527
|
+
product_id, start_timestamp, interval_secs
|
|
528
|
+
)
|
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
|
-
|
|
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
|
afp/enums.py
CHANGED
|
@@ -4,12 +4,14 @@ 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
|
|
|
10
11
|
class OrderType(StrEnum):
|
|
11
12
|
LIMIT_ORDER = "LIMIT_ORDER"
|
|
12
13
|
CANCEL_ORDER = "CANCEL_ORDER"
|
|
14
|
+
EXCHANGE_INITIATED_CANCELLATION = "EXCHANGE_INITIATED_CANCELLATION"
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class OrderState(StrEnum):
|
afp/exchange.py
CHANGED
|
@@ -16,11 +16,14 @@ 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,
|
|
24
|
+
OHLCVItem,
|
|
23
25
|
Order,
|
|
26
|
+
OrderFilter,
|
|
24
27
|
OrderFill,
|
|
25
28
|
OrderFillFilter,
|
|
26
29
|
OrderSubmission,
|
|
@@ -51,8 +54,12 @@ class ExchangeClient:
|
|
|
51
54
|
return ExchangeParameters(**response.json())
|
|
52
55
|
|
|
53
56
|
# GET /products
|
|
54
|
-
def get_approved_products(
|
|
55
|
-
|
|
57
|
+
def get_approved_products(
|
|
58
|
+
self, filter: ExchangeProductFilter
|
|
59
|
+
) -> list[ExchangeProduct]:
|
|
60
|
+
response = self._send_request(
|
|
61
|
+
"GET", "/products", params=filter.model_dump(exclude_none=True)
|
|
62
|
+
)
|
|
56
63
|
return [ExchangeProduct(**item) for item in response.json()["products"]]
|
|
57
64
|
|
|
58
65
|
# GET /products/{product_id}
|
|
@@ -86,9 +93,9 @@ class ExchangeClient:
|
|
|
86
93
|
return Order(**response.json())
|
|
87
94
|
|
|
88
95
|
# GET /orders
|
|
89
|
-
def
|
|
96
|
+
def get_orders(self, filter: OrderFilter) -> list[Order]:
|
|
90
97
|
response = self._send_request(
|
|
91
|
-
"GET", "/orders", params=(
|
|
98
|
+
"GET", "/orders", params=filter.model_dump(exclude_none=True)
|
|
92
99
|
)
|
|
93
100
|
return [Order(**item) for item in response.json()["orders"]]
|
|
94
101
|
|
|
@@ -132,6 +139,30 @@ class ExchangeClient:
|
|
|
132
139
|
for line in response.iter_lines():
|
|
133
140
|
yield MarketDepthData.model_validate_json(line)
|
|
134
141
|
|
|
142
|
+
# GET /time-series/{product_id}
|
|
143
|
+
def get_time_series_data(
|
|
144
|
+
self, product_id: str, start: int, interval: int
|
|
145
|
+
) -> list[OHLCVItem]:
|
|
146
|
+
response = self._send_request(
|
|
147
|
+
"GET",
|
|
148
|
+
f"/time-series/{product_id}",
|
|
149
|
+
params=dict(start=start, interval=interval),
|
|
150
|
+
)
|
|
151
|
+
return [OHLCVItem(**item) for item in response.json()["data"]]
|
|
152
|
+
|
|
153
|
+
# GET /stream/time-series/{product_id}
|
|
154
|
+
def iter_time_series_data(
|
|
155
|
+
self, product_id: str, start: int, interval: int
|
|
156
|
+
) -> Generator[OHLCVItem, None, None]:
|
|
157
|
+
response = self._send_request(
|
|
158
|
+
"GET",
|
|
159
|
+
f"/stream/time-series/{product_id}",
|
|
160
|
+
params=dict(start=start, interval=interval),
|
|
161
|
+
stream=True,
|
|
162
|
+
)
|
|
163
|
+
for line in response.iter_lines():
|
|
164
|
+
yield OHLCVItem.model_validate_json(line)
|
|
165
|
+
|
|
135
166
|
def _send_request(
|
|
136
167
|
self,
|
|
137
168
|
method: str,
|
|
@@ -167,11 +198,19 @@ class ExchangeClient:
|
|
|
167
198
|
raise AuthorizationError(http_error) from http_error
|
|
168
199
|
if http_error.response.status_code == requests.codes.NOT_FOUND:
|
|
169
200
|
raise NotFoundError(http_error) from http_error
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
if http_error.response.status_code == requests.codes.BAD_REQUEST:
|
|
202
|
+
try:
|
|
203
|
+
reason = response.json()["detail"]
|
|
204
|
+
except (json.JSONDecodeError, KeyError):
|
|
205
|
+
reason = http_error
|
|
206
|
+
raise ValidationError(reason) from http_error
|
|
207
|
+
if http_error.response.status_code == requests.codes.UNPROCESSABLE:
|
|
208
|
+
try:
|
|
209
|
+
reason = ", ".join(err["msg"] for err in response.json()["detail"])
|
|
210
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
211
|
+
reason = http_error
|
|
212
|
+
raise ValidationError(reason) from http_error
|
|
213
|
+
|
|
214
|
+
raise ExchangeError(http_error) from http_error
|
|
176
215
|
|
|
177
216
|
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(
|
|
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):
|
|
@@ -160,6 +209,15 @@ class MarketDepthData(Model):
|
|
|
160
209
|
asks: list[MarketDepthItem]
|
|
161
210
|
|
|
162
211
|
|
|
212
|
+
class OHLCVItem(Model):
|
|
213
|
+
timestamp: Timestamp
|
|
214
|
+
open: Decimal
|
|
215
|
+
high: Decimal
|
|
216
|
+
low: Decimal
|
|
217
|
+
close: Decimal
|
|
218
|
+
volume: int
|
|
219
|
+
|
|
220
|
+
|
|
163
221
|
# Clearing API
|
|
164
222
|
|
|
165
223
|
|
|
@@ -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=
|
|
8
|
+
afp/api/trading.py,sha256=y5Q2KApEpUau9R8G9KUqOdm4ymhw9Hbu9d2dkG80AsQ,17262
|
|
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=
|
|
25
|
+
afp/constants.py,sha256=EvDhLpKBOsc8OHGm1paiUAdAetPGD4nyi13coB8rd14,1930
|
|
26
26
|
afp/decorators.py,sha256=SEUQtbgPGc4iVPtBQV2eiCejcDAVImmXcI0uPXFhtJA,2774
|
|
27
|
-
afp/enums.py,sha256=
|
|
27
|
+
afp/enums.py,sha256=9JhwdLcTNhyKabKdrALAlCeL3C5XfeXYSSSXSCsuTzE,688
|
|
28
28
|
afp/exceptions.py,sha256=frdS-EH84K0fOf92RgRgNkTe3VII2m36XNCS8eyXLLM,390
|
|
29
|
-
afp/exchange.py,sha256=
|
|
29
|
+
afp/exchange.py,sha256=QQAfglOzsxo_T0BUOFAkxI2wUbda1sKStMXUO9IcDbw,7498
|
|
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=
|
|
32
|
+
afp/schemas.py,sha256=zyZOPvAdDnXp60kqYqX-OoWnKvRLPZT7at0A0MHlurc,6746
|
|
33
33
|
afp/validators.py,sha256=zQvPu3HDu6ADnEE72EJlS5hc1-xfru78Mzd74s1u_EM,1841
|
|
34
|
-
afp_sdk-0.5.
|
|
35
|
-
afp_sdk-0.5.
|
|
36
|
-
afp_sdk-0.5.
|
|
37
|
-
afp_sdk-0.5.
|
|
34
|
+
afp_sdk-0.5.3.dist-info/licenses/LICENSE,sha256=ZdaKItgc2ppfqta2OJV0oHpSJiK87PUxmUkUo-_0SB8,1065
|
|
35
|
+
afp_sdk-0.5.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
36
|
+
afp_sdk-0.5.3.dist-info/METADATA,sha256=__Jo9oroNSkI1O9-vAOSA2HrSEONWX95AB4nLKkrjg0,6354
|
|
37
|
+
afp_sdk-0.5.3.dist-info/RECORD,,
|
|
File without changes
|