tpluspy 0.2.3__tar.gz → 0.3.0__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.3 → tpluspy-0.3.0}/PKG-INFO +54 -18
  2. {tpluspy-0.2.3 → tpluspy-0.3.0}/README.md +52 -16
  3. {tpluspy-0.2.3 → tpluspy-0.3.0}/setup.py +2 -2
  4. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/__init__.py +4 -0
  5. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/_context.py +71 -21
  6. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/env.py +11 -1
  7. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/markets.py +5 -0
  8. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/orders.py +13 -7
  9. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/trades.py +6 -3
  10. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/vaults.py +1 -4
  11. tpluspy-0.3.0/tplus/asset_metadata.py +116 -0
  12. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/__init__.py +2 -0
  13. tpluspy-0.3.0/tplus/client/api.py +50 -0
  14. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/auth.py +9 -4
  15. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/base.py +37 -32
  16. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/admin.py +9 -1
  17. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/cross_venue.py +5 -3
  18. tpluspy-0.3.0/tplus/client/market_data.py +336 -0
  19. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/oms/assetregistry.py +2 -4
  20. tpluspy-0.3.0/tplus/client/oms/oms_admin.py +82 -0
  21. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/orderbook.py +123 -99
  22. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/contracts.py +16 -0
  23. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/dev/env.py +1 -1
  24. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/settle.py +1 -0
  25. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/withdraw.py +106 -5
  26. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/exceptions.py +17 -0
  27. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/cancel_order.py +2 -1
  28. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/chain_address.py +10 -0
  29. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/multisig.py +3 -3
  30. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/order.py +14 -34
  31. tpluspy-0.3.0/tplus/model/order_id.py +17 -0
  32. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/order_trigger.py +3 -1
  33. tpluspy-0.3.0/tplus/model/pagination.py +24 -0
  34. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/position.py +3 -12
  35. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/replace_order.py +20 -12
  36. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/risk_parameters.py +2 -0
  37. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/settlement.py +12 -0
  38. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/signed_message.py +2 -1
  39. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/trades.py +3 -12
  40. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/types.py +3 -1
  41. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/limit_order.py +5 -2
  42. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/market_order.py +5 -2
  43. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/replace_order.py +21 -9
  44. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/signing.py +8 -2
  45. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/__init__.py +2 -2
  46. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/model.py +74 -0
  47. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/PKG-INFO +54 -18
  48. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/SOURCES.txt +4 -0
  49. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/requires.txt +1 -1
  50. tpluspy-0.2.3/tplus/client/market_data.py +0 -159
  51. tpluspy-0.2.3/tplus/client/oms/oms_admin.py +0 -41
  52. {tpluspy-0.2.3 → tpluspy-0.3.0}/pyproject.toml +0 -0
  53. {tpluspy-0.2.3 → tpluspy-0.3.0}/setup.cfg +0 -0
  54. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/__init__.py +0 -0
  55. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/__main__.py +0 -0
  56. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/accounts.py +0 -0
  57. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/assets.py +0 -0
  58. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/balance.py +0 -0
  59. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/debug.py +0 -0
  60. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/decimals.py +0 -0
  61. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/deposit.py +0 -0
  62. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/params.py +0 -0
  63. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/settle.py +0 -0
  64. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/sign.py +0 -0
  65. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/stream.py +0 -0
  66. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/withdraw.py +0 -0
  67. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/_cli/withdrawal.py +0 -0
  68. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/cli_tools/__init__.py +0 -0
  69. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/cli_tools/options.py +0 -0
  70. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/cli_tools/output.py +0 -0
  71. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/blockchain.py +0 -0
  72. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/__init__.py +0 -0
  73. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/admin_settlement.py +0 -0
  74. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/assetregistry.py +0 -0
  75. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/base.py +0 -0
  76. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/decimal.py +0 -0
  77. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/clearingengine/vault.py +0 -0
  78. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/oms/__init__.py +0 -0
  79. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/client/withdrawal.py +0 -0
  80. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/constants.py +0 -0
  81. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/__init__.py +0 -0
  82. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/abi.py +0 -0
  83. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/address.py +0 -0
  84. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/constants.py +0 -0
  85. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/dev/__init__.py +0 -0
  86. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/dev/contracts.py +0 -0
  87. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/dev/token.py +0 -0
  88. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/exceptions.py +0 -0
  89. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/__init__.py +0 -0
  90. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/chaindata.py +0 -0
  91. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/credential_manager.py +0 -0
  92. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/deposit.py +0 -0
  93. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/evm.py +0 -0
  94. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/registry.py +0 -0
  95. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/managers/vault.py +0 -0
  96. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/evm/utils.py +0 -0
  97. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/logger.py +0 -0
  98. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/managers/__init__.py +0 -0
  99. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/managers/base.py +0 -0
  100. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/__init__.py +0 -0
  101. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/approval.py +0 -0
  102. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/asset_identifier.py +0 -0
  103. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/batch_order.py +0 -0
  104. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/close_all_positions_preview.py +0 -0
  105. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/config.py +0 -0
  106. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/interest_rates.py +0 -0
  107. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/klines.py +0 -0
  108. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/limit_order.py +0 -0
  109. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/market.py +0 -0
  110. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/market_order.py +0 -0
  111. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/orderbook.py +0 -0
  112. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/user_event.py +0 -0
  113. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/user_inventory.py +0 -0
  114. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/user_margin.py +0 -0
  115. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/user_simulated_margin.py +0 -0
  116. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/user_solvency.py +0 -0
  117. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/model/withdrawal.py +0 -0
  118. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/py.typed +0 -0
  119. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/types.py +0 -0
  120. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/__init__.py +0 -0
  121. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/address.py +0 -0
  122. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/amount.py +0 -0
  123. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/bytes32.py +0 -0
  124. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/decimals.py +0 -0
  125. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/domain.py +0 -0
  126. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/hex.py +0 -0
  127. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/operator.py +0 -0
  128. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/serializers.py +0 -0
  129. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/timeout.py +0 -0
  130. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/decrypt.py +0 -0
  131. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/ed_keyfile.py +0 -0
  132. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/manager.py +0 -0
  133. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/token_cache.py +0 -0
  134. {tpluspy-0.2.3 → tpluspy-0.3.0}/tplus/utils/user/validate.py +0 -0
  135. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/dependency_links.txt +0 -0
  136. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/entry_points.txt +0 -0
  137. {tpluspy-0.2.3 → tpluspy-0.3.0}/tpluspy.egg-info/not-zip-safe +0 -0
  138. {tpluspy-0.2.3 → tpluspy-0.3.0}/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.3
3
+ Version: 0.3.0
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"
@@ -81,6 +81,28 @@ To install, use either `pip` or `uv pip`:
81
81
  uv pip install -e .
82
82
  ```
83
83
 
84
+ ## Onboarding with Codex CLI
85
+
86
+ This repo includes a Codex-discoverable onboarding skill at
87
+ `.agents/skills/onboard` for connecting an EVM wallet key to T+, checking whether
88
+ it already controls a T+ account, and guiding account creation when needed.
89
+
90
+ From the repo root:
91
+
92
+ ```text
93
+ codex
94
+ /skills
95
+ ```
96
+
97
+ Choose `onboard`, or invoke it directly with:
98
+
99
+ ```text
100
+ $onboard onboard me to T+
101
+ ```
102
+
103
+ The flow reads `TPLUS_PRIVATE_KEY` and `TPLUS_API_BASE_URL` from `.env`. Do not
104
+ paste private keys into chat.
105
+
84
106
  ## CLI
85
107
 
86
108
  `tpluspy` installs a `tplus` command for interacting with T+ services from the
@@ -109,12 +131,14 @@ from tplus.utils.user import User
109
131
  API_BASE_URL = "http://127.0.0.1:8000" # Replace with your API URL
110
132
  user = User()
111
133
 
134
+
112
135
  async def run_client():
113
136
  # Use async context manager for automatic cleanup
114
137
  async with OrderBookClient(API_BASE_URL, default_user=user) as client:
115
138
  print("Client initialized.")
116
139
  # ... use client methods ...
117
140
 
141
+
118
142
  asyncio.run(run_client())
119
143
  ```
120
144
 
@@ -138,14 +162,19 @@ print(f"Klines: {klines}")
138
162
 
139
163
  # Get Market Details for an asset
140
164
  market_details = await client.get_market(example_asset)
141
- 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
+ )
142
168
 
143
169
  # Get orders for the user
144
170
  user_orders, _ = await client.get_user_orders()
145
171
  print(f"User Orders: {user_orders}")
146
172
 
147
- # Get trades for the user and asset
148
- 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)
149
178
  print(f"User Asset Trades: {user_asset_trades}")
150
179
 
151
180
  # Get user inventory
@@ -158,6 +187,7 @@ print(f"Inventory: {inventory}")
158
187
  ```python
159
188
  # Ensure example_asset is defined (e.g., from "Fetching Data" section)
160
189
  from tplus.model.asset_identifier import AssetIdentifier
190
+
161
191
  example_asset = AssetIdentifier(200)
162
192
 
163
193
  # Create a Market for an asset (idempotent)
@@ -176,6 +206,7 @@ print(f"Market Order Response: {market_response}")
176
206
  # Create a Limit Order for a specific asset
177
207
  # Good-Till-Cancelled limit order
178
208
  from tplus.model.limit_order import GTC
209
+
179
210
  limit_response = await client.create_limit_order(
180
211
  asset_id=example_asset,
181
212
  quantity=5,
@@ -188,10 +219,7 @@ print(f"Limit Order Response: {limit_response}")
188
219
  # Cancel an Order
189
220
  # Order ID should be obtained from an order creation response.
190
221
  order_id_to_cancel = "actual-order-id-from-api" # Replace with a real order ID
191
- cancel_response = await client.cancel_order(
192
- order_id=order_id_to_cancel,
193
- asset_id=example_asset
194
- )
222
+ cancel_response = await client.cancel_order(order_id=order_id_to_cancel, asset_id=example_asset)
195
223
  print(f"Cancel Order Response: {cancel_response}")
196
224
 
197
225
  # Replace an Order
@@ -200,8 +228,8 @@ original_order_id_to_replace = "actual-original-order-id" # Replace with a real
200
228
  replace_response = await client.replace_order(
201
229
  original_order_id=original_order_id_to_replace,
202
230
  asset_id=example_asset,
203
- new_quantity=6, # Optional: New integer quantity
204
- new_price=1050 # Optional: New integer price
231
+ new_quantity=6, # Optional: New integer quantity
232
+ new_price=1050, # Optional: New integer price
205
233
  )
206
234
  print(f"Replace Order Response: {replace_response}")
207
235
  ```
@@ -222,14 +250,16 @@ example_asset = AssetIdentifier(200)
222
250
  # Stream Order Book Diffs
223
251
  async for diff_update in client.stream_depth(example_asset):
224
252
  if isinstance(diff_update, OrderBookDiff):
225
- 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
+ )
226
256
  # Add logic to handle the update, e.g., update a local order book
227
257
 
228
258
  # Stream Finalized Trades
229
259
  async for trade in client.stream_finalized_trades():
230
- if isinstance(trade, Trade):
260
+ if isinstance(trade, Trade):
231
261
  print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
232
- # Add logic to handle the trade
262
+ # Add logic to handle the trade
233
263
 
234
264
  # Other available streams:
235
265
  # client.stream_orders() -> OrderEvent
@@ -259,10 +289,16 @@ ape console --network ethereum:sepolia:alchemy
259
289
  Then, once in the console, you will already have access to contracts that you can call methods on:
260
290
 
261
291
  ```python
262
- In [1]: registry.getAssets()
263
- Out[1]: [getAssets_return(assetAddress=HexBytes('0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715'), chainId=11155111, maxDeposits=100)]
264
- In [2]: registry.admin()
265
- 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"
266
302
  ```
267
303
 
268
304
  #### Settlement signatures
@@ -10,6 +10,28 @@ To install, use either `pip` or `uv pip`:
10
10
  uv pip install -e .
11
11
  ```
12
12
 
13
+ ## Onboarding with Codex CLI
14
+
15
+ This repo includes a Codex-discoverable onboarding skill at
16
+ `.agents/skills/onboard` for connecting an EVM wallet key to T+, checking whether
17
+ it already controls a T+ account, and guiding account creation when needed.
18
+
19
+ From the repo root:
20
+
21
+ ```text
22
+ codex
23
+ /skills
24
+ ```
25
+
26
+ Choose `onboard`, or invoke it directly with:
27
+
28
+ ```text
29
+ $onboard onboard me to T+
30
+ ```
31
+
32
+ The flow reads `TPLUS_PRIVATE_KEY` and `TPLUS_API_BASE_URL` from `.env`. Do not
33
+ paste private keys into chat.
34
+
13
35
  ## CLI
14
36
 
15
37
  `tpluspy` installs a `tplus` command for interacting with T+ services from the
@@ -38,12 +60,14 @@ from tplus.utils.user import User
38
60
  API_BASE_URL = "http://127.0.0.1:8000" # Replace with your API URL
39
61
  user = User()
40
62
 
63
+
41
64
  async def run_client():
42
65
  # Use async context manager for automatic cleanup
43
66
  async with OrderBookClient(API_BASE_URL, default_user=user) as client:
44
67
  print("Client initialized.")
45
68
  # ... use client methods ...
46
69
 
70
+
47
71
  asyncio.run(run_client())
48
72
  ```
49
73
 
@@ -67,14 +91,19 @@ print(f"Klines: {klines}")
67
91
 
68
92
  # Get Market Details for an asset
69
93
  market_details = await client.get_market(example_asset)
70
- 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
+ )
71
97
 
72
98
  # Get orders for the user
73
99
  user_orders, _ = await client.get_user_orders()
74
100
  print(f"User Orders: {user_orders}")
75
101
 
76
- # Get trades for the user and asset
77
- 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)
78
107
  print(f"User Asset Trades: {user_asset_trades}")
79
108
 
80
109
  # Get user inventory
@@ -87,6 +116,7 @@ print(f"Inventory: {inventory}")
87
116
  ```python
88
117
  # Ensure example_asset is defined (e.g., from "Fetching Data" section)
89
118
  from tplus.model.asset_identifier import AssetIdentifier
119
+
90
120
  example_asset = AssetIdentifier(200)
91
121
 
92
122
  # Create a Market for an asset (idempotent)
@@ -105,6 +135,7 @@ print(f"Market Order Response: {market_response}")
105
135
  # Create a Limit Order for a specific asset
106
136
  # Good-Till-Cancelled limit order
107
137
  from tplus.model.limit_order import GTC
138
+
108
139
  limit_response = await client.create_limit_order(
109
140
  asset_id=example_asset,
110
141
  quantity=5,
@@ -117,10 +148,7 @@ print(f"Limit Order Response: {limit_response}")
117
148
  # Cancel an Order
118
149
  # Order ID should be obtained from an order creation response.
119
150
  order_id_to_cancel = "actual-order-id-from-api" # Replace with a real order ID
120
- cancel_response = await client.cancel_order(
121
- order_id=order_id_to_cancel,
122
- asset_id=example_asset
123
- )
151
+ cancel_response = await client.cancel_order(order_id=order_id_to_cancel, asset_id=example_asset)
124
152
  print(f"Cancel Order Response: {cancel_response}")
125
153
 
126
154
  # Replace an Order
@@ -129,8 +157,8 @@ original_order_id_to_replace = "actual-original-order-id" # Replace with a real
129
157
  replace_response = await client.replace_order(
130
158
  original_order_id=original_order_id_to_replace,
131
159
  asset_id=example_asset,
132
- new_quantity=6, # Optional: New integer quantity
133
- new_price=1050 # Optional: New integer price
160
+ new_quantity=6, # Optional: New integer quantity
161
+ new_price=1050, # Optional: New integer price
134
162
  )
135
163
  print(f"Replace Order Response: {replace_response}")
136
164
  ```
@@ -151,14 +179,16 @@ example_asset = AssetIdentifier(200)
151
179
  # Stream Order Book Diffs
152
180
  async for diff_update in client.stream_depth(example_asset):
153
181
  if isinstance(diff_update, OrderBookDiff):
154
- 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
+ )
155
185
  # Add logic to handle the update, e.g., update a local order book
156
186
 
157
187
  # Stream Finalized Trades
158
188
  async for trade in client.stream_finalized_trades():
159
- if isinstance(trade, Trade):
189
+ if isinstance(trade, Trade):
160
190
  print(f"[Trade] ID: {trade.trade_id}, Price: {trade.price}, Qty: {trade.quantity}")
161
- # Add logic to handle the trade
191
+ # Add logic to handle the trade
162
192
 
163
193
  # Other available streams:
164
194
  # client.stream_orders() -> OrderEvent
@@ -188,10 +218,16 @@ ape console --network ethereum:sepolia:alchemy
188
218
  Then, once in the console, you will already have access to contracts that you can call methods on:
189
219
 
190
220
  ```python
191
- In [1]: registry.getAssets()
192
- Out[1]: [getAssets_return(assetAddress=HexBytes('0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715'), chainId=11155111, maxDeposits=100)]
193
- In [2]: registry.admin()
194
- 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"
195
231
  ```
196
232
 
197
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.3",
11
+ version="0.3.0",
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
  ]
@@ -1,7 +1,9 @@
1
+ import os
1
2
  from collections.abc import Callable
2
3
  from functools import cached_property
3
4
  from pathlib import Path
4
5
  from typing import TYPE_CHECKING
6
+ from urllib.parse import urlsplit, urlunsplit
5
7
 
6
8
  import click
7
9
 
@@ -18,12 +20,38 @@ if TYPE_CHECKING:
18
20
  _AUTH_CACHE_DIR = Path.home() / ".tplus" / "auth"
19
21
 
20
22
 
23
+ def _derive_market_data_base_url(oms_base_url: str) -> str:
24
+ """Derive the market-data-service URL from the OMS URL (swap the host label
25
+ ``oms``->``mds``). Returns the input unchanged for non-oms hosts (e.g. localhost)."""
26
+ parsed = urlsplit(oms_base_url)
27
+ host = parsed.hostname
28
+ if host is None:
29
+ return oms_base_url
30
+ if host.startswith("oms."):
31
+ mds_host = f"mds.{host.removeprefix('oms.')}"
32
+ elif host.startswith("oms-"):
33
+ mds_host = f"mds-{host.removeprefix('oms-')}"
34
+ else:
35
+ return oms_base_url
36
+ userinfo = ""
37
+ if parsed.username is not None:
38
+ userinfo = parsed.username
39
+ if parsed.password is not None:
40
+ userinfo = f"{userinfo}:{parsed.password}"
41
+ userinfo = f"{userinfo}@"
42
+ port = f":{parsed.port}" if parsed.port is not None else ""
43
+ return urlunsplit(
44
+ (parsed.scheme, f"{userinfo}{mds_host}{port}", parsed.path, parsed.query, parsed.fragment)
45
+ )
46
+
47
+
21
48
  # ``dict`` base mirrors ``ape.cli.options.ApeCliContextObject`` so ape's
22
49
  # ``network_option`` callback can do ``ctx.obj["network"] = value`` without
23
50
  # importing ape here (which would slow down ``tplus --help``).
24
51
  class CLIContext(dict):
25
52
  def __init__(self):
26
53
  super().__init__()
54
+ self.api_base_url: str | None = None
27
55
  self.orderbook_base_url: str | None = None
28
56
  self.clearing_base_url: str | None = None
29
57
  self.market_data_base_url: str | None = None
@@ -51,22 +79,44 @@ class CLIContext(dict):
51
79
  manager = self.user_manager
52
80
  return manager._default_user or next(iter(manager.usernames), None)
53
81
 
54
- def orderbook_client(
55
- self, alias: str | None = None, *, anonymous: bool = False
56
- ) -> "OrderBookClient":
57
- if not self.orderbook_base_url:
82
+ def _api_base_url(self) -> str | None:
83
+ """The single T+ base URL (the OMS). Per-service URLs derive from it; an
84
+ explicit per-service flag / env var overrides it."""
85
+ return self.api_base_url or os.environ.get("TPLUS_API_BASE_URL")
86
+
87
+ def _resolved_orderbook_url(self) -> str:
88
+ # The OMS host *is* the orderbook service, so TPLUS_API_BASE_URL is used directly.
89
+ url = self.orderbook_base_url or self._api_base_url()
90
+ if not url:
58
91
  raise click.UsageError(
59
- "No orderbook base URL specified. "
60
- "Pass --orderbook-base-url or set TPLUS_ORDERBOOK_BASE_URL."
92
+ "No T+ base URL. Set TPLUS_API_BASE_URL (the OMS URL — the single T+ URL "
93
+ "everything derives from), or pass --orderbook-base-url / set "
94
+ "TPLUS_ORDERBOOK_BASE_URL to override."
61
95
  )
96
+ return url
62
97
 
98
+ def _resolved_market_data_url(self) -> str:
99
+ if self.market_data_base_url:
100
+ return self.market_data_base_url
101
+ api = self._api_base_url()
102
+ if not api:
103
+ raise click.UsageError(
104
+ "No T+ base URL. Set TPLUS_API_BASE_URL (the market-data URL is derived "
105
+ "by swapping the host label oms->mds), or pass --market-data-base-url / "
106
+ "set TPLUS_MARKET_DATA_BASE_URL to override."
107
+ )
108
+ return _derive_market_data_base_url(api)
109
+
110
+ def orderbook_client(
111
+ self, alias: str | None = None, *, anonymous: bool = False
112
+ ) -> "OrderBookClient":
63
113
  from tplus.client.auth import Auth
64
114
  from tplus.client.orderbook import OrderBookClient
65
115
  from tplus.utils.user.model import User
66
116
 
67
117
  user = User() if anonymous else self.load_user(alias)
68
118
  return OrderBookClient(
69
- base_url=self.orderbook_base_url,
119
+ base_url=self._resolved_orderbook_url(),
70
120
  default_user=user,
71
121
  auth=Auth(cache_dir=_AUTH_CACHE_DIR),
72
122
  insecure_ssl=self.ignore_ssl,
@@ -92,12 +142,7 @@ class CLIContext(dict):
92
142
  )
93
143
 
94
144
  def _oms_base_url(self) -> str:
95
- if not self.orderbook_base_url:
96
- raise click.UsageError(
97
- "No orderbook base URL specified. "
98
- "Pass --orderbook-base-url or set TPLUS_ORDERBOOK_BASE_URL."
99
- )
100
- return self.orderbook_base_url
145
+ return self._resolved_orderbook_url()
101
146
 
102
147
  def withdrawal_client(self, alias: str | None = None) -> "WithdrawalClient":
103
148
  from tplus.client.withdrawal import WithdrawalClient
@@ -119,17 +164,22 @@ class CLIContext(dict):
119
164
 
120
165
  return BlockchainClient(base_url=self.blockchain_base_url, insecure_ssl=self.ignore_ssl)
121
166
 
122
- def market_data_client(self) -> "MarketDataClient":
123
- if not self.market_data_base_url:
124
- raise click.UsageError(
125
- "No market-data base URL specified. "
126
- "Pass --market-data-base-url or set TPLUS_MARKET_DATA_BASE_URL."
127
- )
128
-
167
+ def market_data_client(
168
+ self, alias: str | None = None, *, authed: bool = False
169
+ ) -> "MarketDataClient":
170
+ from tplus.client.auth import Auth
129
171
  from tplus.client.market_data import MarketDataClient
130
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
+
131
181
  return MarketDataClient(
132
- base_url=self.market_data_base_url,
182
+ base_url=self._resolved_market_data_url(),
133
183
  insecure_ssl=self.ignore_ssl,
134
184
  )
135
185
 
@@ -20,7 +20,17 @@ def env(output_format: str):
20
20
  """Show CLI-relevant environment variables and their current values."""
21
21
  entries: list[tuple[str, str, str]] = [
22
22
  ("TPLUS_ACCOUNT", _default_account(), "Default local account alias."),
23
- ("TPLUS_ORDERBOOK_BASE_URL", "", "Orderbook service base URL."),
23
+ (
24
+ "TPLUS_API_BASE_URL",
25
+ "",
26
+ "Single T+ base URL (OMS); orderbook + market-data derive from it.",
27
+ ),
28
+ ("TPLUS_ORDERBOOK_BASE_URL", "", "Override: orderbook service base URL."),
29
+ (
30
+ "TPLUS_MARKET_DATA_BASE_URL",
31
+ "",
32
+ "Override: market-data URL (else derived from API URL, oms->mds).",
33
+ ),
24
34
  ("TPLUS_CLEARING_BASE_URL", "", "Clearing engine base URL."),
25
35
  ("TPLUS_IGNORE_SSL", "false", "Skip TLS certificate verification."),
26
36
  ("TPLUS_OUTPUT_FORMAT", "table", "Default output format (table | json)."),
@@ -11,6 +11,7 @@ from tplus._cli._context import (
11
11
  pass_cli_context,
12
12
  tplus_account_option,
13
13
  )
14
+ from tplus.asset_metadata import asset_metadata_dict
14
15
  from tplus.cli_tools import (
15
16
  echo_with_pager,
16
17
  ignore_ssl_option,
@@ -71,9 +72,13 @@ def _list(cli_ctx: CLIContext, output_format: str, no_pager: bool):
71
72
  records = []
72
73
  for market in response or []:
73
74
  fee_schedule = market.get("fee_schedule") or {}
75
+ asset_metadata = asset_metadata_dict(market.get("asset_id")) or {}
74
76
  records.append(
75
77
  {
76
78
  "asset_id": market.get("asset_id"),
79
+ "symbol": asset_metadata.get("symbol"),
80
+ "asset_class": asset_metadata.get("asset_class"),
81
+ "representations": asset_metadata.get("representations"),
77
82
  "price_decimals": market.get("book_price_decimals"),
78
83
  "quantity_decimals": market.get("book_quantity_decimals"),
79
84
  "max_leverage": market.get("max_leverage"),
@@ -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)