From d1aa9d61020912a904d5c011c6f82d5889f10c15 Mon Sep 17 00:00:00 2001 From: CaoKha Date: Sun, 9 Nov 2025 19:39:29 +0100 Subject: [PATCH 1/2] update: some fields, skip validation for output, add examples --- examples/basic.py | 8 +- pyproject.toml | 4 +- src/lnmarkets_sdk/v3/_internal/__init__.py | 2 +- src/lnmarkets_sdk/v3/_internal/models.py | 2 +- src/lnmarkets_sdk/v3/http/client/__init__.py | 54 +- src/lnmarkets_sdk/v3/http/client/account.py | 198 +++++- .../v3/http/client/futures/__init__.py | 63 +- .../v3/http/client/futures/cross.py | 166 ++++- .../v3/http/client/futures/isolated.py | 173 ++++- src/lnmarkets_sdk/v3/http/client/oracle.py | 30 +- .../v3/http/client/synthetic_usd.py | 44 +- src/lnmarkets_sdk/v3/models/account.py | 240 ++++--- src/lnmarkets_sdk/v3/models/funding_fees.py | 22 +- src/lnmarkets_sdk/v3/models/futures_cross.py | 112 ++-- src/lnmarkets_sdk/v3/models/futures_data.py | 40 +- .../v3/models/futures_isolated.py | 98 +-- src/lnmarkets_sdk/v3/models/oracle.py | 14 +- src/lnmarkets_sdk/v3/models/synthetic_usd.py | 26 +- .../v3/tests/test_integration.py | 594 +++++++++++++++++- uv.lock | 39 +- 20 files changed, 1597 insertions(+), 332 deletions(-) diff --git a/examples/basic.py b/examples/basic.py index 08b8c84..a08a21f 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -105,7 +105,7 @@ async def example_authenticated_endpoints(): # Get lightning deposits (last 5) deposits = await client.account.get_lightning_deposits( - GetLightningDepositsParams(from_="2022-01-01", limit=5) + GetLightningDepositsParams(from_="1970-01-01T00:00:00.000Z", limit=5) ) print(f"\n--- Recent Lightning Deposits (Last {len(deposits)}) ---") for deposit in deposits: @@ -136,7 +136,11 @@ async def example_authenticated_endpoints(): try: print("\n--- Try to open a new cross order ---") order_params = FuturesCrossOrderLimit( - type="limit", price=1.5, quantity=1, side="b" + type="limit", + price=101000, + quantity=10, + side="s", + client_id="custom-ref-123", ) new_order = await client.futures.cross.new_order(order_params) print(f"New order: {new_order}") diff --git a/pyproject.toml b/pyproject.toml index 0496ad7..51e763c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" urls = { "Homepage" = "https://github.com/ln-markets/sdk-python", "Repository" = "https://github.com/ln-markets/sdk-python", "Bug Tracker" = "https://github.com/ln-markets/sdk-python/issues" } name = "lnmarkets-sdk" -version = "0.0.11" +version = "0.0.12" description = "LN Markets API Python SDK" readme = "README.md" license = { text = "MIT" } @@ -42,6 +42,8 @@ dev = [ { include-group = "test" }, "python-dotenv>=1.0.1", "playwright>=1.40.0", + "faker>=37.12.0", + "faker-crypto>=1.0.1", ] lint = ["ruff>=0.12.0", "pyright>=1.1.390"] test = ["pytest", "pytest-asyncio", "pytest-httpx>=0.35.0"] diff --git a/src/lnmarkets_sdk/v3/_internal/__init__.py b/src/lnmarkets_sdk/v3/_internal/__init__.py index 461ca9b..0097d2a 100644 --- a/src/lnmarkets_sdk/v3/_internal/__init__.py +++ b/src/lnmarkets_sdk/v3/_internal/__init__.py @@ -62,7 +62,7 @@ async def request( data = "" if params_dict: if method == "GET": - data = f"?{urlencode({k: str(v) for k, v in params_dict.items()})}" + data = f"?{urlencode(params_dict)}" else: data = json.dumps(params_dict, separators=(",", ":")) headers.update({"Content-Type": "application/json"}) diff --git a/src/lnmarkets_sdk/v3/_internal/models.py b/src/lnmarkets_sdk/v3/_internal/models.py index 455159c..3b050cd 100644 --- a/src/lnmarkets_sdk/v3/_internal/models.py +++ b/src/lnmarkets_sdk/v3/_internal/models.py @@ -14,7 +14,7 @@ class BaseConfig: """Base configuration for all Pydantic models.""" model_config = ConfigDict( - extra="forbid", + extra="allow", validate_assignment=True, str_strip_whitespace=True, use_enum_values=True, diff --git a/src/lnmarkets_sdk/v3/http/client/__init__.py b/src/lnmarkets_sdk/v3/http/client/__init__.py index 05cc507..e43e7f6 100644 --- a/src/lnmarkets_sdk/v3/http/client/__init__.py +++ b/src/lnmarkets_sdk/v3/http/client/__init__.py @@ -27,30 +27,40 @@ def __init__(self, config: APIClientConfig | None = None): Initialize the LN Markets client. Args: - config: Client configuration. If None, will use environment variables. + config: Client configuration Example: - >>> from src.lnmarkets_sdk.v3.client import LNMClient - >>> from src.lnmarkets_sdk.v3.types.api.base import APIClientConfig, APIAuthContext - >>> - >>> config = APIClientConfig( - ... authentication=APIAuthContext( - ... key="your-key", - ... secret="your-secret", - ... passphrase="your-passphrase", - ... ), - ... network="mainnet", - ... ) - >>> - >>> async with LNMClient(config) as client: - ... # Get account info - ... account = await client.account.get_account() - ... - ... # Get market ticker - ... ticker = await client.futures.get_ticker() - ... - ... # Place a futures order - ... order = await client.futures.isolated.new_trade(params) + ```python + from lnmarkets_sdk.v3.http.client import LNMClient, APIClientConfig, APIAuthContext + from lnmarkets_sdk.v3.models.futures_isolated import FuturesOrder + + config = APIClientConfig( + authentication=APIAuthContext( + key="your-key", + secret="your-secret", + passphrase="your-passphrase", + ), + network="mainnet", + timeout=30, + ) + + async with LNMClient(config) as client: + # Get account info + account = await client.account.get_account() + + # Get market ticker + ticker = await client.futures.get_ticker() + + # Place a futures order + params = FuturesOrder( + type="l", # limit order + side="b", # buy + price=100_000, + quantity=1, + leverage=100, + ) + order = await client.futures.isolated.new_trade(params) + ``` """ if config is None: config = APIClientConfig() diff --git a/src/lnmarkets_sdk/v3/http/client/account.py b/src/lnmarkets_sdk/v3/http/client/account.py index 1196820..21c5ebf 100644 --- a/src/lnmarkets_sdk/v3/http/client/account.py +++ b/src/lnmarkets_sdk/v3/http/client/account.py @@ -38,7 +38,17 @@ def __init__(self, client: "LNMClient"): self._client = client async def get_account(self): - """Get account information.""" + """ + Get account information. + + Example: + ```python + async with LNMClient(config) as client: + account = await client.account.get_account() + print(f"Balance: {account.balance} sats") + print(f"Username: {account.username}") + ``` + """ return await self._client.request( "GET", "/account", @@ -47,7 +57,16 @@ async def get_account(self): ) async def get_bitcoin_address(self): - """Get Bitcoin address for deposits.""" + """ + Get Bitcoin address for deposits. + + Example: + ```python + async with LNMClient(config) as client: + address = await client.account.get_bitcoin_address() + print(f"Bitcoin address: {address.address}") + ``` + """ return await self._client.request( "GET", "/account/address/bitcoin", @@ -56,7 +75,19 @@ async def get_bitcoin_address(self): ) async def add_bitcoin_address(self, params: AddBitcoinAddressParams | None = None): - """Add a new Bitcoin address.""" + """ + Add a new Bitcoin address. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import AddBitcoinAddressParams + + async with LNMClient(config) as client: + params = AddBitcoinAddressParams(format="p2wpkh") + address = await client.account.add_bitcoin_address(params) + print(f"New address: {address.address}") + ``` + """ return await self._client.request( "POST", "/account/address/bitcoin", @@ -66,7 +97,22 @@ async def add_bitcoin_address(self, params: AddBitcoinAddressParams | None = Non ) async def deposit_lightning(self, params: DepositLightningParams): - """Create a Lightning invoice for deposit.""" + """ + Create a Lightning invoice for deposit. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import DepositLightningParams + + async with LNMClient(config) as client: + params = DepositLightningParams( + amount=100_000, + comment="Deposit for trading" + ) + deposit = await client.account.deposit_lightning(params) + print(f"Payment request: {deposit.payment_request}") + ``` + """ return await self._client.request( "POST", "/account/deposit/lightning", @@ -76,7 +122,19 @@ async def deposit_lightning(self, params: DepositLightningParams): ) async def withdraw_lightning(self, params: WithdrawLightningParams): - """Withdraw via Lightning Network.""" + """ + Withdraw via Lightning Network. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import WithdrawLightningParams + + async with LNMClient(config) as client: + params = WithdrawLightningParams(invoice="lnbc...") + withdrawal = await client.account.withdraw_lightning(params) + print(f"Withdrawal ID: {withdrawal.id}") + ``` + """ return await self._client.request( "POST", "/account/withdraw/lightning", @@ -86,7 +144,19 @@ async def withdraw_lightning(self, params: WithdrawLightningParams): ) async def withdraw_internal(self, params: WithdrawInternalParams): - """Withdraw to another LN Markets account.""" + """ + Withdraw to another LN Markets account. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import WithdrawInternalParams + + async with LNMClient(config) as client: + params = WithdrawInternalParams(amount=100_000, to_username="user123") + withdrawal = await client.account.withdraw_internal(params) + print(f"Withdrawal ID: {withdrawal.id}") + ``` + """ return await self._client.request( "POST", "/account/withdraw/internal", @@ -96,7 +166,22 @@ async def withdraw_internal(self, params: WithdrawInternalParams): ) async def withdraw_on_chain(self, params: WithdrawOnChainParams): - """Withdraw via on-chain Bitcoin transaction.""" + """ + Withdraw via on-chain Bitcoin transaction. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import WithdrawOnChainParams + + async with LNMClient(config) as client: + params = WithdrawOnChainParams( + amount=100_000, + address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" + ) + withdrawal = await client.account.withdraw_on_chain(params) + print(f"Withdrawal ID: {withdrawal.id}") + ``` + """ return await self._client.request( "POST", "/account/withdraw/on-chain", @@ -108,7 +193,20 @@ async def withdraw_on_chain(self, params: WithdrawOnChainParams): async def get_lightning_deposits( self, params: GetLightningDepositsParams | None = None ): - """Get Lightning deposit history.""" + """ + Get Lightning deposit history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetLightningDepositsParams + + async with LNMClient(config) as client: + params = GetLightningDepositsParams(limit=10, settled=True) + deposits = await client.account.get_lightning_deposits(params) + for deposit in deposits: + print(f"Deposit: {deposit.id}, Amount: {deposit.amount}") + ``` + """ return await self._client.request( "GET", "/account/deposits/lightning", @@ -120,7 +218,23 @@ async def get_lightning_deposits( async def get_lightning_withdrawals( self, params: GetLightningWithdrawalsParams | None = None ): - """Get Lightning withdrawal history.""" + """ + Get Lightning withdrawal history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetLightningWithdrawalsParams + + async with LNMClient(config) as client: + params = GetLightningWithdrawalsParams( + limit=10, + status="processed" + ) + withdrawals = await client.account.get_lightning_withdrawals(params) + for withdrawal in withdrawals: + print(f"Withdrawal: {withdrawal.id}, Status: {withdrawal.status}") + ``` + """ return await self._client.request( "GET", "/account/withdrawals/lightning", @@ -132,7 +246,20 @@ async def get_lightning_withdrawals( async def get_internal_deposits( self, params: GetInternalDepositsParams | None = None ): - """Get internal deposit history.""" + """ + Get internal deposit history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetInternalDepositsParams + + async with LNMClient(config) as client: + params = GetInternalDepositsParams(limit=10) + deposits = await client.account.get_internal_deposits(params) + for deposit in deposits: + print(f"From: {deposit.from_username}, Amount: {deposit.amount}") + ``` + """ return await self._client.request( "GET", "/account/deposits/internal", @@ -144,7 +271,20 @@ async def get_internal_deposits( async def get_internal_withdrawals( self, params: GetInternalWithdrawalsParams | None = None ): - """Get internal withdrawal history.""" + """ + Get internal withdrawal history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetInternalWithdrawalsParams + + async with LNMClient(config) as client: + params = GetInternalWithdrawalsParams(limit=10) + withdrawals = await client.account.get_internal_withdrawals(params) + for withdrawal in withdrawals: + print(f"To: {withdrawal.to_username}, Amount: {withdrawal.amount}") + ``` + """ return await self._client.request( "GET", "/account/withdrawals/internal", @@ -156,7 +296,23 @@ async def get_internal_withdrawals( async def get_on_chain_deposits( self, params: GetOnChainDepositsParams | None = None ): - """Get on-chain deposit history.""" + """ + Get on-chain deposit history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetOnChainDepositsParams + + async with LNMClient(config) as client: + params = GetOnChainDepositsParams( + limit=10, + status="CONFIRMED" + ) + deposits = await client.account.get_on_chain_deposits(params) + for deposit in deposits: + print(f"TX ID: {deposit.tx_id}, Status: {deposit.status}") + ``` + """ return await self._client.request( "GET", "/account/deposits/bitcoin", @@ -168,7 +324,23 @@ async def get_on_chain_deposits( async def get_on_chain_withdrawals( self, params: GetOnChainWithdrawalsParams | None = None ): - """Get on-chain withdrawal history.""" + """ + Get on-chain withdrawal history. + + Example: + ```python + from lnmarkets_sdk.v3.models.account import GetOnChainWithdrawalsParams + + async with LNMClient(config) as client: + params = GetOnChainWithdrawalsParams( + limit=10, + status="processed" + ) + withdrawals = await client.account.get_on_chain_withdrawals(params) + for withdrawal in withdrawals: + print(f"Address: {withdrawal.address}, Status: {withdrawal.status}") + ``` + """ return await self._client.request( "GET", "/account/withdrawals/bitcoin", diff --git a/src/lnmarkets_sdk/v3/http/client/futures/__init__.py b/src/lnmarkets_sdk/v3/http/client/futures/__init__.py index 38f7eb6..2d8cd31 100644 --- a/src/lnmarkets_sdk/v3/http/client/futures/__init__.py +++ b/src/lnmarkets_sdk/v3/http/client/futures/__init__.py @@ -25,7 +25,19 @@ def __init__(self, client: "LNMClient"): self.cross = FuturesCrossClient(client) async def get_ticker(self): - """Get current futures ticker data.""" + """ + Get current futures ticker data. + + Example: + ```python + async with LNMClient(config) as client: + ticker = await client.futures.get_ticker() + print(f"Index: {ticker.index}, Last Price: {ticker.last_price}") + print(f"Funding Rate: {ticker.funding_rate}") + if ticker.prices: + print(f"Best bid: {ticker.prices[0].bid_price}") + ``` + """ return await self._client.request( "GET", "/futures/ticker", @@ -34,7 +46,19 @@ async def get_ticker(self): ) async def get_leaderboard(self): - """Get futures trading leaderboard.""" + """ + Get futures trading leaderboard. + + Example: + ```python + async with LNMClient(config) as client: + leaderboard = await client.futures.get_leaderboard() + if leaderboard.daily: + print(f"Daily top: {leaderboard.daily[0].username}") + if leaderboard.all_time: + print(f"All-time top: {leaderboard.all_time[0].username}") + ``` + """ return await self._client.request( "GET", "/futures/leaderboard", @@ -43,7 +67,25 @@ async def get_leaderboard(self): ) async def get_candles(self, params: GetCandlesParams): - """Get OHLC candle data.""" + """ + Get OHLC candle data. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_data import GetCandlesParams + + async with LNMClient(config) as client: + params = GetCandlesParams( + from_="2023-05-23T09:52:57.863Z", + range="1h", + limit=100, + to="2023-05-24T09:52:57.863Z" + ) + candles = await client.futures.get_candles(params) + for candle in candles: + print(f"Time: {candle.time}, OHLC: {candle.open}/{candle.high}/{candle.low}/{candle.close}") + ``` + """ return await self._client.request( "GET", "/futures/candles", @@ -55,7 +97,20 @@ async def get_candles(self, params: GetCandlesParams): async def get_funding_settlements( self, params: GetFundingSettlementsParams | None = None ): - """Get funding settlement history.""" + """ + Get funding settlement history. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_data import GetFundingSettlementsParams + + async with LNMClient(config) as client: + params = GetFundingSettlementsParams(limit=10) + settlements = await client.futures.get_funding_settlements(params) + for settlement in settlements: + print(f"Rate: {settlement.funding_rate}, Price: {settlement.fixing_price}") + ``` + """ return await self._client.request( "GET", "/futures/funding-settlements", diff --git a/src/lnmarkets_sdk/v3/http/client/futures/cross.py b/src/lnmarkets_sdk/v3/http/client/futures/cross.py index 8a857a7..51dbcfa 100644 --- a/src/lnmarkets_sdk/v3/http/client/futures/cross.py +++ b/src/lnmarkets_sdk/v3/http/client/futures/cross.py @@ -31,7 +31,25 @@ def __init__(self, client: "LNMClient"): async def new_order( self, params: FuturesCrossOrderLimit | FuturesCrossOrderMarket ) -> FuturesCrossOpenOrder | FuturesCrossFilledOrder | FuturesCrossCanceledOrder: - """Place a new cross margin order.""" + """ + Place a new cross margin order. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import FuturesCrossOrderLimit + + async with LNMClient(config) as client: + params = FuturesCrossOrderLimit( + type="limit", + side="b", + price=100_000, + quantity=1, + client_id="my-order-123" + ) + order = await client.futures.cross.new_order(params) + print(f"Order ID: {order.id}") + ``` + """ return await self._client.request( "POST", "/futures/cross/order", @@ -43,7 +61,16 @@ async def new_order( ) async def get_position(self): - """Get current cross margin position.""" + """ + Get current cross margin position. + + Example: + ```python + async with LNMClient(config) as client: + position = await client.futures.cross.get_position() + print(f"Margin: {position.margin}, P&L: {position.total_pl}") + ``` + """ return await self._client.request( "GET", "/futures/cross/position", @@ -52,7 +79,17 @@ async def get_position(self): ) async def get_open_orders(self): - """Get all open cross margin orders.""" + """ + Get all open cross margin orders. + + Example: + ```python + async with LNMClient(config) as client: + orders = await client.futures.cross.get_open_orders() + for order in orders: + print(f"Order ID: {order.id}, Price: {order.price}") + ``` + """ return await self._client.request( "GET", "/futures/cross/orders/open", @@ -61,7 +98,20 @@ async def get_open_orders(self): ) async def get_filled_orders(self, params: GetFilledOrdersParams | None = None): - """Get filled cross margin orders history.""" + """ + Get filled cross margin orders history. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import GetFilledOrdersParams + + async with LNMClient(config) as client: + params = GetFilledOrdersParams(limit=10) + orders = await client.futures.cross.get_filled_orders(params) + for order in orders: + print(f"Order ID: {order.id}, Type: {order.type}") + ``` + """ return await self._client.request( "GET", "/futures/cross/orders/filled", @@ -73,7 +123,16 @@ async def get_filled_orders(self, params: GetFilledOrdersParams | None = None): async def close( self, ) -> FuturesCrossOpenOrder | FuturesCrossFilledOrder | FuturesCrossCanceledOrder: - """Close cross margin position.""" + """ + Close cross margin position. + + Example: + ```python + async with LNMClient(config) as client: + result = await client.futures.cross.close() + print(f"Position closed: {result}") + ``` + """ return await self._client.request( "POST", "/futures/cross/position/close", @@ -84,7 +143,19 @@ async def close( ) async def cancel(self, params: CancelOrderParams): - """Cancel a cross margin order.""" + """ + Cancel a cross margin order. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import CancelOrderParams + + async with LNMClient(config) as client: + params = CancelOrderParams(id=order_id) + canceled = await client.futures.cross.cancel(params) + print(f"Canceled: {canceled.canceled}") + ``` + """ return await self._client.request( "POST", "/futures/cross/order/cancel", @@ -94,7 +165,16 @@ async def cancel(self, params: CancelOrderParams): ) async def cancel_all(self): - """Cancel all cross margin orders.""" + """ + Cancel all cross margin orders. + + Example: + ```python + async with LNMClient(config) as client: + canceled = await client.futures.cross.cancel_all() + print(f"Canceled {len(canceled)} orders") + ``` + """ return await self._client.request( "POST", "/futures/cross/orders/cancel-all", @@ -103,7 +183,19 @@ async def cancel_all(self): ) async def deposit(self, params: DepositParams): - """Deposit funds to cross margin account.""" + """ + Deposit funds to cross margin account. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import DepositParams + + async with LNMClient(config) as client: + params = DepositParams(amount=100_000) + position = await client.futures.cross.deposit(params) + print(f"New margin: {position.margin}") + ``` + """ return await self._client.request( "POST", "/futures/cross/deposit", @@ -113,7 +205,19 @@ async def deposit(self, params: DepositParams): ) async def withdraw(self, params: WithdrawParams): - """Withdraw funds from cross margin account.""" + """ + Withdraw funds from cross margin account. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import WithdrawParams + + async with LNMClient(config) as client: + params = WithdrawParams(amount=50_000) + position = await client.futures.cross.withdraw(params) + print(f"Remaining margin: {position.margin}") + ``` + """ return await self._client.request( "POST", "/futures/cross/withdraw", @@ -123,7 +227,19 @@ async def withdraw(self, params: WithdrawParams): ) async def set_leverage(self, params: SetLeverageParams): - """Set leverage for cross margin trading.""" + """ + Set leverage for cross margin trading. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import SetLeverageParams + + async with LNMClient(config) as client: + params = SetLeverageParams(leverage=50) + position = await client.futures.cross.set_leverage(params) + print(f"New leverage: {position.leverage}") + ``` + """ return await self._client.request( "PUT", "/futures/cross/leverage", @@ -133,7 +249,20 @@ async def set_leverage(self, params: SetLeverageParams): ) async def get_transfers(self, params: GetTransfersParams | None = None): - """Get cross margin transfer history.""" + """ + Get cross margin transfer history. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import GetTransfersParams + + async with LNMClient(config) as client: + params = GetTransfersParams(limit=10) + transfers = await client.futures.cross.get_transfers(params) + for transfer in transfers: + print(f"Transfer: {transfer.id}, Amount: {transfer.amount}") + ``` + """ return await self._client.request( "GET", "/futures/cross/transfers", @@ -143,7 +272,20 @@ async def get_transfers(self, params: GetTransfersParams | None = None): ) async def get_funding_fees(self, params: GetCrossFundingFeesParams | None = None): - """Get funding fees for cross margin.""" + """ + Get funding fees for cross margin. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_cross import GetCrossFundingFeesParams + + async with LNMClient(config) as client: + params = GetCrossFundingFeesParams(limit=10) + fees = await client.futures.cross.get_funding_fees(params) + for fee in fees: + print(f"Fee: {fee.fee}, Time: {fee.time}") + ``` + """ return await self._client.request( "GET", "/futures/cross/funding-fees", diff --git a/src/lnmarkets_sdk/v3/http/client/futures/isolated.py b/src/lnmarkets_sdk/v3/http/client/futures/isolated.py index 46ac8d0..2cbe3ee 100644 --- a/src/lnmarkets_sdk/v3/http/client/futures/isolated.py +++ b/src/lnmarkets_sdk/v3/http/client/futures/isolated.py @@ -28,7 +28,27 @@ def __init__(self, client: "LNMClient"): self._client = client async def new_trade(self, params: FuturesOrder): - """Open a new isolated margin futures trade.""" + """ + Open a new isolated margin futures trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import FuturesOrder + + async with LNMClient(config) as client: + params = FuturesOrder( + type="l", # limit order + side="b", # buy + price=100_000, + quantity=1, + leverage=100, + stoploss=90_000, # optional stop loss + takeprofit=110_000, # optional take profit + ) + trade = await client.futures.isolated.new_trade(params) + print(f"Trade ID: {trade.id}") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trade", @@ -38,7 +58,17 @@ async def new_trade(self, params: FuturesOrder): ) async def get_running_trades(self): - """Get all running isolated margin trades.""" + """ + Get all running isolated margin trades. + + Example: + ```python + async with LNMClient(config) as client: + trades = await client.futures.isolated.get_running_trades() + for trade in trades: + print(f"Trade ID: {trade.id}, P&L: {trade.pl}") + ``` + """ return await self._client.request( "GET", "/futures/isolated/trades/running", @@ -47,7 +77,17 @@ async def get_running_trades(self): ) async def get_open_trades(self): - """Get all open isolated margin trades.""" + """ + Get all open isolated margin trades. + + Example: + ```python + async with LNMClient(config) as client: + trades = await client.futures.isolated.get_open_trades() + for trade in trades: + print(f"Trade ID: {trade.id}, Price: {trade.price}") + ``` + """ return await self._client.request( "GET", "/futures/isolated/trades/open", @@ -56,17 +96,42 @@ async def get_open_trades(self): ) async def get_closed_trades(self, params: GetClosedTradesParams | None = None): - """Get closed isolated margin trades history.""" + """ + Get closed isolated margin trades history. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import GetClosedTradesParams + + async with LNMClient(config) as client: + params = GetClosedTradesParams(limit=10) + trades = await client.futures.isolated.get_closed_trades(params) + for trade in trades: + print(f"Trade ID: {trade.id}, P&L: {trade.pl}") + ``` + """ return await self._client.request( "GET", "/futures/isolated/trades/closed", params=params, credentials=True, - response_model=list[FuturesClosedTrade], + response_model=list[FuturesClosedTrade | FuturesCanceledTrade], ) async def close(self, params: CloseTradeParams): - """Close an isolated margin trade.""" + """ + Close an isolated margin trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import CloseTradeParams + + async with LNMClient(config) as client: + params = CloseTradeParams(id=trade_id) + closed = await client.futures.isolated.close(params) + print(f"Closed: {closed.closed}, P&L: {closed.pl}") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trade/close", @@ -76,7 +141,19 @@ async def close(self, params: CloseTradeParams): ) async def cancel(self, params: CancelTradeParams): - """Cancel an isolated margin trade.""" + """ + Cancel an isolated margin trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import CancelTradeParams + + async with LNMClient(config) as client: + params = CancelTradeParams(id=trade_id) + canceled = await client.futures.isolated.cancel(params) + print(f"Canceled: {canceled.canceled}") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trade/cancel", @@ -86,7 +163,16 @@ async def cancel(self, params: CancelTradeParams): ) async def cancel_all(self): - """Cancel all isolated margin trades.""" + """ + Cancel all isolated margin trades. + + Example: + ```python + async with LNMClient(config) as client: + canceled = await client.futures.isolated.cancel_all() + print(f"Canceled {len(canceled)} trades") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trades/cancel-all", @@ -95,7 +181,19 @@ async def cancel_all(self): ) async def add_margin(self, params: AddMarginParams): - """Add margin to an isolated trade.""" + """ + Add margin to an isolated trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import AddMarginParams + + async with LNMClient(config) as client: + params = AddMarginParams(id=trade_id, amount=10_000) + updated = await client.futures.isolated.add_margin(params) + print(f"New margin: {updated.margin}") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trade/add-margin", @@ -105,7 +203,19 @@ async def add_margin(self, params: AddMarginParams): ) async def cash_in(self, params: CashInParams): - """Cash in on an isolated trade.""" + """ + Cash in on an isolated trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import CashInParams + + async with LNMClient(config) as client: + params = CashInParams(id=trade_id, amount=10_000) + updated = await client.futures.isolated.cash_in(params) + print(f"Trade margin: {updated.margin}") + ``` + """ return await self._client.request( "POST", "/futures/isolated/trade/cash-in", @@ -115,7 +225,19 @@ async def cash_in(self, params: CashInParams): ) async def update_stoploss(self, params: UpdateStoplossParams): - """Update stop loss for an isolated trade.""" + """ + Update stop loss for an isolated trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import UpdateStoplossParams + + async with LNMClient(config) as client: + params = UpdateStoplossParams(id=trade_id, stoploss=90_000) + updated = await client.futures.isolated.update_stoploss(params) + print(f"New stop loss: {updated.stoploss}") + ``` + """ return await self._client.request( "PUT", "/futures/isolated/trade/stoploss", @@ -125,7 +247,19 @@ async def update_stoploss(self, params: UpdateStoplossParams): ) async def update_takeprofit(self, params: UpdateTakeprofitParams): - """Update take profit for an isolated trade.""" + """ + Update take profit for an isolated trade. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import UpdateTakeprofitParams + + async with LNMClient(config) as client: + params = UpdateTakeprofitParams(id=trade_id, takeprofit=110_000) + updated = await client.futures.isolated.update_takeprofit(params) + print(f"New take profit: {updated.takeprofit}") + ``` + """ return await self._client.request( "PUT", "/futures/isolated/trade/takeprofit", @@ -137,7 +271,20 @@ async def update_takeprofit(self, params: UpdateTakeprofitParams): async def get_funding_fees( self, params: GetIsolatedFundingFeesParams | None = None ): - """Get funding fees for isolated trades.""" + """ + Get funding fees for isolated trades. + + Example: + ```python + from lnmarkets_sdk.v3.models.futures_isolated import GetIsolatedFundingFeesParams + + async with LNMClient(config) as client: + params = GetIsolatedFundingFeesParams(limit=10, trade_id=trade_id) + fees = await client.futures.isolated.get_funding_fees(params) + for fee in fees: + print(f"Fee: {fee.fee}, Time: {fee.time}") + ``` + """ return await self._client.request( "GET", "/futures/isolated/funding-fees", diff --git a/src/lnmarkets_sdk/v3/http/client/oracle.py b/src/lnmarkets_sdk/v3/http/client/oracle.py index 4e36bbc..57d517f 100644 --- a/src/lnmarkets_sdk/v3/http/client/oracle.py +++ b/src/lnmarkets_sdk/v3/http/client/oracle.py @@ -18,7 +18,20 @@ def __init__(self, client: "LNMClient"): self._client = client async def get_index(self, params: GetIndexParams | None = None): - """Get index data.""" + """ + Get index data. + + Example: + ```python + from lnmarkets_sdk.v3.models.oracle import GetIndexParams + + async with LNMClient(config) as client: + params = GetIndexParams(limit=10, from_="2023-05-23T09:52:57.863Z") + indices = await client.oracle.get_index(params) + for index in indices: + print(f"Index: {index.index}, Time: {index.time}") + ``` + """ return await self._client.request( "GET", "/oracle/index", @@ -30,7 +43,20 @@ async def get_index(self, params: GetIndexParams | None = None): async def get_last_price( self, params: GetLastPriceParams | None = None ) -> list[OracleLastPrice]: - """Get last price data.""" + """ + Get last price data. + + Example: + ```python + from lnmarkets_sdk.v3.models.oracle import GetLastPriceParams + + async with LNMClient(config) as client: + params = GetLastPriceParams(limit=10, from_="2023-05-23T09:52:57.863Z") + prices = await client.oracle.get_last_price(params) + for price in prices: + print(f"Price: {price.last_price}, Time: {price.time}") + ``` + """ return await self._client.request( "GET", "/oracle/last-price", diff --git a/src/lnmarkets_sdk/v3/http/client/synthetic_usd.py b/src/lnmarkets_sdk/v3/http/client/synthetic_usd.py index f26c988..5730386 100644 --- a/src/lnmarkets_sdk/v3/http/client/synthetic_usd.py +++ b/src/lnmarkets_sdk/v3/http/client/synthetic_usd.py @@ -19,7 +19,16 @@ def __init__(self, client: "LNMClient"): self._client = client async def get_best_price(self): - """Get best price for USD swaps.""" + """ + Get best price for USD swaps. + + Example: + ```python + async with LNMClient(config) as client: + price = await client.synthetic_usd.get_best_price() + print(f"Ask: {price.ask_price}, Bid: {price.bid_price}") + ``` + """ return await self._client.request( "GET", "/synthetic-usd/best-price", @@ -28,7 +37,20 @@ async def get_best_price(self): ) async def get_swaps(self, params: GetSwapsParams | None = None): - """Get swap history.""" + """ + Get swap history. + + Example: + ```python + from lnmarkets_sdk.v3.models.synthetic_usd import GetSwapsParams + + async with LNMClient(config) as client: + params = GetSwapsParams(limit=10) + swaps = await client.synthetic_usd.get_swaps(params) + for swap in swaps: + print(f"Swap: {swap.in_asset} -> {swap.out_asset}") + ``` + """ return await self._client.request( "GET", "/synthetic-usd/swaps", @@ -38,7 +60,23 @@ async def get_swaps(self, params: GetSwapsParams | None = None): ) async def new_swap(self, params: NewSwapParams): - """Create a new USD swap.""" + """ + Create a new USD swap. + + Example: + ```python + from lnmarkets_sdk.v3.models.synthetic_usd import NewSwapParams + + async with LNMClient(config) as client: + params = NewSwapParams( + in_amount=100, + in_asset="USD", + out_asset="BTC" + ) + swap = await client.synthetic_usd.new_swap(params) + print(f"Received: {swap.out_amount} {swap.out_asset}") + ``` + """ return await self._client.request( "POST", "/synthetic-usd/swap", diff --git a/src/lnmarkets_sdk/v3/models/account.py b/src/lnmarkets_sdk/v3/models/account.py index 94c6ee5..714854b 100644 --- a/src/lnmarkets_sdk/v3/models/account.py +++ b/src/lnmarkets_sdk/v3/models/account.py @@ -1,157 +1,207 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import UUID, BaseConfig, FromToLimitParams class Account(BaseModel, BaseConfig): - username: str = Field(..., description="Username of the user") - synthetic_usd_balance: float = Field( + username: SkipValidation[str] = Field(..., description="Username of the user") + synthetic_usd_balance: SkipValidation[float] = Field( ..., description="Synthetic USD balance of the user (in dollars)" ) - balance: float = Field(..., description="Balance of the user (in satoshis)") - fee_tier: int = Field(..., description="Fee tier of the user") - email: str | None = Field(default=None, description="Email of the user") - id: UUID = Field(..., description="Unique identifier for this account") - linking_public_key: str | None = Field( + balance: SkipValidation[float] = Field( + ..., description="Balance of the user (in satoshis)" + ) + fee_tier: SkipValidation[int] = Field(..., description="Fee tier of the user") + email: SkipValidation[str] | None = Field( + default=None, description="Email of the user" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for this account" + ) + linking_public_key: SkipValidation[str] | None = Field( default=None, description="Public key of the user" ) class GetOnChainDepositsResponse(BaseModel, BaseConfig): - amount: float = Field(..., description="The amount of the deposit") - block_height: int | None = Field( + amount: SkipValidation[float] = Field(..., description="The amount of the deposit") + block_height: SkipValidation[int] | None = Field( default=None, description="The block height of the deposit" ) - confirmations: int = Field( + confirmations: SkipValidation[int] = Field( ..., description="The number of confirmations of the deposit" ) - created_at: str = Field(..., description="The date the deposit was created") - id: UUID = Field(..., description="The unique identifier for the deposit") - status: Literal["MEMPOOL", "CONFIRMED", "IRREVERSIBLE"] = Field( + created_at: SkipValidation[str] = Field( + ..., description="The date the deposit was created" + ) + id: SkipValidation[UUID] = Field( + ..., description="The unique identifier for the deposit" + ) + status: SkipValidation[Literal["MEMPOOL", "CONFIRMED", "IRREVERSIBLE"]] = Field( ..., description="The status of the deposit" ) - tx_id: str = Field(..., description="The transaction ID of the deposit") + tx_id: SkipValidation[str] = Field( + ..., description="The transaction ID of the deposit" + ) class GetInternalDepositsResponse(BaseModel, BaseConfig): - amount: float = Field(..., description="Amount of the deposit (in satoshis)") - created_at: str = Field(..., description="Timestamp when the deposit was created") - from_username: str = Field(..., description="Username of the sender") - id: UUID = Field(..., description="Unique identifier for this deposit") + amount: SkipValidation[float] = Field( + ..., description="Amount of the deposit (in satoshis)" + ) + created_at: SkipValidation[str] = Field( + ..., description="Timestamp when the deposit was created" + ) + from_username: SkipValidation[str] = Field( + ..., description="Username of the sender" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for this deposit" + ) class GetInternalWithdrawalsResponse(BaseModel, BaseConfig): - amount: float = Field(..., description="Amount of the transfer (in satoshis)") - created_at: str = Field(..., description="Timestamp when the transfer was created") - id: UUID = Field(..., description="Unique identifier for this transfer") - to_username: str = Field(..., description="Username of the recipient") + amount: SkipValidation[float] = Field( + ..., description="Amount of the transfer (in satoshis)" + ) + created_at: SkipValidation[str] = Field( + ..., description="Timestamp when the transfer was created" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for this transfer" + ) + to_username: SkipValidation[str] = Field( + ..., description="Username of the recipient" + ) class GetLightningDepositsResponse(BaseModel, BaseConfig): - amount: float | None = Field( + amount: SkipValidation[float] | None = Field( None, description="Amount of the deposit (in satoshis)" ) - comment: str | None = Field(default=None, description="Comment of the deposit") - created_at: str = Field(..., description="Timestamp when the deposit was created") - id: UUID = Field(..., description="Unique identifier for this deposit") - payment_hash: str | None = Field( + comment: SkipValidation[str] | None = Field( + default=None, description="Comment of the deposit" + ) + created_at: SkipValidation[str] = Field( + ..., description="Timestamp when the deposit was created" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for this deposit" + ) + payment_hash: SkipValidation[str] | None = Field( default=None, description="Payment hash of the deposit" ) - settled_at: str | None = Field( + settled_at: SkipValidation[str] | None = Field( default=None, description="Timestamp when the deposit was settled" ) class GetLightningWithdrawalsResponse(BaseModel, BaseConfig): - amount: float = Field(..., description="Amount of the withdrawal (in satoshis)") - created_at: str = Field( + amount: SkipValidation[float] = Field( + ..., description="Amount of the withdrawal (in satoshis)" + ) + created_at: SkipValidation[str] = Field( ..., description="Timestamp when the withdrawal was created" ) - fee: float = Field(..., description="Fee of the withdrawal (in satoshis)") - id: UUID = Field(..., description="Unique identifier for the withdrawal") - payment_hash: str = Field(..., description="Payment hash of the withdrawal") - status: Literal["failed", "processed", "processing"] = Field( + fee: SkipValidation[float] = Field( + ..., description="Fee of the withdrawal (in satoshis)" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for the withdrawal" + ) + payment_hash: SkipValidation[str] = Field( + ..., description="Payment hash of the withdrawal" + ) + status: SkipValidation[Literal["failed", "processed", "processing"]] = Field( ..., description="Status of the withdrawal" ) class GetOnChainWithdrawalsResponse(BaseModel, BaseConfig): - address: str = Field(..., description="Address to withdraw to") - amount: float = Field(..., description="Amount to withdraw") - created_at: str = Field( + address: SkipValidation[str] = Field(..., description="Address to withdraw to") + amount: SkipValidation[float] = Field(..., description="Amount to withdraw") + created_at: SkipValidation[str] = Field( ..., description="Timestamp when the withdrawal was created" ) - fee: float | None = Field( + fee: SkipValidation[float] | None = Field( default=None, description="Fee of the withdrawal (in satoshis)" ) - id: UUID = Field(..., description="Unique identifier for the withdrawal") - status: Literal["canceled", "pending", "processed", "processing", "rejected"] = ( - Field(..., description="Status of the withdrawal") + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for the withdrawal" ) - tx_id: str | None = Field( + status: SkipValidation[ + Literal["canceled", "pending", "processed", "processing", "rejected"] + ] = Field(..., description="Status of the withdrawal") + tx_id: SkipValidation[str] | None = Field( default=None, description="Transaction ID of the withdrawal" ) class InternalTransfer(BaseModel, BaseConfig): - amount: float - created_at: str - from_uid: UUID - from_username: str - id: UUID - settled_at: str | None - success: bool | None - to_uid: UUID - to_username: str + amount: SkipValidation[float] + created_at: SkipValidation[str] + from_uid: SkipValidation[UUID] + from_username: SkipValidation[str] + id: SkipValidation[UUID] + settled_at: SkipValidation[str] | None + success: SkipValidation[bool] | None + to_uid: SkipValidation[UUID] + to_username: SkipValidation[str] class PendingOnChainWithdrawalRequest(BaseModel, BaseConfig): - address: str - amount: float - created_at: str - fee: float | None - id: UUID - status: Literal["pending"] + address: SkipValidation[str] + amount: SkipValidation[float] + created_at: SkipValidation[str] + fee: SkipValidation[float] | None + id: SkipValidation[UUID] + status: SkipValidation[Literal["pending"]] tx_id: None = None - updated_at: str + updated_at: SkipValidation[str] class DepositLightningResponse(BaseModel, BaseConfig): - deposit_id: UUID = Field(..., description="Deposit ID") - payment_request: str = Field(..., description="Lightning payment request invoice") + deposit_id: SkipValidation[UUID] = Field(..., description="Deposit ID") + payment_request: SkipValidation[str] = Field( + ..., description="Lightning payment request invoice" + ) class WithdrawInternalResponse(BaseModel, BaseConfig): - id: UUID - created_at: str - from_uid: UUID - to_uid: UUID - amount: float + id: SkipValidation[UUID] + created_at: SkipValidation[str] + from_uid: SkipValidation[UUID] + to_uid: SkipValidation[UUID] + amount: SkipValidation[float] class WithdrawOnChainResponse(BaseModel, BaseConfig): - id: UUID - uid: UUID - amount: float - address: str - created_at: str - updated_at: str - block_id: str | None - confirmation_height: int | None - fee: float | None - status: Literal["pending"] + id: SkipValidation[UUID] + uid: SkipValidation[UUID] + amount: SkipValidation[float] + address: SkipValidation[str] + created_at: SkipValidation[str] + updated_at: SkipValidation[str] + block_id: SkipValidation[str] | None + confirmation_height: SkipValidation[int] | None + fee: SkipValidation[float] | None + status: SkipValidation[Literal["pending"]] tx_id: None = None class GetBitcoinAddressResponse(BaseModel, BaseConfig): - address: str = Field(..., description="Bitcoin address") + address: SkipValidation[str] = Field(..., description="Bitcoin address") class AddBitcoinAddressResponse(BaseModel, BaseConfig): - address: str = Field(..., description="The generated Bitcoin address") - created_at: str = Field(..., description="The creation time of the address") + address: SkipValidation[str] = Field( + ..., description="The generated Bitcoin address" + ) + created_at: SkipValidation[str] = Field( + ..., description="The creation time of the address" + ) class AddBitcoinAddressParams(BaseModel, BaseConfig): @@ -175,12 +225,18 @@ class WithdrawLightningParams(BaseModel, BaseConfig): class WithdrawLightningResponse(BaseModel, BaseConfig): - amount: float = Field(..., description="Amount of the withdrawal (in satoshis)") - id: UUID = Field(..., description="Unique identifier for the withdrawal") - max_fees: float = Field( + amount: SkipValidation[float] = Field( + ..., description="Amount of the withdrawal (in satoshis)" + ) + id: SkipValidation[UUID] = Field( + ..., description="Unique identifier for the withdrawal" + ) + max_fees: SkipValidation[float] = Field( ..., description="Maximum fees of the withdrawal (in satoshis)" ) - payment_hash: str = Field(..., description="Payment hash of the withdrawal") + payment_hash: SkipValidation[str] = Field( + ..., description="Payment hash of the withdrawal" + ) class WithdrawInternalParams(BaseModel, BaseConfig): @@ -193,10 +249,14 @@ class WithdrawOnChainParams(BaseModel, BaseConfig): amount: float = Field(..., gt=0, description="Amount to withdraw (in satoshis)") -class GetLightningDepositsParams(FromToLimitParams): ... +class GetLightningDepositsParams(FromToLimitParams): + settled: bool | None = Field(default=None, description="Filter by settled deposits") -class GetLightningWithdrawalsParams(FromToLimitParams): ... +class GetLightningWithdrawalsParams(FromToLimitParams): + status: Literal["failed", "processed", "processing"] | None = Field( + default=None, description="Filter by withdrawal status" + ) class GetInternalDepositsParams(FromToLimitParams): ... @@ -205,7 +265,13 @@ class GetInternalDepositsParams(FromToLimitParams): ... class GetInternalWithdrawalsParams(FromToLimitParams): ... -class GetOnChainDepositsParams(FromToLimitParams): ... +class GetOnChainDepositsParams(FromToLimitParams): + status: Literal["MEMPOOL", "CONFIRMED", "IRREVERSIBLE"] | None = Field( + default=None, description="Filter by deposit status" + ) -class GetOnChainWithdrawalsParams(FromToLimitParams): ... +class GetOnChainWithdrawalsParams(FromToLimitParams): + status: ( + Literal["canceled", "pending", "processed", "processing", "rejected"] | None + ) = Field(default=None, description="Filter by withdrawal status") diff --git a/src/lnmarkets_sdk/v3/models/funding_fees.py b/src/lnmarkets_sdk/v3/models/funding_fees.py index c4d8682..60d98c9 100644 --- a/src/lnmarkets_sdk/v3/models/funding_fees.py +++ b/src/lnmarkets_sdk/v3/models/funding_fees.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import UUID, BaseConfig @@ -6,16 +6,20 @@ class FundingFees(BaseModel, BaseConfig): """Funding fee entry.""" - fee: float = Field(..., description="Funding fee amount") - settlement_id: UUID = Field(..., description="Funding settlement ID") - time: str = Field(..., description="Timestamp in ISO format") - trade_id: UUID | None = Field(default=None, description="Associated trade ID") + fee: SkipValidation[float] = Field(..., description="Funding fee amount") + settlement_id: SkipValidation[UUID] = Field( + ..., description="Funding settlement ID" + ) + time: SkipValidation[str] = Field(..., description="Timestamp in ISO format") + trade_id: SkipValidation[UUID] | None = Field( + default=None, description="Associated trade ID" + ) class FundingSettlement(BaseModel, BaseConfig): """Funding settlement entry.""" - funding_rate: float = Field(..., description="Funding rate") - id: UUID = Field(..., description="Funding settlement ID") - fixing_price: float = Field(..., description="Fixing price") - time: str = Field(..., description="Funding settlement time") + funding_rate: SkipValidation[float] = Field(..., description="Funding rate") + id: SkipValidation[UUID] = Field(..., description="Funding settlement ID") + fixing_price: SkipValidation[float] = Field(..., description="Fixing price") + time: SkipValidation[str] = Field(..., description="Funding settlement time") diff --git a/src/lnmarkets_sdk/v3/models/futures_cross.py b/src/lnmarkets_sdk/v3/models/futures_cross.py index aa32c73..8a32966 100644 --- a/src/lnmarkets_sdk/v3/models/futures_cross.py +++ b/src/lnmarkets_sdk/v3/models/futures_cross.py @@ -1,6 +1,6 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import UUID, BaseConfig, FromToLimitParams @@ -10,6 +10,7 @@ class FuturesCrossOrderSideQuantity(BaseModel, BaseConfig): ..., description="Trade side: b (buy/long) or s (sell/short)" ) quantity: int = Field(..., gt=0, description="Quantity of the position") + client_id: str class FuturesCrossOrderLimit(FuturesCrossOrderSideQuantity): @@ -25,71 +26,82 @@ class FuturesCrossOrderMarket(FuturesCrossOrderSideQuantity): class FuturesCrossOpenOrder(BaseModel, BaseConfig): - canceled: Literal[False] = False + canceled: SkipValidation[Literal[False]] = False canceled_at: None = None - created_at: str - filled: Literal[False] = False + created_at: SkipValidation[str] + filled: SkipValidation[bool] = False filled_at: None = None - id: UUID - open: Literal[True] = True - price: float - quantity: float - side: Literal["b", "s"] - trading_fee: float - type: Literal["limit"] + id: SkipValidation[UUID] + open: SkipValidation[bool] = True + price: SkipValidation[float] + quantity: SkipValidation[float] + side: SkipValidation[Literal["b", "s"]] + trading_fee: SkipValidation[float] + type: SkipValidation[Literal["limit"]] + client_id: SkipValidation[str] | None = None class FuturesCrossFilledOrder(BaseModel, BaseConfig): - canceled: Literal[False] = False + canceled: SkipValidation[bool] = False canceled_at: None = None - created_at: str - filled: Literal[True] = True - filled_at: str - id: UUID - open: Literal[False] = False - price: float - quantity: float - side: Literal["b", "s"] - trading_fee: float - type: Literal["limit", "liquidation", "market"] + created_at: SkipValidation[str] + filled: SkipValidation[bool] = True + filled_at: SkipValidation[str] | None = Field( + default=None, description="Timestamp when the order was filled" + ) + id: SkipValidation[UUID] + open: SkipValidation[bool] = False + price: SkipValidation[float] + quantity: SkipValidation[float] + side: SkipValidation[Literal["b", "s"]] + trading_fee: SkipValidation[float] + type: SkipValidation[Literal["limit", "liquidation", "market"]] + client_id: SkipValidation[str] | None = Field(default=None, description="Client ID") class FuturesCrossCanceledOrder(BaseModel, BaseConfig): - canceled: Literal[True] = True - canceled_at: str - created_at: str - filled: Literal[False] = False + canceled: SkipValidation[bool] = True + canceled_at: SkipValidation[str] | None + created_at: SkipValidation[str] + filled: SkipValidation[bool] = False filled_at: None = None - id: UUID - open: Literal[False] = False - price: float - quantity: float - side: Literal["b", "s"] - trading_fee: float - type: Literal["limit"] + id: SkipValidation[UUID] + open: SkipValidation[bool] = False + price: SkipValidation[float] + quantity: SkipValidation[float] + side: SkipValidation[Literal["b", "s"]] + trading_fee: SkipValidation[float] + type: SkipValidation[Literal["limit"]] + client_id: SkipValidation[str] | None = Field(default=None, description="Client ID") class FuturesCrossPosition(BaseModel, BaseConfig): - delta_pl: float = Field(..., description="Delta P&L") - entry_price: float | None = Field(default=None, description="Entry price") - funding_fees: float = Field(..., description="Funding fees") - id: UUID = Field(..., description="Position ID") - initial_margin: float = Field(..., description="Initial margin") - leverage: int = Field(..., gt=0, description="Leverage") - liquidation: float | None = Field(default=None, description="Liquidation price") - maintenance_margin: float = Field(..., description="Maintenance margin") - margin: float = Field(..., description="Current margin") - quantity: float = Field(..., description="Position quantity") - running_margin: float = Field(..., description="Running margin") - total_pl: float = Field(..., description="Total P&L") - trading_fees: float = Field(..., description="Trading fees") - updated_at: str = Field(..., description="Last update timestamp") + delta_pl: SkipValidation[float] = Field(..., description="Delta P&L") + entry_price: SkipValidation[float] | None = Field( + default=None, description="Entry price" + ) + funding_fees: SkipValidation[float] = Field(..., description="Funding fees") + id: SkipValidation[UUID] = Field(..., description="Position ID") + initial_margin: SkipValidation[float] = Field(..., description="Initial margin") + leverage: SkipValidation[int] = Field(..., gt=0, description="Leverage") + liquidation: SkipValidation[float] | None = Field( + default=None, description="Liquidation price" + ) + maintenance_margin: SkipValidation[float] = Field( + ..., description="Maintenance margin" + ) + margin: SkipValidation[float] = Field(..., description="Current margin") + quantity: SkipValidation[float] = Field(..., description="Position quantity") + running_margin: SkipValidation[float] = Field(..., description="Running margin") + total_pl: SkipValidation[float] = Field(..., description="Total P&L") + trading_fees: SkipValidation[float] = Field(..., description="Trading fees") + updated_at: SkipValidation[str] = Field(..., description="Last update timestamp") class FuturesCrossTransfer(BaseModel, BaseConfig): - amount: float - id: UUID - time: str + amount: SkipValidation[float] + id: SkipValidation[UUID] + time: SkipValidation[str] class DepositParams(BaseModel, BaseConfig): diff --git a/src/lnmarkets_sdk/v3/models/futures_data.py b/src/lnmarkets_sdk/v3/models/futures_data.py index 4f894a3..1284ffc 100644 --- a/src/lnmarkets_sdk/v3/models/futures_data.py +++ b/src/lnmarkets_sdk/v3/models/futures_data.py @@ -1,6 +1,6 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import BaseConfig, FromToLimitParams @@ -26,29 +26,33 @@ class PriceBucket(BaseModel, BaseConfig): """Price bucket for ticker.""" - ask_price: float | None = Field( + ask_price: SkipValidation[float] | None = Field( default=None, description="Current best ask/sell price available (in USD)" ) - bid_price: float | None = Field( + bid_price: SkipValidation[float] | None = Field( default=None, description="Current best bid price available (in USD)" ) - max_size: int = Field(..., description="Maximum order size (in BTC)") - min_size: int = Field(..., description="Minimum order size (in BTC)") + max_size: SkipValidation[int] = Field( + ..., description="Maximum order size (in BTC)" + ) + min_size: SkipValidation[int] = Field( + ..., description="Minimum order size (in BTC)" + ) class Ticker(BaseModel, BaseConfig): """Futures ticker data.""" - funding_rate: float = Field(..., description="Current funding rate") - funding_time: str = Field( + funding_rate: SkipValidation[float] = Field(..., description="Current funding rate") + funding_time: SkipValidation[str] = Field( ..., description="ISO date string when the next funding rate will be established", ) - index: float = Field( + index: SkipValidation[float] = Field( ..., description="Bitcoin price index aggregated from multiple exchanges (in USD)", ) - last_price: float = Field( + last_price: SkipValidation[float] = Field( ..., description="Last executed trade price on the platform (in USD)" ) prices: list[PriceBucket] = Field( @@ -59,20 +63,20 @@ class Ticker(BaseModel, BaseConfig): class Candle(BaseModel, BaseConfig): """OHLC candlestick data.""" - close: float = Field(..., description="Closing price") - high: float = Field(..., description="Highest price") - low: float = Field(..., description="Lowest price") - open: float = Field(..., description="Opening price") - time: str = Field(..., description="Timestamp in ISO format") - volume: float = Field(..., description="Trading volume") + close: SkipValidation[float] = Field(..., description="Closing price") + high: SkipValidation[float] = Field(..., description="Highest price") + low: SkipValidation[float] = Field(..., description="Lowest price") + open: SkipValidation[float] = Field(..., description="Opening price") + time: SkipValidation[str] = Field(..., description="Timestamp in ISO format") + volume: SkipValidation[float] = Field(..., description="Trading volume") class UserInfo(BaseModel, BaseConfig): """User leaderboard info.""" - direction: int - pl: float - username: str + direction: SkipValidation[int] + pl: SkipValidation[float] + username: SkipValidation[str] class Leaderboard(BaseModel, BaseConfig): diff --git a/src/lnmarkets_sdk/v3/models/futures_isolated.py b/src/lnmarkets_sdk/v3/models/futures_isolated.py index 7058803..41e95ec 100644 --- a/src/lnmarkets_sdk/v3/models/futures_isolated.py +++ b/src/lnmarkets_sdk/v3/models/futures_isolated.py @@ -1,6 +1,6 @@ from typing import Literal -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, SkipValidation, model_validator from lnmarkets_sdk.v3._internal.models import UUID, BaseConfig, FromToLimitParams @@ -34,6 +34,9 @@ class FuturesOrder(BaseModel, BaseConfig): type: Literal["l", "m"] = Field( ..., description="Trade type: l (limit) or m (market)" ) + client_id: str | None = Field( + default=None, description="Unique client ID for the trade" + ) @model_validator(mode="after") def validate_schema(self): @@ -47,68 +50,69 @@ def validate_schema(self): class FuturesTrade(BaseModel, BaseConfig): - canceled: bool - closed: bool - closed_at: str | None = None - closing_fee: float - created_at: str - entry_margin: float | None = None - entry_price: float | None = None - exit_price: float | None = None - filled_at: str | None = None - id: UUID - leverage: float - liquidation: float - maintenance_margin: float - margin: float - open: bool - opening_fee: float - pl: float - price: float - quantity: float - running: bool - side: Literal["b", "s"] - stoploss: float - sum_funding_fees: float - takeprofit: float - type: Literal["l", "m"] + canceled: SkipValidation[bool] + closed: SkipValidation[bool] + closed_at: SkipValidation[str] | None = None + closing_fee: SkipValidation[float] + created_at: SkipValidation[str] + entry_margin: SkipValidation[float] | None = None + entry_price: SkipValidation[float] | None = None + exit_price: SkipValidation[float] | None = None + filled_at: SkipValidation[str] | None = None + id: SkipValidation[UUID] + leverage: SkipValidation[float] + liquidation: SkipValidation[float] + maintenance_margin: SkipValidation[float] + margin: SkipValidation[float] + open: SkipValidation[bool] + opening_fee: SkipValidation[float] + pl: SkipValidation[float] + price: SkipValidation[float] + quantity: SkipValidation[float] + running: SkipValidation[bool] + side: SkipValidation[Literal["b", "s"]] + stoploss: SkipValidation[float] + sum_funding_fees: SkipValidation[float] + takeprofit: SkipValidation[float] + type: SkipValidation[Literal["l", "m"]] + client_id: SkipValidation[str] | None = None class FuturesOpenTrade(FuturesTrade): - canceled: Literal[False] = False - closed: Literal[False] = False + canceled: SkipValidation[bool] = False + closed: SkipValidation[bool] = False closed_at: None = None filled_at: None = None - running: Literal[False] = False - type: Literal["l"] = "l" + running: SkipValidation[bool] = False + type: SkipValidation[Literal["l"]] = "l" class FuturesRunningTrade(FuturesTrade): - canceled: Literal[False] = False - closed: Literal[False] = False + canceled: SkipValidation[bool] = False + closed: SkipValidation[bool] = False closed_at: None = None - filled_at: str = "" - running: Literal[True] = True + filled_at: SkipValidation[str] | None = None + running: SkipValidation[bool] = True class FuturesClosedTrade(FuturesTrade): - canceled: Literal[False] = False - closed: Literal[True] = True - closed_at: str = "" - exit_price: float = 0.0 - filled_at: str = "" - open: Literal[False] = False - running: Literal[False] = False + canceled: SkipValidation[bool] = False + closed: SkipValidation[bool] = True + closed_at: SkipValidation[str] = "" + exit_price: SkipValidation[float] = 0.0 + filled_at: SkipValidation[str] = "" + open: SkipValidation[bool] = False + running: SkipValidation[bool] = False class FuturesCanceledTrade(FuturesTrade): - canceled: Literal[True] = True - closed: Literal[False] = False - closed_at: str = "" + canceled: SkipValidation[bool] = True + closed: SkipValidation[bool] = False + closed_at: SkipValidation[str] = "" filled_at: None = None - open: Literal[False] = False - running: Literal[False] = False - type: Literal["l"] = "l" + open: SkipValidation[bool] = False + running: SkipValidation[bool] = False + type: SkipValidation[Literal["l"]] = "l" class AddMarginParams(BaseModel, BaseConfig): diff --git a/src/lnmarkets_sdk/v3/models/oracle.py b/src/lnmarkets_sdk/v3/models/oracle.py index dd29360..c0da775 100644 --- a/src/lnmarkets_sdk/v3/models/oracle.py +++ b/src/lnmarkets_sdk/v3/models/oracle.py @@ -1,16 +1,20 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import BaseConfig, FromToLimitParams class OracleIndex(BaseModel, BaseConfig): - index: float = Field(..., description="Index value") - time: str = Field(..., description="Time as a string value in ISO format") + index: SkipValidation[float] = Field(..., description="Index value") + time: SkipValidation[str] = Field( + ..., description="Time as a string value in ISO format" + ) class OracleLastPrice(BaseModel, BaseConfig): - last_price: float = Field(..., description="Last price value") - time: str = Field(..., description="Timestamp as a string value in ISO format") + last_price: SkipValidation[float] = Field(..., description="Last price value") + time: SkipValidation[str] = Field( + ..., description="Timestamp as a string value in ISO format" + ) class GetIndexParams(FromToLimitParams): ... diff --git a/src/lnmarkets_sdk/v3/models/synthetic_usd.py b/src/lnmarkets_sdk/v3/models/synthetic_usd.py index a6f276f..bd8eb41 100644 --- a/src/lnmarkets_sdk/v3/models/synthetic_usd.py +++ b/src/lnmarkets_sdk/v3/models/synthetic_usd.py @@ -1,6 +1,6 @@ from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SkipValidation from lnmarkets_sdk.v3._internal.models import UUID, BaseConfig, FromToLimitParams @@ -8,25 +8,25 @@ class Swap(BaseModel, BaseConfig): - created_at: str - id: UUID - in_amount: float - in_asset: str - out_amount: float - out_asset: str + created_at: SkipValidation[str] + id: SkipValidation[UUID] + in_amount: SkipValidation[float] + in_asset: SkipValidation[str] + out_amount: SkipValidation[float] + out_asset: SkipValidation[str] class CreateSwapOutput(BaseModel, BaseConfig): - in_amount: float = Field( + in_amount: SkipValidation[float] = Field( ..., description="Amount to swap (in satoshis if BTC, in dollars with 2 decimal places if USD)", ) - in_asset: SwapAssets = Field(..., description="Asset to swap from") - out_amount: float = Field( + in_asset: SkipValidation[SwapAssets] = Field(..., description="Asset to swap from") + out_amount: SkipValidation[float] = Field( ..., description="Amount received after conversion (in satoshis if BTC, in dollars with 2 decimal places if USD)", ) - out_asset: SwapAssets = Field(..., description="Asset to swap to") + out_asset: SkipValidation[SwapAssets] = Field(..., description="Asset to swap to") class NewSwapParams(BaseModel, BaseConfig): @@ -44,8 +44,8 @@ class BestPriceParams(BaseModel, BaseConfig): class BestPriceResponse(BaseModel, BaseConfig): - ask_price: float = Field(..., description="Best ask price") - bid_price: float = Field(..., description="Best bid price") + ask_price: SkipValidation[float] = Field(..., description="Best ask price") + bid_price: SkipValidation[float] = Field(..., description="Best bid price") class GetSwapsParams(FromToLimitParams): ... diff --git a/src/lnmarkets_sdk/v3/tests/test_integration.py b/src/lnmarkets_sdk/v3/tests/test_integration.py index a6a1cce..345aabd 100644 --- a/src/lnmarkets_sdk/v3/tests/test_integration.py +++ b/src/lnmarkets_sdk/v3/tests/test_integration.py @@ -15,11 +15,38 @@ GetLightningDepositsParams, GetLightningWithdrawalsParams, GetOnChainDepositsParams, + GetOnChainWithdrawalsParams, WithdrawInternalParams, WithdrawLightningParams, WithdrawOnChainParams, ) -from lnmarkets_sdk.v3.models.futures_isolated import FuturesOrder +from lnmarkets_sdk.v3.models.futures_cross import ( + CancelOrderParams, + DepositParams, + FuturesCrossOrderLimit, + GetCrossFundingFeesParams, + GetFilledOrdersParams, + GetTransfersParams, + SetLeverageParams, + WithdrawParams, +) +from lnmarkets_sdk.v3.models.futures_data import ( + GetCandlesParams, + GetFundingSettlementsParams, +) +from lnmarkets_sdk.v3.models.futures_isolated import ( + AddMarginParams, + CancelTradeParams, + CashInParams, + CloseTradeParams, + FuturesOrder, + GetClosedTradesParams, + GetIsolatedFundingFeesParams, + UpdateStoplossParams, + UpdateTakeprofitParams, +) +from lnmarkets_sdk.v3.models.oracle import GetIndexParams +from lnmarkets_sdk.v3.models.synthetic_usd import GetSwapsParams, NewSwapParams load_dotenv() @@ -28,13 +55,13 @@ @pytest.fixture async def public_rate_limit_delay(): """Add delay between tests for public endpoints to avoid rate limiting.""" - await asyncio.sleep(0.5) # 0.5s delay between tests (2 requests per second) + await asyncio.sleep(1) # 1s delay between tests (1 requests per second) @pytest.fixture async def auth_rate_limit_delay(): """Add delay between tests for authentication endpoints to avoid rate limiting.""" - await asyncio.sleep(0.1) # 0.1s delay between tests (10 requests per second) + await asyncio.sleep(0.2) # 0.2s delay between tests (5 requests per second) def create_public_config() -> APIClientConfig: @@ -84,10 +111,15 @@ async def test_get_account(self): async with LNMClient(create_auth_config()) as client: account = await client.account.get_account() assert account.balance >= 0 - assert isinstance(account.email, str) + assert account.synthetic_usd_balance >= 0 assert isinstance(account.username, str) assert account.fee_tier >= 0 assert account.id is not None + # email and linking_public_key are optional + if account.email is not None: + assert isinstance(account.email, str) + if account.linking_public_key is not None: + assert isinstance(account.linking_public_key, str) @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), @@ -138,6 +170,7 @@ async def test_withdraw_lightning(self): assert result.id is not None assert result.amount is not None assert result.max_fees is not None + assert result.payment_hash is not None except Exception as e: assert "Send a correct BOLT 11 invoice" in str(e) @@ -168,8 +201,12 @@ async def test_withdraw_on_chain(self): try: result = await client.account.withdraw_on_chain(params) assert result.id is not None + assert result.uid is not None assert result.amount is not None + assert result.address is not None assert result.created_at is not None + assert result.updated_at is not None + assert result.status == "pending" except Exception as e: assert "Invalid address" in str(e) @@ -185,9 +222,15 @@ async def test_get_lightning_deposits(self): if len(result) > 0: assert result[0].id is not None assert result[0].created_at is not None - assert result[0].amount is not None - assert result[0].comment is None - assert result[0].settled_at is None + # amount, comment, payment_hash, settled_at are optional + if result[0].amount is not None: + assert result[0].amount > 0 + if result[0].comment is not None: + assert isinstance(result[0].comment, str) + if result[0].payment_hash is not None: + assert isinstance(result[0].payment_hash, str) + if result[0].settled_at is not None: + assert isinstance(result[0].settled_at, str) @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), @@ -203,6 +246,8 @@ async def test_get_lightning_withdrawals(self): assert result[0].created_at is not None assert result[0].amount is not None assert result[0].fee is not None + assert result[0].payment_hash is not None + assert result[0].status in ["failed", "processed", "processing"] @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), @@ -248,7 +293,40 @@ async def test_get_on_chain_deposits(self): assert result[0].id is not None assert result[0].created_at is not None assert result[0].amount is not None - assert result[0].block_height is not None + assert result[0].confirmations is not None + assert result[0].status in ["MEMPOOL", "CONFIRMED", "IRREVERSIBLE"] + assert result[0].tx_id is not None + if result[0].block_height is not None: + assert result[0].block_height > 0 + except Exception as e: + assert "HTTP 404: Not found" in str(e) + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_on_chain_withdrawals(self): + async with LNMClient(create_auth_config()) as client: + params = GetOnChainWithdrawalsParams(limit=2) + try: + result = await client.account.get_on_chain_withdrawals(params) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].id is not None + assert result[0].created_at is not None + assert result[0].amount is not None + assert result[0].address is not None + assert result[0].status in [ + "canceled", + "pending", + "processed", + "processing", + "rejected", + ] + if result[0].fee is not None: + assert result[0].fee >= 0 + if result[0].tx_id is not None: + assert isinstance(result[0].tx_id, str) except Exception as e: assert "HTTP 404: Not found" in str(e) @@ -263,15 +341,33 @@ async def test_get_ticker(self): ticker = await client.futures.get_ticker() assert ticker.index > 0 assert ticker.last_price > 0 + assert isinstance(ticker.funding_rate, float) + assert ticker.funding_time is not None + assert isinstance(ticker.prices, list) + if len(ticker.prices) > 0: + price_bucket = ticker.prices[0] + print(price_bucket) + assert price_bucket.max_size > 0 + assert price_bucket.min_size >= 0 + if price_bucket.ask_price is not None: + assert price_bucket.ask_price >= 0 + if price_bucket.bid_price is not None: + assert price_bucket.bid_price >= 0 async def test_get_leaderboard(self): async with LNMClient(create_public_config()) as client: leaderboard = await client.futures.get_leaderboard() assert isinstance(leaderboard.daily, list) + assert isinstance(leaderboard.weekly, list) + assert isinstance(leaderboard.monthly, list) + assert isinstance(leaderboard.all_time, list) + if len(leaderboard.daily) > 0: + user = leaderboard.daily[0] + assert user.username is not None + assert user.pl is not None + assert user.direction is not None async def test_get_candles(self): - from lnmarkets_sdk.v3.models.futures_data import GetCandlesParams - async with LNMClient(create_public_config()) as client: params = GetCandlesParams( from_="2023-05-23T09:52:57.863Z", range="1m", limit=1 @@ -283,18 +379,33 @@ async def test_get_candles(self): assert candles[0].high > 0 assert candles[0].low > 0 assert candles[0].close > 0 + assert candles[0].time is not None + assert candles[0].volume >= 0 + + async def test_get_funding_settlements(self): + async with LNMClient(create_public_config()) as client: + params = GetFundingSettlementsParams(limit=5) + result = await client.futures.get_funding_settlements(params) + assert isinstance(result, list) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].id is not None + assert result[0].time is not None + assert isinstance(result[0].funding_rate, float) + assert result[0].fixing_price > 0 @pytest.mark.asyncio @pytest.mark.usefixtures("auth_rate_limit_delay") class TestFuturesIsolatedIntegration: + """Integration tests for isolated margin futures endpoints.""" + @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), reason="V3_API_KEY not set in environment", ) - async def test_futures_isolated(self): + async def test_new_trade(self): async with LNMClient(create_auth_config()) as client: - # Create a new trade params = FuturesOrder( type="l", # limit order side="b", # buy @@ -307,24 +418,236 @@ async def test_futures_isolated(self): assert trade.side == "b" assert trade.type == "l" assert trade.leverage == 100 + assert trade.canceled is False + assert trade.closed is False + assert trade.open is True + assert trade.running is False or trade.running is True + assert trade.created_at is not None + assert trade.price > 0 + assert trade.quantity > 0 + assert trade.margin > 0 + assert trade.pl is not None + assert trade.opening_fee >= 0 + assert trade.closing_fee >= 0 + assert trade.sum_funding_fees is not None - # Get open trades + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_open_trades(self): + async with LNMClient(create_auth_config()) as client: open_trades = await client.futures.isolated.get_open_trades() assert isinstance(open_trades, list) - # Our trade should be in the list - trade_ids = [t.id for t in open_trades] - assert trade.id in trade_ids + if len(open_trades) > 0: + open_trade = open_trades[0] + assert open_trade.id is not None + assert open_trade.open is True + assert open_trade.canceled is False + assert open_trade.closed is False + assert open_trade.running is False + assert open_trade.type == "l" + assert open_trade.price > 0 + assert open_trade.quantity > 0 + assert open_trade.leverage > 0 - # Cancel the trade - from lnmarkets_sdk.v3.models.futures_isolated import CancelTradeParams + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_running_trades(self): + async with LNMClient(create_auth_config()) as client: + running_trades = await client.futures.isolated.get_running_trades() + assert isinstance(running_trades, list) + if len(running_trades) > 0: + running_trade = running_trades[0] + assert running_trade.id is not None + assert running_trade.running is True + assert running_trade.canceled is False + assert running_trade.closed is False + assert running_trade.margin > 0 + assert running_trade.pl is not None + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_closed_trades(self): + async with LNMClient(create_auth_config()) as client: + closed_params = GetClosedTradesParams(limit=5) + closed_trades = await client.futures.isolated.get_closed_trades( + closed_params + ) + assert isinstance(closed_trades, list) + assert len(closed_trades) <= closed_params.limit + if len(closed_trades) > 0: + closed_trade = closed_trades[0] + assert closed_trade.id is not None + assert closed_trade.closed is True + assert closed_trade.open is False + assert closed_trade.running is False + if closed_trade.closed_at is not None: + assert isinstance(closed_trade.closed_at, str) + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_cancel_trade(self): + async with LNMClient(create_auth_config()) as client: + # Create a trade first + params = FuturesOrder( + type="l", + side="b", + price=100_000, + quantity=1, + leverage=100, + ) + trade = await client.futures.isolated.new_trade(params) + # Cancel the trade cancel_params = CancelTradeParams(id=trade.id) canceled = await client.futures.isolated.cancel(cancel_params) assert canceled.id == trade.id assert canceled.canceled is True + assert canceled.open is False + assert canceled.running is False + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_cancel_all_trades(self): + async with LNMClient(create_auth_config()) as client: + result = await client.futures.isolated.cancel_all() + assert isinstance(result, list) + for canceled in result: + assert canceled.canceled is True + assert canceled.open is False + assert canceled.running is False + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_close_trade(self): + async with LNMClient(create_auth_config()) as client: + # Create a running trade first (market order) + params = FuturesOrder( + type="m", # market order + side="b", + quantity=1, + leverage=100, + ) + try: + trade = await client.futures.isolated.new_trade(params) + # Try to close the trade + close_params = CloseTradeParams(id=trade.id) + closed = await client.futures.isolated.close(close_params) + assert closed.id == trade.id + assert closed.closed is True + assert closed.open is False + assert closed.running is False + if closed.closed_at is not None: + assert isinstance(closed.closed_at, str) + except Exception as e: + # May fail if no running trades or insufficient margin + assert len(str(e)) > 0 + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_add_margin(self): + async with LNMClient(create_auth_config()) as client: + # Get a running trade first + running_trades = await client.futures.isolated.get_running_trades() + if len(running_trades) > 0: + trade = running_trades[0] + params = AddMarginParams(id=trade.id, amount=10_000) + updated = await client.futures.isolated.add_margin(params) + assert updated.id == trade.id + assert updated.running is True + assert updated.margin >= trade.margin + else: + # Skip if no running trades + pytest.skip("No running trades to test add_margin") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_cash_in(self): + async with LNMClient(create_auth_config()) as client: + # Get a running trade first + running_trades = await client.futures.isolated.get_running_trades() + if len(running_trades) > 0: + trade = running_trades[0] + params = CashInParams(id=trade.id, amount=10_000) + updated = await client.futures.isolated.cash_in(params) + assert updated.id == trade.id + assert updated.running is True + else: + # Skip if no running trades + pytest.skip("No running trades to test cash_in") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_update_stoploss(self): + async with LNMClient(create_auth_config()) as client: + # Get a running trade first + running_trades = await client.futures.isolated.get_running_trades() + if len(running_trades) > 0: + trade = running_trades[0] + params = UpdateStoplossParams(id=trade.id, stoploss=50_000) + updated = await client.futures.isolated.update_stoploss(params) + assert updated.id == trade.id + assert updated.running is True + assert updated.stoploss == 50_000 + else: + # Skip if no running trades + pytest.skip("No running trades to test update_stoploss") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_update_takeprofit(self): + async with LNMClient(create_auth_config()) as client: + # Get a running trade first + running_trades = await client.futures.isolated.get_running_trades() + if len(running_trades) > 0: + trade = running_trades[0] + params = UpdateTakeprofitParams(id=trade.id, takeprofit=150_000) + updated = await client.futures.isolated.update_takeprofit(params) + assert updated.id == trade.id + assert updated.running is True + assert updated.takeprofit == 150_000 + else: + # Skip if no running trades + pytest.skip("No running trades to test update_takeprofit") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_funding_fees_isolated(self): + async with LNMClient(create_auth_config()) as client: + params = GetIsolatedFundingFeesParams(limit=5) + result = await client.futures.isolated.get_funding_fees(params) + assert isinstance(result, list) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].fee is not None + assert result[0].settlement_id is not None + assert result[0].time is not None + if result[0].trade_id is not None: + assert result[0].trade_id is not None @pytest.mark.asyncio +@pytest.mark.usefixtures("auth_rate_limit_delay") class TestFuturesCrossIntegration: """Integration tests for cross margin futures.""" @@ -335,65 +658,280 @@ class TestFuturesCrossIntegration: async def test_get_position(self): async with LNMClient(create_auth_config()) as client: position = await client.futures.cross.get_position() + assert position.id is not None assert position.margin >= 0 assert position.leverage > 0 + assert position.quantity >= 0 + assert position.initial_margin >= 0 + assert position.maintenance_margin >= 0 + assert position.running_margin >= 0 + assert position.delta_pl is not None + assert position.total_pl is not None + assert position.funding_fees is not None + assert position.trading_fees >= 0 + assert position.updated_at is not None + if position.entry_price is not None: + assert position.entry_price > 0 + if position.liquidation is not None: + assert position.liquidation > 0 + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_new_order(self): + async with LNMClient(create_auth_config()) as client: + params = FuturesCrossOrderLimit( + type="limit", + side="b", + price=100_000, + quantity=1, + client_id="test-order-123", + ) + order = await client.futures.cross.new_order(params) + assert order.id is not None + assert order.side == "b" + assert order.price == 100_000 + assert order.quantity == 1 + assert order.trading_fee >= 0 + assert order.created_at is not None @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), reason="V3_API_KEY not set in environment", ) - async def test_cross_orders(self): + async def test_get_open_orders(self): async with LNMClient(create_auth_config()) as client: - # Get open orders open_orders = await client.futures.cross.get_open_orders() assert isinstance(open_orders, list) + if len(open_orders) > 0: + order = open_orders[0] + assert order.id is not None + assert order.open is True + assert order.canceled is False + assert order.filled is False + assert order.price > 0 + assert order.quantity > 0 + assert order.side in ["b", "s"] + assert order.type == "limit" + assert order.trading_fee >= 0 + assert order.created_at is not None - # Get filled orders - from lnmarkets_sdk.v3.models.futures_cross import GetFilledOrdersParams - + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_filled_orders(self): + async with LNMClient(create_auth_config()) as client: params = GetFilledOrdersParams(limit=5) filled_orders = await client.futures.cross.get_filled_orders(params) assert isinstance(filled_orders, list) + assert len(filled_orders) <= params.limit + if len(filled_orders) > 0: + order = filled_orders[0] + assert order.id is not None + assert order.filled is True + assert order.open is False + assert order.price > 0 + assert order.quantity > 0 + assert order.side in ["b", "s"] + assert order.type in ["limit", "liquidation", "market"] + assert order.trading_fee >= 0 + assert order.created_at is not None + if order.filled_at is not None: + assert isinstance(order.filled_at, str) + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_cancel_order(self): + async with LNMClient(create_auth_config()) as client: + # Create an order first + params = FuturesCrossOrderLimit( + type="limit", + side="b", + price=100_000, + quantity=1, + client_id="test-cancel-123", + ) + order = await client.futures.cross.new_order(params) + # Cancel the order + cancel_params = CancelOrderParams(id=order.id) + canceled = await client.futures.cross.cancel(cancel_params) + assert canceled.id == order.id + assert canceled.canceled is True + assert canceled.open is False + assert canceled.filled is False + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_cancel_all_orders(self): + async with LNMClient(create_auth_config()) as client: + result = await client.futures.cross.cancel_all() + assert isinstance(result, list) + for canceled in result: + assert canceled.canceled is True + assert canceled.open is False + assert canceled.filled is False + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_close_position(self): + async with LNMClient(create_auth_config()) as client: + # Check if there's a position to close + position = await client.futures.cross.get_position() + if position.quantity > 0: + result = await client.futures.cross.close() + # Result can be an order or position update + assert result is not None + else: + # Skip if no position + pytest.skip("No position to close") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_deposit(self): + async with LNMClient(create_auth_config()) as client: + params = DepositParams(amount=10_000) + position = await client.futures.cross.deposit(params) + assert position.id is not None + assert position.margin >= 0 + assert position.leverage > 0 + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_withdraw(self): + async with LNMClient(create_auth_config()) as client: + # Get current position to check margin + position = await client.futures.cross.get_position() + if position.margin > 10_000: + params = WithdrawParams(amount=10_000) + updated = await client.futures.cross.withdraw(params) + assert updated.id is not None + assert updated.margin >= 0 + else: + # Skip if insufficient margin + pytest.skip("Insufficient margin to test withdraw") + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_set_leverage(self): + async with LNMClient(create_auth_config()) as client: + params = SetLeverageParams(leverage=50) + position = await client.futures.cross.set_leverage(params) + assert position.id is not None + assert position.leverage == 50 + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_transfers(self): + async with LNMClient(create_auth_config()) as client: + params = GetTransfersParams(limit=5) + result = await client.futures.cross.get_transfers(params) + assert isinstance(result, list) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].id is not None + assert result[0].amount is not None + assert result[0].time is not None + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_get_funding_fees_cross(self): + async with LNMClient(create_auth_config()) as client: + params = GetCrossFundingFeesParams(limit=5) + result = await client.futures.cross.get_funding_fees(params) + assert isinstance(result, list) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].fee is not None + assert result[0].settlement_id is not None + assert result[0].time is not None + if result[0].trade_id is not None: + assert result[0].trade_id is not None @pytest.mark.asyncio +@pytest.mark.usefixtures("public_rate_limit_delay") class TestOracleIntegration: """Integration tests for oracle endpoints.""" async def test_get_last_price(self): async with LNMClient(create_public_config()) as client: result = await client.oracle.get_last_price() + assert isinstance(result, list) + assert len(result) > 0 assert result[0].last_price > 0 assert result[0].time is not None async def test_get_index(self): - from lnmarkets_sdk.v3.models.oracle import GetIndexParams - async with LNMClient(create_public_config()) as client: params = GetIndexParams(limit=5) result = await client.oracle.get_index(params) assert isinstance(result, list) assert len(result) > 0 assert result[0].index > 0 + assert result[0].time is not None @pytest.mark.asyncio +@pytest.mark.usefixtures("public_rate_limit_delay") class TestSyntheticUSDIntegration: """Integration tests for synthetic USD endpoints.""" async def test_get_best_price(self): async with LNMClient(create_public_config()) as client: result = await client.synthetic_usd.get_best_price() - assert result.ask_price + assert result.ask_price > 0 + assert result.bid_price > 0 @pytest.mark.skipif( not os.environ.get("V3_API_KEY"), reason="V3_API_KEY not set in environment", ) async def test_get_swaps(self): - from lnmarkets_sdk.v3.models.synthetic_usd import GetSwapsParams - async with LNMClient(create_auth_config()) as client: params = GetSwapsParams(limit=5) result = await client.synthetic_usd.get_swaps(params) assert isinstance(result, list) + assert len(result) <= params.limit + if len(result) > 0: + assert result[0].id is not None + assert result[0].created_at is not None + assert result[0].in_amount > 0 + assert result[0].out_amount > 0 + assert result[0].in_asset in ["BTC", "USD"] + assert result[0].out_asset in ["BTC", "USD"] + + @pytest.mark.skipif( + not os.environ.get("V3_API_KEY"), + reason="V3_API_KEY not set in environment", + ) + async def test_new_swap(self): + async with LNMClient(create_auth_config()) as client: + # Try to create a swap (may fail due to insufficient balance or other reasons) + params = NewSwapParams(in_amount=100, in_asset="USD", out_asset="BTC") + try: + result = await client.synthetic_usd.new_swap(params) + assert result.in_amount > 0 + assert result.out_amount > 0 + assert result.in_asset in ["BTC", "USD"] + assert result.out_asset in ["BTC", "USD"] + except Exception as e: + # Expected to fail if insufficient balance or other validation errors + assert len(str(e)) > 0 diff --git a/uv.lock b/uv.lock index c0d8b55..3e8836d 100644 --- a/uv.lock +++ b/uv.lock @@ -100,6 +100,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "faker" +version = "37.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, +] + +[[package]] +name = "faker-crypto" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/f7/b9db524a89497ed496dc2f0487d4bc5244652dea8fe085a6e90f1d2c6867/faker_crypto-1.0.1.tar.gz", hash = "sha256:c22e105f2833ba76c66181c1fcd73a6dbabb0d9e332ef4b55784a4b077aefc79", size = 40001, upload-time = "2025-10-23T13:35:40.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/2a/04f9b13a25f7705e7900f18918258361fb29abf50fb29e3cdf76164927db/faker_crypto-1.0.1-py3-none-any.whl", hash = "sha256:96bd12a561c4c35070cdf8088a4099126d6752263fa4e4000051382197ed1058", size = 3635, upload-time = "2025-10-23T13:35:39.225Z" }, +] + [[package]] name = "greenlet" version = "3.2.4" @@ -196,7 +220,7 @@ wheels = [ [[package]] name = "lnmarkets-sdk" -version = "0.0.11" +version = "0.0.12" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -206,6 +230,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "faker" }, + { name = "faker-crypto" }, { name = "playwright" }, { name = "pyright" }, { name = "pytest" }, @@ -233,6 +259,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "faker", specifier = ">=37.12.0" }, + { name = "faker-crypto", specifier = ">=1.0.1" }, { name = "playwright", specifier = ">=1.40.0" }, { name = "pyright", specifier = ">=1.1.390" }, { name = "pytest" }, @@ -535,6 +563,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" From 2e847f23fcdfdd6a0a5021eab386cafd8bedc48c Mon Sep 17 00:00:00 2001 From: CaoKha Date: Sun, 9 Nov 2025 19:44:43 +0100 Subject: [PATCH 2/2] update: bump to 0.0.13 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51e763c..efc45ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" urls = { "Homepage" = "https://github.com/ln-markets/sdk-python", "Repository" = "https://github.com/ln-markets/sdk-python", "Bug Tracker" = "https://github.com/ln-markets/sdk-python/issues" } name = "lnmarkets-sdk" -version = "0.0.12" +version = "0.0.13" description = "LN Markets API Python SDK" readme = "README.md" license = { text = "MIT" } diff --git a/uv.lock b/uv.lock index 3e8836d..ed9b975 100644 --- a/uv.lock +++ b/uv.lock @@ -220,7 +220,7 @@ wheels = [ [[package]] name = "lnmarkets-sdk" -version = "0.0.12" +version = "0.0.13" source = { editable = "." } dependencies = [ { name = "httpx" },