tpluspy 0.2.4__tar.gz → 0.3.1__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.
- {tpluspy-0.2.4 → tpluspy-0.3.1}/PKG-INFO +32 -18
- {tpluspy-0.2.4 → tpluspy-0.3.1}/README.md +30 -16
- {tpluspy-0.2.4 → tpluspy-0.3.1}/setup.py +2 -2
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/__init__.py +4 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/_context.py +12 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/orders.py +13 -7
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/trades.py +6 -3
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/vaults.py +1 -4
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/__init__.py +2 -0
- tpluspy-0.3.1/tplus/client/api.py +50 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/auth.py +9 -4
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/base.py +37 -32
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/admin.py +9 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/cross_venue.py +5 -3
- tpluspy-0.3.1/tplus/client/market_data.py +336 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/oms/assetregistry.py +2 -4
- tpluspy-0.3.1/tplus/client/oms/oms_admin.py +82 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/orderbook.py +127 -99
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/contracts.py +16 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/env.py +1 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/settle.py +1 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/withdraw.py +106 -5
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/exceptions.py +17 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/cancel_order.py +2 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/chain_address.py +10 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/multisig.py +3 -3
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/order.py +14 -34
- tpluspy-0.3.1/tplus/model/order_id.py +17 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/order_trigger.py +3 -1
- tpluspy-0.3.1/tplus/model/pagination.py +24 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/position.py +3 -12
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/replace_order.py +20 -12
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/risk_parameters.py +2 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/settlement.py +12 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/signed_message.py +2 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/trades.py +3 -12
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/types.py +3 -1
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/limit_order.py +5 -2
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/market_order.py +5 -2
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/replace_order.py +21 -9
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/signing.py +8 -2
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/__init__.py +2 -2
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/model.py +74 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/PKG-INFO +32 -18
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/SOURCES.txt +3 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/requires.txt +1 -1
- tpluspy-0.2.4/tplus/client/market_data.py +0 -159
- tpluspy-0.2.4/tplus/client/oms/oms_admin.py +0 -41
- {tpluspy-0.2.4 → tpluspy-0.3.1}/pyproject.toml +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/setup.cfg +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/__main__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/accounts.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/assets.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/balance.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/debug.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/decimals.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/deposit.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/env.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/markets.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/params.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/settle.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/sign.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/stream.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/withdraw.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/withdrawal.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/asset_metadata.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/options.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/output.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/blockchain.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/admin_settlement.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/assetregistry.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/base.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/decimal.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/vault.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/oms/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/withdrawal.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/constants.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/abi.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/address.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/constants.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/contracts.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/token.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/exceptions.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/chaindata.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/credential_manager.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/deposit.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/evm.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/registry.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/vault.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/utils.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/logger.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/managers/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/managers/base.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/approval.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/asset_identifier.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/batch_order.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/close_all_positions_preview.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/config.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/interest_rates.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/klines.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/limit_order.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/market.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/market_order.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/orderbook.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_event.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_inventory.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_margin.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_simulated_margin.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_solvency.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/withdrawal.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/py.typed +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/types.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/__init__.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/address.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/amount.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/bytes32.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/decimals.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/domain.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/hex.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/operator.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/serializers.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/timeout.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/decrypt.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/ed_keyfile.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/manager.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/token_cache.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/validate.py +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/dependency_links.txt +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/entry_points.txt +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/not-zip-safe +0 -0
- {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tpluspy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: tpluspy: Client utilities for interacting with tplus
|
|
5
5
|
Home-page: https://github.com/tpluslabs/tpluspy
|
|
6
6
|
Author: TPlus Labs
|
|
@@ -37,7 +37,7 @@ Requires-Dist: anyio>=4; extra == "test"
|
|
|
37
37
|
Requires-Dist: pytest-anyio>=0.0.0; extra == "test"
|
|
38
38
|
Requires-Dist: trio>=0.24; extra == "test"
|
|
39
39
|
Provides-Extra: lint
|
|
40
|
-
Requires-Dist: ruff
|
|
40
|
+
Requires-Dist: ruff<0.16,>=0.11.7; extra == "lint"
|
|
41
41
|
Requires-Dist: mypy<2,>=1.18.2; extra == "lint"
|
|
42
42
|
Provides-Extra: release
|
|
43
43
|
Requires-Dist: setuptools>=75.6.0; extra == "release"
|
|
@@ -131,12 +131,14 @@ from tplus.utils.user import User
|
|
|
131
131
|
API_BASE_URL = "http://127.0.0.1:8000" # Replace with your API URL
|
|
132
132
|
user = User()
|
|
133
133
|
|
|
134
|
+
|
|
134
135
|
async def run_client():
|
|
135
136
|
# Use async context manager for automatic cleanup
|
|
136
137
|
async with OrderBookClient(API_BASE_URL, default_user=user) as client:
|
|
137
138
|
print("Client initialized.")
|
|
138
139
|
# ... use client methods ...
|
|
139
140
|
|
|
141
|
+
|
|
140
142
|
asyncio.run(run_client())
|
|
141
143
|
```
|
|
142
144
|
|
|
@@ -160,14 +162,19 @@ print(f"Klines: {klines}")
|
|
|
160
162
|
|
|
161
163
|
# Get Market Details for an asset
|
|
162
164
|
market_details = await client.get_market(example_asset)
|
|
163
|
-
print(
|
|
165
|
+
print(
|
|
166
|
+
f"Market Details: Price Decimals={market_details.book_price_decimals}, Quantity Decimals={market_details.book_quantity_decimals}"
|
|
167
|
+
)
|
|
164
168
|
|
|
165
169
|
# Get orders for the user
|
|
166
170
|
user_orders, _ = await client.get_user_orders()
|
|
167
171
|
print(f"User Orders: {user_orders}")
|
|
168
172
|
|
|
169
|
-
# Get trades for the user and asset
|
|
170
|
-
|
|
173
|
+
# Get trades for the user and asset (per-user trades live on the market-data-service)
|
|
174
|
+
from tplus.client import MarketDataClient
|
|
175
|
+
|
|
176
|
+
md_client = MarketDataClient("http://localhost:8011", default_user=user)
|
|
177
|
+
user_asset_trades = await md_client.get_user_trades_for_asset(example_asset, user=user)
|
|
171
178
|
print(f"User Asset Trades: {user_asset_trades}")
|
|
172
179
|
|
|
173
180
|
# Get user inventory
|
|
@@ -180,6 +187,7 @@ print(f"Inventory: {inventory}")
|
|
|
180
187
|
```python
|
|
181
188
|
# Ensure example_asset is defined (e.g., from "Fetching Data" section)
|
|
182
189
|
from tplus.model.asset_identifier import AssetIdentifier
|
|
190
|
+
|
|
183
191
|
example_asset = AssetIdentifier(200)
|
|
184
192
|
|
|
185
193
|
# Create a Market for an asset (idempotent)
|
|
@@ -198,6 +206,7 @@ print(f"Market Order Response: {market_response}")
|
|
|
198
206
|
# Create a Limit Order for a specific asset
|
|
199
207
|
# Good-Till-Cancelled limit order
|
|
200
208
|
from tplus.model.limit_order import GTC
|
|
209
|
+
|
|
201
210
|
limit_response = await client.create_limit_order(
|
|
202
211
|
asset_id=example_asset,
|
|
203
212
|
quantity=5,
|
|
@@ -210,10 +219,7 @@ print(f"Limit Order Response: {limit_response}")
|
|
|
210
219
|
# Cancel an Order
|
|
211
220
|
# Order ID should be obtained from an order creation response.
|
|
212
221
|
order_id_to_cancel = "actual-order-id-from-api" # Replace with a real order ID
|
|
213
|
-
cancel_response = await client.cancel_order(
|
|
214
|
-
order_id=order_id_to_cancel,
|
|
215
|
-
asset_id=example_asset
|
|
216
|
-
)
|
|
222
|
+
cancel_response = await client.cancel_order(order_id=order_id_to_cancel, asset_id=example_asset)
|
|
217
223
|
print(f"Cancel Order Response: {cancel_response}")
|
|
218
224
|
|
|
219
225
|
# Replace an Order
|
|
@@ -222,8 +228,8 @@ original_order_id_to_replace = "actual-original-order-id" # Replace with a real
|
|
|
222
228
|
replace_response = await client.replace_order(
|
|
223
229
|
original_order_id=original_order_id_to_replace,
|
|
224
230
|
asset_id=example_asset,
|
|
225
|
-
new_quantity=6,
|
|
226
|
-
new_price=1050
|
|
231
|
+
new_quantity=6, # Optional: New integer quantity
|
|
232
|
+
new_price=1050, # Optional: New integer price
|
|
227
233
|
)
|
|
228
234
|
print(f"Replace Order Response: {replace_response}")
|
|
229
235
|
```
|
|
@@ -244,14 +250,16 @@ example_asset = AssetIdentifier(200)
|
|
|
244
250
|
# Stream Order Book Diffs
|
|
245
251
|
async for diff_update in client.stream_depth(example_asset):
|
|
246
252
|
if isinstance(diff_update, OrderBookDiff):
|
|
247
|
-
print(
|
|
253
|
+
print(
|
|
254
|
+
f"[Depth] Seq={diff_update.sequence_number}, Asks={len(diff_update.asks)}, Bids={len(diff_update.bids)}"
|
|
255
|
+
)
|
|
248
256
|
# Add logic to handle the update, e.g., update a local order book
|
|
249
257
|
|
|
250
258
|
# Stream Finalized Trades
|
|
251
259
|
async for trade in client.stream_finalized_trades():
|
|
252
|
-
|
|
260
|
+
if isinstance(trade, Trade):
|
|
253
261
|
print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
|
|
254
|
-
|
|
262
|
+
# Add logic to handle the trade
|
|
255
263
|
|
|
256
264
|
# Other available streams:
|
|
257
265
|
# client.stream_orders() -> OrderEvent
|
|
@@ -281,10 +289,16 @@ ape console --network ethereum:sepolia:alchemy
|
|
|
281
289
|
Then, once in the console, you will already have access to contracts that you can call methods on:
|
|
282
290
|
|
|
283
291
|
```python
|
|
284
|
-
In
|
|
285
|
-
Out[1]: [
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
In[1]: registry.getAssets()
|
|
293
|
+
Out[1]: [
|
|
294
|
+
getAssets_return(
|
|
295
|
+
assetAddress=HexBytes("0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715"),
|
|
296
|
+
chainId=11155111,
|
|
297
|
+
maxDeposits=100,
|
|
298
|
+
)
|
|
299
|
+
]
|
|
300
|
+
In[2]: registry.admin()
|
|
301
|
+
Out[2]: "0x467a95fC5359edE5d5dDc4f10A1F4B680694858E"
|
|
288
302
|
```
|
|
289
303
|
|
|
290
304
|
#### Settlement signatures
|
|
@@ -60,12 +60,14 @@ from tplus.utils.user import User
|
|
|
60
60
|
API_BASE_URL = "http://127.0.0.1:8000" # Replace with your API URL
|
|
61
61
|
user = User()
|
|
62
62
|
|
|
63
|
+
|
|
63
64
|
async def run_client():
|
|
64
65
|
# Use async context manager for automatic cleanup
|
|
65
66
|
async with OrderBookClient(API_BASE_URL, default_user=user) as client:
|
|
66
67
|
print("Client initialized.")
|
|
67
68
|
# ... use client methods ...
|
|
68
69
|
|
|
70
|
+
|
|
69
71
|
asyncio.run(run_client())
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -89,14 +91,19 @@ print(f"Klines: {klines}")
|
|
|
89
91
|
|
|
90
92
|
# Get Market Details for an asset
|
|
91
93
|
market_details = await client.get_market(example_asset)
|
|
92
|
-
print(
|
|
94
|
+
print(
|
|
95
|
+
f"Market Details: Price Decimals={market_details.book_price_decimals}, Quantity Decimals={market_details.book_quantity_decimals}"
|
|
96
|
+
)
|
|
93
97
|
|
|
94
98
|
# Get orders for the user
|
|
95
99
|
user_orders, _ = await client.get_user_orders()
|
|
96
100
|
print(f"User Orders: {user_orders}")
|
|
97
101
|
|
|
98
|
-
# Get trades for the user and asset
|
|
99
|
-
|
|
102
|
+
# Get trades for the user and asset (per-user trades live on the market-data-service)
|
|
103
|
+
from tplus.client import MarketDataClient
|
|
104
|
+
|
|
105
|
+
md_client = MarketDataClient("http://localhost:8011", default_user=user)
|
|
106
|
+
user_asset_trades = await md_client.get_user_trades_for_asset(example_asset, user=user)
|
|
100
107
|
print(f"User Asset Trades: {user_asset_trades}")
|
|
101
108
|
|
|
102
109
|
# Get user inventory
|
|
@@ -109,6 +116,7 @@ print(f"Inventory: {inventory}")
|
|
|
109
116
|
```python
|
|
110
117
|
# Ensure example_asset is defined (e.g., from "Fetching Data" section)
|
|
111
118
|
from tplus.model.asset_identifier import AssetIdentifier
|
|
119
|
+
|
|
112
120
|
example_asset = AssetIdentifier(200)
|
|
113
121
|
|
|
114
122
|
# Create a Market for an asset (idempotent)
|
|
@@ -127,6 +135,7 @@ print(f"Market Order Response: {market_response}")
|
|
|
127
135
|
# Create a Limit Order for a specific asset
|
|
128
136
|
# Good-Till-Cancelled limit order
|
|
129
137
|
from tplus.model.limit_order import GTC
|
|
138
|
+
|
|
130
139
|
limit_response = await client.create_limit_order(
|
|
131
140
|
asset_id=example_asset,
|
|
132
141
|
quantity=5,
|
|
@@ -139,10 +148,7 @@ print(f"Limit Order Response: {limit_response}")
|
|
|
139
148
|
# Cancel an Order
|
|
140
149
|
# Order ID should be obtained from an order creation response.
|
|
141
150
|
order_id_to_cancel = "actual-order-id-from-api" # Replace with a real order ID
|
|
142
|
-
cancel_response = await client.cancel_order(
|
|
143
|
-
order_id=order_id_to_cancel,
|
|
144
|
-
asset_id=example_asset
|
|
145
|
-
)
|
|
151
|
+
cancel_response = await client.cancel_order(order_id=order_id_to_cancel, asset_id=example_asset)
|
|
146
152
|
print(f"Cancel Order Response: {cancel_response}")
|
|
147
153
|
|
|
148
154
|
# Replace an Order
|
|
@@ -151,8 +157,8 @@ original_order_id_to_replace = "actual-original-order-id" # Replace with a real
|
|
|
151
157
|
replace_response = await client.replace_order(
|
|
152
158
|
original_order_id=original_order_id_to_replace,
|
|
153
159
|
asset_id=example_asset,
|
|
154
|
-
new_quantity=6,
|
|
155
|
-
new_price=1050
|
|
160
|
+
new_quantity=6, # Optional: New integer quantity
|
|
161
|
+
new_price=1050, # Optional: New integer price
|
|
156
162
|
)
|
|
157
163
|
print(f"Replace Order Response: {replace_response}")
|
|
158
164
|
```
|
|
@@ -173,14 +179,16 @@ example_asset = AssetIdentifier(200)
|
|
|
173
179
|
# Stream Order Book Diffs
|
|
174
180
|
async for diff_update in client.stream_depth(example_asset):
|
|
175
181
|
if isinstance(diff_update, OrderBookDiff):
|
|
176
|
-
print(
|
|
182
|
+
print(
|
|
183
|
+
f"[Depth] Seq={diff_update.sequence_number}, Asks={len(diff_update.asks)}, Bids={len(diff_update.bids)}"
|
|
184
|
+
)
|
|
177
185
|
# Add logic to handle the update, e.g., update a local order book
|
|
178
186
|
|
|
179
187
|
# Stream Finalized Trades
|
|
180
188
|
async for trade in client.stream_finalized_trades():
|
|
181
|
-
|
|
189
|
+
if isinstance(trade, Trade):
|
|
182
190
|
print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
|
|
183
|
-
|
|
191
|
+
# Add logic to handle the trade
|
|
184
192
|
|
|
185
193
|
# Other available streams:
|
|
186
194
|
# client.stream_orders() -> OrderEvent
|
|
@@ -210,10 +218,16 @@ ape console --network ethereum:sepolia:alchemy
|
|
|
210
218
|
Then, once in the console, you will already have access to contracts that you can call methods on:
|
|
211
219
|
|
|
212
220
|
```python
|
|
213
|
-
In
|
|
214
|
-
Out[1]: [
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
In[1]: registry.getAssets()
|
|
222
|
+
Out[1]: [
|
|
223
|
+
getAssets_return(
|
|
224
|
+
assetAddress=HexBytes("0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715"),
|
|
225
|
+
chainId=11155111,
|
|
226
|
+
maxDeposits=100,
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
In[2]: registry.admin()
|
|
230
|
+
Out[2]: "0x467a95fC5359edE5d5dDc4f10A1F4B680694858E"
|
|
217
231
|
```
|
|
218
232
|
|
|
219
233
|
#### Settlement signatures
|
|
@@ -8,7 +8,7 @@ long_description = (here / "README.md").read_text(encoding="utf-8")
|
|
|
8
8
|
|
|
9
9
|
setup(
|
|
10
10
|
name="tpluspy",
|
|
11
|
-
version="0.
|
|
11
|
+
version="0.3.1",
|
|
12
12
|
description="""tpluspy: Client utilities for interacting with tplus""",
|
|
13
13
|
long_description=long_description,
|
|
14
14
|
long_description_content_type="text/markdown",
|
|
@@ -43,7 +43,7 @@ setup(
|
|
|
43
43
|
"trio>=0.24",
|
|
44
44
|
],
|
|
45
45
|
"lint": [
|
|
46
|
-
"ruff>=0.11.7",
|
|
46
|
+
"ruff>=0.11.7,<0.16",
|
|
47
47
|
"mypy>=1.18.2,<2",
|
|
48
48
|
],
|
|
49
49
|
"release": [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from tplus.client import TplusApiClient
|
|
1
2
|
from tplus.exceptions import (
|
|
2
3
|
AuthError,
|
|
3
4
|
NotFoundError,
|
|
@@ -5,6 +6,7 @@ from tplus.exceptions import (
|
|
|
5
6
|
OrderRejected,
|
|
6
7
|
RateLimitError,
|
|
7
8
|
ServerError,
|
|
9
|
+
SignerRegistryUnavailable,
|
|
8
10
|
)
|
|
9
11
|
|
|
10
12
|
__all__ = [
|
|
@@ -14,4 +16,6 @@ __all__ = [
|
|
|
14
16
|
"OrderRejected",
|
|
15
17
|
"RateLimitError",
|
|
16
18
|
"ServerError",
|
|
19
|
+
"SignerRegistryUnavailable",
|
|
20
|
+
"TplusApiClient",
|
|
17
21
|
]
|
|
@@ -164,9 +164,20 @@ class CLIContext(dict):
|
|
|
164
164
|
|
|
165
165
|
return BlockchainClient(base_url=self.blockchain_base_url, insecure_ssl=self.ignore_ssl)
|
|
166
166
|
|
|
167
|
-
def market_data_client(
|
|
167
|
+
def market_data_client(
|
|
168
|
+
self, alias: str | None = None, *, authed: bool = False
|
|
169
|
+
) -> "MarketDataClient":
|
|
170
|
+
from tplus.client.auth import Auth
|
|
168
171
|
from tplus.client.market_data import MarketDataClient
|
|
169
172
|
|
|
173
|
+
if authed:
|
|
174
|
+
return MarketDataClient(
|
|
175
|
+
base_url=self._resolved_market_data_url(),
|
|
176
|
+
default_user=self.load_user(alias),
|
|
177
|
+
auth=Auth(cache_dir=_AUTH_CACHE_DIR),
|
|
178
|
+
insecure_ssl=self.ignore_ssl,
|
|
179
|
+
)
|
|
180
|
+
|
|
170
181
|
return MarketDataClient(
|
|
171
182
|
base_url=self._resolved_market_data_url(),
|
|
172
183
|
insecure_ssl=self.ignore_ssl,
|
|
@@ -94,20 +94,26 @@ def _cancel(cli_ctx: CLIContext, order_id: str, asset_id: str):
|
|
|
94
94
|
@tplus_account_option()
|
|
95
95
|
@click.argument("order_id")
|
|
96
96
|
@click.option("--asset", "asset_id", required=True, help="Asset identifier.")
|
|
97
|
-
@click.option(
|
|
98
|
-
|
|
97
|
+
@click.option(
|
|
98
|
+
"--quantity",
|
|
99
|
+
type=int,
|
|
100
|
+
required=True,
|
|
101
|
+
help="Effective lifetime-total base quantity after the replace.",
|
|
102
|
+
)
|
|
103
|
+
@click.option("--price", type=int, required=True, help="Effective price after the replace.")
|
|
99
104
|
@pass_cli_context
|
|
100
105
|
def _replace(
|
|
101
106
|
cli_ctx: CLIContext,
|
|
102
107
|
order_id: str,
|
|
103
108
|
asset_id: str,
|
|
104
|
-
quantity: int
|
|
105
|
-
price: int
|
|
109
|
+
quantity: int,
|
|
110
|
+
price: int,
|
|
106
111
|
):
|
|
107
|
-
"""Replace ORDER_ID with new parameters.
|
|
108
|
-
if quantity is None and price is None:
|
|
109
|
-
raise click.UsageError("Pass --quantity and/or --price.")
|
|
112
|
+
"""Replace ORDER_ID with new parameters.
|
|
110
113
|
|
|
114
|
+
A replacement is signed over its complete effective terms, so both --quantity and
|
|
115
|
+
--price are required even when only one of them changes.
|
|
116
|
+
"""
|
|
111
117
|
from tplus.model.asset_identifier import AssetIdentifier
|
|
112
118
|
|
|
113
119
|
client = cli_ctx.orderbook_client()
|
|
@@ -4,6 +4,7 @@ import click
|
|
|
4
4
|
|
|
5
5
|
from tplus._cli._context import (
|
|
6
6
|
CLIContext,
|
|
7
|
+
market_data_url_option,
|
|
7
8
|
orderbook_url_option,
|
|
8
9
|
pass_cli_context,
|
|
9
10
|
tplus_account_option,
|
|
@@ -23,6 +24,7 @@ def trades():
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
@trades.command("list")
|
|
27
|
+
@market_data_url_option()
|
|
26
28
|
@orderbook_url_option()
|
|
27
29
|
@ignore_ssl_option()
|
|
28
30
|
@tplus_account_option()
|
|
@@ -34,11 +36,12 @@ def _list(cli_ctx: CLIContext, output_format: str, no_pager: bool, asset_id: str
|
|
|
34
36
|
"""List user trades."""
|
|
35
37
|
from tplus.model.asset_identifier import AssetIdentifier
|
|
36
38
|
|
|
37
|
-
client = cli_ctx.
|
|
39
|
+
client = cli_ctx.market_data_client(authed=True)
|
|
40
|
+
user = cli_ctx.load_user()
|
|
38
41
|
if asset_id:
|
|
39
|
-
result = asyncio.run(client.get_user_trades_for_asset(AssetIdentifier(asset_id)))
|
|
42
|
+
result = asyncio.run(client.get_user_trades_for_asset(AssetIdentifier(asset_id), user=user))
|
|
40
43
|
else:
|
|
41
|
-
result = asyncio.run(client.get_user_trades())
|
|
44
|
+
result = asyncio.run(client.get_user_trades(user=user))
|
|
42
45
|
|
|
43
46
|
if not result:
|
|
44
47
|
click.echo("No trades.")
|
|
@@ -36,11 +36,8 @@ def vaults():
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _list_via_oms(cli_ctx: CLIContext, output_format: str, no_pager: bool):
|
|
39
|
-
from tplus.model.chain_address import ChainAddress
|
|
40
|
-
|
|
41
39
|
client = cli_ctx.orderbook_client(anonymous=True)
|
|
42
|
-
|
|
43
|
-
addresses = [ChainAddress.model_validate(a) for a in raw]
|
|
40
|
+
addresses = asyncio.run(client.assets.get_vaults())
|
|
44
41
|
|
|
45
42
|
if output_format == "raw":
|
|
46
43
|
echo_with_pager([str(a) for a in addresses], no_pager=no_pager)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .api import TplusApiClient
|
|
1
2
|
from .blockchain import BlockchainClient
|
|
2
3
|
from .clearingengine import ClearingEngineClient
|
|
3
4
|
from .market_data import MarketDataClient
|
|
@@ -10,6 +11,7 @@ __all__ = (
|
|
|
10
11
|
"ClearingEngineClient",
|
|
11
12
|
"MarketDataClient",
|
|
12
13
|
"OrderBookClient",
|
|
14
|
+
"TplusApiClient",
|
|
13
15
|
"WithdrawalClient",
|
|
14
16
|
"AssetRegistryClient",
|
|
15
17
|
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Unified client for the T+ API gateway (one origin fronting OMS + MDS)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from tplus.client.auth import AuthenticatedClient
|
|
6
|
+
from tplus.client.market_data import MarketDataClient
|
|
7
|
+
from tplus.client.orderbook import OrderBookClient
|
|
8
|
+
|
|
9
|
+
DEFAULT_BASE_URL = "http://localhost:8080"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TplusApiClient(AuthenticatedClient):
|
|
13
|
+
"""Single-token client for the T+ API gateway.
|
|
14
|
+
|
|
15
|
+
The gateway is a reverse proxy exposing both the OMS and the MDS behind one
|
|
16
|
+
origin and one auth token, so a single nonce/token handshake authenticates
|
|
17
|
+
every endpoint. This client owns the shared httpx connection and ``Auth``;
|
|
18
|
+
the two service namespaces are composed off it so they reuse both:
|
|
19
|
+
|
|
20
|
+
- ``.oms`` — :class:`OrderBookClient` (orders, inventory, positions, margin).
|
|
21
|
+
- ``.mds`` — :class:`MarketDataClient` (klines, depth, tickers, user trades).
|
|
22
|
+
|
|
23
|
+
It is a **drop-in replacement** for either sub-client: any method not defined
|
|
24
|
+
on the facade itself is proxied to ``.oms`` first, then ``.mds`` (OMS is the
|
|
25
|
+
primary trading surface), so code written against one sub-client keeps working
|
|
26
|
+
when swapped to this facade. Use ``.oms`` / ``.mds`` explicitly to disambiguate
|
|
27
|
+
when both define the same name.
|
|
28
|
+
|
|
29
|
+
Closing this client (or exiting its ``async with`` block) closes the one
|
|
30
|
+
shared connection.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, base_url: str = DEFAULT_BASE_URL, **kwargs) -> None:
|
|
34
|
+
super().__init__(base_url, **kwargs)
|
|
35
|
+
self.oms = OrderBookClient.from_client(self)
|
|
36
|
+
self.mds = MarketDataClient.from_client(self)
|
|
37
|
+
|
|
38
|
+
def __getattr__(self, name: str) -> Any:
|
|
39
|
+
# Only reached when normal lookup fails, so real inherited attributes
|
|
40
|
+
# (_client, _auth, oms, mds, close, …) never hit this path.
|
|
41
|
+
if name.startswith("_"):
|
|
42
|
+
raise AttributeError(name)
|
|
43
|
+
|
|
44
|
+
for sub_client in ("oms", "mds"):
|
|
45
|
+
# __dict__ avoids re-entering __getattr__ before __init__ assigns them.
|
|
46
|
+
client = self.__dict__.get(sub_client)
|
|
47
|
+
if client is not None and hasattr(client, name):
|
|
48
|
+
return getattr(client, name)
|
|
49
|
+
|
|
50
|
+
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
|
|
@@ -96,9 +96,11 @@ class AuthenticatedClient(BaseClient):
|
|
|
96
96
|
*,
|
|
97
97
|
requires_auth: bool = True,
|
|
98
98
|
user: "UserType | None" = None,
|
|
99
|
+
headers: dict[str, str] | None = None,
|
|
99
100
|
request_timeout: float | None = None,
|
|
100
101
|
) -> dict[str, Any]:
|
|
101
102
|
relative_url = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
103
|
+
extra_headers = headers
|
|
102
104
|
|
|
103
105
|
# Sign in opportunistically so authed callers get the higher-tier rate
|
|
104
106
|
# limits even on endpoints flagged requires_auth=False.
|
|
@@ -120,7 +122,10 @@ class AuthenticatedClient(BaseClient):
|
|
|
120
122
|
)
|
|
121
123
|
use_auth = False
|
|
122
124
|
|
|
123
|
-
headers = self._build_headers(with_auth=use_auth, user=
|
|
125
|
+
headers = self._build_headers(with_auth=use_auth, user=user)
|
|
126
|
+
if extra_headers:
|
|
127
|
+
headers.update(extra_headers)
|
|
128
|
+
|
|
124
129
|
response = await self._send(
|
|
125
130
|
method,
|
|
126
131
|
relative_url,
|
|
@@ -150,7 +155,7 @@ class AuthenticatedClient(BaseClient):
|
|
|
150
155
|
)
|
|
151
156
|
use_auth = False
|
|
152
157
|
|
|
153
|
-
headers = self._build_headers(with_auth=use_auth, user=
|
|
158
|
+
headers = self._build_headers(with_auth=use_auth, user=user)
|
|
154
159
|
response = await self._send(
|
|
155
160
|
method,
|
|
156
161
|
relative_url,
|
|
@@ -219,8 +224,7 @@ class AuthenticatedClient(BaseClient):
|
|
|
219
224
|
# NOTE: nonce_value **must** be a `str` here.
|
|
220
225
|
nonce_value = f"{nonce_data['value']}" if isinstance(nonce_data, dict) else f"{nonce_data}"
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
signature_array = list(signature_bytes)
|
|
227
|
+
signature_array, additional_signers = user.signing_parts(nonce_value)
|
|
224
228
|
nonce_value_len = len(nonce_value)
|
|
225
229
|
|
|
226
230
|
self.logger.debug(f"AUTH DEBUG: nonce={nonce_value} (len={nonce_value_len})")
|
|
@@ -232,6 +236,7 @@ class AuthenticatedClient(BaseClient):
|
|
|
232
236
|
"user_id": user.public_key,
|
|
233
237
|
"nonce": nonce_value,
|
|
234
238
|
"signature": signature_array,
|
|
239
|
+
"additional_signers": [signer.model_dump(mode="json") for signer in additional_signers],
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
token_resp = await self._client.post("/auth", json=auth_payload)
|
|
@@ -10,7 +10,7 @@ import websockets
|
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
|
-
from tplus.exceptions import MissingClientUserError, from_error_body
|
|
13
|
+
from tplus.exceptions import MissingClientUserError, from_error_body, from_flat_error
|
|
14
14
|
from tplus.logger import get_logger
|
|
15
15
|
from tplus.utils.user import User
|
|
16
16
|
|
|
@@ -74,6 +74,16 @@ def create_httpx_client(settings: ClientSettings) -> httpx.AsyncClient:
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def page_params(page: int | None, limit: int | None, **extra: Any) -> dict[str, Any] | None:
|
|
78
|
+
"""Query params for a paginated GET; drops `None`s and returns `None` if empty."""
|
|
79
|
+
params: dict[str, Any] = {k: v for k, v in extra.items() if v is not None}
|
|
80
|
+
if page is not None:
|
|
81
|
+
params["page"] = int(page)
|
|
82
|
+
if limit is not None:
|
|
83
|
+
params["limit"] = int(limit)
|
|
84
|
+
return params or None
|
|
85
|
+
|
|
86
|
+
|
|
77
87
|
class BaseClient:
|
|
78
88
|
"""
|
|
79
89
|
Base client to use across T+ services.
|
|
@@ -135,37 +145,20 @@ class BaseClient:
|
|
|
135
145
|
return user
|
|
136
146
|
return self._resolve_user(user=user).public_key
|
|
137
147
|
|
|
138
|
-
async def _get(
|
|
139
|
-
self,
|
|
140
|
-
endpoint: str,
|
|
141
|
-
json_data: dict[str, Any] | None = None,
|
|
142
|
-
*,
|
|
143
|
-
requires_auth: bool = True,
|
|
144
|
-
request_timeout: float | None = None,
|
|
145
|
-
) -> dict[str, Any]:
|
|
146
|
-
return await self._request(
|
|
147
|
-
"GET",
|
|
148
|
-
endpoint,
|
|
149
|
-
json_data=json_data,
|
|
150
|
-
requires_auth=requires_auth,
|
|
151
|
-
request_timeout=request_timeout,
|
|
152
|
-
)
|
|
148
|
+
async def _get(self, endpoint: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
149
|
+
return await self._request("GET", endpoint, *args, **kwargs)
|
|
153
150
|
|
|
154
|
-
async def _post(
|
|
155
|
-
self,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
json_data=json_data,
|
|
166
|
-
requires_auth=requires_auth,
|
|
167
|
-
request_timeout=request_timeout,
|
|
168
|
-
)
|
|
151
|
+
async def _post(self, endpoint: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
152
|
+
return await self._request("POST", endpoint, *args, **kwargs)
|
|
153
|
+
|
|
154
|
+
async def _put(self, endpoint: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
155
|
+
return await self._request("PUT", endpoint, *args, **kwargs)
|
|
156
|
+
|
|
157
|
+
async def _patch(self, endpoint: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
158
|
+
return await self._request("PATCH", endpoint, *args, **kwargs)
|
|
159
|
+
|
|
160
|
+
async def _delete(self, endpoint: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
161
|
+
return await self._request("DELETE", endpoint, *args, **kwargs)
|
|
169
162
|
|
|
170
163
|
async def _request(
|
|
171
164
|
self,
|
|
@@ -176,13 +169,19 @@ class BaseClient:
|
|
|
176
169
|
*,
|
|
177
170
|
requires_auth: bool = True,
|
|
178
171
|
user: "UserType | None" = None,
|
|
172
|
+
headers: dict[str, str] | None = None,
|
|
179
173
|
request_timeout: float | None = None,
|
|
180
174
|
) -> dict[str, Any]:
|
|
175
|
+
merged_headers = None
|
|
176
|
+
if headers is not None:
|
|
177
|
+
merged_headers = {**self._get_request_headers(), **headers}
|
|
178
|
+
|
|
181
179
|
response = await self._send(
|
|
182
180
|
method,
|
|
183
181
|
endpoint,
|
|
184
182
|
json_data=json_data,
|
|
185
183
|
params=params,
|
|
184
|
+
headers=merged_headers,
|
|
186
185
|
request_timeout=request_timeout,
|
|
187
186
|
)
|
|
188
187
|
return self._handle_response(response)
|
|
@@ -393,14 +392,20 @@ def raise_for_status_with_body(response: httpx.Response) -> None:
|
|
|
393
392
|
if response.is_success:
|
|
394
393
|
return
|
|
395
394
|
|
|
396
|
-
# Try to parse the standardised error envelope
|
|
395
|
+
# Try to parse the standardised error envelope, or a flat `{"error": "<reason>"}`.
|
|
396
|
+
flat_message = None
|
|
397
397
|
try:
|
|
398
398
|
data = response.json()
|
|
399
399
|
if isinstance(data, dict) and isinstance(data.get("error"), dict):
|
|
400
400
|
raise from_error_body(data["error"], response.status_code, response=response)
|
|
401
|
+
if isinstance(data, dict) and isinstance(data.get("error"), str):
|
|
402
|
+
flat_message = data["error"]
|
|
401
403
|
except (json.JSONDecodeError, ValueError, KeyError):
|
|
402
404
|
pass
|
|
403
405
|
|
|
406
|
+
if flat_message is not None:
|
|
407
|
+
raise from_flat_error(flat_message, response.status_code, response=response)
|
|
408
|
+
|
|
404
409
|
# Fallback: plain httpx error with body context (pre-existing behaviour)
|
|
405
410
|
try:
|
|
406
411
|
response.raise_for_status()
|
|
@@ -131,6 +131,7 @@ class AdminClient(BaseClearingEngineClient):
|
|
|
131
131
|
premium_clamp: int,
|
|
132
132
|
buffer_multiplier: int,
|
|
133
133
|
min_sub_account_balance: str | int = 0,
|
|
134
|
+
max_adl: str | int = 0,
|
|
134
135
|
):
|
|
135
136
|
asset_index = _asset_id_to_index(asset_id)
|
|
136
137
|
risk_parameters = {
|
|
@@ -155,6 +156,8 @@ class AdminClient(BaseClearingEngineClient):
|
|
|
155
156
|
"premium_clamp": premium_clamp,
|
|
156
157
|
"buffer_multiplier": buffer_multiplier,
|
|
157
158
|
"min_sub_account_balance": min_sub_account_balance,
|
|
159
|
+
# Zero disables auto-deleverage for the asset.
|
|
160
|
+
"max_adl_usd": max_adl,
|
|
158
161
|
}
|
|
159
162
|
await self._post(
|
|
160
163
|
"admin/risk-parameters/modify",
|
|
@@ -240,10 +243,15 @@ class AdminClient(BaseClearingEngineClient):
|
|
|
240
243
|
|
|
241
244
|
async def set_book_decimals(
|
|
242
245
|
self,
|
|
243
|
-
asset_id: AssetIdentifier,
|
|
246
|
+
asset_id: int | AssetIdentifier | str,
|
|
244
247
|
book_price_decimals: int,
|
|
245
248
|
book_quantity_decimals: int,
|
|
246
249
|
):
|
|
250
|
+
if not isinstance(asset_id, AssetIdentifier):
|
|
251
|
+
asset_id = AssetIdentifier.model_validate(asset_id)
|
|
252
|
+
|
|
253
|
+
asset_id = asset_id.model_dump()
|
|
254
|
+
|
|
247
255
|
await self._post(
|
|
248
256
|
"admin/book-decimals/modify",
|
|
249
257
|
json_data={
|
|
@@ -15,9 +15,11 @@ class CrossVenueClient(BaseClearingEngineClient):
|
|
|
15
15
|
|
|
16
16
|
User-authenticated: the query is signed with ``user``'s key, so a user
|
|
17
17
|
can only read their own venue state. Returns
|
|
18
|
-
``{venue_present, allocation_bps, locked, usd_balance}`` —
|
|
19
|
-
flips true once the adapter binding reaches the CE,
|
|
20
|
-
is non-zero once the credit line is applied
|
|
18
|
+
``{venue_present, allocation_bps, assigned_to, locked, usd_balance}`` —
|
|
19
|
+
``venue_present`` flips true once the adapter binding reaches the CE,
|
|
20
|
+
``allocation_bps`` is non-zero once the credit line is applied, and
|
|
21
|
+
``assigned_to`` is its sub-account index under ``user`` (or ``None``
|
|
22
|
+
before a credit line exists).
|
|
21
23
|
"""
|
|
22
24
|
ts = time.time_ns()
|
|
23
25
|
sep = b"\x1f"
|