From c50f8f3e89a3b5b01d8f5939958a7c1881b8c6ba Mon Sep 17 00:00:00 2001 From: yevheniilavro Date: Thu, 19 Mar 2026 13:34:21 +0100 Subject: [PATCH 1/4] update for docs --- README.md | 345 +++++++++++++++++++++++++++++++++++++++----------- tests/test.py | 127 ++++++++++++++++++- 2 files changed, 391 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index ab98c3d..74a0ea2 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,320 @@ -## A Python SDK for [whitebit](https://www.whitebit.com) +# WhiteBit Python SDK + [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PyPI version](https://badge.fury.io/py/python-whitebit-sdk.svg)](https://badge.fury.io/py/python-whitebit-sdk) +[![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/downloads/) -Please read [whitebit API document](https://whitebit-exchange.github.io/api-docs/) before continuing. +An unofficial Python SDK for the [WhiteBit](https://www.whitebit.com) cryptocurrency exchange API. -## API List +> Please read the [WhiteBit API documentation](https://whitebit-exchange.github.io/api-docs/) before using this SDK. -- [Private API](https://whitebit-exchange.github.io/api-docs/private/http-trade-v4/) -- [Public API](https://whitebit-exchange.github.io/api-docs/public/http-v4/) -- [Private WS](https://whitebit-exchange.github.io/api-docs/private/websocket/) -- [Public WS](https://whitebit-exchange.github.io/api-docs/public/websocket/) +--- -v4 is the preferred one to use +## Table of Contents -## Disclaimer -“You acknowledge that the software is provided “as is”. Author makes no representations or warranties with respect to -the software whether express or implied, including but not limited to, implied warranties of merchantability and fitness -for a particular purpose. author makes no representation or warranty that: (i) the use and distribution of the software -will be uninterrupted or error free, and (ii) any use and distribution of the software is free from infringement of any -third party intellectual property rights. It shall be your sole responsibility to make such determination before the use -of software. Author disclaims any liability in case any such use and distribution infringe any third party’s -intellectual property rights. Author hereby disclaims any warranty and liability whatsoever for any development created -by or for you with respect to your customers. You acknowledge that you have relied on no warranties and that no -warranties are made by author or granted by law whenever it is permitted by law.” +- [Installation](#installation) +- [Quick Start](#quick-start) +- [REST API](#rest-api) + - [Trade Market (Public)](#trade-market-public) + - [Trade Account (Private)](#trade-account-private) + - [Trade Orders (Private)](#trade-orders-private) + - [Main Account (Private)](#main-account-private) + - [Collateral Trading (Private)](#collateral-trading-private) +- [WebSocket API](#websocket-api) +- [Examples](#examples) +- [Running Tests](#running-tests) +- [Contributing](#contributing) +- [Disclaimer](#disclaimer) -## REST API +--- -### Setup - -#### Install the Python module: +## Installation ```bash -python3 -m pip install python-whitebit-sdk +pip install python-whitebit-sdk ``` -Init client for API services. Get APIKey/SecretKey from your whitebit account. +Python 3.9 or higher is required. + +--- + +## The Whitebit Base Client + +All SDK clients inherit from the `Whitebit` base class located in `whitebit/client.py`. You do not instantiate it directly — instead use the specific clients listed below. It handles: + +- **Authentication** — HMAC-SHA512 signature using `X-TXC-APIKEY`, `X-TXC-SIGNATURE`, and `X-TXC-PAYLOAD` headers +- **Nonce** — automatically adds `nonce` (Unix ms timestamp) and `nonceWindow: true` to every private request +- **HTTP transport** — wraps `requests.Session` with `User-Agent: python-whitebit-sdk` +- **Error handling** — raises `Exception` for any non-2xx response ```python -from whitebit import MainAccountClient +from whitebit.client import Whitebit -account = MainAccountClient(api_key="", api_secret="")) +# Base class — used internally by all clients +class Whitebit: + def __init__(self, api_key: str = '', api_secret: str = ''): + ... + + def _request(self, method, uri, timeout=10, auth=True, params=None, return_raw=False) -> dict: + ... ``` -Following are some simple examples. +Private endpoints require credentials — passing empty strings raises `ValueError: Missing credentials.` -See the **examples** folder for full references. +--- -#### Create Spot Limit Order +## Quick Start ```python -# Create order/spot client -order = OrderClient(api_key="", - api_secret="") +from whitebit import TradeMarketClient, TradeAccountClient, TradeOrderClient + +# Public endpoints — no credentials required +market = TradeMarketClient() +print(market.get_tickers()) + +# Private endpoints — API key and secret required +account = TradeAccountClient(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET") +print(account.get_balance("BTC")) -# Call SDK function put_limit -print(order.put_limit("BTC_USDT", "sell", "0.1", "40000", True)) +# Place a limit order +order = TradeOrderClient(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET") +print(order.put_limit("BTC_USDT", "buy", "0.001", "30000")) ``` -## Websocket API +Get your API key and secret from your [WhiteBit account settings](https://whitebit.com/settings/api). -### Setup +--- + +## REST API -Init bot class and "on_message" method for work with ws responses. Get APIKey/SecretKey from your whitebit account. +### Trade Market (Public) + +No authentication required. ```python -class Bot(WhitebitWsClient): - def __init__(self): - super().__init__(key="", secret="") +from whitebit import TradeMarketClient + +client = TradeMarketClient() + +client.get_markets_info() # All markets info +client.get_tickers() # All ticker data +client.get_available_tickers() # Available tickers (v4) +client.get_market_activity() # All market activity +client.get_single_market_activity("BTC_USDT") +client.get_symbols() # Available trading symbols +client.get_kline("BTC_USDT", start=1609459200, end=1609545600, interval="1h", limit=100) +client.get_trading_fee() # Global trading fees +client.get_fee_list() # Fee list (v4) +client.get_order_book("BTC_USDT", limit=20, level=2) +client.get_depth("BTC_USDT") +client.get_trade_history("BTC_USDT", last_id=0, limit=100) +client.get_deals("BTC_USDT") +client.get_assets() # Asset information +``` - async def on_message(self, event) -> None: - logging.info(event) - +### Trade Account (Private) + +```python +from whitebit import TradeAccountClient + +client = TradeAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") + +client.get_balance("BTC") # Balance for specific ticker +client.get_balance() # All balances +client.get_order_deals(order_id=12345, offset=0, limit=50) +client.get_executed_history(market="BTC_USDT", offset=0, limit=50) +client.get_history(market="BTC_USDT", offset=0, limit=50) +client.get_unexecuted_orders(market="BTC_USDT") +client.get_ping() +client.get_time() +client.get_ws_token() # WebSocket authentication token ``` -Following are some simple examples. +### Trade Orders (Private) + +```python +from whitebit import TradeOrderClient + +client = TradeOrderClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") + +# Limit order +client.put_limit("BTC_USDT", "buy", "0.001", "30000") +client.put_limit("BTC_USDT", "sell", "0.001", "50000", post_only=True) + +# Market order +client.put_market("BTC_USDT", "buy", "10") # buy $10 worth +client.put_market_stock("BTC_USDT", "buy", "0.001") # buy 0.001 BTC + +# Stop orders +client.put_stop_limit("BTC_USDT", "sell", "0.001", "29000", activation_price="29500") +client.put_stop_market("BTC_USDT", "sell", "0.001", activation_price="29500") -See the **examples** folder for full references. +# Cancel an order +client.cancel_order("BTC_USDT", order_id=12345) -#### Subscribe on deals topic +# Bulk limit orders +orders = [ + client.build_limit_order("BTC_USDT", "buy", "0.001", "28000"), + client.build_limit_order("BTC_USDT", "buy", "0.001", "27000"), +] +client.limit_bulk(orders) + +# Kill switch — auto-cancel all orders after a timeout +client.put_kill_switch("BTC_USDT", timeout=60, types=["limit"]) +client.get_kill_switch_status("BTC_USDT") +``` + +### Main Account (Private) ```python -class Bot(WhitebitWsClient): - '''Can be used to create a custom trading strategy/bot''' +from whitebit import MainAccountClient + +client = MainAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") + +client.get_balance("BTC") +client.get_fee() +client.get_history(ticker="BTC", offset=0, limit=50) +client.transfer(limit=10, offset=0) + +# Transfer codes +client.create_code("USDT", "100", passw="secret", description="Gift") +client.apply_code(code="WBT-...", passw="secret") +client.get_my_codes(limit=10, offset=0) +client.get_codes_history(limit=10, offset=0) +# Custom fees +client.get_custom_fee() +client.get_custom_fee_by_market("BTC_USDT") +``` + +### Collateral Trading (Private) + +```python +from whitebit import CollateralMarketClient, CollateralAccountClient, CollateralOrderClient + +# Market info (public) +market = CollateralMarketClient() +market.get_markets_info() +market.get_futures_markets() + +# Account (private) +account = CollateralAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") +account.get_balance("BTC") +account.get_summary() +account.set_leverage(leverage=10) +account.get_open_positions("BTC_USDT") +account.get_positions_history("BTC_USDT", limit=50, offset=0) +account.get_oco_orders("BTC_USDT", offset=0, limit=50) + +# Orders (private) +order = CollateralOrderClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") +order.put_limit("BTC_USDT", "buy", "0.001", "30000") +order.put_market("BTC_USDT", "buy", "0.001") +order.put_stop_limit("BTC_USDT", "sell", "0.001", "29000", activation_price="29500") +order.put_stop_market("BTC_USDT", "sell", "0.001", activation_price="29500") +order.put_oco("BTC_USDT", "sell", "0.001", "31000", activation_price="29500", stop_limit_price="29000") +order.cancel_order("BTC_USDT", order_id=12345) +order.cancel_oco("BTC_USDT", order_id=12345) +``` + +--- + +## WebSocket API + +Extend `WhitebitWsClient` and implement the `on_message` async callback to receive real-time updates. + +```python +import asyncio +import logging +from whitebit import WhitebitWsClient + + +class Bot(WhitebitWsClient): def __init__(self): - super().__init__(key="", secret="") + super().__init__(key="YOUR_KEY", secret="YOUR_SECRET") async def on_message(self, event) -> None: - '''receives the websocket events''' - if 'result' in event: - result = event['result'] - match result: - case 'pong': return - logging.info(event['result']) - return + if "result" in event: + if event["result"] == "pong": + return + logging.info("Result: %s", event["result"]) else: - method = event['method'] - match method: - case WhitebitWsClient.DEALS_UPDATE: - logging.info(event['params']) - return + method = event.get("method") + params = event.get("params") + logging.info("[%s] %s", method, params) async def main() -> None: bot = Bot() - await bot.get_deals("BTC_USDT", 0, 100) - await bot.subscribe_deals(["BTC_USDT"]) + + # Public subscriptions + await bot.subscribe_last_price(["BTC_USDT", "ETH_USDT"]) + await bot.subscribe_market_trades(["BTC_USDT"]) + await bot.subscribe_market_depth("BTC_USDT", limit=20, interval="0") + await bot.subscribe_kline("BTC_USDT", interval=60) + + # Private subscriptions (require valid API credentials) + await bot.subscribe_spot_balance() + await bot.subscribe_pending_orders(["BTC_USDT"]) + await bot.subscribe_orders_executed(["BTC_USDT"]) + while not bot.exception_occur: - await asyncio.sleep(100) - return + await asyncio.sleep(10) -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass - finally: - loop.close() + asyncio.run(main()) +``` + +### Available WebSocket Topics + +| Method | Description | +|---|---| +| `subscribe_kline` / `get_kline` | Candlestick (OHLCV) data | +| `subscribe_last_price` / `get_last_price` | Last price updates | +| `subscribe_market_depth` / `get_market_depth` | Order book depth | +| `subscribe_market_stat` / `get_market_stat` | 24h market statistics | +| `subscribe_market_stat_today` / `get_market_stat_today` | Today's market statistics | +| `subscribe_market_trades` / `get_market_trades` | Public trades stream | +| `subscribe_deals` / `get_deals` | Deals stream | +| `subscribe_spot_balance` / `get_spot_balance` | Spot account balance | +| `subscribe_margin_balance` / `get_margin_balance` | Margin account balance | +| `subscribe_pending_orders` / `get_pending_orders` | Open orders updates | +| `subscribe_orders_executed` / `get_orders_executed` | Executed orders | +| `send_ping` | Keep-alive ping | +| `get_time` | Server time | + +--- + +## Examples + +Full working examples are in the [`examples/`](examples/) directory: + +| File | Description | +|---|---| +| `examples/trade_examples.py` | Spot market, account, and order endpoints | +| `examples/main_examples.py` | Main account endpoints | +| `examples/collateral_examples.py` | Collateral / futures trading | +| `examples/ws_example.py` | WebSocket real-time subscriptions | + +--- + +## Running Tests + +```bash +pip install -r requirements.txt +python -m pytest tests/ -v ``` + +--- + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on reporting bugs, requesting features, and submitting pull requests. + +--- + +## Disclaimer + +The software is provided "as is" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. The author makes no representation that the use of the software will be uninterrupted or error-free. Use at your own risk. \ No newline at end of file diff --git a/tests/test.py b/tests/test.py index cf05789..4989ce3 100644 --- a/tests/test.py +++ b/tests/test.py @@ -116,13 +116,126 @@ def test_trade_order_positive(self): assert str(responses.calls[0].response.json()) == str(response) -def main(): - MyTestCase.test_trade_private_query_without_api_key() - MyTestCase.test_trade_account_positive() - MyTestCase.test_trade_account_negative() - MyTestCase.test_trade_order_positive() - MyTestCase.test_trade_order_negative() +class MainAccountTestCase(unittest.TestCase): + @responses.activate + def test_main_account_no_api_key(self): + client = MainAccountClient() + with self.assertRaises(ValueError): + client.get_balance("BTC") + + @responses.activate + def test_main_account_balance_positive(self): + expected_response = {"BTC": {"main_balance": "2.5"}} + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/main-account/balance', + json=expected_response, + status=200, + ) + + client = MainAccountClient("key", "secret") + response = client.get_balance("BTC") + + req = json.loads(responses.calls[0].request.body) + assert req["ticker"] == "BTC" + assert req["request"] == "/api/v4/main-account/balance" + assert response == expected_response + + @responses.activate + def test_main_account_balance_negative(self): + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/main-account/balance', + json={'error': 'unauthorized'}, + status=401, + ) + + client = MainAccountClient("bad_key", "bad_secret") + with self.assertRaises(Exception): + client.get_balance("BTC") + + @responses.activate + def test_main_account_get_fee(self): + expected_response = {"BTC": {"deposit": "0", "withdraw": "0.0004"}} + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/main-account/fee', + json=expected_response, + status=200, + ) + + client = MainAccountClient("key", "secret") + response = client.get_fee() + + assert response == expected_response + assert responses.calls[0].request.method == "POST" + + +class CollateralOrderTestCase(unittest.TestCase): + @responses.activate + def test_collateral_no_api_key(self): + client = CollateralOrderClient() + with self.assertRaises(ValueError): + client.cancel_order("BTC_USDT", 1) + + @responses.activate + def test_collateral_limit_order_positive(self): + expected_response = { + "orderId": 99, + "market": "BTC_USDT", + "side": "buy", + "type": "collateral limit", + "amount": "0.001", + "price": "30000", + } + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/order/collateral/limit', + json=expected_response, + status=200, + ) + + client = CollateralOrderClient("key", "secret") + response = client.put_limit("BTC_USDT", "buy", "0.001", "30000") + + req = json.loads(responses.calls[0].request.body) + assert req["market"] == "BTC_USDT" + assert req["side"] == "buy" + assert req["amount"] == "0.001" + assert req["price"] == "30000" + assert response == expected_response + + @responses.activate + def test_collateral_cancel_order_positive(self): + expected_response = {"orderId": 99, "market": "BTC_USDT"} + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/order/cancel', + json=expected_response, + status=200, + ) + + client = CollateralOrderClient("key", "secret") + response = client.cancel_order("BTC_USDT", 99) + + req = json.loads(responses.calls[0].request.body) + assert req["market"] == "BTC_USDT" + assert req["orderId"] == 99 + assert response == expected_response + + @responses.activate + def test_collateral_cancel_order_negative(self): + responses.add( + responses.POST, + 'https://whitebit.com/api/v4/order/cancel', + json={'error': 'order not found'}, + status=404, + ) + + client = CollateralOrderClient("key", "secret") + with self.assertRaises(Exception): + client.cancel_order("BTC_USDT", 0) if __name__ == '__main__': - main() + unittest.main() From a43bab9a41116f76fd4de7b8b99f7d112c7428c2 Mon Sep 17 00:00:00 2001 From: yevheniilavro Date: Thu, 19 Mar 2026 14:03:53 +0100 Subject: [PATCH 2/4] removed old sdk --- CONTRIBUTING.md | 97 ++++ README.md | 315 ++++--------- whitebit/__init__.py | 4 - whitebit/__version__.py | 3 - whitebit/client.py | 89 ---- whitebit/collateral/__init__.py | 3 - whitebit/collateral/account/__init__.py | 1 - whitebit/collateral/account/account.py | 53 --- whitebit/collateral/market/__init__.py | 1 - whitebit/collateral/market/market.py | 13 - whitebit/collateral/order/__init__.py | 1 - whitebit/collateral/order/order.py | 85 ---- whitebit/main/__init__.py | 1 - whitebit/main/account/__init__.py | 1 - whitebit/main/account/account.py | 86 ---- whitebit/stream/__init__.py | 1 - whitebit/stream/ws.py | 583 ------------------------ whitebit/trade/__init__.py | 3 - whitebit/trade/account/__init__.py | 1 - whitebit/trade/account/account.py | 79 ---- whitebit/trade/market/__init__.py | 1 - whitebit/trade/market/market.py | 83 ---- whitebit/trade/order/__init__.py | 1 - whitebit/trade/order/order.py | 108 ----- 24 files changed, 178 insertions(+), 1435 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 whitebit/__init__.py delete mode 100644 whitebit/__version__.py delete mode 100644 whitebit/client.py delete mode 100644 whitebit/collateral/__init__.py delete mode 100644 whitebit/collateral/account/__init__.py delete mode 100644 whitebit/collateral/account/account.py delete mode 100644 whitebit/collateral/market/__init__.py delete mode 100644 whitebit/collateral/market/market.py delete mode 100644 whitebit/collateral/order/__init__.py delete mode 100644 whitebit/collateral/order/order.py delete mode 100644 whitebit/main/__init__.py delete mode 100644 whitebit/main/account/__init__.py delete mode 100644 whitebit/main/account/account.py delete mode 100644 whitebit/stream/__init__.py delete mode 100644 whitebit/stream/ws.py delete mode 100644 whitebit/trade/__init__.py delete mode 100644 whitebit/trade/account/__init__.py delete mode 100644 whitebit/trade/account/account.py delete mode 100644 whitebit/trade/market/__init__.py delete mode 100644 whitebit/trade/market/market.py delete mode 100644 whitebit/trade/order/__init__.py delete mode 100644 whitebit/trade/order/order.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d5c52b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing to WhiteBit Python SDK + +Thank you for your interest in contributing! This document explains how to report bugs, suggest features, and submit code changes. + +--- + +## Table of Contents + +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Features](#suggesting-features) +- [Development Setup](#development-setup) +- [Running Tests](#running-tests) +- [Submitting a Pull Request](#submitting-a-pull-request) +- [Code Style](#code-style) + +--- + +## Reporting Bugs + +Before opening an issue, please search existing issues to avoid duplicates. + +When reporting a bug, include: + +- Python version (`python --version`) +- SDK version (`pip show python-whitebit-sdk`) +- Minimal code snippet that reproduces the problem +- Full error traceback +- Expected vs actual behaviour + +--- + +## Suggesting Features + +Open an issue with the label **enhancement** and describe: + +- The use case you are trying to solve +- The API endpoint involved (link to the [WhiteBit API docs](https://whitebit-exchange.github.io/api-docs/) if applicable) +- Any alternative approaches you considered + +--- + +## Development Setup + +1. **Fork** the repository and clone your fork: + + ```bash + git clone https://github.com/YOUR_USERNAME/python-sdk.git + cd python-sdk + ``` + +2. Create a virtual environment and install dependencies: + + ```bash + python -m venv .venv + source .venv/bin/activate # Windows: .venv\Scripts\activate + pip install -r requirements.txt + pip install -e . + ``` + +3. Create a feature branch: + + ```bash + git checkout -b feature/my-feature + ``` + +--- + +## Running Tests + +```bash +python -m pytest tests/ -v +``` + +All tests use the `responses` library to mock HTTP calls — no real API credentials are required. + +When adding a new endpoint or fixing a bug, add or update the corresponding test in `tests/test.py`. + +--- + +## Submitting a Pull Request + +1. Make sure all existing tests pass before submitting. +2. Add tests for any new functionality. +3. Keep commits focused — one logical change per commit. +4. Write a clear PR description explaining **what** changed and **why**. +5. Reference any related issue in the PR description (e.g. `Closes #42`). + +Once submitted, your PR will be reviewed. Feedback may be requested before merging. + +--- + +## Code Style + +- Follow [PEP 8](https://peps.python.org/pep-0008/). +- Use type hints where possible. +- Keep method signatures consistent with existing clients (see `whitebit/trade/` for reference). +- Do not commit API keys, secrets, or any credentials. diff --git a/README.md b/README.md index 74a0ea2..b6c83b5 100644 --- a/README.md +++ b/README.md @@ -1,273 +1,136 @@ # WhiteBit Python SDK [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![PyPI version](https://badge.fury.io/py/python-whitebit-sdk.svg)](https://badge.fury.io/py/python-whitebit-sdk) +[![PyPI version](https://badge.fury.io/py/whitebit-python-sdk.svg)](https://badge.fury.io/py/whitebit-python-sdk) [![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/downloads/) An unofficial Python SDK for the [WhiteBit](https://www.whitebit.com) cryptocurrency exchange API. -> Please read the [WhiteBit API documentation](https://whitebit-exchange.github.io/api-docs/) before using this SDK. - ---- - -## Table of Contents - -- [Installation](#installation) -- [Quick Start](#quick-start) -- [REST API](#rest-api) - - [Trade Market (Public)](#trade-market-public) - - [Trade Account (Private)](#trade-account-private) - - [Trade Orders (Private)](#trade-orders-private) - - [Main Account (Private)](#main-account-private) - - [Collateral Trading (Private)](#collateral-trading-private) -- [WebSocket API](#websocket-api) -- [Examples](#examples) -- [Running Tests](#running-tests) -- [Contributing](#contributing) -- [Disclaimer](#disclaimer) +> Please read the [WhiteBit API documentation](https://docs.whitebit.com/) before using this SDK. --- ## Installation ```bash -pip install python-whitebit-sdk +pip install whitebit-python-sdk ``` Python 3.9 or higher is required. --- -## The Whitebit Base Client - -All SDK clients inherit from the `Whitebit` base class located in `whitebit/client.py`. You do not instantiate it directly — instead use the specific clients listed below. It handles: - -- **Authentication** — HMAC-SHA512 signature using `X-TXC-APIKEY`, `X-TXC-SIGNATURE`, and `X-TXC-PAYLOAD` headers -- **Nonce** — automatically adds `nonce` (Unix ms timestamp) and `nonceWindow: true` to every private request -- **HTTP transport** — wraps `requests.Session` with `User-Agent: python-whitebit-sdk` -- **Error handling** — raises `Exception` for any non-2xx response - -```python -from whitebit.client import Whitebit - -# Base class — used internally by all clients -class Whitebit: - def __init__(self, api_key: str = '', api_secret: str = ''): - ... - - def _request(self, method, uri, timeout=10, auth=True, params=None, return_raw=False) -> dict: - ... -``` - -Private endpoints require credentials — passing empty strings raises `ValueError: Missing credentials.` - ---- - ## Quick Start -```python -from whitebit import TradeMarketClient, TradeAccountClient, TradeOrderClient - -# Public endpoints — no credentials required -market = TradeMarketClient() -print(market.get_tickers()) - -# Private endpoints — API key and secret required -account = TradeAccountClient(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET") -print(account.get_balance("BTC")) - -# Place a limit order -order = TradeOrderClient(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET") -print(order.put_limit("BTC_USDT", "buy", "0.001", "30000")) -``` - -Get your API key and secret from your [WhiteBit account settings](https://whitebit.com/settings/api). - ---- - -## REST API - -### Trade Market (Public) - -No authentication required. - -```python -from whitebit import TradeMarketClient - -client = TradeMarketClient() - -client.get_markets_info() # All markets info -client.get_tickers() # All ticker data -client.get_available_tickers() # Available tickers (v4) -client.get_market_activity() # All market activity -client.get_single_market_activity("BTC_USDT") -client.get_symbols() # Available trading symbols -client.get_kline("BTC_USDT", start=1609459200, end=1609545600, interval="1h", limit=100) -client.get_trading_fee() # Global trading fees -client.get_fee_list() # Fee list (v4) -client.get_order_book("BTC_USDT", limit=20, level=2) -client.get_depth("BTC_USDT") -client.get_trade_history("BTC_USDT", last_id=0, limit=100) -client.get_deals("BTC_USDT") -client.get_assets() # Asset information -``` - -### Trade Account (Private) +### Sync client ```python -from whitebit import TradeAccountClient - -client = TradeAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") - -client.get_balance("BTC") # Balance for specific ticker -client.get_balance() # All balances -client.get_order_deals(order_id=12345, offset=0, limit=50) -client.get_executed_history(market="BTC_USDT", offset=0, limit=50) -client.get_history(market="BTC_USDT", offset=0, limit=50) -client.get_unexecuted_orders(market="BTC_USDT") -client.get_ping() -client.get_time() -client.get_ws_token() # WebSocket authentication token -``` - -### Trade Orders (Private) - -```python -from whitebit import TradeOrderClient - -client = TradeOrderClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") - -# Limit order -client.put_limit("BTC_USDT", "buy", "0.001", "30000") -client.put_limit("BTC_USDT", "sell", "0.001", "50000", post_only=True) +from whitebit import WhitebitApi -# Market order -client.put_market("BTC_USDT", "buy", "10") # buy $10 worth -client.put_market_stock("BTC_USDT", "buy", "0.001") # buy 0.001 BTC +client = WhitebitApi(txc_apikey="YOUR_API_KEY", token="YOUR_TOKEN") -# Stop orders -client.put_stop_limit("BTC_USDT", "sell", "0.001", "29000", activation_price="29500") -client.put_stop_market("BTC_USDT", "sell", "0.001", activation_price="29500") +# Market data (public) +print(client.public_api_v4.get_tickers()) -# Cancel an order -client.cancel_order("BTC_USDT", order_id=12345) +# Spot trading +print(client.spot_trading.get_trade_account_balance(ticker="BTC")) +print(client.spot_trading.create_limit_order(market="BTC_USDT", side="buy", amount="0.001", price="30000")) -# Bulk limit orders -orders = [ - client.build_limit_order("BTC_USDT", "buy", "0.001", "28000"), - client.build_limit_order("BTC_USDT", "buy", "0.001", "27000"), -] -client.limit_bulk(orders) - -# Kill switch — auto-cancel all orders after a timeout -client.put_kill_switch("BTC_USDT", timeout=60, types=["limit"]) -client.get_kill_switch_status("BTC_USDT") +# Collateral +print(client.collateral_trading.get_open_positions(market="BTC_USDT")) ``` -### Main Account (Private) +### Async client ```python -from whitebit import MainAccountClient - -client = MainAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") +import asyncio +from whitebit import AsyncWhitebitApi -client.get_balance("BTC") -client.get_fee() -client.get_history(ticker="BTC", offset=0, limit=50) -client.transfer(limit=10, offset=0) +async def main(): + client = AsyncWhitebitApi(txc_apikey="YOUR_API_KEY", token="YOUR_TOKEN") -# Transfer codes -client.create_code("USDT", "100", passw="secret", description="Gift") -client.apply_code(code="WBT-...", passw="secret") -client.get_my_codes(limit=10, offset=0) -client.get_codes_history(limit=10, offset=0) + # Market data (public) + tickers = await client.public_api_v4.get_tickers() + print(tickers) -# Custom fees -client.get_custom_fee() -client.get_custom_fee_by_market("BTC_USDT") -``` + # Spot trading + balance = await client.spot_trading.get_trade_account_balance(ticker="BTC") + print(balance) -### Collateral Trading (Private) + order = await client.spot_trading.create_limit_order( + market="BTC_USDT", side="buy", amount="0.001", price="30000" + ) + print(order) -```python -from whitebit import CollateralMarketClient, CollateralAccountClient, CollateralOrderClient - -# Market info (public) -market = CollateralMarketClient() -market.get_markets_info() -market.get_futures_markets() - -# Account (private) -account = CollateralAccountClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") -account.get_balance("BTC") -account.get_summary() -account.set_leverage(leverage=10) -account.get_open_positions("BTC_USDT") -account.get_positions_history("BTC_USDT", limit=50, offset=0) -account.get_oco_orders("BTC_USDT", offset=0, limit=50) - -# Orders (private) -order = CollateralOrderClient(api_key="YOUR_KEY", api_secret="YOUR_SECRET") -order.put_limit("BTC_USDT", "buy", "0.001", "30000") -order.put_market("BTC_USDT", "buy", "0.001") -order.put_stop_limit("BTC_USDT", "sell", "0.001", "29000", activation_price="29500") -order.put_stop_market("BTC_USDT", "sell", "0.001", activation_price="29500") -order.put_oco("BTC_USDT", "sell", "0.001", "31000", activation_price="29500", stop_limit_price="29000") -order.cancel_order("BTC_USDT", order_id=12345) -order.cancel_oco("BTC_USDT", order_id=12345) +asyncio.run(main()) ``` +Get your API key from your [WhiteBit account settings](https://whitebit.com/settings/api). + --- -## WebSocket API +## Clients + +The SDK exposes two top-level clients — `WhitebitApi` (sync) and `AsyncWhitebitApi` (async). Both share the same sub-client structure: + +| Sub-client | Auth | Description | +|---|---|---| +| `client.public_api_v4` | No | Tickers, order books, klines, assets, trades | +| `client.spot_trading` | Yes | Place/cancel spot orders, balance, order history | +| `client.collateral_trading` | Yes | Margin/futures orders, positions, OCO | +| `client.main_account` | Yes | Main balance, deposit/withdraw history | +| `client.deposit` | Yes | Deposit addresses, fiat deposit URLs | +| `client.withdraw` | Yes | Withdrawals | +| `client.transfer` | Yes | Transfer between balances | +| `client.codes` | Yes | Transfer codes (create, apply, history) | +| `client.fees` | Yes | Fee information | +| `client.market_fee` | Yes | Market-specific fees | +| `client.jwt` | Yes | WebSocket JWT token | +| `client.authentication` | Yes | OAuth2 token management | +| `client.sub_account` | Yes | Sub-account management | +| `client.sub_account_api_keys` | Yes | Sub-account API key management | +| `client.crypto_lending_fixed` | Yes | Fixed crypto lending | +| `client.crypto_lending_flex` | Yes | Flex crypto lending | +| `client.credit_line` | Yes | Credit line | +| `client.mining_pool` | Yes | Mining pool stats and management | -Extend `WhitebitWsClient` and implement the `on_message` async callback to receive real-time updates. +--- -```python -import asyncio -import logging -from whitebit import WhitebitWsClient +## Examples +Full working examples are in the [`examples/`](examples/) directory: -class Bot(WhitebitWsClient): - def __init__(self): - super().__init__(key="YOUR_KEY", secret="YOUR_SECRET") +### [trade_examples.py](examples/trade_examples.py) - async def on_message(self, event) -> None: - if "result" in event: - if event["result"] == "pong": - return - logging.info("Result: %s", event["result"]) - else: - method = event.get("method") - params = event.get("params") - logging.info("[%s] %s", method, params) +Covers `TradeMarketClient`, `TradeAccountClient`, and `TradeOrderClient`: +- Get tickers, order books, klines, assets +- Get spot balance, order history, unexecuted orders +- Place limit, market, stop-limit, stop-market orders +- Bulk limit orders, kill switch +### [main_examples.py](examples/main_examples.py) -async def main() -> None: - bot = Bot() +Covers `MainAccountClient`: +- Main account balance and fee info +- Transaction history +- Create and apply transfer codes - # Public subscriptions - await bot.subscribe_last_price(["BTC_USDT", "ETH_USDT"]) - await bot.subscribe_market_trades(["BTC_USDT"]) - await bot.subscribe_market_depth("BTC_USDT", limit=20, interval="0") - await bot.subscribe_kline("BTC_USDT", interval=60) +### [collateral_examples.py](examples/collateral_examples.py) - # Private subscriptions (require valid API credentials) - await bot.subscribe_spot_balance() - await bot.subscribe_pending_orders(["BTC_USDT"]) - await bot.subscribe_orders_executed(["BTC_USDT"]) +Covers `CollateralMarketClient`, `CollateralAccountClient`, `CollateralOrderClient`: +- Futures/collateral market info +- Margin balance, open positions, leverage +- Place and cancel collateral/OCO orders - while not bot.exception_occur: - await asyncio.sleep(10) +### [ws_example.py](examples/ws_example.py) +Covers `WhitebitWsClient`: +- Extend the client and implement `on_message` for real-time events +- Subscribe to deals, prices, order book depth, balance, pending orders -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - asyncio.run(main()) -``` +--- -### Available WebSocket Topics +## WebSocket Topics | Method | Description | |---|---| @@ -275,28 +138,12 @@ if __name__ == "__main__": | `subscribe_last_price` / `get_last_price` | Last price updates | | `subscribe_market_depth` / `get_market_depth` | Order book depth | | `subscribe_market_stat` / `get_market_stat` | 24h market statistics | -| `subscribe_market_stat_today` / `get_market_stat_today` | Today's market statistics | | `subscribe_market_trades` / `get_market_trades` | Public trades stream | | `subscribe_deals` / `get_deals` | Deals stream | | `subscribe_spot_balance` / `get_spot_balance` | Spot account balance | | `subscribe_margin_balance` / `get_margin_balance` | Margin account balance | | `subscribe_pending_orders` / `get_pending_orders` | Open orders updates | | `subscribe_orders_executed` / `get_orders_executed` | Executed orders | -| `send_ping` | Keep-alive ping | -| `get_time` | Server time | - ---- - -## Examples - -Full working examples are in the [`examples/`](examples/) directory: - -| File | Description | -|---|---| -| `examples/trade_examples.py` | Spot market, account, and order endpoints | -| `examples/main_examples.py` | Main account endpoints | -| `examples/collateral_examples.py` | Collateral / futures trading | -| `examples/ws_example.py` | WebSocket real-time subscriptions | --- @@ -317,4 +164,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on reporting bugs, request ## Disclaimer -The software is provided "as is" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. The author makes no representation that the use of the software will be uninterrupted or error-free. Use at your own risk. \ No newline at end of file +The software is provided "as is" without warranty of any kind. Use at your own risk. diff --git a/whitebit/__init__.py b/whitebit/__init__.py deleted file mode 100644 index 5191eaf..0000000 --- a/whitebit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .trade import * -from .main import * -from .collateral import * -from .stream import * diff --git a/whitebit/__version__.py b/whitebit/__version__.py deleted file mode 100644 index d695222..0000000 --- a/whitebit/__version__.py +++ /dev/null @@ -1,3 +0,0 @@ -'''This is the Whitebit main module version''' -VERSION = (1, 0, 5) -__version__ = '.'.join(map(str, VERSION)) diff --git a/whitebit/client.py b/whitebit/client.py deleted file mode 100644 index 7fb8185..0000000 --- a/whitebit/client.py +++ /dev/null @@ -1,89 +0,0 @@ -import math -import time -import requests -import base64 -import hashlib -import hmac -import json - - -def _create_uri(params) -> str: - data = '' - strl = [] - for key in sorted(params): - strl.append(f'{key}={params[key]}') - data += '&'.join(strl) - return f'?{data}'.replace(' ', '%20') - - -class Whitebit: - def __init__(self, api_key: str = '', api_secret: str = ''): - self.__api_key = api_key - self.__api_secret = api_secret - self.__url = "https://whitebit.com" - self.__session = requests.Session() - self.__session.headers.update({'User-Agent': 'python-whitebit-sdk'}) - - def _request(self, - method: str = "GET", - uri: str = '', - timeout: int = 10, - auth: bool = True, - params: dict = None, - return_raw: bool = False - ) -> dict: - if params is None: - params = {} - - headers = {'Content-Type': 'application/json'} - - if auth: - self.__create_authed_request(params, headers, uri) - return self.__check_response_data( - self.__session.request(method=method, url=self.__url + uri, headers=headers, json=params, - timeout=timeout), - return_raw - ) - - if params: - uri += _create_uri(params) - url = f'{self.__url}{uri}' - - return self.__check_response_data( - self.__session.request(method=method, url=url, headers=headers, timeout=timeout), - return_raw - ) - - def __create_authed_request(self, params, headers, uri): - if not self.__api_key or self.__api_key == '' or not self.__api_secret or self.__api_secret == '': raise ValueError( - 'Missing credentials.') - params['request'] = uri - params['nonce'] = int(time.time() * 1000) - params['nonceWindow'] = True - headers.update({ - 'X-TXC-APIKEY': self.__api_key, - 'X-TXC-SIGNATURE': self.__get_signature(params), - 'X-TXC-PAYLOAD': self.__payload.decode('ascii'), - }) - - def __get_signature(self, data: dict) -> str: - data_json = json.dumps(data, separators=(',', ':')) # use separators param for deleting spaces - self.__payload = base64.b64encode(data_json.encode('ascii')) - return hmac.new(self.__api_secret.encode('ascii'), self.__payload, hashlib.sha512).hexdigest() - - def __check_response_data(self, response_data, return_raw: bool = False) -> dict: - if response_data.status_code in ['200', 200, '201', 201]: - if return_raw: - return response_data - try: - data = response_data.json() - except ValueError as exc: - raise ValueError(response_data.content) from exc - else: - return data - raise Exception(f'{response_data.status_code} - {response_data.text}') - - def _to_str_list(self, value) -> str: - if isinstance(value, str): return value - if isinstance(value, list): return ','.join(value) - raise ValueError('a must be type of str or list of strings') diff --git a/whitebit/collateral/__init__.py b/whitebit/collateral/__init__.py deleted file mode 100644 index c4a09fb..0000000 --- a/whitebit/collateral/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .account import * -from .market import * -from .order import * diff --git a/whitebit/collateral/account/__init__.py b/whitebit/collateral/account/__init__.py deleted file mode 100644 index 96c79ae..0000000 --- a/whitebit/collateral/account/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .account import * diff --git a/whitebit/collateral/account/account.py b/whitebit/collateral/account/account.py deleted file mode 100644 index 3071c34..0000000 --- a/whitebit/collateral/account/account.py +++ /dev/null @@ -1,53 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class CollateralAccountClient(Whitebit): - __BALANCE_URL = "/api/v4/collateral-account/balance" - __LEVERAGE_URL = "/api/v4/collateral-account/leverage" - __POSITION_HISTORY_URL = "/api/v4/collateral-account/positions/history" - __OPEN_POSITIONS_URL = "/api/v4/collateral-account/positions/open" - __SUMMARY_URL = "/api/v4/collateral-account/summary" - __OCO_ORDERS_URL = "/api/v4/oco-orders" - - def get_balance(self, ticker: str = ""): - params = {} - if ticker != "": - params = {'ticker': ticker} - return self._request(method='POST', uri=self.__BALANCE_URL, params=params, auth=True) - - def get_summary(self): - return self._request(method='POST', uri=self.__SUMMARY_URL, auth=True) - - def set_leverage(self, leverage): - params = {'leverage': leverage} - return self._request(method='POST', uri=self.__LEVERAGE_URL, params=params, auth=True) - - def get_open_positions(self, market): - params = {'market': market} - return self._request(method='POST', uri=self.__OPEN_POSITIONS_URL, params=params, auth=True) - - def get_positions_history(self, market: str = None, position_id: int = None, start_date: int = None, - end_date: int = None, limit: str = None, offset: str = None): - params = {} - if market is not None: - params['market'] = market - if position_id is not None: - params['positionId'] = position_id - if start_date is not None: - params['startDate'] = start_date - if end_date is not None: - params['endDate'] = end_date - if limit is not None: - params['limit'] = limit - if offset is not None: - params['offset'] = offset - return self._request(method='POST', uri=self.__OPEN_POSITIONS_URL, params=params, auth=True) - - def get_oco_orders(self, market: str, offset: int = None, limit: int = None): - params = {'market': market} - if offset is not None: - params["offset"] = offset - if limit is not None: - params["limit"] = limit - return self._request(method='POST', uri=self.__OPEN_POSITIONS_URL, params=params, auth=True) diff --git a/whitebit/collateral/market/__init__.py b/whitebit/collateral/market/__init__.py deleted file mode 100644 index 387d7ac..0000000 --- a/whitebit/collateral/market/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .market import * diff --git a/whitebit/collateral/market/market.py b/whitebit/collateral/market/market.py deleted file mode 100644 index 12ccd79..0000000 --- a/whitebit/collateral/market/market.py +++ /dev/null @@ -1,13 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class CollateralMarketClient(Whitebit): - __COLLATERAL_MARKETS_ENDPOINT = "/api/v4/public/collateral/markets" - __FUTURES_MARKETS_ENDPOINT = "/api/v4/public/futures" - - def get_markets_info(self): - return self._request(method='GET', uri=self.__COLLATERAL_MARKETS_ENDPOINT, auth=False) - - def get_futures_markets(self): - return self._request(method='GET', uri=self.__FUTURES_MARKETS_ENDPOINT, auth=False) diff --git a/whitebit/collateral/order/__init__.py b/whitebit/collateral/order/__init__.py deleted file mode 100644 index 4ca2c09..0000000 --- a/whitebit/collateral/order/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .order import * diff --git a/whitebit/collateral/order/order.py b/whitebit/collateral/order/order.py deleted file mode 100644 index 271dc7a..0000000 --- a/whitebit/collateral/order/order.py +++ /dev/null @@ -1,85 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class CollateralOrderClient(Whitebit): - __ORDER_CANCEL_URL = "/api/v4/order/cancel" - __LIMIT_URL = "/api/v4/order/collateral/limit" - __MARKET_URL = "/api/v4/order/collateral/market" - __OCO_URL = "/api/v4/order/collateral/oco" - __CANCEL_OCO_URL = "/api/v4/order/oco-cancel" - __STOP_MARKET_URL = "/api/v4/order/collateral/trigger-market" - __STOP_LIMIT_URL = "/api/v4/order/collateral/stop-limit" - - def put_market(self, market: str, side: str, amount, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__MARKET_URL, params=params, auth=True) - - def put_limit(self, market: str, side: str, amount, price, post_only: bool = False, ioc: bool = False, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "price": price, - "postOnly": post_only, - "ioc": ioc - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__LIMIT_URL, params=params, auth=True) - - def put_stop_limit(self, market: str, side: str, amount, price, activation_price, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "price": price, - "activation_price": activation_price - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__STOP_LIMIT_URL, params=params, auth=True) - - def put_stop_market(self, market: str, side: str, amount, activation_price, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "activation_price": activation_price - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__STOP_MARKET_URL, params=params, auth=True) - - def cancel_order(self, market: str, order_id): - params = { - "market": market, - "orderId": order_id, - } - return self._request(method='POST', uri=self.__ORDER_CANCEL_URL, params=params, auth=True) - - def put_oco(self, market: str, side: str, amount, price, activation_price, stop_limit_price, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "activation_price": activation_price, - "stop_limit_price": stop_limit_price, - "price": price - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__OCO_URL, params=params, auth=True) - - def cancel_oco(self, market: str, order_id): - params = { - "market": market, - "orderId": order_id, - } - return self._request(method='POST', uri=self.__CANCEL_OCO_URL, params=params, auth=True) \ No newline at end of file diff --git a/whitebit/main/__init__.py b/whitebit/main/__init__.py deleted file mode 100644 index 96c79ae..0000000 --- a/whitebit/main/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .account import * diff --git a/whitebit/main/account/__init__.py b/whitebit/main/account/__init__.py deleted file mode 100644 index d7ecb3e..0000000 --- a/whitebit/main/account/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .account import MainAccountClient diff --git a/whitebit/main/account/account.py b/whitebit/main/account/account.py deleted file mode 100644 index 0765847..0000000 --- a/whitebit/main/account/account.py +++ /dev/null @@ -1,86 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class MainAccountClient(Whitebit): - - __FEE_URL = "/api/v4/main-account/fee" - __BALANCE_URL = "/api/v4/main-account/balance" - __HISTORY_URL = "/api/v4/main-account/history" - __TRANSFER_URL = "/api/v4/main-account/transfer" - - __CODES_URL = "/api/v4/main-account/codes" - __CODES_APPLY_URL = "/api/v4/main-account/codes/apply" - __CODES_MY_URL = "/api/v4/main-account/codes/my" - __CODES_HISTORY_URL = "/api/v4/main-account/codes/history" - - __CUSTOM_FEE_BY_MARKET_URL = "/api/v4/market/fee/single" - __CUSTOM_FEE_URL = "/api/v4/market/fee" - - def transfer(self, limit: int = None, offset: int = None): - params = {} - if limit is not None: - params['limit'] = limit - if offset is not None: - offset['offset'] = offset - return self._request(method='POST', uri=self.__TRANSFER_URL, params=params, auth=True) - - def get_fee(self): - return self._request(method='POST', uri=self.__FEE_URL, auth=True) - - def get_history(self, transaction_method: str = None, ticker: str = None, addresses: [str] = None, - unique_id: str = None, status: [int] = None, offset: int = 0, limit: int = 100): - params = {'offset': offset, 'limit': limit} - if transaction_method is not None: - params['transactionMethod'] = transaction_method - if ticker is not None: - params['ticker'] = ticker - if addresses is not None and len(addresses) != 0: - params['addresses'] = addresses - if unique_id is not None: - params['unique_id'] = unique_id - if status is not None and len(status) != 0: - params['status'] = status - return self._request(method='POST', uri=self.__HISTORY_URL, params=params, auth=True) - - def get_balance(self, ticker: str = ""): - params = {'ticker': ticker} - return self._request(method='POST', uri=self.__BALANCE_URL, params=params, auth=True) - - def create_code(self, ticker: str, amount: str, passw: str = None, description: str = None): - params = {'ticker': ticker, "amount": amount} - if passw is not None: - params['passphrase'] = passw - if description is not None: - params['description'] = description - return self._request(method='POST', uri=self.__CODES_URL, params=params, auth=True) - - def apply_code(self, code: str, passw: str = None): - params = {'code': code} - if passw is not None: - params['passphrase'] = passw - return self._request(method='POST', uri=self.__CODES_APPLY_URL, params=params, auth=True) - - def get_my_codes(self, limit: int = None, offset: int = None): - params = {} - if limit is not None: - params['limit'] = limit - if offset is not None: - offset['offset'] = offset - return self._request(method='POST', uri=self.__CODES_MY_URL, params=params, auth=True) - - def get_codes_history(self, limit: int = None, offset: int = None): - params = {} - if limit is not None: - params['limit'] = limit - if offset is not None: - offset['offset'] = offset - return self._request(method='POST', uri=self.__CODES_HISTORY_URL, params=params, auth=True) - - def get_custom_fee(self): - params = {} - return self._request(method='POST', uri=self.__CODES_HISTORY_URL, params=params, auth=True) - - def get_custom_fee_by_market(self, market: str): - params = {'market': market} - return self._request(method='POST', uri=self.__CODES_HISTORY_URL, params=params, auth=True) diff --git a/whitebit/stream/__init__.py b/whitebit/stream/__init__.py deleted file mode 100644 index f43f43c..0000000 --- a/whitebit/stream/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .ws import ConnectWebsocket diff --git a/whitebit/stream/ws.py b/whitebit/stream/ws.py deleted file mode 100644 index 8d63ce6..0000000 --- a/whitebit/stream/ws.py +++ /dev/null @@ -1,583 +0,0 @@ -import logging -import json -import time -import asyncio -from enum import Enum -from random import random -import traceback -from typing import List -import websockets -from whitebit.trade.account.account import TradeAccountClient - - -class ConnectWebsocket: - __TIME_REQUEST = "ping" - __AUTHORIZE_REQUEST = "authorize" - - MAX_RECONNECT_NUM = 10 - - def __init__(self, client, url: str, callback, token: str = ''): - self.__client = client - self.__ws_url = url - self.__callback = callback - - self.__reconnect_num = 0 - self.__ws_conn_authed = None - - self._token = token - - self.__last_ping = None - self.__socket = None - self.__subscriptions = [] - - asyncio.ensure_future( - self.__run_forever(), - loop=asyncio.get_running_loop() - ) - - @property - def subscriptions(self) -> list: - '''Returns the active subscriptions''' - return self.__subscriptions - - async def __run(self, event: asyncio.Event): - keep_alive = True - self.__last_ping = time.time() - - async with websockets.connect(f'{self.__ws_url}', ping_interval=None) as socket: - logging.info('Websocket connected!') - self.__socket = socket - - if not event.is_set(): - await self.send_ping() - event.set() - self.__reconnect_num = 0 - - while keep_alive: - if time.time() - self.__last_ping > 10: await self.send_ping() - try: - _msg = await asyncio.wait_for(self.__socket.recv(), timeout=15) - except asyncio.TimeoutError: - await self.send_ping() - except asyncio.CancelledError: - logging.exception('asyncio.CancelledError') - keep_alive = False - await self.__callback({'error': 'asyncio.CancelledError'}) - else: - try: - msg = json.loads(_msg) - except ValueError: - logging.warning(_msg) - else: - if 'result' in msg and msg["id"] == 0: - continue - await self.__callback(msg) - - async def __run_forever(self) -> None: - try: - while True: await self.__reconnect() - except Exception("MaxReconnectError"): - await self.__callback({'error': 'MaxReconnectError'}) - except Exception as execption: - self.__callback({'error': f'{execption}: {traceback.format_exc()}'}) - finally: - self.__client.exception_occur = True - - async def __reconnect(self): - logging.info('Websocket start connect/reconnect ' + websockets.version.tag) - - self.__reconnect_num += 1 - if self.__reconnect_num >= self.MAX_RECONNECT_NUM: - raise Exception("MaxReconnectError") - - reconnect_wait = self.__get_reconnect_wait(self.__reconnect_num) - logging.debug( - f'asyncio sleep reconnect_wait={reconnect_wait} s reconnect_num={self.__reconnect_num}' - ) - await asyncio.sleep(reconnect_wait) - logging.debug('asyncio sleep done') - event = asyncio.Event() - - tasks = { - asyncio.ensure_future(self.__recover_subscriptions(event)): self.__recover_subscriptions, - asyncio.ensure_future(self.__run(event)): self.__run - } - - while set(tasks.keys()): - finished, pending = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_EXCEPTION) - exception_occur = False - for task in finished: - if task.exception(): - exception_occur = True - traceback.print_stack() - message = f'{task} got an exception {task.exception()}\n {task.get_stack()}' - logging.warning(message) - for process in pending: - logging.warning(f'pending {process}') - try: - process.cancel() - except asyncio.CancelledError: - logging.exception('asyncio.CancelledError') - logging.warning('Cancel OK') - await self.__callback({'error': message}) - if exception_occur: break - logging.warning('reconnect over') - - async def __recover_subscriptions(self, event): - logging.info( - f'Recover subscriptions {self.__subscriptions} waiting.' - ) - await event.wait() - - await self.auth() - time.sleep(1) - - for sub in self.__subscriptions: - await self.send_message(sub) - logging.info(f'{sub} OK') - - logging.info( - f'Recovering subscriptions {self.__subscriptions} done.' - ) - - async def send_ping(self): - msg = { - 'id': 0, - 'method': self.__TIME_REQUEST, - 'params': [] - } - await self.__socket.send(json.dumps(msg)) - self.__last_ping = time.time() - - async def send_message(self, msg): - while not self.__socket: await asyncio.sleep(.4) - await self.__socket.send(json.dumps(msg)) - - def append_subscription(self, sub_data: dict) -> None: - self.remove_subscription(sub_data) # remove from list, to avoid duplicates - self.__subscriptions.append(sub_data) - - def remove_subscription(self, sub_data: dict) -> None: - self.__subscriptions = [x for x in self.__subscriptions if x["method"] != sub_data["method"]] - - def __get_reconnect_wait(self, attempts: int) -> float: - return round(random() * min(60 * 3, (2 ** attempts) - 1) + 1) - - @property - def token(self): - return self._token - - async def auth(self): - msg = { - 'id': int(time.time() * 1000), - 'method': self.__AUTHORIZE_REQUEST, - 'params': [self.token, "python sdk"] - } - await self.send_message(msg) - - -class WhitebitWsClient(TradeAccountClient): - PROD_ENV_URL = 'wss://api.whitebit.com/ws' - - __AUTHORIZE_REQUEST = "authorize" - __TIME_REQUEST = "time" - __PING_REQUEST = "ping" - __KLINE_REQUEST = "candles_request" - __KLINE_SUBSCRIBE = "candles_subscribe" - KLINE_UPDATE = "candles_update" - __KLINE_UNSUBSCRIBE = "candles_unsubscribe" - - __DEPTH_REQUEST = "depth_request" - __DEPTH_SUBSCRIBE = "depth_subscribe" - DEPTH_UPDATE = "depth_update" - __DEPTH_UNSUBSCRIBE = "depth_unsubscribe" - - __LAST_PRICE_REQUEST = "lastprice_request" - __LAST_PRICE_SUBSCRIBE = "lastprice_subscribe" - LAST_PRICE_UPDATE = "lastprice_update" - __LAST_PRICE_UNSUBSCRIBE = "lastprice_unsubscribe" - - __MARKET_STAT_REQUEST = "market_request" - __MARKET_STAT_SUBSCRIBE = "market_subscribe" - MARKET_STAT_UPDATE = "market_update" - __MARKET_STAT_UNSUBSCRIBE = "market_unsubscribe" - - __MARKET_STAT_TODAY_REQUEST = "marketToday_query" - __MARKET_STAT_TODAY_SUBSCRIBE = "marketToday_subscribe" - MARKET_STAT_TODAY_UPDATE = "marketToday_update" - __MARKET_STAT_TODAY_UNSUBSCRIBE = "marketToday_unsubscribe" - - __TRADES_REQUEST = "trades_request" - __TRADES_SUBSCRIBE = "trades_subscribe" - TRADES_UPDATE = "trades_update" - __TRADES_UNSUBSCRIBE = "trades_unsubscribe" - - __ORDER_PENDING_REQUEST = "ordersPending_request" - __ORDERS_PENDING_SUBSCRIBE = "ordersPending_subscribe" - ORDERS_PENDING_UPDATE = "ordersPending_update" - __ORDERS_PENDING_UNSUBSCRIBE = "ordersPending_unsubscribe" - - __DEALS_REQUEST = "deals_request" - __DEALS_SUBSCRIBE = "deals_subscribe" - DEALS_UPDATE = "deals_update" - __DEALS_UNSUBSCRIBE = "deals_unsubscribe" - - __SPOT_BALANCE_REQUEST = "balanceSpot_request" - __SPOT_BALANCE_SUBSCRIBE = "balanceSpot_subscribe" - SPOT_BALANCE_UPDATE = "balanceSpot_update" - __SPOT_BALANCE_UNSUBSCRIBE = "balanceSpot_unsubscribe" - - __ORDERS_EXECUTED_REQUEST = "ordersExecuted_request" - __ORDERS_EXECUTED_SUBSCRIBE = "ordersExecuted_subscribe" - ORDERS_EXECUTED_UPDATE = "ordersExecuted_update" - __ORDERS_EXECUTED_UNSUBSCRIBE = "ordersExecuted_unsubscribe" - - __MARGIN_BALANCE_REQUEST = "balanceMargin_request" - __MARGIN_BALANCE_SUBSCRIBE = "balanceMargin_subscribe" - MARGIN_BALANCE_UPDATE = "balanceMargin_update" - __MARGIN_BALANCE_UNSUBSCRIBE = "balanceMargin_unsubscribe" - - def __init__(self, key: str = '', secret: str = '', - callback=None): - super().__init__(api_key=key, api_secret=secret) - self.__callback = callback - token = self.get_ws_token() - self.exception_occur = False - self._conn = ConnectWebsocket( - client=self, - url=self.PROD_ENV_URL, - token=token["websocket_token"], - callback=self.on_message - ) - - async def on_message(self, msg: dict): - if self.__callback is not None: - await self.__callback(msg) - else: - logging.warning('Received event but no callback is defined') - logging.info(msg) - - async def __subscribe(self, subscription: dict) -> None: - self._conn.append_subscription(subscription) - await self._conn.send_message(subscription) - - async def __unsubscribe(self, subscription: dict) -> None: - self._conn.remove_subscription(subscription) - await self._conn.send_message(subscription) - - async def get_authorize(self, token: str, com_id=int(time.time() * 1000)): - await self._conn.send_message( - {'id': com_id, - 'method': self.__AUTHORIZE_REQUEST, - 'params': [token, 'python-sdk']}) - - async def get_spot_balance(self, assets: List[str], com_id=int(time.time() * 1000)): - assets_as_interface = [i for i in assets] - await self._conn.send_message( - {'id': com_id, - 'method': self.__SPOT_BALANCE_REQUEST, - 'params': assets_as_interface}) - - async def subscribe_spot_balance(self, assets: List[str], com_id=int(time.time() * 1000)): - assets_as_interface = [i for i in assets] - await self.__subscribe( - {'id': com_id, - 'method': self.__SPOT_BALANCE_SUBSCRIBE, - 'params': assets_as_interface}) - - async def unsubscribe_spot_balance(self, com_id=int(time.time() * 1000)): - await self.__unsubscribe( - {'id': com_id, - 'method': self.__SPOT_BALANCE_UNSUBSCRIBE, - 'params': []}) - - async def get_margin_balance(self, assets: List[str], com_id=int(time.time() * 1000)): - assets_as_interface = [asset for asset in assets] - await self._conn.send_message( - {'id': com_id, - 'method': self.__MARGIN_BALANCE_REQUEST, - 'params': assets_as_interface}) - - async def subscribe_margin_balance(self, assets: List[str], com_id=int(time.time() * 1000)): - assets_as_interface = [asset for asset in assets] - await self.__subscribe( - {'id': com_id, - 'method': self.__MARGIN_BALANCE_SUBSCRIBE, - 'params': assets_as_interface}) - - async def unsubscribe_margin_balance(self, com_id=int(time.time() * 1000)): - await self.__unsubscribe( - {'id': com_id, - 'method': self.__MARGIN_BALANCE_UNSUBSCRIBE, - 'params': []}) - - async def get_pending_orders(self, market: str, offset: int = 0, limit: int = 100, com_id=int(time.time() * 1000)): - await self._conn.send_message({ - 'id': com_id, - 'method': self.__ORDER_PENDING_REQUEST, - 'params': [market, offset, limit]}) - - async def subscribe_pending_orders(self, markets: List[str], com_id=int(time.time() * 1000)): - markets_as_interface = [market for market in markets] - await self.__subscribe( - {'id': com_id, - 'method': self.__ORDERS_PENDING_SUBSCRIBE, - 'params': markets_as_interface}) - - async def unsubscribe_pending_orders(self, com_id=int(time.time() * 1000)): - await self.__unsubscribe( - {'id': com_id, - 'method': self.__ORDERS_PENDING_UNSUBSCRIBE, - 'params': []}) - - class GetOrdersExecFilter(Enum): - LIMIT = 1 - MARKET = 2 - MARKET_STOCK = 202 - STOP_LIMIT = 3 - STOP_MARKET = 4 - MARGIN_LIMIT = 7 - MARGIN_MARKET = 8 - MARGIN_STOP_LIMIT = 9 - MARGIN_TRIGGER_STOP_MARKET = 10 - MARGIN_NORMALIZATION = 14 - - async def get_orders_executed(self, market: str, order_types: List[int], offset: int = 0, limit: int = 100, - com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__ORDERS_EXECUTED_REQUEST, - 'params': [ - { - 'market': market, - 'order_types': order_types - }, - offset, - limit - ] - } - await self._conn.send_message(msg) - - class OrdersSubscribeExecFilter(Enum): - ALL = 0 - LIMIT = 1 - MARKET = 2 - - async def subscribe_orders_executed(self, markets: List[str], order_filter: OrdersSubscribeExecFilter, - com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__ORDERS_EXECUTED_SUBSCRIBE, - 'params': [markets, order_filter] - } - await self.__subscribe(msg) - - async def unsubscribe_orders_executed(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__ORDERS_EXECUTED_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_deals(self, market: str, offset: int = 0, limit: int = 100, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__DEALS_REQUEST, - 'params': [market, offset, limit], - } - await self._conn.send_message(msg) - - async def subscribe_deals(self, markets: List[str], com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__DEALS_SUBSCRIBE, - 'params': [markets] - } - await self.__subscribe(msg) - - async def unsubscribe_deals(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__DEALS_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def send_ping(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__PING_REQUEST, - 'params': [] - } - await self._conn.send_message(msg) - - async def get_time(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__TIME_REQUEST, - 'params': [] - } - await self._conn.send_message(msg) - - async def get_kline(self, market: str, start_time: int, end_time: int, interval: int, - com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__KLINE_REQUEST, - 'params': [market, start_time, end_time, interval] - } - await self._conn.send_message(msg) - - async def subscribe_kline(self, market: str, interval: int, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__KLINE_SUBSCRIBE, - 'params': [market, interval] - } - await self.__subscribe(msg) - - async def unsubscribe_kline(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__KLINE_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_last_price(self, market: str, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__LAST_PRICE_REQUEST, - 'params': [market] - } - await self._conn.send_message(msg) - - async def subscribe_last_price(self, market: List[str], com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__LAST_PRICE_SUBSCRIBE, - 'params': market - } - await self.__subscribe(msg) - - async def unsubscribe_last_price(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__LAST_PRICE_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_market_stat(self, market: str, period: int, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_REQUEST, - 'params': [market, period] - } - await self._conn.send_message(msg) - - async def subscribe_market_stat(self, market: List[str], com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_SUBSCRIBE, - 'params': market - } - await self.__subscribe(msg) - - async def unsubscribe_market_stat(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_market_stat_today(self, market: str, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_TODAY_REQUEST, - 'params': [market] - } - await self._conn.send_message(msg) - - async def subscribe_market_stat_today(self, market: List[str], com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_TODAY_SUBSCRIBE, - 'params': market - } - await self.__subscribe(msg) - - async def unsubscribe_market_stat_today(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__MARKET_STAT_TODAY_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_market_trades(self, market: str, limit: int = 0, start_trade_id: int = 0, - com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__TRADES_REQUEST, - 'params': [market, limit, start_trade_id] - } - await self._conn.send_message(msg) - - async def subscribe_market_trades(self, market: List[str], com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__TRADES_SUBSCRIBE, - 'params': market - } - await self.__subscribe(msg) - - async def unsubscribe_market_trades(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__TRADES_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) - - async def get_market_depth(self, market: str, price_interval: str = "0", limit: int = 100, - com_id=int(time.time() * 1000)): - """Support price intervals ["0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", - "0.1", "0"] """ - market_depth_filter_variants = ["0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", - "0.1", "0"] - if price_interval not in market_depth_filter_variants: - logging.warning("You should use only this interval variants = " + str(market_depth_filter_variants)) - return - msg = { - 'id': com_id, - 'method': self.__DEPTH_REQUEST, - 'params': [market, limit, price_interval] - } - await self._conn.send_message(msg) - - async def subscribe_market_depth(self, market: str, price_interval: str = "0", limit: int = 100, - multiple_sub: bool = False, com_id=int(time.time() * 1000)): - """Support price intervals ["0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", - "0.1", "0"] """ - market_depth_filter_variants = ["0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", - "0.1", "0"] - if price_interval not in market_depth_filter_variants: - logging.warning("You should use only this interval variants = " + str(market_depth_filter_variants)) - return - msg = { - 'id': com_id, - 'method': self.__DEPTH_SUBSCRIBE, - 'params': [market, limit, price_interval, multiple_sub] - } - await self.__subscribe(msg) - - async def unsubscribe_market_depth(self, com_id=int(time.time() * 1000)): - msg = { - 'id': com_id, - 'method': self.__DEPTH_UNSUBSCRIBE, - 'params': [] - } - await self.__unsubscribe(msg) diff --git a/whitebit/trade/__init__.py b/whitebit/trade/__init__.py deleted file mode 100644 index 3df7cbf..0000000 --- a/whitebit/trade/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from whitebit.trade.market.market import TradeMarketClient -from whitebit.trade.account.account import TradeAccountClient -from whitebit.trade.order.order import TradeOrderClient diff --git a/whitebit/trade/account/__init__.py b/whitebit/trade/account/__init__.py deleted file mode 100644 index 0ca409e..0000000 --- a/whitebit/trade/account/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .account import TradeAccountClient diff --git a/whitebit/trade/account/account.py b/whitebit/trade/account/account.py deleted file mode 100644 index ed60bd1..0000000 --- a/whitebit/trade/account/account.py +++ /dev/null @@ -1,79 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class TradeAccountClient(Whitebit): - __BALANCE_URL = "/api/v4/trade-account/balance" - __ORDER_URL = "/api/v4/trade-account/order" - __ORDER_HISTORY_URL = "/api/v4/trade-account/order/history" - __ORDER_EXECUTED_HISTORY_URL = "/api/v4/trade-account/executed-history" - __ORDERS_URL = "/api/v4/orders" - __TIME_URL = "/api/v4/public/time" - __PING_URL = "/api/v4/public/ping" - __WS_TOKEN_URL = "/api/v4/profile/websocket_token" - - def get_time(self): - return self._request(method='GET', uri=self.__TIME_URL, auth=False) - - def get_ping(self): - return self._request(method='GET', uri=self.__PING_URL, auth=False) - - def get_ws_token(self): - return self._request(method='POST', uri=self.__WS_TOKEN_URL, auth=True) - - def get_balance(self, ticker: str = ""): - params = {} - if ticker != "": - params = {'ticker': ticker} - return self._request(method='POST', uri=self.__BALANCE_URL, params=params, auth=True) - - def get_order_deals(self, order_id: int, offset: int = None, limit: int = None): - params = {"orderId": order_id} - if offset is not None: - params["offset"] = offset - if limit is not None: - params["limit"] = limit - return self._request(method='POST', uri=self.__ORDER_URL, params=params, auth=True) - - def get_executed_history(self, market: str = None, client_order_id: str = None, - offset: int = None, limit: int = None): - params = {} - if market is not None: - params["market"] = market - if client_order_id is not None: - params["clientOrderId"] = client_order_id - if offset is not None: - params["offset"] = offset - if limit is not None: - params["limit"] = limit - return self._request(method='POST', uri=self.__ORDER_EXECUTED_HISTORY_URL, params=params, auth=True) - - def get_history(self, market: str = None, order_id: str = None, client_order_id: str = None, - offset: int = None, limit: int = None): - params = {} - if market is not None: - params["market"] = market - if order_id is not None: - params["orderId"] = order_id - if client_order_id is not None: - params["clientOrderId"] = client_order_id - if limit is not None: - params["limit"] = limit - if offset is not None: - params["offset"] = offset - return self._request(method='POST', uri=self.__ORDER_HISTORY_URL, params=params, auth=True) - - def get_unexecuted_orders(self, market: str = None, order_id: str = None, client_order_id: str = None, - offset: int = None, limit: int = None): - params = {} - if market is not None: - params["market"] = market - if order_id is not None: - params["orderId"] = order_id - if client_order_id is not None: - params["clientOrderId"] = client_order_id - if limit is not None: - params["limit"] = limit - if offset is not None: - params["offset"] = offset - return self._request(method='POST', uri=self.__ORDERS_URL, params=params, auth=True) diff --git a/whitebit/trade/market/__init__.py b/whitebit/trade/market/__init__.py deleted file mode 100644 index 8d498dd..0000000 --- a/whitebit/trade/market/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .market import TradeMarketClient \ No newline at end of file diff --git a/whitebit/trade/market/market.py b/whitebit/trade/market/market.py deleted file mode 100644 index 0e75d0a..0000000 --- a/whitebit/trade/market/market.py +++ /dev/null @@ -1,83 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class TradeMarketClient(Whitebit): - __MARKETS_URL = "/api/v2/public/markets" - __MARKET_ACTIVITY_URL = "/api/v2/public/ticker" - __SINGLE_MARKET_ACTIVITY_URL = "/api/v1/public/ticker" - __TICKER_URL = "/api/v4/public/ticker" - __TICKERS_URL = "/api/v1/public/tickers" - __SYMBOLS_URL = "/api/v1/public/symbols" - __KLINE_URL = "/api/v1/public/kline" - __TRADING_FEE_URL = "/api/v2/public/fee" - __FEE_LIST_URL = "/api/v4/public/fee" - __DEPTH_URL = "/api/v2/public/depth/" - __ORDERBOOK_URL = "/api/v4/public/orderbook/" - __DEALS_URL = "/api/v4/public/trades/" - __TRADE_HISTORY_URL = "/api/v1/public/history" - __ASSETS_URL = "/api/v4/public/assets" - - def __init__(self, api_key: str = '', api_secret: str = ''): - super().__init__(api_key, api_secret) - - def get_markets_info(self): - return self._request(method='GET', uri=self.__MARKETS_URL, auth=False) - - def get_tickers(self): - return self._request(method='GET', uri=self.__TICKERS_URL, auth=False) - - def get_available_tickers(self): - return self._request(method='GET', uri=self.__TICKER_URL, auth=False) - - def get_market_activity(self): - return self._request(method='GET', uri=self.__MARKET_ACTIVITY_URL, auth=False) - - def get_single_market_activity(self, market): - params = {'market': market} - return self._request(method='GET', uri=self.__SINGLE_MARKET_ACTIVITY_URL, params=params, auth=False) - - def get_symbols(self): - return self._request(method='GET', uri=self.__SYMBOLS_URL, auth=False) - - def get_kline(self, market: str, start: str = None, end: str = None, interval: str = None, limit: str = None): - params = {'market': market} - if start is not None: - params['start'] = start - if end is not None: - params['end'] = end - if interval is not None: - params['interval'] = interval - if limit is not None: - params['limit'] = limit - - return self._request(method='GET', uri=self.__KLINE_URL, params=params, auth=False) - - def get_trading_fee(self): - return self._request(method='GET', uri=self.__TRADING_FEE_URL, auth=False) - - def get_fee_list(self): - return self._request(method='GET', uri=self.__FEE_LIST_URL, auth=False) - - def get_order_book(self, market, limit: str = None, level: str = None): - params = {} - if limit is not None: - params['limit'] = limit - if level is not None: - params['level'] = level - return self._request(method='GET', uri=self.__ORDERBOOK_URL + market, params=params, auth=False) - - def get_depth(self, market): - return self._request(method='GET', uri=self.__DEPTH_URL + market, auth=False) - - def get_trade_history(self, market, last_id, limit: str = None): - params = {'market': market, 'lastId': last_id} - if limit is not None: - params['limit'] = limit - return self._request(method='GET', uri=self.__TRADE_HISTORY_URL, params=params, auth=False) - - def get_deals(self, market): - return self._request(method='GET', uri=self.__DEALS_URL + market, auth=False) - - def get_assets(self): - return self._request(method='GET', uri=self.__ASSETS_URL, auth=False) diff --git a/whitebit/trade/order/__init__.py b/whitebit/trade/order/__init__.py deleted file mode 100644 index cee96eb..0000000 --- a/whitebit/trade/order/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .order import TradeOrderClient diff --git a/whitebit/trade/order/order.py b/whitebit/trade/order/order.py deleted file mode 100644 index 5476538..0000000 --- a/whitebit/trade/order/order.py +++ /dev/null @@ -1,108 +0,0 @@ -import whitebit -from whitebit.client import Whitebit - - -class TradeOrderClient(Whitebit): - __ORDER_CANCEL_URL = "/api/v4/order/cancel" - __LIMIT_URL = "/api/v4/order/new" - __MARKET_URL = "/api/v4/order/market" - __MARKET_STOCK_URL = "/api/v4/order/stock_market" - __STOP_MARKET_URL = "/api/v4/order/stop_market" - __STOP_LIMIT_URL = "/api/v4/order/stop_limit" - __BULK_URL = "/api/v4/order/bulk" - __KILL_SWITCH_TIMER = "/api/v4/order/kill-switch" - __KILL_SWITCH_STATUS = "/api/v4/order/kill-switch/status" - - ORDER_TYPE_SPOT = "spot" - ORDER_TYPE_MARGIN = "margin" - ORDER_TYPE_FUTURES = "futures" - - def put_market(self, market: str, side: str, amount, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__MARKET_URL, params=params, auth=True) - - def put_market_stock(self, market: str, side: str, amount, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__MARKET_STOCK_URL, params=params, auth=True) - - def put_limit(self, market: str, side: str, amount, price, post_only: bool = False, ioc: bool = False, - client_order_id: str = ""): - params = self.build_limit_order(market, side, amount, price, post_only, ioc, client_order_id) - return self._request(method='POST', uri=self.__LIMIT_URL, params=params, auth=True) - - def put_stop_limit(self, market: str, side: str, amount, price, activation_price, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "price": price, - "activation_price": activation_price, - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__STOP_LIMIT_URL, params=params, auth=True) - - def put_stop_market(self, market: str, side: str, amount, activation_price, client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "activation_price": activation_price, - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return self._request(method='POST', uri=self.__STOP_MARKET_URL, params=params, auth=True) - - def cancel_order(self, market: str, order_id): - params = { - "market": market, - "orderId": order_id, - } - return self._request(method='POST', uri=self.__ORDER_CANCEL_URL, params=params, auth=True) - - def limit_bulk(self, orders: list): - params = { - "orders": orders - } - return self._request(method='POST', uri=self.__BULK_URL, params=params, auth=True) - - def build_limit_order(self, market: str, side: str, amount, price, post_only: bool = False, ioc: bool = False, - client_order_id: str = ""): - params = { - "market": market, - "side": side, - "amount": amount, - "price": price, - "postOnly": post_only, - "ioc": ioc - } - if client_order_id != "": - params['clientOrderId'] = client_order_id - return params - - def put_kill_switch(self, market: str, timeout: str, types: list = None): - params = { - "market": market, - "timeout": timeout, - } - if types: - params['types'] = types - return self._request(method='POST', uri=self.__KILL_SWITCH_TIMER, params=params, auth=True) - - def get_kill_switch_status(self, market: str = ""): - params = {} - if market != "": - params['market'] = market - return self._request(method='POST', uri=self.__KILL_SWITCH_STATUS, params=params, auth=True) From e78830f14027b77e2596b5d5548a696e078a2712 Mon Sep 17 00:00:00 2001 From: yevheniilavro Date: Thu, 19 Mar 2026 16:30:40 +0100 Subject: [PATCH 3/4] added notes about autogeneration --- CONTRIBUTING.md | 100 ++++++++++++++++++++++++------------------------ README.md | 2 + 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5c52b8..d2b3332 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,97 +1,95 @@ # Contributing to WhiteBit Python SDK -Thank you for your interest in contributing! This document explains how to report bugs, suggest features, and submit code changes. +Thank you for your interest in contributing! --- -## Table of Contents +## Important: Auto-generated Core -- [Reporting Bugs](#reporting-bugs) -- [Suggesting Features](#suggesting-features) -- [Development Setup](#development-setup) -- [Running Tests](#running-tests) -- [Submitting a Pull Request](#submitting-a-pull-request) -- [Code Style](#code-style) +The core SDK (`src/whitebit/`) is **automatically generated** from the WhiteBit API definition using [Fern](https://buildwithfern.com). This means: + +- **Do not open PRs that modify files inside `src/whitebit/`** — changes will be overwritten on the next generation run. +- To request a new endpoint or fix an incorrect API definition, open an issue instead (see [Reporting Bugs](#reporting-bugs) and [Requesting Features](#requesting-features)). + +--- + +## What you CAN contribute + +| Area | How | +|---|---| +| **Bug reports** | Open an issue with reproduction steps | +| **Feature requests** | Open an issue describing the use case | +| **Examples** | Add or improve files in `examples/` | +| **Tests** | Add test cases in `tests/` | +| **Use cases & guides** | Real-world usage patterns, strategies, bots | +| **Documentation** | Fixes and improvements to `README.md` | --- ## Reporting Bugs -Before opening an issue, please search existing issues to avoid duplicates. +Before opening an issue, search existing ones to avoid duplicates. -When reporting a bug, include: +Include: - Python version (`python --version`) -- SDK version (`pip show python-whitebit-sdk`) +- SDK version (`pip show whitebit-python-sdk`) - Minimal code snippet that reproduces the problem - Full error traceback - Expected vs actual behaviour --- -## Suggesting Features +## Requesting Features Open an issue with the label **enhancement** and describe: - The use case you are trying to solve -- The API endpoint involved (link to the [WhiteBit API docs](https://whitebit-exchange.github.io/api-docs/) if applicable) +- The API endpoint involved (link to the [WhiteBit API docs](https://docs.whitebit.com/) if applicable) - Any alternative approaches you considered --- -## Development Setup - -1. **Fork** the repository and clone your fork: - - ```bash - git clone https://github.com/YOUR_USERNAME/python-sdk.git - cd python-sdk - ``` +## Contributing Examples and Tests -2. Create a virtual environment and install dependencies: +Examples and tests are the best place to contribute code directly. - ```bash - python -m venv .venv - source .venv/bin/activate # Windows: .venv\Scripts\activate - pip install -r requirements.txt - pip install -e . - ``` +**Examples** live in `examples/` — feel free to add: +- New usage patterns for existing clients +- Real-world trading strategies or bots +- Integration patterns (e.g. asyncio, frameworks) -3. Create a feature branch: - - ```bash - git checkout -b feature/my-feature - ``` - ---- - -## Running Tests +**Tests** live in `tests/test.py`. All tests use the `responses` library to mock HTTP calls — no real API credentials are needed. ```bash +# Setup +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +pip install -e . + +# Run tests python -m pytest tests/ -v ``` -All tests use the `responses` library to mock HTTP calls — no real API credentials are required. - -When adding a new endpoint or fixing a bug, add or update the corresponding test in `tests/test.py`. +When adding a test: +1. Mock the HTTP call with `@responses.activate` +2. Cover both a success case and an error/missing-credentials case +3. Assert the request URL, method, body, and response --- ## Submitting a Pull Request -1. Make sure all existing tests pass before submitting. -2. Add tests for any new functionality. -3. Keep commits focused — one logical change per commit. -4. Write a clear PR description explaining **what** changed and **why**. -5. Reference any related issue in the PR description (e.g. `Closes #42`). - -Once submitted, your PR will be reviewed. Feedback may be requested before merging. +1. Fork the repo and create a branch: `git checkout -b feature/my-feature` +2. Make sure all existing tests pass +3. Keep commits focused — one logical change per commit +4. Reference any related issue in the PR description (e.g. `Closes #42`) --- ## Code Style -- Follow [PEP 8](https://peps.python.org/pep-0008/). -- Use type hints where possible. -- Keep method signatures consistent with existing clients (see `whitebit/trade/` for reference). -- Do not commit API keys, secrets, or any credentials. +- Follow [PEP 8](https://peps.python.org/pep-0008/) +- Use type hints where possible +- Do not commit API keys, secrets, or any credentials diff --git a/README.md b/README.md index b6c83b5..8662371 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Get your API key from your [WhiteBit account settings](https://whitebit.com/sett --- +> **Note:** The SDK client (`WhitebitApi` / `AsyncWhitebitApi`) is auto-generated from the WhiteBit API definition using [Fern](https://buildwithfern.com). To report issues or request new endpoints, open an issue — see [Contributing](CONTRIBUTING.md). + ## Clients The SDK exposes two top-level clients — `WhitebitApi` (sync) and `AsyncWhitebitApi` (async). Both share the same sub-client structure: From 68543ef8a6a4d177d5d999a9ed34ec1f72b1505c Mon Sep 17 00:00:00 2001 From: yevheniilavro Date: Fri, 20 Mar 2026 10:32:03 +0100 Subject: [PATCH 4/4] changed to official --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8662371..fdf3943 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI version](https://badge.fury.io/py/whitebit-python-sdk.svg)](https://badge.fury.io/py/whitebit-python-sdk) [![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/downloads/) -An unofficial Python SDK for the [WhiteBit](https://www.whitebit.com) cryptocurrency exchange API. +An official Python SDK for the [WhiteBit](https://www.whitebit.com) cryptocurrency exchange API. > Please read the [WhiteBit API documentation](https://docs.whitebit.com/) before using this SDK.