afp-sdk 0.1.0__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 ADDED
@@ -0,0 +1,342 @@
1
+ import secrets
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from typing import Generator
5
+
6
+ from web3 import Web3
7
+
8
+ from .. import signing, validators
9
+ from ..decorators import refresh_token_on_expiry
10
+ from ..enums import OrderType
11
+ from ..schemas import (
12
+ ExchangeProduct,
13
+ Intent,
14
+ IntentData,
15
+ MarketDepthData,
16
+ Order,
17
+ OrderCancellationData,
18
+ OrderFill,
19
+ OrderFillFilter,
20
+ OrderSide,
21
+ OrderSubmission,
22
+ TradeState,
23
+ )
24
+ from .base import ExchangeAPI
25
+
26
+
27
+ class Trading(ExchangeAPI):
28
+ """API for trading in the AutEx exchange.
29
+
30
+ Authenticates with the exchange on creation.
31
+
32
+ Parameters
33
+ ----------
34
+ private_key : str
35
+ The private key of the account that submits intents to the exchange.
36
+
37
+ Raises
38
+ ------
39
+ afp.exceptions.AuthenticationError
40
+ If the exchange rejects the login attempt.
41
+ """
42
+
43
+ @staticmethod
44
+ def _generate_nonce() -> int:
45
+ return secrets.randbelow(2**31) # Postgres integer range
46
+
47
+ def create_intent(
48
+ self,
49
+ *,
50
+ product: ExchangeProduct,
51
+ side: str,
52
+ limit_price: Decimal,
53
+ quantity: int,
54
+ max_trading_fee_rate: Decimal,
55
+ good_until_time: datetime,
56
+ margin_account_id: str | None = None,
57
+ ) -> Intent:
58
+ """Creates an intent with the given intent data, generates its hash and signs it
59
+ with the configured account's private key.
60
+
61
+ The intent account's address is derived from the private key. The intent account
62
+ is assumed to be the same as the margin account if the margin account ID is not
63
+ specified.
64
+
65
+ Parameters
66
+ ----------
67
+ product : afp.schemas.ExchangeProduct
68
+ side : str
69
+ limit_price : decimal.Decimal
70
+ quantity : decimal.Decimal
71
+ max_trading_fee_rate : decimal.Decimal
72
+ good_until_time : datetime.datetime
73
+ margin_account_id : str, optional
74
+
75
+ Returns
76
+ -------
77
+ afp.schemas.Intent
78
+ """
79
+ margin_account_id = (
80
+ validators.validate_address(margin_account_id)
81
+ if margin_account_id is not None
82
+ else self._account.address
83
+ )
84
+
85
+ intent_data = IntentData(
86
+ trading_protocol_id=self._trading_protocol_id,
87
+ product_id=product.id,
88
+ limit_price=limit_price,
89
+ quantity=quantity,
90
+ max_trading_fee_rate=max_trading_fee_rate,
91
+ side=getattr(OrderSide, side.upper()),
92
+ good_until_time=good_until_time,
93
+ nonce=self._generate_nonce(),
94
+ )
95
+ intent_hash = signing.generate_intent_hash(
96
+ intent_data=intent_data,
97
+ margin_account_id=margin_account_id,
98
+ intent_account_id=self._account.address,
99
+ tick_size=product.tick_size,
100
+ )
101
+ signature = signing.sign_message(self._account, intent_hash)
102
+ return Intent(
103
+ hash=Web3.to_hex(intent_hash),
104
+ margin_account_id=margin_account_id,
105
+ intent_account_id=self._account.address,
106
+ signature=Web3.to_hex(signature),
107
+ data=intent_data,
108
+ )
109
+
110
+ @refresh_token_on_expiry
111
+ def submit_limit_order(self, intent: Intent) -> Order:
112
+ """Sends an intent expressing a limit order to the exchange.
113
+
114
+ Parameters
115
+ ----------
116
+ intent : afp.schemas.Intent
117
+
118
+ Returns
119
+ -------
120
+ afp.schemas.Order
121
+
122
+ Raises
123
+ ------
124
+ afp.exceptions.ValidationError
125
+ If the exchange rejects the intent.
126
+ """
127
+ submission = OrderSubmission(
128
+ type=OrderType.LIMIT_ORDER,
129
+ intent=intent,
130
+ )
131
+ return self._exchange.submit_order(submission)
132
+
133
+ @refresh_token_on_expiry
134
+ def submit_cancel_order(self, intent_hash: str) -> Order:
135
+ """Sends a cancellation order to the exchange.
136
+
137
+ Parameters
138
+ ----------
139
+ intent_hash : str
140
+
141
+ Returns
142
+ -------
143
+ afp.schemas.Order
144
+
145
+ Raises
146
+ ------
147
+ afp.exceptions.ValidationError
148
+ If the exchange rejects the cancellation.
149
+ """
150
+ nonce = self._generate_nonce()
151
+ cancellation_hash = signing.generate_order_cancellation_hash(nonce, intent_hash)
152
+ signature = signing.sign_message(self._account, cancellation_hash)
153
+ cancellation_data = OrderCancellationData(
154
+ intent_hash=intent_hash,
155
+ nonce=nonce,
156
+ intent_account_id=self._account.address,
157
+ signature=Web3.to_hex(signature),
158
+ )
159
+ submission = OrderSubmission(
160
+ type=OrderType.CANCEL_ORDER,
161
+ cancellation_data=cancellation_data,
162
+ )
163
+ return self._exchange.submit_order(submission)
164
+
165
+ @refresh_token_on_expiry
166
+ def products(self) -> list[ExchangeProduct]:
167
+ """Retrieves the products approved for trading on the exchange.
168
+
169
+ Returns
170
+ -------
171
+ list of afp.schemas.ExchangeProduct
172
+ """
173
+ return self._exchange.get_approved_products()
174
+
175
+ @refresh_token_on_expiry
176
+ def product(self, product_id: str) -> ExchangeProduct:
177
+ """Retrieves a product for trading by its ID.
178
+
179
+ Parameters
180
+ ----------
181
+ product_id : str
182
+
183
+ Returns
184
+ -------
185
+ afp.schemas.ExchangeProduct
186
+
187
+ Raises
188
+ ------
189
+ afp.exceptions.NotFoundError
190
+ If no such product exists.
191
+ """
192
+ value = validators.validate_hexstr32(product_id)
193
+ return self._exchange.get_product_by_id(value)
194
+
195
+ @refresh_token_on_expiry
196
+ def order(self, order_id: str) -> Order:
197
+ """Retrieves an order by its ID from the orders that have been submitted by the
198
+ authenticated account.
199
+
200
+ Parameters
201
+ ----------
202
+ order_id : str
203
+
204
+ Returns
205
+ -------
206
+ afp.schemas.Order
207
+
208
+ Raises
209
+ ------
210
+ afp.exceptions.NotFoundError
211
+ If no such order exists.
212
+ """
213
+ value = validators.validate_hexstr32(order_id)
214
+ return self._exchange.get_order_by_id(value)
215
+
216
+ @refresh_token_on_expiry
217
+ def open_orders(self) -> list[Order]:
218
+ """Retrieves all open and partially filled limit orders that have been submitted
219
+ by the authenticated account.
220
+
221
+ Returns
222
+ -------
223
+ list of afp.schemas.Order
224
+ """
225
+ return self._exchange.get_open_orders()
226
+
227
+ @refresh_token_on_expiry
228
+ def order_fills(
229
+ self,
230
+ *,
231
+ product_id: str | None = None,
232
+ margin_account_id: str | None = None,
233
+ intent_hash: str | None = None,
234
+ start: datetime | None = None,
235
+ end: datetime | None = None,
236
+ ) -> list[OrderFill]:
237
+ """Retrieves the authenticated account's order fills that match the given
238
+ parameters.
239
+
240
+ Parameters
241
+ ----------
242
+ product_id : str, optional
243
+ margin_account_id : str, optional
244
+ intent_hash : str, optional
245
+ start : datetime.datetime, optional
246
+ end : datetime.datetime, optional
247
+
248
+ Returns
249
+ -------
250
+ list of afp.schemas.OrderFill
251
+ """
252
+ filter = OrderFillFilter(
253
+ intent_account_id=self._account.address,
254
+ product_id=product_id,
255
+ margin_account_id=margin_account_id,
256
+ intent_hash=intent_hash,
257
+ start=start,
258
+ end=end,
259
+ trade_state=None,
260
+ )
261
+ return self._exchange.get_order_fills(filter)
262
+
263
+ @refresh_token_on_expiry
264
+ def iter_order_fills(
265
+ self,
266
+ *,
267
+ product_id: str | None = None,
268
+ margin_account_id: str | None = None,
269
+ intent_hash: str | None = None,
270
+ ) -> Generator[OrderFill, None, None]:
271
+ """Subscribes to the authenticated account's new order fills that match the
272
+ given parameters.
273
+
274
+ Returns a generator that yields new order fills as they are published by the
275
+ exchange. A new order fill gets publised as soon as there is a match in the
276
+ order book, before the trade is submitted to clearing.
277
+
278
+ Parameters
279
+ ----------
280
+ product_id : str, optional
281
+ margin_account_id : str, optional
282
+ intent_hash : str, optional
283
+
284
+ Yields
285
+ -------
286
+ afp.schemas.OrderFill
287
+ """
288
+ filter = OrderFillFilter(
289
+ intent_account_id=self._account.address,
290
+ product_id=product_id,
291
+ margin_account_id=margin_account_id,
292
+ intent_hash=intent_hash,
293
+ start=None,
294
+ end=None,
295
+ trade_state=TradeState.PENDING,
296
+ )
297
+ yield from self._exchange.iter_order_fills(filter)
298
+
299
+ @refresh_token_on_expiry
300
+ def market_depth(self, product_id: str) -> MarketDepthData:
301
+ """Retrieves the depth of market for the given product.
302
+
303
+ Parameters
304
+ ----------
305
+ product_id : str
306
+
307
+ Returns
308
+ -------
309
+ afp.schemas.MarketDepthData
310
+
311
+ Raises
312
+ ------
313
+ afp.exceptions.NotFoundError
314
+ If no such product exists.
315
+ """
316
+ value = validators.validate_hexstr32(product_id)
317
+ return self._exchange.get_market_depth_data(value)
318
+
319
+ @refresh_token_on_expiry
320
+ def iter_market_depth(
321
+ self, product_id: str
322
+ ) -> Generator[MarketDepthData, None, None]:
323
+ """Subscribes to updates of the depth of market for the given product.
324
+
325
+ Returns a generator that yields the updated market depth data as it is published
326
+ by the exhange.
327
+
328
+ Parameters
329
+ ----------
330
+ product_id : str
331
+
332
+ Yields
333
+ -------
334
+ afp.schemas.MarketDepthData
335
+
336
+ Raises
337
+ ------
338
+ afp.exceptions.NotFoundError
339
+ If no such product exists.
340
+ """
341
+ value = validators.validate_hexstr32(product_id)
342
+ yield from self._exchange.iter_market_depth_data(value)
@@ -0,0 +1,48 @@
1
+ """Typed bindings around the smart contracts of the Autonity Futures Protocol."""
2
+
3
+ from .auctioneer_facet import AuctionConfig, AuctionData, BidData
4
+ from .bankruptcy_facet import LAAData, PositionLossData
5
+ from .clearing_facet import ClearingConfig, Config, Intent, IntentData, Side, Trade
6
+ from .facade import (
7
+ ClearingDiamond,
8
+ MarginAccountRegistry,
9
+ OracleProvider,
10
+ ProductRegistry,
11
+ )
12
+ from .margin_account import MarginAccount, PositionData, Settlement
13
+ from .product_registry import (
14
+ OracleSpecification,
15
+ Product,
16
+ ProductMetadata,
17
+ ProductState,
18
+ )
19
+ from .trading_protocol import TradingProtocol
20
+
21
+ __all__ = (
22
+ # Contract bindings
23
+ "ClearingDiamond",
24
+ "MarginAccount",
25
+ "MarginAccountRegistry",
26
+ "OracleProvider",
27
+ "ProductRegistry",
28
+ "TradingProtocol",
29
+ # Structures
30
+ "AuctionConfig",
31
+ "AuctionData",
32
+ "BidData",
33
+ "ClearingConfig",
34
+ "Config",
35
+ "Intent",
36
+ "IntentData",
37
+ "LAAData",
38
+ "OracleSpecification",
39
+ "PositionData",
40
+ "PositionLossData",
41
+ "Product",
42
+ "ProductMetadata",
43
+ "Settlement",
44
+ "Trade",
45
+ # Enumerations
46
+ "ProductState",
47
+ "Side",
48
+ )