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.
Files changed (138) hide show
  1. {tpluspy-0.2.4 → tpluspy-0.3.1}/PKG-INFO +32 -18
  2. {tpluspy-0.2.4 → tpluspy-0.3.1}/README.md +30 -16
  3. {tpluspy-0.2.4 → tpluspy-0.3.1}/setup.py +2 -2
  4. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/__init__.py +4 -0
  5. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/_context.py +12 -1
  6. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/orders.py +13 -7
  7. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/trades.py +6 -3
  8. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/vaults.py +1 -4
  9. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/__init__.py +2 -0
  10. tpluspy-0.3.1/tplus/client/api.py +50 -0
  11. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/auth.py +9 -4
  12. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/base.py +37 -32
  13. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/admin.py +9 -1
  14. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/cross_venue.py +5 -3
  15. tpluspy-0.3.1/tplus/client/market_data.py +336 -0
  16. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/oms/assetregistry.py +2 -4
  17. tpluspy-0.3.1/tplus/client/oms/oms_admin.py +82 -0
  18. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/orderbook.py +127 -99
  19. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/contracts.py +16 -0
  20. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/env.py +1 -1
  21. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/settle.py +1 -0
  22. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/withdraw.py +106 -5
  23. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/exceptions.py +17 -0
  24. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/cancel_order.py +2 -1
  25. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/chain_address.py +10 -0
  26. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/multisig.py +3 -3
  27. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/order.py +14 -34
  28. tpluspy-0.3.1/tplus/model/order_id.py +17 -0
  29. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/order_trigger.py +3 -1
  30. tpluspy-0.3.1/tplus/model/pagination.py +24 -0
  31. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/position.py +3 -12
  32. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/replace_order.py +20 -12
  33. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/risk_parameters.py +2 -0
  34. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/settlement.py +12 -0
  35. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/signed_message.py +2 -1
  36. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/trades.py +3 -12
  37. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/types.py +3 -1
  38. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/limit_order.py +5 -2
  39. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/market_order.py +5 -2
  40. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/replace_order.py +21 -9
  41. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/signing.py +8 -2
  42. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/__init__.py +2 -2
  43. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/model.py +74 -0
  44. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/PKG-INFO +32 -18
  45. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/SOURCES.txt +3 -0
  46. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/requires.txt +1 -1
  47. tpluspy-0.2.4/tplus/client/market_data.py +0 -159
  48. tpluspy-0.2.4/tplus/client/oms/oms_admin.py +0 -41
  49. {tpluspy-0.2.4 → tpluspy-0.3.1}/pyproject.toml +0 -0
  50. {tpluspy-0.2.4 → tpluspy-0.3.1}/setup.cfg +0 -0
  51. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/__init__.py +0 -0
  52. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/__main__.py +0 -0
  53. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/accounts.py +0 -0
  54. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/assets.py +0 -0
  55. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/balance.py +0 -0
  56. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/debug.py +0 -0
  57. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/decimals.py +0 -0
  58. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/deposit.py +0 -0
  59. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/env.py +0 -0
  60. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/markets.py +0 -0
  61. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/params.py +0 -0
  62. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/settle.py +0 -0
  63. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/sign.py +0 -0
  64. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/stream.py +0 -0
  65. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/withdraw.py +0 -0
  66. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/_cli/withdrawal.py +0 -0
  67. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/asset_metadata.py +0 -0
  68. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/__init__.py +0 -0
  69. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/options.py +0 -0
  70. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/cli_tools/output.py +0 -0
  71. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/blockchain.py +0 -0
  72. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/__init__.py +0 -0
  73. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/admin_settlement.py +0 -0
  74. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/assetregistry.py +0 -0
  75. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/base.py +0 -0
  76. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/decimal.py +0 -0
  77. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/clearingengine/vault.py +0 -0
  78. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/oms/__init__.py +0 -0
  79. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/client/withdrawal.py +0 -0
  80. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/constants.py +0 -0
  81. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/__init__.py +0 -0
  82. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/abi.py +0 -0
  83. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/address.py +0 -0
  84. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/constants.py +0 -0
  85. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/__init__.py +0 -0
  86. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/contracts.py +0 -0
  87. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/dev/token.py +0 -0
  88. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/exceptions.py +0 -0
  89. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/__init__.py +0 -0
  90. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/chaindata.py +0 -0
  91. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/credential_manager.py +0 -0
  92. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/deposit.py +0 -0
  93. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/evm.py +0 -0
  94. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/registry.py +0 -0
  95. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/managers/vault.py +0 -0
  96. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/evm/utils.py +0 -0
  97. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/logger.py +0 -0
  98. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/managers/__init__.py +0 -0
  99. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/managers/base.py +0 -0
  100. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/__init__.py +0 -0
  101. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/approval.py +0 -0
  102. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/asset_identifier.py +0 -0
  103. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/batch_order.py +0 -0
  104. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/close_all_positions_preview.py +0 -0
  105. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/config.py +0 -0
  106. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/interest_rates.py +0 -0
  107. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/klines.py +0 -0
  108. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/limit_order.py +0 -0
  109. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/market.py +0 -0
  110. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/market_order.py +0 -0
  111. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/orderbook.py +0 -0
  112. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_event.py +0 -0
  113. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_inventory.py +0 -0
  114. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_margin.py +0 -0
  115. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_simulated_margin.py +0 -0
  116. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/user_solvency.py +0 -0
  117. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/model/withdrawal.py +0 -0
  118. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/py.typed +0 -0
  119. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/types.py +0 -0
  120. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/__init__.py +0 -0
  121. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/address.py +0 -0
  122. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/amount.py +0 -0
  123. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/bytes32.py +0 -0
  124. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/decimals.py +0 -0
  125. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/domain.py +0 -0
  126. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/hex.py +0 -0
  127. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/operator.py +0 -0
  128. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/serializers.py +0 -0
  129. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/timeout.py +0 -0
  130. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/decrypt.py +0 -0
  131. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/ed_keyfile.py +0 -0
  132. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/manager.py +0 -0
  133. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/token_cache.py +0 -0
  134. {tpluspy-0.2.4 → tpluspy-0.3.1}/tplus/utils/user/validate.py +0 -0
  135. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/dependency_links.txt +0 -0
  136. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/entry_points.txt +0 -0
  137. {tpluspy-0.2.4 → tpluspy-0.3.1}/tpluspy.egg-info/not-zip-safe +0 -0
  138. {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.2.4
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>=0.11.7; extra == "lint"
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(f"Market Details: Price Decimals={market_details.book_price_decimals}, Quantity Decimals={market_details.book_quantity_decimals}")
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
- user_asset_trades = await client.get_user_trades_for_asset(example_asset)
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, # Optional: New integer quantity
226
- new_price=1050 # Optional: New integer price
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(f"[Depth] Seq={diff_update.sequence_number}, Asks={len(diff_update.asks)}, Bids={len(diff_update.bids)}")
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
- if isinstance(trade, Trade):
260
+ if isinstance(trade, Trade):
253
261
  print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
254
- # Add logic to handle the trade
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 [1]: registry.getAssets()
285
- Out[1]: [getAssets_return(assetAddress=HexBytes('0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715'), chainId=11155111, maxDeposits=100)]
286
- In [2]: registry.admin()
287
- Out[2]: '0x467a95fC5359edE5d5dDc4f10A1F4B680694858E'
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(f"Market Details: Price Decimals={market_details.book_price_decimals}, Quantity Decimals={market_details.book_quantity_decimals}")
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
- user_asset_trades = await client.get_user_trades_for_asset(example_asset)
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, # Optional: New integer quantity
155
- new_price=1050 # Optional: New integer price
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(f"[Depth] Seq={diff_update.sequence_number}, Asks={len(diff_update.asks)}, Bids={len(diff_update.bids)}")
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
- if isinstance(trade, Trade):
189
+ if isinstance(trade, Trade):
182
190
  print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
183
- # Add logic to handle the trade
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 [1]: registry.getAssets()
214
- Out[1]: [getAssets_return(assetAddress=HexBytes('0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715'), chainId=11155111, maxDeposits=100)]
215
- In [2]: registry.admin()
216
- Out[2]: '0x467a95fC5359edE5d5dDc4f10A1F4B680694858E'
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.2.4",
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(self) -> "MarketDataClient":
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("--quantity", type=int, help="New base quantity.")
98
- @click.option("--price", type=int, help="New price.")
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 | None,
105
- price: int | None,
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.orderbook_client()
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
- raw = asyncio.run(client.assets.get_vaults())
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=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=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
- signature_bytes = user.sign(nonce_value)
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
- endpoint: str,
157
- json_data: dict[str, Any] | None = None,
158
- *,
159
- requires_auth: bool = True,
160
- request_timeout: float | None = None,
161
- ) -> dict[str, Any]:
162
- return await self._request(
163
- "POST",
164
- endpoint,
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}`` — ``venue_present``
19
- flips true once the adapter binding reaches the CE, and ``allocation_bps``
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"