diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d3c415a6..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..0f777f4e --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# OKX API Credentials +# Copy this file to .env and fill in your actual credentials +# NEVER commit .env to version control! + +OKX_API_KEY=your_api_key_here +OKX_API_SECRET=your_api_secret_here +OKX_PASSPHRASE=your_passphrase_here + +# Optional: Set to '0' for live trading, '1' for demo trading +OKX_FLAG=1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..bd4ca7fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,173 @@ +# GitHub Actions CI/CD Configuration for python-okx +name: CI + +on: + push: + branches: + - master + - 'release/*' + - 'releases/*' + pull_request: + branches: + - master + - 'release/*' + - 'releases/*' + +jobs: + # ============================================ + # DEPENDENCY CHECK JOB + # Ensures all imports are satisfied by requirements.txt + # ============================================ + dependency-check: + name: Dependency Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install only from requirements.txt (clean environment) + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Verify all production imports work + run: | + # This catches missing dependencies in requirements.txt + python -c " + import okx + from okx import Account, Trade, Funding, MarketData, PublicData + from okx import SubAccount, Convert, BlockTrading, CopyTrading + from okx import SpreadTrading, Grid, TradingData, Status + from okx.websocket import WsPublicAsync, WsPrivateAsync + print('✅ All imports successful') + print(f' okx version: {okx.__version__}') + " + + - name: Verify test imports + run: | + pip install pytest + python -c " + import pytest + import unittest + print('✅ Test imports successful') + " + + # ============================================ + # LINT JOB + # ============================================ + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run ruff + run: | + ruff check okx/ --ignore=E501 + continue-on-error: true # Set to false once codebase is cleaned up + + # ============================================ + # TEST JOB + # ============================================ + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + needs: [dependency-check] # Only run tests if dependencies are valid + strategy: + fail-fast: true + max-parallel: 1 + matrix: + python-version: ["3.9", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + + - name: Run tests + run: | + python -m pytest test/unit/ -v --cov=okx --cov-report=term-missing --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + + # ============================================ + # BUILD JOB + # ============================================ + build: + name: Build Package + runs-on: ubuntu-latest + needs: [test] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build package + run: python -m build --no-isolation + + - name: Check package + run: twine check dist/* + + - name: Test install from wheel (clean environment) + run: | + # Create a fresh venv and install the built wheel + python -m venv /tmp/test-install + /tmp/test-install/bin/pip install dist/*.whl + /tmp/test-install/bin/python -c " + import okx + from okx import Account, Trade, Funding + print(f'✅ Package installs correctly: okx {okx.__version__}') + " + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index c45a7445..41c5c7c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ build/ dist/ python_okx.egg-info/ +*.DS_Store +**/__pycache__ +**/**/__pycache__ ### STS ### .apt_generated @@ -29,4 +32,9 @@ build/ ### VS Code ### .vscode/ -setup.py \ No newline at end of file +id_rsa* + +# Environment files +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..23682e95 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include requirements.txt +include README.md + diff --git a/README.md b/README.md index e67d4b95..3e2d5b07 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,124 @@ -[TOC] +### Overview +This is an unofficial Python wrapper for the [OKX exchange v5 API](https://www.okx.com/okx-api) -### 如何使用? +If you came here looking to purchase cryptocurrencies from the OKX exchange, please go [here](https://www.okx.com/). -`python版本:>=3.9 +#### Source code +https://github.com/okxapi/python-okx +#### API trading tutorials +- Spot trading: https://www.okx.com/help/how-can-i-do-spot-trading-with-the-jupyter-notebook +- Derivative trading: https://www.okx.com/help/how-can-i-do-derivatives-trading-with-the-jupyter-notebook -`WebSocketAPI:autobahn.twisted>=22.10.0` +Make sure you update often and check the [Changelog](https://www.okx.com/docs-v5/log_en/) for new features and bug fixes. -#### 第一步:下载SDK,安装相关所需库 +### Features +- Implementation of all Rest API endpoints. +- Private and Public Websocket implementation +- Testnet support +- Websocket handling with reconnection and multiplexed connections -1.1 下载`python SDK` +### Quick start +#### Prerequisites -* 将SDK目录`Clone`或者`Download`到本地,选择使用`okx-python-sdk-api-v5`即可 +`python version:>=3.7` -1.2 安装所需库 +#### Step 1: Register an account on OKX and apply for an API key +- Register for an account: https://www.okx.com/account/register +- Apply for an API key: https://www.okx.com/account/users/myApi -```python -pip install requests -pip install autobahn\[twisted\] -pip install pyOpenSSL +#### Step 2: Install python-okx + +```bash +pip install python-okx ``` -#### 第二步:配置个人信息 +### API Credentials -2.1 如果还未有API,可[点击](https://www.okx.com/account/users/myApi)前往官网进行申请 +#### Option 1: Hardcoded credentials ```python -api_key = "" -secret_key = "" -passphrase = "" +from okx import Account + +account = Account.AccountAPI( + api_key="your-api-key-here", + api_secret_key="your-api-secret-here", + passphrase="your-passphrase-here", + flag="1", # 0 = live trading, 1 = demo trading + debug=False +) ``` -#### 第三步:调用接口 - -* RestAPI - - * 运行`example.py` - - * 解开相应方法的注释传参调用各接口即可 - -* WebSocketAPI - * 参考Test文件夹下`WsPrivate`和`WsPublic`文件示例; - * 根据`公共频道`/`私有频道`选择对应`url`(如果是私有频道需要设置登陆信息),传入相应参数即可。 - - ```python - # WebSocket公共频道 - url = "wss://ws.okx.com:8443/ws/v5/public" - # WebSocket私有频道 - url = "wss://ws.okx.com:8443/ws/v5/private" - ``` - - ```python - # 公共频道 不需要登录(行情,持仓总量,K线,标记价格,深度,资金费率等) - 参考 WsPublicTest.py - - # 私有频道 需要登录(账户,持仓,订单等) - 参考 WsPrivateTest.py - ``` - -附言: - -* 如果对API尚不了解,建议参考`OKX`官方[API文档](https://www.okx.com/docs-v5/zh/) - -* 使用RestAPI的用户可以通过参考Test文件夹下的示例,设置正确的参数即可 - -* 使用WebSocketAPI的用户可以通过参考Test文件夹下的`WsPublicTest.py`和`WsPrivateTest.py`,设置正确的参数即可 - -* 若使用`WebSocketAPI`遇到问题建议参考相关链接 - - * `asyncio`、`websockets`文档/`github`: - https://docs.python.org/3/library/asyncio-dev.html - https://websockets.readthedocs.io/en/stable/intro.html - https://github.com/aaugustin/websockets - - * 关于`code=1006`: - https://github.com/Rapptz/discord.py/issues/1996 - https://github.com/aaugustin/websockets/issues/587 +#### Option 2: Using `.env` file (recommended) +Create a `.env` file in your project root: +```bash +OKX_API_KEY=your-api-key-here +OKX_API_SECRET=your-api-secret-here +OKX_PASSPHRASE=your-passphrase-here +OKX_FLAG=1 +``` -### How to use ? - -`python version:>=3.9` +Then load it in your code: -`WebSocketAPI: autobahn.twisted>=22.10.0` +```python +import os +from dotenv import load_dotenv +from okx import Account + +load_dotenv() + +account = Account.AccountAPI( + api_key=os.getenv('OKX_API_KEY'), + api_secret_key=os.getenv('OKX_API_SECRET'), + passphrase=os.getenv('OKX_PASSPHRASE'), + flag=os.getenv('OKX_FLAG', '1'), + debug=False +) +``` -#### Step 1: Download the SDK and install the necessary libraries +### Development Setup -1.1 Download python SDK +For contributors or local development: -- `Clone` or `Download` the SDK directory to your local directory,choose to use `okx-python-sdk-api-v5` +```bash +# Clone the repository +git clone https://github.com/okxapi/python-okx.git +cd python-okx -1.2 Install the necessary libraries +# Install dependencies +pip install -r requirements.txt +pip install -e . -```python -pip install requests -pip install autobahn\[twisted\] -pip install pyOpenSSL +# Run tests +pytest test/unit/ -v ``` -#### Step 2: Configure Personal Information - -2.1 If you have no API,[Click here](https://www.okx.com/account/users/myApi) to the official websit to apply for the API - -2.2 Fill out all necessary informatiuon in `example.py(RestAPI)` and `websocket_example.py(WebSocketAPI)` +#### Step 3: Run examples +- Fill in API credentials in the corresponding examples ```python api_key = "" secret_key = "" passphrase = "" ``` - -#### Step 3: Call API - - RestAPI - - Run `example.py` - - Uncomment the corresponding method and then pass the arguments and call the interfaces + - For spot trading: run example/get_started_en.ipynb + - For derivative trading: run example/trade_derivatives_en.ipynb + - Tweak the value of the parameter `flag` (live trading: 0, demo trading: 1 +) to switch between live and demo trading environment - WebSocketAPI - - Open `websocket_example.py` - - According to the `public channel`/`private channel`, select the corresponding `url`, the corresponding start method, and pass in the corresponding parameters + - Run test/WsPrivateTest.py for private websocket channels + - Run test/WsPublicTest.py for public websocket channels + - Use different URLs for different environment + - Live trading URLs: https://www.okx.com/docs-v5/en/#overview-production-trading-services + - Demo trading URLs: https://www.okx.com/docs-v5/en/#overview-demo-trading-services -```python -# WebSocket public channel -url = "wss://ws.okx.com:8443/ws/v5/public?brokerId=9999" - -# WebSocket private channel -url = "wss://ws.okx.com:8443/ws/v5/private?brokerId=9999" -``` - -P.S. - -- If you know little about API, advise consulting the offical [API document](https://www.okx.com/docs-v5/en/) +Note -- User with RestAPI can configure parameter `flag` in `example.py` in to choose to access to real trading or demo trading +- To learn more about OKX API, visit official [OKX API documentation](https://www.okx.com/docs-v5/en/) -- User with WebSocketAPI can ucomment the corresponding `url` to choose to access to real trading or demo trading - -- Rest API support request by http2, you can refer to http2_example - -- If you face any questions when using `WebSocketAPI`,you can consult related link +- If you face any questions when using `WebSocketAPI`,you can consult the following links - `asyncio`、`websockets` document/`github`: @@ -153,6 +134,3 @@ P.S. https://github.com/Rapptz/discord.py/issues/1996 https://github.com/aaugustin/websockets/issues/587 ``` - - - diff --git a/example/.ipynb_checkpoints/get_started_en-checkpoint.ipynb b/example/.ipynb_checkpoints/get_started_en-checkpoint.ipynb new file mode 100644 index 00000000..706e4096 --- /dev/null +++ b/example/.ipynb_checkpoints/get_started_en-checkpoint.ipynb @@ -0,0 +1,1109 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# Get Started\n", + "## Install python package\n", + "You can install `python-okx` from PyPi server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "! pip install python-okx --upgrade" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Sign up as an OKX user\n", + "Please refer to [Create account](https://www.okx.com/account/register)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Create API Key\n", + "Please refer to [Create API Key](https://www.okx.com/account/my-api)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Import API modules\n", + "The following modules are available\n", + "- Trade\n", + "- BlockTrading\n", + "- Funding\n", + "- Account\n", + "- Convert\n", + "- Earning\n", + "- SubAccount\n", + "- MarketData\n", + "- PublicData\n", + "- TradingData\n", + "- Status\n", + "- NDBroker\n", + "- FDBroker" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Trade as Trade" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Fill in your API key details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "api_key = \"xxxxx\"\n", + "secret_key = \"xxxxx\"\n", + "passphrase = \"xxxxxx\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get available funds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Funding as Funding\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "fundingAPI = Funding.FundingAPI(api_key, secret_key, passphrase, False, flag)\n", + "\n", + "result = fundingAPI.get_currencies()\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get market data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.MarketData as MarketData\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "marketDataAPI = MarketData.MarketAPI(flag=flag)\n", + "\n", + "result = marketDataAPI.get_tickers(instType=\"SPOT\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Handle errors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "You will the error code `51000` when. you run the following code. More details about the error code can be found in `msg`.\n", + "Please refer to [error code](https://www.okx.com/docs-v5/en/#error-code) for addtional information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "import okx.MarketData as MarketData\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "marketDataAPI = MarketData.MarketAPI(flag=flag)\n", + "\n", + "result = marketDataAPI.get_tickers( instType=\"SPOT\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# Prepare for trading\n", + "- Make sure you understand the basic trading rules. Please refer to [Basic Trading Rules](https://www.okx.com/support/hc/en-us/sections/360011507312)\n", + "- Make sure you have enough funds in your trading account。Please refer to [Get balance](https://www.okx.com/docs-v5/en/#rest-api-account-get-balance)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get account balance. Please refer to [Get balance](https://www.okx.com/docs-v5/en/#rest-api-account-get-balance)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "import okx.Account as Account\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "accountAPI = Account.AccountAPI(api_key, secret_key, passphrase, False, flag)\n", + "\n", + "result = accountAPI.get_account_balance()\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get available trading pairs from [Get instruments](https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "import okx.PublicData as PublicData\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "PublicDataAPI = PublicData.PublicAPI(flag=flag)\n", + "\n", + "result = PublicDataAPI.get_instruments(\n", + " instType=\"SPOT\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Make sure you have enough funds to trade a certain pair. Please refer to [Get maximum tradable amount](https://www.okx.com/docs-v5/en/#rest-api-account-get-maximum-available-tradable-amount)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Account as Account\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "accountAPI = Account.AccountAPI(api_key, secret_key, passphrase, False, flag)\n", + "\n", + "result = accountAPI.get_max_avail_size(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Account as Account\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "accountAPI = Account.AccountAPI(api_key, secret_key, passphrase, False, flag)\n", + "\n", + "result = accountAPI.get_max_avail_size(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## In unified account, you can trade Spot under simple, single currency, multi currency and portfolio margin account mode. Please refer to [Introduction on Unified Account](https://www.okx.com/support/hc/en-us/articles/360054690791-1-统一交易账户介绍)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get the current account configuration from the `acctLv` parameter in [Get account configuration](https://www.okx.com/docs-v5/en/#rest-api-account-get-account-configuration)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Account as Account\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "accountAPI = Account.AccountAPI(api_key, secret_key, passphrase, False, flag)\n", + "result = accountAPI.get_account_config()\n", + "print(result)\n", + "\n", + "if result['code'] == \"0\":\n", + " acctLv = result[\"data\"][0][\"acctLv\"]\n", + " if acctLv == \"1\":\n", + " print(\"Simple mode\")\n", + " elif acctLv == \"2\":\n", + " print(\"Single-currency margin mode\")\n", + " elif acctLv == \"3\":\n", + " print(\"Multi-currency margin mode\")\n", + " elif acctLv == \"4\":\n", + " print(\"Portfolio margin mode\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# Start Spot Trading" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### Spot trading under simple/single-currency margin mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import okx.Trade as Trade\n", + "\n", + "flag = \"1\" # live trading: 0, demo trading: 1\n", + "\n", + "tradeAPI = Trade.TradeAPI(api_key, secret_key, passphrase, False, flag)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "#### place a limit order" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# limit order\n", + "result = tradeAPI.place_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"buy\",\n", + " ordType=\"limit\",\n", + " px=\"19000\",\n", + " sz=\"0.01\"\n", + ")\n", + "print(result)\n", + "\n", + "if result[\"code\"] == \"0\":\n", + " print(\"Successful order request,order_id = \",result[\"data\"][0][\"ordId\"])\n", + "else:\n", + " print(\"Unsuccessful order request,error_code = \",result[\"data\"][0][\"sCode\"], \", Error_message = \", result[\"data\"][0][\"sMsg\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "#### place a market order" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# market order\n", + "result = tradeAPI.place_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"buy\",\n", + " ordType=\"market\",\n", + " sz=\"100\",\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "#### place an order with tgtCcy=quote_ccy (only applicable to spot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# market order\n", + "result = tradeAPI.place_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"buy\",\n", + " ordType=\"market\",\n", + " sz=\"100\",\n", + " tgtCcy=\"quote_ccy\" # this determines the unit of the sz parameter. base_ccy is the default value\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "#### place an order with your own clOrdId" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# market order\n", + "result = tradeAPI.place_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"buy\",\n", + " ordType=\"market\",\n", + " sz=\"100\",\n", + " clOrdId=\"003\" # you can define your own client defined order ID\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### Spot trading under multi-currency/porfolio margin mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "# cross-margin spot trading\n", + "result = tradeAPI.place_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cross\",\n", + " side=\"buy\",\n", + " ordType=\"limit\",\n", + " px=\"1000\",\n", + " sz=\"0.01\"\n", + ")\n", + "print(result)\n", + "\n", + "if result[\"code\"] == \"0\":\n", + " print(\"Successful order request,order_id = \",result[\"data\"][0][\"ordId\"])\n", + "else:\n", + " print(\"Unsuccessful order request,error_code = \",result[\"data\"][0][\"sCode\"], \", Error_message = \", result[\"data\"][0][\"sMsg\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### For additional information on the place order endpoint,please refer to [Place order](https://www.okx.com/docs-v5/en/#rest-api-trade-place-order)\n", + "\n", + "### To place orders in a batch, please refer to [Get account configuration](https://www.okx.com/docs-v5/en/#rest-api-trade-place-multiple-orders)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "place_orders = [\n", + " {\"instId\":\"BTC-USDT\", \"tdMode\":\"cash\", \"side\":\"buy\", \"ordType\" : \"limit\",\"px\":\"1000\",\"sz\":\"0.01\"},\n", + " {\"instId\": \"BTC-USDT\", \"tdMode\": \"cash\", \"side\": \"buy\", \"ordType\": \"limit\", \"px\": \"1000\", \"sz\": \"0.02\"}\n", + "]\n", + "\n", + "result = tradeAPI.place_multiple_orders(place_orders)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To amend pending orders,please refer to [Amend order](https://www.okx.com/docs-v5/en/#rest-api-trade-amend-order)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.amend_order(\n", + " instId=\"BTC-USDT\",\n", + " ordId=\"489103565508685824\",\n", + " newSz=\"0.012\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To amend orders in a batch, please refer to [Amend multiple orders](https://www.okx.com/docs-v5/en/#rest-api-trade-amend-multiple-orders)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "amend_orders = [\n", + " {\"instId\": \"BTC-USDT\", \"ordId\": \"489106394289909760\",\"newSz\":\"0.001\"},\n", + " {\"instId\": \"BTC-USDT\", \"ordId\": \"489106394289909761\",\"newSz\":\"0.001\"},\n", + "]\n", + "\n", + "result = tradeAPI.amend_multiple_orders(amend_orders)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To cancel pending orders,please refer to [Cancel order](https://www.okx.com/docs-v5/en/#rest-api-trade-cancel-order)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.cancel_order(instId=\"BTC-USDT\", ordId = \"489093931993509888\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To cancel orders in a batch,please refer to [Cancel multiple orders](https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-multiple-orders)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "cancel_orders = [\n", + " {\"instId\": \"BTC-USDT\", \"ordId\": \"489102222534488064\"},\n", + " {\"instId\": \"BTC-USDT\", \"ordId\": \"489102222534488065\"},\n", + "]\n", + "\n", + "result = tradeAPI.cancel_multiple_orders(cancel_orders)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Get details of a certain order, please refer to [Get order details](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-details)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_order(instId=\"BTC-USDT\", clOrdId=\"002\")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_order(instId=\"BTC-USDT\", ordId=\"497819823594909696\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## To get the list of open orders,please refer to [Get order List](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-list)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_order_list()\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To get past orders,please refer to [Get order history (last 7 days)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-history-last-7-days) and [Get order history (last 3 months)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-history-last-3-months)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_orders_history(\n", + " instType=\"SPOT\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_orders_history_archive(\n", + " instType=\"SPOT\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To get past trades,please refer to [Get transaction details (last 3 days)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-days) and [Get transaction details (last 3 months) ](https://www.okx.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-months)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_fills(\n", + " instType=\"SPOT\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.get_fills_history(\n", + " instType=\"SPOT\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### If you wish to place orders when the price reaches a certain level, you can place an algo order" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.place_algo_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"buy\", # buy\n", + " ordType=\"trigger\", # order type\n", + " sz=\"100\", # order amount: 100USDT\n", + " triggerPx=\"10000\", # trigger price\n", + " orderPx=\"-1\", # order price. When orderPx=-1, the order will be placed as an market order\n", + " triggerPxType=\"last\" # trigger price type。last:last trade price\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## You can also use Stop Loss or Take Profit order to sell the currencies in your account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.place_algo_order(\n", + " instId=\"BTC-USDT\",\n", + " tdMode=\"cash\",\n", + " side=\"sell\", # sell\n", + " ordType=\"conditional\", # one-way take profit or stop loss\n", + " sz=\"0.01\", # order amount: 0.01BTC\n", + " tpTriggerPx=\"30000\", # take profit trigger price\n", + " tpOrdPx=\"-1\", # taker profit order price。When it is set to -1,the order will be placed as an market order\n", + " tpTriggerPxType=\"last\" # take profit trigger price type。last:last trade price\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### For additional information, please refer to [Place algo order](https://www.okx.com/docs-v5/en/#rest-api-trade-place-algo-order)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### Cancel pending algo orders (not including Iceberg order, TWAP order, Trailing Stop order),please refer to [Cancel algo order](https://www.okx.com/docs-v5/en/#rest-api-trade-cancel-algo-order)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "algo_orders = [\n", + " {\"instId\": \"BTC-USDT\", \"algoId\": \"495001187587043328\"},\n", + "]\n", + "\n", + "result = tradeAPI.cancel_algo_order(algo_orders)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To get list of currently pending algo orders,please refer to [Get algo order list](https://www.okx.com/docs-v5/en/#rest-api-trade-get-algo-order-list)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.order_algos_list(\n", + " ordType=\"trigger\" # order type\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "### To get the past algo orders (last three months),please refer to [Get algo order history](https://www.okx.com/docs-v5/en/#rest-api-trade-get-algo-order-history)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "result = tradeAPI.order_algos_history(\n", + " ordType=\"conditional\", # order type\n", + " state=\"canceled\" # state of the orders\n", + ")\n", + "print(result)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + }, + "vscode": { + "interpreter": { + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/example/get_started_en.ipynb b/example/get_started_en.ipynb index de8db080..d75dcb65 100644 --- a/example/get_started_en.ipynb +++ b/example/get_started_en.ipynb @@ -2,56 +2,29 @@ "cells": [ { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, "source": [ "# Get Started\n", "## Install python package\n", "You can install `python-okx` from PyPi server." - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: python-okx in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (0.0.12)\n", - "Requirement already satisfied: h2 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from python-okx) (4.1.0)\n", - "Requirement already satisfied: httpx in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from python-okx) (0.23.0)\n", - "Requirement already satisfied: hyperframe<7,>=6.0 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from h2->python-okx) (6.0.1)\n", - "Requirement already satisfied: hpack<5,>=4.0 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from h2->python-okx) (4.0.0)\n", - "Requirement already satisfied: rfc3986[idna2008]<2,>=1.3 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpx->python-okx) (1.5.0)\n", - "Requirement already satisfied: certifi in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpx->python-okx) (2021.10.8)\n", - "Requirement already satisfied: httpcore<0.16.0,>=0.15.0 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpx->python-okx) (0.15.0)\n", - "Requirement already satisfied: sniffio in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpx->python-okx) (1.2.0)\n", - "Requirement already satisfied: h11<0.13,>=0.11 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpcore<0.16.0,>=0.15.0->httpx->python-okx) (0.12.0)\n", - "Requirement already satisfied: anyio==3.* in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from httpcore<0.16.0,>=0.15.0->httpx->python-okx) (3.5.0)\n", - "Requirement already satisfied: idna>=2.8 in /Users/skylerfeng/opt/anaconda3/lib/python3.9/site-packages (from anyio==3.*->httpcore<0.16.0,>=0.15.0->httpx->python-okx) (3.3)\n" - ] - } - ], + "execution_count": null, + "outputs": [], "source": [ "! pip install python-okx --upgrade" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Sign up as an OKX user\n", "Please refer to [Create account](https://www.okx.com/account/register)" @@ -59,11 +32,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Create API Key\n", "Please refer to [Create API Key](https://www.okx.com/account/my-api)" @@ -71,11 +40,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Import API modules\n", "The following modules are available\n", @@ -96,36 +61,20 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "pycharm": { - "name": "#%%\n" + "is_executing": true } }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'okx'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn [2], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mokx\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mTrade\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mTrade\u001b[39;00m\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'okx'" - ] - } - ], + "outputs": [], "source": [ "import okx.Trade as Trade" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Fill in your API key details" ] @@ -133,11 +82,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "api_key = \"xxxxx\"\n", @@ -147,11 +92,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get available funds" ] @@ -161,7 +102,7 @@ "execution_count": null, "metadata": { "pycharm": { - "name": "#%%\n" + "is_executing": true } }, "outputs": [], @@ -178,11 +119,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get market data" ] @@ -192,7 +129,7 @@ "execution_count": null, "metadata": { "pycharm": { - "name": "#%%\n" + "is_executing": true } }, "outputs": [], @@ -209,22 +146,14 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Handle errors" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "You will the error code `51000` when. you run the following code. More details about the error code can be found in `msg`.\n", "Please refer to [error code](https://www.okx.com/docs-v5/en/#error-code) for addtional information." @@ -234,10 +163,10 @@ "cell_type": "code", "execution_count": null, "metadata": { + "scrolled": true, "pycharm": { - "name": "#%%\n" - }, - "scrolled": true + "is_executing": true + } }, "outputs": [], "source": [ @@ -247,17 +176,13 @@ "\n", "marketDataAPI = MarketData.MarketAPI(flag=flag)\n", "\n", - "result = marketDataAPI.get_tickers( instType=\"SPO\")\n", + "result = marketDataAPI.get_tickers( instType=\"SPOT\")\n", "print(result)" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "# Prepare for trading\n", "- Make sure you understand the basic trading rules. Please refer to [Basic Trading Rules](https://www.okx.com/support/hc/en-us/sections/360011507312)\n", @@ -266,11 +191,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get account balance. Please refer to [Get balance](https://www.okx.com/docs-v5/en/#rest-api-account-get-balance)." ] @@ -279,9 +200,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "pycharm": { - "name": "#%%\n" - }, "scrolled": true }, "outputs": [], @@ -297,11 +215,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get available trading pairs from [Get instruments](https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments)." ] @@ -310,20 +224,17 @@ "cell_type": "code", "execution_count": null, "metadata": { - "pycharm": { - "name": "#%%\n" - }, "scrolled": true }, "outputs": [], "source": [ - "import okx.MarketData as MarketData\n", + "import okx.PublicData as PublicData\n", "\n", "flag = \"1\" # live trading: 0, demo trading: 1\n", "\n", - "marketDataAPI = MarketData.MarketAPI(flag=flag)\n", + "PublicDataAPI = PublicData.PublicAPI(flag=flag)\n", "\n", - "result = marketDataAPI.get_instruments(\n", + "result = PublicDataAPI.get_instruments(\n", " instType=\"SPOT\"\n", ")\n", "print(result)" @@ -331,11 +242,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Make sure you have enough funds to trade a certain pair. Please refer to [Get maximum tradable amount](https://www.okx.com/docs-v5/en/#rest-api-account-get-maximum-available-tradable-amount)" ] @@ -343,11 +250,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import okx.Account as Account\n", @@ -366,11 +269,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import okx.Account as Account\n", @@ -388,22 +287,14 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## In unified account, you can trade Spot under simple, single currency, multi currency and portfolio margin account mode. Please refer to [Introduction on Unified Account](https://www.okx.com/support/hc/en-us/articles/360054690791-1-统一交易账户介绍)" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get the current account configuration from the `acctLv` parameter in [Get account configuration](https://www.okx.com/docs-v5/en/#rest-api-account-get-account-configuration)." ] @@ -411,11 +302,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import okx.Account as Account\n", @@ -440,22 +327,14 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "# Start Spot Trading" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### Spot trading under simple/single-currency margin mode" ] @@ -463,11 +342,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import okx.Trade as Trade\n", @@ -479,11 +354,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "#### place a limit order" ] @@ -491,11 +362,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# limit order\n", @@ -517,11 +384,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "#### place a market order" ] @@ -529,11 +392,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# market order\n", @@ -549,11 +408,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "#### place an order with tgtCcy=quote_ccy (only applicable to spot)" ] @@ -561,11 +416,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# market order\n", @@ -582,11 +433,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "#### place an order with your own clOrdId" ] @@ -594,11 +441,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# market order\n", @@ -615,11 +458,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### Spot trading under multi-currency/porfolio margin mode" ] @@ -627,11 +466,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "# cross-margin spot trading\n", @@ -653,11 +488,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### For additional information on the place order endpoint,please refer to [Place order](https://www.okx.com/docs-v5/en/#rest-api-trade-place-order)\n", "\n", @@ -667,11 +498,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "place_orders = [\n", @@ -685,11 +512,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To amend pending orders,please refer to [Amend order](https://www.okx.com/docs-v5/en/#rest-api-trade-amend-order)" ] @@ -697,11 +520,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.amend_order(\n", @@ -714,11 +533,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To amend orders in a batch, please refer to [Amend multiple orders](https://www.okx.com/docs-v5/en/#rest-api-trade-amend-multiple-orders)" ] @@ -726,11 +541,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "amend_orders = [\n", @@ -744,11 +555,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To cancel pending orders,please refer to [Cancel order](https://www.okx.com/docs-v5/en/#rest-api-trade-cancel-order)" ] @@ -756,11 +563,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.cancel_order(instId=\"BTC-USDT\", ordId = \"489093931993509888\")\n", @@ -769,11 +572,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To cancel orders in a batch,please refer to [Cancel multiple orders](https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-multiple-orders)" ] @@ -781,11 +580,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "cancel_orders = [\n", @@ -799,11 +594,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## Get details of a certain order, please refer to [Get order details](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-details)" ] @@ -811,11 +602,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_order(instId=\"BTC-USDT\", clOrdId=\"002\")\n", @@ -825,11 +612,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_order(instId=\"BTC-USDT\", ordId=\"497819823594909696\")\n", @@ -838,11 +621,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## To get the list of open orders,please refer to [Get order List](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-list)" ] @@ -850,11 +629,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_order_list()\n", @@ -863,11 +638,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To get past orders,please refer to [Get order history (last 7 days)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-history-last-7-days) and [Get order history (last 3 months)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-order-history-last-3-months)" ] @@ -875,11 +646,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_orders_history(\n", @@ -891,11 +658,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_orders_history_archive(\n", @@ -906,11 +669,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To get past trades,please refer to [Get transaction details (last 3 days)](https://www.okx.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-days) and [Get transaction details (last 3 months) ](https://www.okx.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-months)" ] @@ -918,11 +677,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_fills(\n", @@ -934,11 +689,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.get_fills_history(\n", @@ -949,11 +700,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### If you wish to place orders when the price reaches a certain level, you can place an algo order" ] @@ -961,11 +708,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.place_algo_order(\n", @@ -983,11 +726,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "## You can also use Stop Loss or Take Profit order to sell the currencies in your account" ] @@ -995,11 +734,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.place_algo_order(\n", @@ -1017,22 +752,14 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### For additional information, please refer to [Place algo order](https://www.okx.com/docs-v5/en/#rest-api-trade-place-algo-order)" ] }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### Cancel pending algo orders (not including Iceberg order, TWAP order, Trailing Stop order),please refer to [Cancel algo order](https://www.okx.com/docs-v5/en/#rest-api-trade-cancel-algo-order)" ] @@ -1040,11 +767,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "algo_orders = [\n", @@ -1057,11 +780,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To get list of currently pending algo orders,please refer to [Get algo order list](https://www.okx.com/docs-v5/en/#rest-api-trade-get-algo-order-list)" ] @@ -1069,11 +788,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.order_algos_list(\n", @@ -1084,11 +799,7 @@ }, { "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, + "metadata": {}, "source": [ "### To get the past algo orders (last three months),please refer to [Get algo order history](https://www.okx.com/docs-v5/en/#rest-api-trade-get-algo-order-history)" ] @@ -1096,11 +807,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "result = tradeAPI.order_algos_history(\n", @@ -1113,7 +820,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.8.9 64-bit", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1127,7 +834,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.9" + "version": "3.9.12" }, "vscode": { "interpreter": { diff --git a/example/trade_derivatives_en.ipynb b/example/trade_derivatives_en.ipynb index bb43a24d..db5438a0 100644 --- a/example/trade_derivatives_en.ipynb +++ b/example/trade_derivatives_en.ipynb @@ -46,25 +46,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "pycharm": { "name": "#%%\n" } }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'okx'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn [1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mokx\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mTrade\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mTrade\u001b[39;00m\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'okx'" - ] - } - ], + "outputs": [], "source": [ "import okx.Trade as Trade" ] diff --git a/http2_example.py b/http2_example.py deleted file mode 100644 index 7663d235..00000000 --- a/http2_example.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -import time - -import okx.Account as Account - - -async def http2_request(request, parameters): - while 1: - begin = time.time() - if type(parameters) is list: - result = request(*parameters) - else: - result = request(**parameters) - - end = time.time() - cost = end - begin - print(f'request_cost:{cost}\nresponse_body:{json.dumps(result)}') - - -api_key = "" -secret_key = "" -passphrase = "" -# flag是实盘与模拟盘的切换参数 flag is the key parameter which can help you to change between demo and real trading. -# flag = '1' # 模拟盘 demo trading -flag = '0' # 实盘 real tradiang - -if __name__ == '__main__': - # account api - accountAPI = Account.AccountAPI(api_key, secret_key, passphrase, False, flag) - accountAPI.get_account_config() - accountAPI.get_greeks('BTC') diff --git a/okx/Account.py b/okx/Account.py index 2a087c89..5fddf9f7 100644 --- a/okx/Account.py +++ b/okx/Account.py @@ -1,12 +1,12 @@ -from .client import Client from .consts import * +from .okxclient import OkxClient -class AccountAPI(Client): - - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class AccountAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', + domain='https://www.okx.com', debug=False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) # Get Positions def get_position_risk(self, instType=''): @@ -23,22 +23,42 @@ def get_account_balance(self, ccy=''): return self._request_with_params(GET, ACCOUNT_INFO, params) # Get Positions - def get_positions(self, instType='', instId=''): - params = {'instType': instType, 'instId': instId} + def get_positions(self, instType='', instId='', posId=''): + params = {'instType': instType, 'instId': instId, 'posId': posId} return self._request_with_params(GET, POSITION_INFO, params) + def position_builder(self, acctLv=None, inclRealPosAndEq=None, lever=None, greeksType=None, simPos=None, + simAsset=None, idxVol=None): + params = {} + if acctLv is not None: + params['acctLv'] = acctLv + if inclRealPosAndEq is not None: + params['inclRealPosAndEq'] = inclRealPosAndEq + if lever is not None: + params['lever'] = lever + if greeksType is not None: + params['greeksType'] = greeksType + if simPos is not None: + params['simPos'] = simPos + if simAsset is not None: + params['simAsset'] = simAsset + if idxVol is not None: + params['idxVol'] = idxVol + return self._request_with_params(POST, POSITION_BUILDER, params) + # Get Bills Details (recent 7 days) def get_account_bills(self, instType='', ccy='', mgnMode='', ctType='', type='', subType='', after='', before='', - limit=''): + limit=''): params = {'instType': instType, 'ccy': ccy, 'mgnMode': mgnMode, 'ctType': ctType, 'type': type, 'subType': subType, 'after': after, 'before': before, 'limit': limit} return self._request_with_params(GET, BILLS_DETAIL, params) # Get Bills Details (recent 3 months) - def get_account_bills_archive(self, instType='', ccy='', mgnMode='', ctType='', type='', subType='', after='', before='', - limit=''): + def get_account_bills_archive(self, instType='', ccy='', mgnMode='', ctType='', type='', subType='', after='', + before='', + limit='', begin='', end=''): params = {'instType': instType, 'ccy': ccy, 'mgnMode': mgnMode, 'ctType': ctType, 'type': type, - 'subType': subType, 'after': after, 'before': before, 'limit': limit} + 'subType': subType, 'after': after, 'before': before, 'limit': limit, 'begin': begin, 'end': end} return self._request_with_params(GET, BILLS_ARCHIVE, params) # Get Account Configuration @@ -56,33 +76,45 @@ def set_leverage(self, lever, mgnMode, instId='', ccy='', posSide=''): return self._request_with_params(POST, SET_LEVERAGE, params) # Get Maximum Tradable Size For Instrument - def get_max_order_size(self, instId, tdMode, ccy='', px=''): + def get_max_order_size(self, instId, tdMode, ccy='', px='', tradeQuoteCcy=None): params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'px': px} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy return self._request_with_params(GET, MAX_TRADE_SIZE, params) # Get Maximum Available Tradable Amount - def get_max_avail_size(self, instId, tdMode, ccy='', reduceOnly=''): - params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'reduceOnly': reduceOnly} + def get_max_avail_size(self, instId, tdMode, ccy='', reduceOnly='', unSpotOffset='', quickMgnType='', tradeQuoteCcy=None): + params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'reduceOnly': reduceOnly, + 'unSpotOffset': unSpotOffset, 'quickMgnType': quickMgnType} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy return self._request_with_params(GET, MAX_AVAIL_SIZE, params) # Increase / Decrease margin - def adjustment_margin(self, instId, posSide, type, amt,loanTrans=''): - params = {'instId': instId, 'posSide': posSide, 'type': type, 'amt': amt,'loanTrans':loanTrans} + def adjustment_margin(self, instId, posSide, type, amt, loanTrans=''): + params = {'instId': instId, 'posSide': posSide, 'type': type, 'amt': amt, 'loanTrans': loanTrans} return self._request_with_params(POST, ADJUSTMENT_MARGIN, params) # Get Leverage - def get_leverage(self, instId, mgnMode): - params = {'instId': instId, 'mgnMode': mgnMode} + def get_leverage(self, mgnMode, ccy='', instId=''): + params = {'instId': instId, 'mgnMode': mgnMode, 'ccy': ccy} return self._request_with_params(GET, GET_LEVERAGE, params) + # Get instruments + def get_instruments(self, instType='', ugly='', instFamily='', instId=''): + params = {'instType': instType, 'ugly': ugly, 'instFamily': instFamily, 'instId': instId} + return self._request_with_params(GET, GET_INSTRUMENTS, params) + # Get the maximum loan of isolated MARGIN - def get_max_loan(self, instId, mgnMode, mgnCcy): + def get_max_loan(self, instId, mgnMode, mgnCcy='', tradeQuoteCcy=None): params = {'instId': instId, 'mgnMode': mgnMode, 'mgnCcy': mgnCcy} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy return self._request_with_params(GET, MAX_LOAN, params) # Get Fee Rates - def get_fee_rates(self, instType, instId='', uly='', category='',instFamily = ''): - params = {'instType': instType, 'instId': instId, 'uly': uly, 'category': category,'instFamily':instFamily} + def get_fee_rates(self, instType, instId='', uly='', category='', instFamily=''): + params = {'instType': instType, 'instId': instId, 'uly': uly, 'category': category, 'instFamily': instFamily} return self._request_with_params(GET, FEE_RATES, params) # Get interest-accrued @@ -101,8 +133,8 @@ def set_greeks(self, greeksType): return self._request_with_params(POST, SET_GREEKS, params) # Set Isolated Mode - def set_isolated_mode(self, isoMode,type): - params = {'isoMode': isoMode, 'type':type} + def set_isolated_mode(self, isoMode, type): + params = {'isoMode': isoMode, 'type': type} return self._request_with_params(POST, ISOLATED_MODE, params) # Get Maximum Withdrawals @@ -111,23 +143,23 @@ def get_max_withdrawal(self, ccy=''): return self._request_with_params(GET, MAX_WITHDRAWAL, params) # Get borrow repay - def borrow_repay(self, ccy='', side='', amt=''): - params = {'ccy': ccy, 'side': side, 'amt': amt} + def borrow_repay(self, ccy='', side='', amt='', ordId=''): + params = {'ccy': ccy, 'side': side, 'amt': amt, 'ordId': ordId} return self._request_with_params(POST, BORROW_REPAY, params) # Get borrow repay history def get_borrow_repay_history(self, ccy='', after='', before='', limit=''): - params = {'ccy': ccy, 'after': after, 'before': before, 'limit':limit} + params = {'ccy': ccy, 'after': after, 'before': before, 'limit': limit} return self._request_with_params(GET, BORROW_REPAY_HISTORY, params) # Get Obtain borrowing rate and limit - def get_interest_limits(self, type='',ccy=''): + def get_interest_limits(self, type='', ccy=''): params = {'type': type, 'ccy': ccy} return self._request_with_params(GET, INTEREST_LIMITS, params) # Get Simulated Margin - def get_simulated_margin(self, instType ='',inclRealPos=True,instId='',pos=''): - params = {'instType': instType, 'inclRealPos': inclRealPos,'instId': instId,'pos': pos,} + def get_simulated_margin(self, instType='', inclRealPos=True, spotOffsetType='', simPos=[]): + params = {'instType': instType, 'inclRealPos': inclRealPos, 'spotOffsetType': spotOffsetType, 'simPos': simPos} return self._request_with_params(POST, SIMULATED_MARGIN, params) # Get Greeks @@ -135,32 +167,173 @@ def get_greeks(self, ccy=''): params = {'ccy': ccy} return self._request_with_params(GET, GREEKS, params) - #GET /api/v5/account/risk-state + # GET /api/v5/account/risk-state def get_account_position_risk(self): return self._request_without_params(GET, ACCOUNT_RISK) - #GET /api/v5/account/positions-history - def get_positions_history(self,instType = '', instId = '',mgnMode = '',type = '',posId = '',after = '',before ='',limit = ''): + # GET /api/v5/account/positions-history + def get_positions_history(self, instType='', instId='', mgnMode='', type='', posId='', after='', before='', + limit=''): + params = { + 'instType': instType, + 'instId': instId, + 'mgnMode': mgnMode, + 'type': type, + 'posId': posId, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, POSITIONS_HISTORY, params) + + # GET /api/v5/account/position-tiers + def get_account_position_tiers(self, instType='', uly='', instFamily=''): + params = { + 'instType': instType, + 'uly': uly, + 'instFamily': instFamily + } + return self._request_with_params(GET, GET_PM_LIMIT, params) + + # - Get VIP interest accrued data + def get_VIP_interest_accrued_data(self, ccy='', ordId='', after='', before='', limit=''): + params = {'ccy': ccy, 'ordId': ordId, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_VIP_INTEREST_ACCRUED_DATA, params) + + # - Get VIP interest deducted data + def get_VIP_interest_deducted_data(self, ccy='', ordId='', after='', before='', limit=''): + params = {'ccy': ccy, 'ordId': ordId, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_VIP_INTEREST_DEDUCTED_DATA, params) + + # - Get VIP loan order list + def get_VIP_loan_order_list(self, ordId='', state='', ccy='', after='', before='', limit=''): + params = {'ordId': ordId, 'state': state, 'ccy': ccy, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_VIP_LOAN_ORDER_LIST, params) + + # - Get VIP loan order detail + def get_VIP_loan_order_detail(self, ccy='', ordId='', after='', before='', limit=''): + params = {'ccy': ccy, 'ordId': ordId, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_VIP_LOAN_ORDER_DETAIL, params) + + # - Set risk offset type + def set_risk_offset_typel(self, type=''): + params = {'type': type} + return self._request_with_params(POST, SET_RISK_OFFSET_TYPE, params) + + # - Set auto loan + def set_auto_loan(self, autoLoan=''): params = { - 'instType':instType, - 'instId':instId, - 'mgnMode':mgnMode, - 'type':type, - 'posId':posId, - 'after':after, - 'before':before, - 'limit':limit + 'autoLoan': autoLoan } - return self._request_with_params(GET,POSITIONS_HISTORY,params) + return self._request_with_params(POST, SET_AUTO_LOAN, params) - #GET /api/v5/account/position-tiers - def get_account_position_tiers(self,instType = '', uly = '',instFamily = ''): + # - Set auto loan + def set_account_level(self, acctLv): params = { - 'instType':instType, - 'uly':uly, - 'instFamily':instFamily + 'acctLv': acctLv } - return self._request_with_params(GET,GET_PM_LIMIT,params) + return self._request_with_params(POST, SET_ACCOUNT_LEVEL, params) + # - Activate option + def activate_option(self): + return self._request_without_params(POST, ACTIVSTE_OPTION) + def get_fix_loan_borrowing_limit(self): + return self._request_without_params(GET, BORROWING_LIMIT) + def get_fix_loan_borrowing_quote(self, type=None, ccy=None, amt=None, maxRate=None, term=None, ordId=None): + params = {} + if type is not None: + params['type'] = type + if ccy is not None: + params['ccy'] =ccy + if amt is not None: + params['amt'] = amt + if maxRate is not None: + params['maxRate'] = maxRate + if term is not None: + params['term'] = term + if ordId is not None: + params['ordId'] = ordId + return self._request_with_params(GET, BORROWING_QUOTE, params) + + def place_fix_loan_borrowing_order(self, ccy=None, amt=None, maxRate=None, term=None, reborrow=False, reborrowRate=None): + params = {} + if ccy is not None: + params['ccy'] =ccy + if amt is not None: + params['amt'] = amt + if maxRate is not None: + params['maxRate'] = maxRate + if term is not None: + params['term'] = term + if reborrow is not None: + params['reborrow'] = reborrow + if reborrowRate is not None: + params['reborrowRate'] = reborrowRate + return self._request_with_params(POST, PLACE_BORROWING_ORDER, params) + + def amend_fix_loan_borrowing_order(self, ordId=None, reborrow=None, renewMaxRate=None): + params = {} + if ordId is not None: + params['ordId'] = ordId + if reborrow is not None: + params['reborrow'] = reborrow + if renewMaxRate is not None: + params['renewMaxRate'] = renewMaxRate + return self._request_with_params(POST, AMEND_BORROWING_ORDER, params) + + def fix_loan_manual_reborrow(self, ordId=None, maxRate=None): + params = {} + if ordId is not None: + params['ordId'] = ordId + if maxRate is not None: + params['maxRate'] = maxRate + return self._request_with_params(POST, MANUAL_REBORROW, params) + + def repay_fix_loan_borrowing_order(self, ordId=None): + params = {} + if ordId is not None: + params['ordId'] = ordId + return self._request_with_params(POST, REPAY_BORROWING_ORDER, params) + def get_fix_loan_borrowing_orders_list(self, ordId=None, ccy=None, state=None, after=None, before=None, limit=None): + params = {} + if ordId is not None: + params['ordId'] = ordId + if ccy is not None: + params['ccy'] =ccy + if state is not None: + params['state'] = state + if after is not None: + params['after'] = after + if before is not None: + params['before'] = before + if limit is not None: + params['limit'] = limit + return self._request_with_params(GET, BORROWING_ORDERS_LIST, params) + + def spot_manual_borrow_repay(self, ccy=None, side=None, amt=None): + params = {} + if ccy is not None: + params['ccy'] = ccy + if side is not None: + params['side'] = side + if amt is not None: + params['amt'] = amt + return self._request_with_params(POST, MANUAL_REBORROW_REPAY, params) + + def set_auto_repay(self, autoRepay=False): + params = {} + if autoRepay is not None: + params['autoRepay'] = autoRepay + return self._request_with_params(POST, SET_AUTO_REPAY, params) + + def spot_borrow_repay_history(self, ccy='', type='', after='', before='', limit=''): + params = {'ccy': ccy, 'type': type, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_BORROW_REPAY_HISTORY, params) + + def set_auto_earn(self, ccy, action, earnType=None): + params = {'ccy': ccy, 'action': action} + if earnType is not None: + params['earnType'] = earnType + return self._request_with_params(POST, SET_AUTO_EARN, params) diff --git a/okx/BlockTrading.py b/okx/BlockTrading.py index 0275555e..75c6b539 100644 --- a/okx/BlockTrading.py +++ b/okx/BlockTrading.py @@ -1,17 +1,19 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class BlockTradingAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class BlockTradingAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def counterparties(self): params = {} return self._request_with_params(GET, COUNTERPARTIES, params) - def create_rfq(self, counterparties=[], anonymous='false', clRfqId='', legs = []): - params = {'counterparties': counterparties, 'anonymous': anonymous, 'clRfqId': clRfqId, 'legs': legs} + def create_rfq(self, counterparties=[], anonymous='false', clRfqId='', tag='', allowPartialExecution='false', + legs=[]): + params = {'counterparties': counterparties, 'anonymous': anonymous, 'clRfqId': clRfqId, 'tag': tag, + 'allowPartialExecution': allowPartialExecution, 'legs': legs} return self._request_with_params(POST, CREATE_RFQ, params) def cancel_rfq(self, rfqId = '', clRfqId = ''): @@ -26,13 +28,13 @@ def cancel_all_rfqs(self): params = {} return self._request_with_params(POST, CANCEL_ALL_RSQS, params) - def execute_quote(self, rfqId='', quoteId=''): - params = {'rfqId': rfqId, 'quoteId': quoteId} + def execute_quote(self, rfqId='', quoteId='', legs=[]): + params = {'rfqId': rfqId, 'quoteId': quoteId, 'legs': legs} return self._request_with_params(POST, EXECUTE_QUOTE, params) - def create_quote(self, rfqId='', clQuoteId='', quoteSide = '', legs = [],anonymous=False,expiresIn=''): - params = {'rfqId': rfqId, 'clQuoteId': clQuoteId, 'quoteSide': quoteSide, 'legs': legs, - 'anonymous':anonymous,'expiresIn':expiresIn} + def create_quote(self, rfqId='', clQuoteId='', tag='', quoteSide='', legs=[], anonymous=False, expiresIn=''): + params = {'rfqId': rfqId, 'clQuoteId': clQuoteId, 'tag': tag, 'quoteSide': quoteSide, 'legs': legs, + 'anonymous': anonymous, 'expiresIn': expiresIn} return self._request_with_params(POST, CREATE_QUOTE, params) def cancel_quote(self, quoteId = '', clQuoteId = ''): @@ -55,8 +57,10 @@ def get_quotes(self, rfqId = '', clRfqId = '', quoteId = '', clQuoteId = '', sta params = {'rfqId': rfqId, 'clRfqId': clRfqId, 'quoteId':quoteId,'clQuoteId':clQuoteId, 'state': state, 'beginId': beginId, 'endId': endId, 'limit':limit} return self._request_with_params(GET, GET_QUOTES, params) - def get_trades(self, rfqId = '', clRfqId = '', quoteId = '', clQuoteId = '', state = '', beginId = '', endId = '', limit = ''): - params = {'rfqId': rfqId, 'clRfqId': clRfqId, 'quoteId':quoteId,'clQuoteId':clQuoteId, 'state': state, 'beginId': beginId, 'endId': endId, 'limit':limit} + def get_trades(self, rfqId='', clRfqId='', quoteId='', clQuoteId='', state='', beginId='', endId='', beginTs='', + endTs='', limit=''): + params = {'rfqId': rfqId, 'clRfqId': clRfqId, 'quoteId': quoteId, 'clQuoteId': clQuoteId, 'state': state, + 'beginId': beginId, 'endId': endId, 'beginTs': beginTs, 'endTs': endTs, 'limit': limit} return self._request_with_params(GET, GET_RFQ_TRADES, params) def get_public_trades(self, beginId = '', endId = '', limit = ''): @@ -68,4 +72,8 @@ def reset_mmp(self): def set_marker_instrument(self,params = []): - return self._request_with_params(POST, MARKER_INSTRUMENT_SETTING, params) \ No newline at end of file + return self._request_with_params(POST, MARKER_INSTRUMENT_SETTING, params) + + #Get Quote products + def get_quote_products(self): + return self._request_without_params(GET, MARKER_INSTRUMENT_SETTING) diff --git a/okx/Convert.py b/okx/Convert.py index 8d04f446..075607b8 100644 --- a/okx/Convert.py +++ b/okx/Convert.py @@ -1,10 +1,10 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class ConvertAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class ConvertAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False,proxy = None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def get_currencies(self): params = {} diff --git a/okx/CopyTrading.py b/okx/CopyTrading.py new file mode 100644 index 00000000..331b6cfa --- /dev/null +++ b/okx/CopyTrading.py @@ -0,0 +1,73 @@ +from .okxclient import OkxClient +from .consts import * + + +class CopyTradingAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', + domain='https://www.okx.com', debug=False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, + proxy=proxy) + + # Get existing leading positions + def get_existing_leading_positions(self, instId=''): + params = { + 'instId': instId + } + return self._request_with_params(GET, GET_EXISTING_LEADING_POSITIONS, params) + + # Get leading position history + def get_leading_position_history(self, instId='', after='', before='', limit=''): + params = { + 'instId': instId, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_LEADING_POSITIONS_HISTORY, params) + + # Place leading stop order + def place_leading_stop_order(self, subPosId='', tpTriggerPx='', slTriggerPx='', tpTriggerPxType='', + slTriggerPxType=''): + params = { + 'subPosId': subPosId, + 'tpTriggerPx': tpTriggerPx, + 'slTriggerPx': slTriggerPx, + 'tpTriggerPxType': tpTriggerPxType, + 'slTriggerPxType': slTriggerPxType + } + return self._request_with_params(POST, PLACE_LEADING_STOP_ORDER, params) + + # Close leading position + def close_leading_position(self, subPosId=''): + params = { + 'subPosId': subPosId + } + return self._request_with_params(POST, CLOSE_LEADING_POSITIONS, params) + + # Get leading instruments + def get_leading_instruments(self): + return self._request_without_params(GET, GET_LEADING_POSITIONS) + + # Amend leading instruments + def amend_leading_instruments(self, instId=''): + params = { + 'instId': instId + } + return self._request_with_params(POST, AMEND_EXISTING_LEADING_POSITIONS, params) + + # Get profit sharing details + def get_profit_sharing_details(self, after='', before='', limit=''): + params = { + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_PROFIT_SHARING_DETAILS, params) + + # Get total profit sharing + def get_total_profit_sharing(self): + return self._request_without_params(GET, GET_TOTAL_PROFIT_SHARING) + + # Get unrealized profit sharing details + def get_unrealized_profit_sharing_details(self): + return self._request_without_params(GET, GET_UNREALIZED_PROFIT_SHARING_DETAILS) diff --git a/okx/Earning.py b/okx/Earning.py deleted file mode 100644 index 0ecb2566..00000000 --- a/okx/Earning.py +++ /dev/null @@ -1,64 +0,0 @@ -from .client import Client -from .consts import * - - -class EarningAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug) - - def get_offers(self,productId = '',protocolType = '',ccy = ''): - params = { - 'productId':productId, - 'protocolType':protocolType, - 'ccy':ccy - } - return self._request_with_params(GET,STACK_DEFI_OFFERS,params) - - def purchase(self,productId = '',investData = [],term = ''): - - params = { - 'productId':productId, - 'investData':investData - } - if term != '': - params['term'] = term - return self._request_with_params(POST,STACK_DEFI_PURCHASE,params) - - def redeem(self,ordId = '',protocolType = '',allowEarlyRedeem = ''): - params = { - 'ordId':ordId, - 'protocolType':protocolType, - 'allowEarlyRedeem':allowEarlyRedeem - } - return self._request_with_params(POST,STACK_DEFI_REDEEM,params) - - def cancel(self,ordId = '',protocolType = ''): - params = { - 'ordId':ordId, - 'protocolType':protocolType - } - return self._request_with_params(POST,STACK_DEFI_CANCEL,params) - - def get_activity_orders(self,productId = '',protocolType = '',ccy = '',state = ''): - params = { - 'productId':productId, - 'protocolType':protocolType, - 'ccy':ccy, - 'state':state - } - return self._request_with_params(GET,STACK_DEFI_ORDERS_ACTIVITY,params) - - def get_orders_history(self,productId = '',protocolType = '',ccy = '',after = '',before = '',limit = ''): - params = { - 'productId':productId, - 'protocolType':protocolType, - 'ccy':ccy, - 'after':after, - 'before':before, - 'limit':limit - } - return self._request_with_params(GET,STACK_DEFI_ORDERS_HISTORY,params) - - - - diff --git a/okx/FDBroker.py b/okx/FDBroker.py index d0c8f57c..31185644 100644 --- a/okx/FDBroker.py +++ b/okx/FDBroker.py @@ -1,10 +1,10 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class FDBrokerAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class FDBrokerAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def generate_rebate_details_download_link(self, begin ='', end = ''): params = {'begin': begin, 'end': end} diff --git a/okx/Finance/EthStaking.py b/okx/Finance/EthStaking.py new file mode 100644 index 00000000..c7d008dd --- /dev/null +++ b/okx/Finance/EthStaking.py @@ -0,0 +1,52 @@ +from okx.okxclient import OkxClient +from okx.consts import * + + +class EthStakingAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + def eth_product_info(self): + + return self._request_without_params(GET, STACK_ETH_PRODUCT_INFO) + + def eth_purchase(self, amt=''): + + params = { + 'amt': amt, + } + return self._request_with_params(POST, STACK_ETH_PURCHASE, params) + + def eth_redeem(self, amt=''): + + params = { + 'amt': amt, + } + return self._request_with_params(POST, STACK_ETH_REDEEM, params) + + def eth_balance(self): + + params = {} + return self._request_with_params(GET, STACK_ETH_BALANCE, params) + + def eth_purchase_redeem_history(self, type='', status='', after='', before='', limit=''): + + params = {} + if type != '': + params['type'] = type + if status != '': + params['status'] = status + if after != '': + params['after'] = after + if before != '': + params['before'] = before + if limit != '': + params['limit'] = limit + return self._request_with_params(GET, STACK_ETH_PURCHASE_REDEEM_HISTORY, params) + + def eth_apy_history(self, days): + + params = { + 'days': days, + } + return self._request_with_params(GET, STACK_ETH_APY_HISTORY, params) diff --git a/okx/Finance/FlexibleLoan.py b/okx/Finance/FlexibleLoan.py new file mode 100644 index 00000000..44ce4e80 --- /dev/null +++ b/okx/Finance/FlexibleLoan.py @@ -0,0 +1,65 @@ +from okx.okxclient import OkxClient +from okx.consts import * + + +class FlexibleLoanAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', + domain='https://www.okx.com', debug=False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + def borrow_currencies(self): + return self._request_without_params(GET, FINANCE_BORROW_CURRENCIES) + + def collateral_assets(self, ccy=''): + params = {} + if ccy != '': + params['ccy'] = ccy + return self._request_with_params(GET, FINANCE_COLLATERAL_ASSETS, params) + + def max_loan(self, borrowCcy='', supCollateral=[]): + params = { + 'borrowCcy': borrowCcy, + 'supCollateral': supCollateral, + } + return self._request_with_params(POST, FINANCE_MAX_LOAN, params) + + def max_collateral_redeem_amount(self, ccy=''): + params = {} + if ccy != '': + params['ccy'] = ccy + return self._request_with_params(GET, FINANCE_MAX_REDEEM, params) + + def adjust_collateral(self, type='', collateralCcy='', collateralAmt=''): + params = { + 'type': type, + 'collateralCcy': collateralCcy, + 'collateralAmt': collateralAmt, + } + return self._request_with_params(POST, FINANCE_ADJUST_COLLATERAL, params) + + def loan_info(self): + return self._request_without_params(GET, FINANCE_LOAN_INFO) + + def loan_history(self, type='', after='', before='', limit=''): + params = {} + if type != '': + params['type'] = type + if after != '': + params['after'] = after + if before != '': + params['before'] = before + if limit != '': + params['limit'] = limit + return self._request_with_params(GET, FINANCE_LOAN_HISTORY, params) + + def interest_accrued(self, ccy='', after='', before='', limit=''): + params = {} + if ccy != '': + params['ccy'] = ccy + if after != '': + params['after'] = after + if before != '': + params['before'] = before + if limit != '': + params['limit'] = limit + return self._request_with_params(GET, FINANCE_INTEREST_ACCRUED, params) diff --git a/okx/Finance/Savings.py b/okx/Finance/Savings.py new file mode 100644 index 00000000..3fbb3933 --- /dev/null +++ b/okx/Finance/Savings.py @@ -0,0 +1,62 @@ +from okx.okxclient import OkxClient +from okx.consts import * + + +class SavingsAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + # - Get saving balance + def get_saving_balance(self, ccy=''): + params = { + 'ccy': ccy + } + return self._request_with_params(GET, GET_SAVING_BALANCE, params) + + # - Savings purchase/redemption + def savings_purchase_redemption(self, ccy='', amt='', side='', rate=''): + + params = { + 'ccy': ccy, + 'amt': amt, + 'side': side, + 'rate': rate + } + return self._request_with_params(POST, SAVING_PURCHASE_REDEMPTION, params) + + # - Set lending rate + def set_lending_rate(self, ccy='', rate=''): + params = { + 'ccy': ccy, + 'rate': rate + } + return self._request_with_params(POST, SET_LENDING_RATE, params) + + # - Get lending history + def get_lending_history(self, ccy='', after='', before='', limit=''): + params = { + 'ccy': ccy, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_LENDING_HISTORY, params) + + # - Get public borrow history (public) + def get_public_borrow_history(self, ccy='', after='', before='', limit=''): + params = { + 'ccy': ccy, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_PUBLIC_BORROW_HISTORY, params) + + # GET / Public borrow info (public) + def get_public_borrow_info(self, ccy=''): + params = { + 'ccy': ccy + } + return self._request_with_params(GET, GET_PUBLIC_BORROW_INFO, params) + + diff --git a/okx/Finance/SolStaking.py b/okx/Finance/SolStaking.py new file mode 100644 index 00000000..dc1151f0 --- /dev/null +++ b/okx/Finance/SolStaking.py @@ -0,0 +1,51 @@ +from okx.okxclient import OkxClient +from okx.consts import * + + +class SolStakingAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + def sol_purchase(self, amt): + + params = { + 'amt': amt, + } + return self._request_with_params(POST, STACK_SOL_PURCHASE, params) + + def sol_redeem(self, amt=''): + + params = { + 'amt': amt, + } + return self._request_with_params(POST, STACK_SOL_REDEEM, params) + + def sol_balance(self): + + params = {} + return self._request_with_params(GET, STACK_SOL_BALANCE, params) + + def sol_purchase_redeem_history(self, type='', status='', after='', before='', limit=''): + + params = {} + if type != '': + params['type'] = type + if status != '': + params['status'] = status + if after != '': + params['after'] = after + if before != '': + params['before'] = before + if limit != '': + params['limit'] = limit + return self._request_with_params(GET, STACK_SOL_PURCHASE_REDEEM_HISTORY, params) + + def sol_apy_history(self, days): + + params = { + 'days': days, + } + return self._request_with_params(GET, STACK_SOL_APY_HISTORY, params) + + def sol_product_info(self): + return self._request_without_params(GET, STACK_SOL_PRODUCT_INFO) diff --git a/okx/Finance/StakingDefi.py b/okx/Finance/StakingDefi.py new file mode 100644 index 00000000..65d7f47e --- /dev/null +++ b/okx/Finance/StakingDefi.py @@ -0,0 +1,63 @@ +from okx.okxclient import OkxClient +from okx.consts import * + + +class StakingDefiAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + def get_offers(self, productId='', protocolType='', ccy=''): + params = { + 'productId': productId, + 'protocolType': protocolType, + 'ccy': ccy + } + return self._request_with_params(GET, STACK_DEFI_OFFERS, params) + + def purchase(self, productId='', investData=[], term='', tag=''): + + params = { + 'productId': productId, + 'investData': investData + } + if term != '': + params['term'] = term + if tag != '': + params['tag'] = tag + return self._request_with_params(POST, STACK_DEFI_PURCHASE, params) + + def redeem(self, ordId='', protocolType='', allowEarlyRedeem=''): + params = { + 'ordId': ordId, + 'protocolType': protocolType, + 'allowEarlyRedeem': allowEarlyRedeem + } + return self._request_with_params(POST, STACK_DEFI_REDEEM, params) + + def cancel(self, ordId='', protocolType=''): + params = { + 'ordId': ordId, + 'protocolType': protocolType + } + return self._request_with_params(POST, STACK_DEFI_CANCEL, params) + + def get_activity_orders(self, productId='', protocolType='', ccy='', state=''): + params = { + 'productId': productId, + 'protocolType': protocolType, + 'ccy': ccy, + 'state': state + } + return self._request_with_params(GET, STACK_DEFI_ORDERS_ACTIVITY, params) + + def get_orders_history(self, productId='', protocolType='', ccy='', after='', before='', limit=''): + params = { + 'productId': productId, + 'protocolType': protocolType, + 'ccy': ccy, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, STACK_DEFI_ORDERS_HISTORY, params) + diff --git a/okx/Finance/__init__.py b/okx/Finance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/okx/Funding.py b/okx/Funding.py index a4c3c659..a984778d 100644 --- a/okx/Funding.py +++ b/okx/Funding.py @@ -1,12 +1,17 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class FundingAPI(Client): +class FundingAPI(OkxClient): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + # Get Non Tradable Assets + def get_non_tradable_assets(self, ccy: str = ''): + params = {'ccy': ccy} + return self._request_with_params(GET, NON_TRADABLE_ASSETS, params) # Get Deposit Address def get_deposit_address(self, ccy): @@ -30,19 +35,20 @@ def funds_transfer(self, ccy, amt, from_, to, type='0', subAcct='', instId='', t return self._request_with_params(POST, FUNDS_TRANSFER, params) # Withdrawal - def withdrawal(self, ccy, amt, dest, toAddr, fee,chain = '', clientId = ''): - params = {'ccy': ccy, 'amt': amt, 'dest': dest, 'toAddr': toAddr, 'fee': fee,'chain':chain,'clientId':clientId} + def withdrawal(self, ccy, amt, dest, toAddr, chain='', areaCode='', clientId='', toAddrType=None): + params = {'ccy': ccy, 'amt': amt, 'dest': dest, 'toAddr': toAddr, 'chain': chain, + 'areaCode': areaCode, 'clientId': clientId} + if toAddrType is not None: + params['toAddrType'] = toAddrType return self._request_with_params(POST, WITHDRAWAL_COIN, params) # Get Deposit History - def get_deposit_history(self, ccy='', state='', after='', before='', limit='',txId='',depId=''): - params = {'ccy': ccy, 'state': state, 'after': after, 'before': before, 'limit': limit,'txId':txId,'depId':depId} - return self._request_with_params(GET, DEPOSIT_HISTORIY, params) + def get_deposit_history(self, ccy='', type='', state='', after='', before='', limit='', txId='', depId='', fromWdId=''): + params = {'ccy': ccy, 'type': type, 'state': state, 'after': after, 'before': before, 'limit': limit, 'txId': txId, + 'depId': depId, 'fromWdId': fromWdId} + return self._request_with_params(GET, DEPOSIT_HISTORY, params) + - # Get Withdrawal History - def get_withdrawal_history(self, ccy='', state='', after='', before='', limit='',txId=''): - params = {'ccy': ccy, 'state': state, 'after': after, 'before': before, 'limit': limit,'txId':txId} - return self._request_with_params(GET, WITHDRAWAL_HISTORIY, params) # Get Currencies def get_currencies(self, ccy=''): @@ -72,31 +78,6 @@ def withdrawal_lightning(self, ccy,invoice,memo=''): params = {'ccy':ccy, 'invoice':invoice, 'memo':memo} return self._request_with_params(POST, WITHDRAWAL_LIGHTNING, params) - - # POST SET LENDING RATE - def set_lending_rate(self, ccy, rate): - params = {'ccy': ccy, 'rate': rate} - return self._request_with_params(POST, SET_LENDING_RATE, params) - - - # GET LENDING HISTORY - def get_lending_history(self, ccy='', before='', after='', limit='' ): - params = {'ccy': ccy, 'after': after, 'before': before, 'limit': limit } - return self._request_with_params(GET, LENDING_HISTORY, params) - - - # GET LENDING RATE HISTORY - def get_lending_rate_history(self, ccy='',after = '',before = '',limit = '' ): - params = {'ccy': ccy,'after':after,'before':before,'limit':limit} - return self._request_with_params(GET, LENDING_RATE_HISTORY, params) - - - # GET LENDING RATE SUMMARY - def get_lending_rate_summary(self, ccy=''): - params = {'ccy': ccy} - return self._request_with_params(GET, LENDING_RATE_SUMMARY, params) - - #POST /api/v5/asset/cancel-withdrawal def cancel_withdrawal(self,wdId = ''): params = { @@ -118,9 +99,22 @@ def get_asset_valuation(self,ccy = ''): } return self._request_with_params(GET, ASSET_VALUATION, params) - #GET / api / v5 / asset / saving - balance - def get_saving_balance(self,ccy = ''): + #Get non-tradable assets + def get_non_tradable_assets(self, ccy=''): params = { - 'ccy':ccy + 'ccy': ccy } - return self._request_with_params(GET, GET_SAVING_BALANCE, params) + return self._request_with_params(GET, GET_NON_TRADABLE_ASSETS, params) + + #Get deposit withdraw status + def get_deposit_withdraw_status(self, wdId='', txId='', ccy='', to='', chain=''): + params = {'wdId': wdId, 'txId': txId, 'ccy': ccy, 'to': to, 'chain': chain} + return self._request_with_params(GET, GET_DEPOSIT_WITHDrAW_STATUS, params) + + #Get withdrawal history + def get_withdrawal_history(self, ccy='', wdId='', clientId='', txId='', type='', state='', after='', before='', limit='', toAddrType=None): + params = {'ccy': ccy, 'wdId': wdId, 'clientId': clientId, 'txId': txId, 'type': type, 'state': state, 'after': after, 'before': before, 'limit': limit} + if toAddrType is not None: + params['toAddrType'] = toAddrType + return self._request_with_params(GET, GET_WITHDRAWAL_HISTORY, params) + diff --git a/okx/Grid.py b/okx/Grid.py index 6cf49914..d761708b 100644 --- a/okx/Grid.py +++ b/okx/Grid.py @@ -1,17 +1,19 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class GridAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class GridAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def grid_order_algo(self, instId='', algoOrdType='', maxPx='', minPx='', gridNum='', runType='', tpTriggerPx='', - slTriggerPx='', tag='', quoteSz='', baseSz='', sz='', direction='', lever='', basePos=''): + slTriggerPx='', tag='', quoteSz='', baseSz='', sz='', direction='', lever='', basePos='', tradeQuoteCcy=None): params = {'instId': instId, 'algoOrdType': algoOrdType, 'maxPx': maxPx, 'minPx': minPx, 'gridNum': gridNum, 'runType': runType, 'tpTriggerPx': tpTriggerPx, 'slTriggerPx': slTriggerPx, 'tag': tag, 'quoteSz': quoteSz, 'baseSz': baseSz, 'sz': sz, 'direction': direction, 'lever': lever, 'basePos': basePos} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy return self._request_with_params(POST, GRID_ORDER_ALGO, params) def grid_amend_order_algo(self, algoId='', instId='', slTriggerPx='', tpTriggerPx=''): @@ -76,3 +78,59 @@ def grid_ai_param(self, algoOrdType='', instId='', direction='', duration=''): 'duration':duration } return self._request_with_params(GET, GRID_AI_PARAM, params) + + # - Place recurring buy order + def place_recurring_buy_order(self, stgyName='', recurringList=[], period='', recurringDay='', recurringTime='', + timeZone='', amt='', investmentCcy='', tdMode='', algoClOrdId='', tag='', tradeQuoteCcy=None): + params = {'stgyName': stgyName, 'recurringList': recurringList, 'period': period, 'recurringDay': recurringDay, + 'recurringTime': recurringTime, + 'timeZone': timeZone, 'amt': amt, 'investmentCcy': investmentCcy, 'tdMode': tdMode, + 'algoClOrdId': algoClOrdId, 'tag': tag} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy + return self._request_with_params(POST, PLACE_RECURRING_BUY_ORDER, params) + + # - Amend recurring buy order + def amend_recurring_buy_order(self, algoId='', stgyName=''): + params = {'algoId': algoId, 'stgyName': stgyName} + return self._request_with_params(POST, AMEND_RECURRING_BUY_ORDER, params) + + # - Stop recurring buy order + def stop_recurring_buy_order(self, orders_data): + return self._request_with_params(POST, STOP_RECURRING_BUY_ORDER, orders_data) + + # - Get recurring buy order list + def get_recurring_buy_order_list(self, algoId='', after='', before='', limit=''): + params = { + 'algoId': algoId, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_RECURRING_BUY_ORDER_LIST, params) + + # - Get recurring buy order history + def get_recurring_buy_order_history(self, algoId='', after='', before='', limit=''): + params = { + 'algoId': algoId, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_RECURRING_BUY_ORDER_HISTORY, params) + + # - Get recurring buy order details + def get_recurring_buy_order_details(self, algoId=''): + params = {'algoId': algoId} + return self._request_with_params(GET, GET_RECURRING_BUY_ORDER_DETAILS, params) + + # - Get recurring buy sub orders + def get_recurring_buy_sub_orders(self, algoId='', ordId='', after='', before='', limit=''): + params = { + 'algoId': algoId, + 'ordId': ordId, + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, GET_RECURRING_BUY_SUB_ORDERS, params) diff --git a/okx/MarketData.py b/okx/MarketData.py index 1dc2bdf0..35f4891e 100644 --- a/okx/MarketData.py +++ b/okx/MarketData.py @@ -1,11 +1,11 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class MarketAPI(Client): +class MarketAPI(OkxClient): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) # Get Tickers @@ -60,10 +60,6 @@ def get_trades(self, instId, limit=''): def get_volume(self): return self._request_without_params(GET, VOLUMNE) - # Get Oracle - def get_oracle(self): - return self._request_without_params(GET, ORACLE) - # Get Tier def get_tier(self, instType='', tdMode='', uly='', instId='', ccy='', tier=''): params = {'instType': instType, 'tdMode': tdMode, 'uly': uly, 'instId': instId, 'ccy': ccy, 'tier': tier} @@ -115,6 +111,20 @@ def get_block_trades(self,instId = ''): } return self._request_with_params(GET, BLOCK_TRADES, params) + #- Get order lite book + def get_order_lite_book(self,instId = ''): + params = { + 'instId':instId + } + return self._request_with_params(GET, GET_ORDER_LITE_BOOK, params) + + #- Get option trades + def get_option_trades(self,instFamily = ''): + params = { + 'instFamily':instFamily + } + return self._request_with_params(GET, GET_OPTION_TRADES, params) + diff --git a/okx/NDBroker.py b/okx/NDBroker.py deleted file mode 100644 index 01dcb339..00000000 --- a/okx/NDBroker.py +++ /dev/null @@ -1,150 +0,0 @@ -from .client import Client -from .consts import * -class NDBrokerAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) - - #GET /api/v5/broker/nd/info - def get_broker_info(self): - return self._request_without_params(GET, BROKER_INFO) - - #POST /api/v5/broker/nd/create-subaccount - def create_subaccount(self,subAcct = '',label = ''): - params = { - 'subAcct':subAcct, - 'label':label - } - return self._request_with_params(POST,CREATE_SUBACCOUNT,params) - - def delete_subaccount(self,subAcct = ''): - params = { - 'subAcct':subAcct - } - return self._request_with_params(POST,DELETE_SUBACCOUNT,params) - - def get_subaccount_info(self,subAcct = '',page = '',limit = ''): - params = { - 'subAcct':subAcct, - 'page':page, - 'limit':limit - } - return self._request_with_params(GET,SUBACCOUNT_INFO,params) - - def create_subaccount_apikey(self,subAcct = '',label='',passphrase='',ip='',perm=''): - params = { - 'subAcct':subAcct, - 'label':label, - 'passphrase':passphrase, - 'ip':ip, - 'perm':perm - } - return self._request_with_params(POST,ND_CREAET_APIKEY,params) - - def get_subaccount_apikey(self,subAcct = '',apiKey = ''): - params = { - 'subAcct':subAcct, - 'apiKey':apiKey - } - return self._request_with_params(GET,ND_SELECT_APIKEY,params) - - def reset_subaccount_apikey(self,subAcct = '',apiKey = '',label='',perm = '',ip = ''): - params = { - 'subAcct':subAcct, - 'apiKey':apiKey, - 'label':label, - 'perm':perm, - 'ip':ip - } - return self._request_with_params(POST,ND_MODIFY_APIKEY,params) - - def delete_subaccount_apikey(self,subAcct = '',apiKey = ''): - params = { - 'subAcct':subAcct, - 'apiKey':apiKey - } - return self._request_with_params(POST,ND_DELETE_APIKEY,params) - - def set_subaccount_level(self,subAcct = '',acctLv = ''): - params = { - 'subAcct':subAcct, - 'acctLv':acctLv - } - return self._request_with_params(POST,SET_SUBACCOUNT_LEVEL,params) - - def set_subaccount_fee_rate(self,subAcct = '',instType = '',chgType = '',chgTaker = '',chgMaker = '',effDate = ''): - params = { - 'subAcct':subAcct, - 'instType':instType, - 'chgType':chgType, - 'chgTaker':chgTaker, - 'chgMaker':chgMaker, - 'effDate':effDate - } - return self._request_with_params(POST,SET_SUBACCOUNT_FEE_REAT,params) - - def create_subaccount_deposit_address(self,subAcct = '',ccy = '',chain = '',addrType = '', to =''): - params = { - 'subAcct':subAcct, - 'ccy':ccy, - 'chain':chain, - 'addrType':addrType, - 'to':to - } - return self._request_with_params(POST,SUBACCOUNT_DEPOSIT_ADDRESS,params) - - def reset_subaccount_deposit_address(self,subAcct = '',ccy = '',chain = '',addr = '',to = ''): - params = { - 'subAcct':subAcct, - 'ccy':ccy, - 'chain':chain, - 'addr':addr, - 'to':to - } - return self._request_with_params(POST,MODIFY_SUBACCOUNT_DEPOSIT_ADDRESS,params) - - def get_subaccount_deposit_address(self,subAcct = '',ccy = ''): - params = { - 'subAcct':subAcct, - 'ccy':ccy - } - return self._request_with_params(GET,GET_SUBACCOUNT_DEPOSIT,params) - - def get_subaccount_deposit_history(self,subAcct = '',ccy = '',txId = '',state = '',after = '',before = '',limit = ''): - params = { - 'subAcct':subAcct, - 'ccy':ccy, - 'txId':txId, - 'state':state, - 'after':after, - 'before':before, - 'limit':limit - } - return self._request_with_params(GET,SUBACCOUNT_DEPOSIT_HISTORY,params) - - def get_rebate_daily(self,subAcct = '',begin = '',end = '',page = '',limit = ''): - params = { - 'subAcct':subAcct, - 'begin':begin, - 'end':end, - 'page':page, - 'limit':limit - } - return self._request_with_params(GET,REBATE_DAILY,params) - - def get_rebate_details_download_link(self,type ='',begin = '',end = ''): - params ={ - 'type':type, - 'begin':begin, - 'end':end - } - return self._request_with_params(GET,GET_REBATE_PER_ORDERS,params) - - - - def generate_rebate_details_download_link(self,begin = '',end = ''): - params = { - 'begin':begin, - 'end':end - } - return self._request_with_params(POST,REBATE_PER_ORDERS,params) - diff --git a/okx/PublicData.py b/okx/PublicData.py index 10438ebc..f4412668 100644 --- a/okx/PublicData.py +++ b/okx/PublicData.py @@ -1,11 +1,11 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class PublicAPI(Client): +class PublicAPI(OkxClient): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) # Get Instruments def get_instruments(self, instType, uly='', instId='',instFamily = ''): @@ -56,13 +56,6 @@ def discount_interest_free_quota(self, ccy=''): def get_system_time(self): return self._request_without_params(GET, SYSTEM_TIME) - # Get Liquidation Orders - def get_liquidation_orders(self, instType, mgnMode='', instId='', ccy='', uly='', alias='', state='', before='', - after='', limit='',instFamily =''): - params = {'instType': instType, 'mgnMode': mgnMode, 'instId': instId, 'ccy': ccy, 'uly': uly, - 'alias': alias, 'state': state, 'before': before, 'after': after, 'limit': limit,'instFamily':instFamily} - return self._request_with_params(GET, LIQUIDATION_ORDERS, params) - # Get Mark Price def get_mark_price(self, instType, uly='', instId='',instFamily = ''): params = {'instType': instType, 'uly': uly, 'instId': instId,'instFamily':instFamily} @@ -113,5 +106,34 @@ def get_convert_contract_coin(self,type = '',instId = '',sz = '',px = '',unit = } return self._request_with_params(GET, CONVERT_CONTRACT_COIN, params) + # Get option tickBands + def get_option_tickBands(self, instType='', instFamily=''): + params = { + 'instType': instType, + 'instFamily': instFamily + } + return self._request_with_params(GET, GET_OPTION_TICKBANDS, params) + # Get option trades + def get_option_trades(self, instId='', instFamily='', optType=''): + params = { + 'instId': instId, + 'instFamily': instFamily, + 'optType': optType + } + return self._request_with_params(GET, GET_OPTION_TRADES, params) + # Get historical market data + def get_market_data_history(self, module, instType, dateAggrType, begin, end, instIdList=None, instFamilyList=None): + params = { + 'module': module, + 'instType': instType, + 'dateAggrType': dateAggrType, + 'begin': begin, + 'end': end + } + if instIdList is not None: + params['instIdList'] = instIdList + if instFamilyList is not None: + params['instFamilyList'] = instFamilyList + return self._request_with_params(GET, MARKET_DATA_HISTORY, params) diff --git a/okx/SpreadTrading.py b/okx/SpreadTrading.py new file mode 100644 index 00000000..46b0b8be --- /dev/null +++ b/okx/SpreadTrading.py @@ -0,0 +1,67 @@ +from .okxclient import OkxClient +from .consts import * + + +class SpreadTradingAPI(OkxClient): + + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) + + # Place Order + def place_order(self, sprdId='', clOrdId='', tag='', side='', ordType='', sz='', px=''): + params = {'sprdId': sprdId, 'clOrdId': clOrdId, 'tag': tag, 'side': side, 'ordType': ordType, 'sz': sz, + 'px': px} + return self._request_with_params(POST, SPREAD_PLACE_ORDER, params) + + # Cancel Order + def cancel_order(self,ordId='', clOrdId=''): + params = {'ordId': ordId, 'clOrdId': clOrdId} + return self._request_with_params(POST, SPREAD_CANCEL_ORDER, params) + + # Cancel All orders + def cancel_all_orders(self, sprdId=''): + params = {'sprdId': sprdId} + return self._request_with_params(POST, SPREAD_CANCEL_ALL_ORDERS, params) + + # Get order details + def get_order_details(self, ordId='', clOrdId=''): + params = {'ordId': ordId, 'clOrdId': clOrdId} + return self._request_with_params(GET, SPREAD_GET_ORDER_DETAILS, params) + + # Get active orders + def get_active_orders(self, sprdId='', ordType='', state='', beginId='', endId='', limit=''): + params = {'sprdId': sprdId, 'ordType': ordType, 'state': state, 'beginId': beginId, 'endId': endId, 'limit': limit} + return self._request_with_params(GET, SPREAD_GET_ACTIVE_ORDERS, params) + + # Get orders (last 7 days) + def get_orders(self, sprdId='', ordType='', state='', beginId='', endId='', begin='', end='', limit=''): + params = {'sprdId': sprdId, 'ordType': ordType, 'state': state, 'beginId': beginId, 'endId': endId, + 'begin': begin, 'end': end, 'limit': limit} + return self._request_with_params(GET, SPREAD_GET_ORDERS, params) + + # Get trades (last 7 days) + def get_trades(self, sprdId='', tradeId='', ordId='', beginId='', endId='', begin='', end='', limit=''): + params = {'sprdId': sprdId, 'tradeId': tradeId, 'ordId': ordId, 'beginId': beginId, 'endId': endId, + 'begin': begin, 'end': end, 'limit': limit} + return self._request_with_params(GET, SPREAD_GET_TRADES, params) + + # Get Spreads (Public) + def get_spreads(self, baseCcy='',instId='', sprdId='', state=''): + params = {'baseCcy': baseCcy, 'instId': instId, 'sprdId': sprdId, 'state': state} + return self._request_with_params(GET, SPREAD_GET_SPREADS, params) + + # Get order book (Public) + def get_order_book(self, sprdId='', sz=''): + params = {'sprdId': sprdId, 'sz': sz} + return self._request_with_params(GET, SPREAD_GET_ORDER_BOOK, params) + + # Get ticker (Public) + def get_ticker(self, sprdId=''): + params = {'sprdId': sprdId} + return self._request_with_params(GET, SPREAD_GET_TICKER, params) + + # Get public trades (Public) + def get_public_trades(self, sprdId=''): + params = {'sprdId': sprdId} + return self._request_with_params(GET, SPREAD_GET_PUBLIC_TRADES, params) + diff --git a/okx/Status.py b/okx/Status.py index ab9cf803..2304fb60 100644 --- a/okx/Status.py +++ b/okx/Status.py @@ -1,10 +1,10 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class StatusAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class StatusAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def status(self, state=''): params = {'state': state} diff --git a/okx/SubAccount.py b/okx/SubAccount.py index ea0428b4..abd128ab 100644 --- a/okx/SubAccount.py +++ b/okx/SubAccount.py @@ -1,10 +1,10 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class SubAccountAPI(Client): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) +class SubAccountAPI(OkxClient): + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def get_account_balance(self, subAcct): params = {"subAcct": subAcct} @@ -59,5 +59,18 @@ def get_funding_balance(self,subAcct='',ccy=''): } return self._request_with_params(GET, GET_ASSET_SUBACCOUNT_BALANCE, params) + # - Set sub_accounts VIP loan% + def set_sub_accounts_VIP_loan(self, enable='', alloc=[]): + params = { + 'enable': enable, + 'alloc': alloc + } + return self._request_with_params(POST, SET_SUB_ACCOUNTS_VIP_LOAN, params) - + # - Get sub_account borrow interest and limit + def get_sub_account_borrow_interest_and_limit(self, subAcct='', ccy=''): + params = { + 'subAcct': subAcct, + 'ccy': ccy + } + return self._request_with_params(GET, GET_SUB_ACCOUNT_BORROW_INTEREST_AND_LIMIT, params) diff --git a/okx/Trade.py b/okx/Trade.py index 2db2e833..9d6d9e53 100644 --- a/okx/Trade.py +++ b/okx/Trade.py @@ -1,18 +1,26 @@ -from .client import Client +import json + +from .okxclient import OkxClient from .consts import * -class TradeAPI(Client): +class TradeAPI(OkxClient): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', + domain='https://www.okx.com', debug=False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) # Place Order def place_order(self, instId, tdMode, side, ordType, sz, ccy='', clOrdId='', tag='', posSide='', px='', - reduceOnly='', tgtCcy=''): + reduceOnly='', tgtCcy='', stpMode='', attachAlgoOrds=None, pxUsd='', pxVol='', banAmend='', tradeQuoteCcy=None, pxAmendType=None): params = {'instId': instId, 'tdMode': tdMode, 'side': side, 'ordType': ordType, 'sz': sz, 'ccy': ccy, 'clOrdId': clOrdId, 'tag': tag, 'posSide': posSide, 'px': px, 'reduceOnly': reduceOnly, - 'tgtCcy': tgtCcy} + 'tgtCcy': tgtCcy, 'stpMode': stpMode, 'pxUsd': pxUsd, 'pxVol': pxVol, 'banAmend': banAmend} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy + if pxAmendType is not None: + params['pxAmendType'] = pxAmendType + params['attachAlgoOrds'] = attachAlgoOrds return self._request_with_params(POST, PLACR_ORDER, params) # Place Multiple Orders @@ -22,17 +30,23 @@ def place_multiple_orders(self, orders_data): # Cancel Order def cancel_order(self, instId, ordId='', clOrdId=''): params = {'instId': instId, 'ordId': ordId, 'clOrdId': clOrdId} - return self._request_with_params(POST, CANAEL_ORDER, params) + return self._request_with_params(POST, CANCEL_ORDER, params) # Cancel Multiple Orders def cancel_multiple_orders(self, orders_data): - return self._request_with_params(POST, CANAEL_BATCH_ORDERS, orders_data) + return self._request_with_params(POST, CANCEL_BATCH_ORDERS, orders_data) # Amend Order - def amend_order(self, instId, cxlOnFail='', ordId='', clOrdId='', reqId='', newSz='', newPx=''): - params = {'instId': instId, 'cxlOnFailc': cxlOnFail, 'ordId': ordId, 'clOrdId': clOrdId, 'reqId': reqId, - 'newSz': newSz, - 'newPx': newPx} + def amend_order(self, instId, cxlOnFail='', ordId='', clOrdId='', reqId='', newSz='', newPx='', newTpTriggerPx='', + newTpOrdPx='', newSlTriggerPx='', newSlOrdPx='', newTpTriggerPxType='', newSlTriggerPxType='', + attachAlgoOrds='', newTriggerPx='', newOrdPx='', pxAmendType=None): + params = {'instId': instId, 'cxlOnFail': cxlOnFail, 'ordId': ordId, 'clOrdId': clOrdId, 'reqId': reqId, + 'newSz': newSz, 'newPx': newPx, 'newTpTriggerPx': newTpTriggerPx, 'newTpOrdPx': newTpOrdPx, + 'newSlTriggerPx': newSlTriggerPx, 'newSlOrdPx': newSlOrdPx, 'newTpTriggerPxType': newTpTriggerPxType, + 'newSlTriggerPxType': newSlTriggerPxType, 'newTriggerPx': newTriggerPx, 'newOrdPx': newOrdPx} + if pxAmendType is not None: + params['pxAmendType'] = pxAmendType + params['attachAlgoOrds'] = attachAlgoOrds return self._request_with_params(POST, AMEND_ORDER, params) # Amend Multiple Orders @@ -40,8 +54,9 @@ def amend_multiple_orders(self, orders_data): return self._request_with_params(POST, AMEND_BATCH_ORDER, orders_data) # Close Positions - def close_positions(self, instId, mgnMode, posSide='', ccy='',autoCxl=''): - params = {'instId': instId, 'mgnMode': mgnMode, 'posSide': posSide, 'ccy': ccy,'autoCxl':autoCxl} + def close_positions(self, instId, mgnMode, posSide='', ccy='', autoCxl='', clOrdId='', tag=''): + params = {'instId': instId, 'mgnMode': mgnMode, 'posSide': posSide, 'ccy': ccy, 'autoCxl': autoCxl, + 'clOrdId': clOrdId, 'tag': tag} return self._request_with_params(POST, CLOSE_POSITION, params) # Get Order Details @@ -50,27 +65,32 @@ def get_order(self, instId, ordId='', clOrdId=''): return self._request_with_params(GET, ORDER_INFO, params) # Get Order List - def get_order_list(self, instType='', uly='', instId='', ordType='', state='', after='', before='', limit='',instFamily = ''): + def get_order_list(self, instType='', uly='', instId='', ordType='', state='', after='', before='', limit='', + instFamily=''): params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state, - 'after': after, 'before': before, 'limit': limit,'instFamily':instFamily} + 'after': after, 'before': before, 'limit': limit, 'instFamily': instFamily} return self._request_with_params(GET, ORDERS_PENDING, params) # Get Order History (last 7 days) - def get_orders_history(self, instType, uly='', instId='', ordType='', state='', after='', before='', limit='',instFamily = ''): + def get_orders_history(self, instType, uly='', instId='', ordType='', state='', after='', before='', begin='', + end='', limit='', instFamily=''): params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state, - 'after': after, 'before': before, 'limit': limit,'instFamily':instFamily} + 'after': after, 'before': before, 'begin': begin, 'end': end, 'limit': limit, + 'instFamily': instFamily} return self._request_with_params(GET, ORDERS_HISTORY, params) # Get Order History (last 3 months) - def get_orders_history_archive(self, instType, uly='', instId='', ordType='', state='', after='', before='', limit='',instFamily = ''): + def get_orders_history_archive(self, instType, uly='', instId='', ordType='', state='', after='', before='', + begin='', end='', limit='', instFamily=''): params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state, - 'after': after, 'before': before, 'limit': limit,'instFamily':instFamily} + 'after': after, 'before': before, 'begin': begin, 'end': end, 'limit': limit, + 'instFamily': instFamily} return self._request_with_params(GET, ORDERS_HISTORY_ARCHIVE, params) # Get Transaction Details - def get_fills(self, instType='', uly='', instId='', ordId='', after='', before='', limit='',instFamily = ''): + def get_fills(self, instType='', uly='', instId='', ordId='', after='', before='', limit='', instFamily='',begin='',end=''): params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordId': ordId, 'after': after, 'before': before, - 'limit': limit,'instFamily':instFamily} + 'limit': limit, 'instFamily': instFamily,'begin': begin, 'end' :end} return self._request_with_params(GET, ORDER_FILLS, params) # Place Algo Order @@ -80,32 +100,35 @@ def place_algo_order(self, instId='', tdMode='', side='', ordType='', sz='', ccy triggerPx='', orderPx='', tgtCcy='', pxVar='', pxSpread='', szLimit='', pxLimit='', timeInterval='', tpTriggerPxType='', slTriggerPxType='', - callbackRatio='',callbackSpread='',activePx='',tag='',triggerPxType=''): + callbackRatio='', callbackSpread='', activePx='', tag='', triggerPxType='', closeFraction='' + , quickMgnType='', algoClOrdId='', tradeQuoteCcy=None, tpOrdKind='', cxlOnClosePos='' + , chaseType='', chaseVal='', maxChaseType='', maxChaseVal='', attachAlgoOrds=[], pxAmendType=None): params = {'instId': instId, 'tdMode': tdMode, 'side': side, 'ordType': ordType, 'sz': sz, 'ccy': ccy, 'posSide': posSide, 'reduceOnly': reduceOnly, 'tpTriggerPx': tpTriggerPx, 'tpOrdPx': tpOrdPx, 'slTriggerPx': slTriggerPx, 'slOrdPx': slOrdPx, 'triggerPx': triggerPx, 'orderPx': orderPx, 'tgtCcy': tgtCcy, 'pxVar': pxVar, 'szLimit': szLimit, 'pxLimit': pxLimit, 'timeInterval': timeInterval, 'pxSpread': pxSpread, 'tpTriggerPxType': tpTriggerPxType, 'slTriggerPxType': slTriggerPxType, - 'callbackRatio' : callbackRatio, 'callbackSpread':callbackSpread,'activePx':activePx, - 'tag':tag,'triggerPxType':triggerPxType,} + 'callbackRatio': callbackRatio, 'callbackSpread': callbackSpread, 'activePx': activePx, + 'tag': tag, 'triggerPxType': triggerPxType, 'closeFraction': closeFraction, + 'quickMgnType': quickMgnType, 'algoClOrdId': algoClOrdId, + 'tpOrdKind': tpOrdKind, 'cxlOnClosePos': cxlOnClosePos, 'chaseType': chaseType, 'chaseVal': chaseVal, + 'maxChaseType': maxChaseType, 'maxChaseVal': maxChaseVal, 'attachAlgoOrds': attachAlgoOrds} + if tradeQuoteCcy is not None: + params['tradeQuoteCcy'] = tradeQuoteCcy + if pxAmendType is not None: + params['pxAmendType'] = pxAmendType return self._request_with_params(POST, PLACE_ALGO_ORDER, params) - - # Cancel Algo Order def cancel_algo_order(self, params): return self._request_with_params(POST, CANCEL_ALGOS, params) - # Cancel Advance Algos - def cancel_advance_algos(self,params): - return self._request_with_params(POST, Cancel_Advance_Algos, params) - # Get Algo Order List - def order_algos_list(self, ordType ='', algoId='', instType='', instId='', after='', before='', limit=''): + def order_algos_list(self, ordType='', algoId='', instType='', instId='', after='', before='', limit=''): params = {'ordType': ordType, 'algoId': algoId, 'instType': instType, 'instId': instId, 'after': after, 'before': before, 'limit': limit} - return self._request_with_params(GET, ORDERS_ALGO_OENDING, params) + return self._request_with_params(GET, ORDERS_ALGO_PENDING, params) # Get Algo Order History def order_algos_history(self, ordType, state='', algoId='', instType='', instId='', after='', before='', limit=''): @@ -114,46 +137,79 @@ def order_algos_history(self, ordType, state='', algoId='', instType='', instId= return self._request_with_params(GET, ORDERS_ALGO_HISTORY, params) # Get Transaction Details History - def get_fills_history(self, instType, uly='', instId='', ordId='', after='', before='', limit='',instFamily=''): + def get_fills_history(self, instType, uly='', instId='', ordId='', after='', before='', limit='', instFamily=''): params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordId': ordId, 'after': after, 'before': before, - 'limit': limit,'instFamily':instFamily} + 'limit': limit, 'instFamily': instFamily} return self._request_with_params(GET, ORDERS_FILLS_HISTORY, params) def get_easy_convert_currency_list(self): return self._request_without_params(GET, EASY_CONVERT_CURRENCY_LIST) - def easy_convert(self,fromCcy = [],toCcy = ''): + def easy_convert(self, fromCcy=[], toCcy=''): params = { - 'fromCcy':fromCcy, - 'toCcy':toCcy + 'fromCcy': fromCcy, + 'toCcy': toCcy } return self._request_with_params(POST, EASY_CONVERT, params) - def get_easy_convert_history(self,before = '',after = '',limit = ''): + def get_easy_convert_history(self, before='', after='', limit=''): params = { - 'before':before, - 'after':after, - 'limit':limit + 'before': before, + 'after': after, + 'limit': limit } - return self._request_with_params(GET,CONVERT_EASY_HISTORY,params) + return self._request_with_params(GET, CONVERT_EASY_HISTORY, params) - def get_oneclick_repay_list(self,debtType = ''): + def get_oneclick_repay_list(self, debtType=''): params = { - 'debtType':debtType + 'debtType': debtType } - return self._request_with_params(GET,ONE_CLICK_REPAY_SUPPORT,params) + return self._request_with_params(GET, ONE_CLICK_REPAY_SUPPORT, params) - def oneclick_repay(self,debtCcy = [] , repayCcy=''): + def oneclick_repay(self, debtCcy=[], repayCcy=''): + params = { + 'debtCcy': debtCcy, + 'repayCcy': repayCcy + } + return self._request_with_params(POST, ONE_CLICK_REPAY, params) + + def oneclick_repay_history(self, after='', before='', limit=''): + params = { + 'after': after, + 'before': before, + 'limit': limit + } + return self._request_with_params(GET, ONE_CLICK_REPAY_HISTORY, params) + + # Get algo order details + def get_algo_order_details(self, algoId='', algoClOrdId=''): + params = {'algoId': algoId, 'algoClOrdId': algoClOrdId} + return self._request_with_params(GET, GET_ALGO_ORDER_DETAILS, params) + + # Amend algo order + def amend_algo_order(self, instId='', algoId='', algoClOrdId='', cxlOnFail='', reqId='', newSz='', newTriggerPx='', newOrdPx='', + newTpTriggerPx='', newTpOrdPx='', newSlTriggerPx='', newSlOrdPx='', newTpTriggerPxType='', + newSlTriggerPxType=''): + params = {'instId': instId, 'algoId': algoId, 'algoClOrdId': algoClOrdId, 'cxlOnFail': cxlOnFail, + 'reqId': reqId, 'newSz': newSz, 'newTriggerPx': newTriggerPx, 'newOrdPx': newOrdPx, 'newTpTriggerPx': newTpTriggerPx, 'newTpOrdPx': newTpOrdPx, + 'newSlTriggerPx': newSlTriggerPx, 'newSlOrdPx': newSlOrdPx, + 'newTpTriggerPxType': newTpTriggerPxType, 'newSlTriggerPxType': newSlTriggerPxType} + return self._request_with_params(POST, AMEND_ALGO_ORDER, params) + + def get_oneclick_repay_list_v2(self): + return self._request_without_params(GET, ONE_CLICK_REPAY_SUPPORT_V2) + + def oneclick_repay_v2(self, debtCcy='', repayCcyList=[]): params = { - 'debtCcy':debtCcy, - 'repayCcy':repayCcy + 'debtCcy': debtCcy, + 'repayCcyList': repayCcyList } - return self._request_with_params(POST,ONE_CLICK_REPAY,params) + return self._request_with_params(POST, ONE_CLICK_REPAY_V2, params) - def oneclick_repay_history(self,after = '',before = '',limit = ''): + def oneclick_repay_history_v2(self, after='', before='', limit=''): params = { - 'after':after, - 'before':before, - 'limit':limit + 'after': after, + 'before': before, + 'limit': limit } - return self._request_with_params(GET,ONE_CLICK_REPAY_HISTORY,params) + return self._request_with_params(GET, ONE_CLICK_REPAY_HISTORY_V2, params) diff --git a/okx/TradingData.py b/okx/TradingData.py index 0d8bf70c..0c900707 100644 --- a/okx/TradingData.py +++ b/okx/TradingData.py @@ -1,11 +1,11 @@ -from .client import Client +from .okxclient import OkxClient from .consts import * -class TradingDataAPI(Client): +class TradingDataAPI(OkxClient): - def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=False, flag='1', domain = 'https://www.okx.com',debug = True): - Client.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain,debug) + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1', domain = 'https://www.okx.com',debug = False, proxy=None): + OkxClient.__init__(self, api_key, api_secret_key, passphrase, use_server_time, flag, domain, debug, proxy) def get_support_coin(self): @@ -47,6 +47,31 @@ def get_taker_block_volume(self, ccy, period=''): params = {'ccy': ccy, 'period': period} return self._request_with_params(GET, TAKER_FLOW, params) + def get_open_interest_history(self, instId, period=None, begin=None, end=None, limit=None): + """ + Get contract open interest history + Retrieve the contract open interest statistics of futures and perp. + Rate limit: 10 requests per 2 seconds + Rate limit rule: IP + Instrument ID + + :param instId: Instrument ID, e.g. BTC-USDT-SWAP. Only applicable to FUTURES, SWAP + :param period: Bar size, the default is 5m, e.g. [5m/15m/30m/1H/2H/4H] + :param begin: Return records newer than the requested ts + :param end: Pagination of data to return records earlier than the requested ts + :param limit: Number of results per request. The maximum is 100. The default is 100 + :return: API response + """ + params = {'instId': instId} + if period is not None: + params['period'] = period + if begin is not None: + params['begin'] = begin + if end is not None: + params['end'] = end + if limit is not None: + params['limit'] = limit + return self._request_with_params(GET, CONTRACTS_OPEN_INTEREST_HISTORY, params) + diff --git a/okx/__init__.py b/okx/__init__.py index 71c25d8f..57f92fea 100644 --- a/okx/__init__.py +++ b/okx/__init__.py @@ -2,4 +2,4 @@ Python SDK for the OKX API v5 """ -__version__="0.1.0" \ No newline at end of file +__version__="0.4.1" \ No newline at end of file diff --git a/okx/__pycache__/Broker_api.cpython-38.pyc b/okx/__pycache__/Broker_api.cpython-38.pyc deleted file mode 100644 index f22616ab..00000000 Binary files a/okx/__pycache__/Broker_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/Market_api.cpython-38.pyc b/okx/__pycache__/Market_api.cpython-38.pyc deleted file mode 100644 index d2d5e4d3..00000000 Binary files a/okx/__pycache__/Market_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/Public_api.cpython-38.pyc b/okx/__pycache__/Public_api.cpython-38.pyc deleted file mode 100644 index 229588cd..00000000 Binary files a/okx/__pycache__/Public_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/Rfq_api.cpython-38.pyc b/okx/__pycache__/Rfq_api.cpython-38.pyc deleted file mode 100644 index 84090aa2..00000000 Binary files a/okx/__pycache__/Rfq_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/TradingBot_api.cpython-38.pyc b/okx/__pycache__/TradingBot_api.cpython-38.pyc deleted file mode 100644 index 77cbb32b..00000000 Binary files a/okx/__pycache__/TradingBot_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/__init__.cpython-38.pyc b/okx/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 56cc821d..00000000 Binary files a/okx/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/__init__.cpython-39.pyc b/okx/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index b8c662ba..00000000 Binary files a/okx/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/okx/__pycache__/client.cpython-38.pyc b/okx/__pycache__/client.cpython-38.pyc deleted file mode 100644 index aba0e305..00000000 Binary files a/okx/__pycache__/client.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/client.cpython-39.pyc b/okx/__pycache__/client.cpython-39.pyc deleted file mode 100644 index 353bc601..00000000 Binary files a/okx/__pycache__/client.cpython-39.pyc and /dev/null differ diff --git a/okx/__pycache__/consts.cpython-38.pyc b/okx/__pycache__/consts.cpython-38.pyc deleted file mode 100644 index e0d7f968..00000000 Binary files a/okx/__pycache__/consts.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/consts.cpython-39.pyc b/okx/__pycache__/consts.cpython-39.pyc deleted file mode 100644 index 23716f33..00000000 Binary files a/okx/__pycache__/consts.cpython-39.pyc and /dev/null differ diff --git a/okx/__pycache__/exceptions.cpython-38.pyc b/okx/__pycache__/exceptions.cpython-38.pyc deleted file mode 100644 index 9f2068bd..00000000 Binary files a/okx/__pycache__/exceptions.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/exceptions.cpython-39.pyc b/okx/__pycache__/exceptions.cpython-39.pyc deleted file mode 100644 index 02bfc270..00000000 Binary files a/okx/__pycache__/exceptions.cpython-39.pyc and /dev/null differ diff --git a/okx/__pycache__/status_api.cpython-38.pyc b/okx/__pycache__/status_api.cpython-38.pyc deleted file mode 100644 index fc7e9dc7..00000000 Binary files a/okx/__pycache__/status_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/subAccount_api.cpython-38.pyc b/okx/__pycache__/subAccount_api.cpython-38.pyc deleted file mode 100644 index 48781463..00000000 Binary files a/okx/__pycache__/subAccount_api.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/utils.cpython-38.pyc b/okx/__pycache__/utils.cpython-38.pyc deleted file mode 100644 index 675772ed..00000000 Binary files a/okx/__pycache__/utils.cpython-38.pyc and /dev/null differ diff --git a/okx/__pycache__/utils.cpython-39.pyc b/okx/__pycache__/utils.cpython-39.pyc deleted file mode 100644 index a801254b..00000000 Binary files a/okx/__pycache__/utils.cpython-39.pyc and /dev/null differ diff --git a/okx/client.py b/okx/client.py deleted file mode 100644 index 26ae6033..00000000 --- a/okx/client.py +++ /dev/null @@ -1,57 +0,0 @@ -import json - -import httpx - -from . import consts as c, utils, exceptions - - -class Client(object): - - def __init__(self, api_key = '-1', api_secret_key = '-1', passphrase = '-1', use_server_time=False, flag='1', base_api = 'https://www.okx.com',debug = 'True'): - - self.API_KEY = api_key - self.API_SECRET_KEY = api_secret_key - self.PASSPHRASE = passphrase - self.use_server_time = use_server_time - self.flag = flag - self.domain = base_api - self.debug = debug - self.client = httpx.Client(base_url=base_api, http2=True) - - def _request(self, method, request_path, params): - if method == c.GET: - request_path = request_path + utils.parse_params_to_str(params) - timestamp = utils.get_timestamp() - if self.use_server_time: - timestamp = self._get_timestamp() - body = json.dumps(params) if method == c.POST else "" - if self.API_KEY != '-1': - sign = utils.sign(utils.pre_hash(timestamp, method, request_path, str(body), self.debug), self.API_SECRET_KEY) - header = utils.get_header(self.API_KEY, sign, timestamp, self.PASSPHRASE, self.flag, self.debug) - else: - header = utils.get_header_no_sign(self.flag, self.debug) - response = None - if self.debug == True: - print('domain:',self.domain) - print('url:',request_path) - if method == c.GET: - response = self.client.get(request_path, headers=header) - elif method == c.POST: - response = self.client.post(request_path, data=body, headers=header) - if not str(response.status_code).startswith('2'): - raise exceptions.OkxAPIException(response) - return response.json() - - def _request_without_params(self, method, request_path): - return self._request(method, request_path, {}) - - def _request_with_params(self, method, request_path, params): - return self._request(method, request_path, params) - - def _get_timestamp(self): - request_path = c.API_URL + c.SERVER_TIMESTAMP_URL - response = self.client.get(request_path) - if response.status_code == 200: - return response.json()['ts'] - else: - return "" diff --git a/okx/consts.py b/okx/consts.py index 833e44f5..e58d9af7 100644 --- a/okx/consts.py +++ b/okx/consts.py @@ -1,5 +1,5 @@ -# http header -#API_URL = 'https://www.okx.com' +# Http header +API_URL = 'https://www.okx.com' CONTENT_TYPE = 'Content-Type' OK_ACCESS_KEY = 'OK-ACCESS-KEY' @@ -18,7 +18,7 @@ SERVER_TIMESTAMP_URL = '/api/v5/public/time' -# account-complete-testcomplete +# Account POSITION_RISK='/api/v5/account/account-position-risk' ACCOUNT_INFO = '/api/v5/account/balance' POSITION_INFO = '/api/v5/account/positions' @@ -44,33 +44,52 @@ INTEREST_LIMITS = '/api/v5/account/interest-limits' SIMULATED_MARGIN = '/api/v5/account/simulated_margin' GREEKS = '/api/v5/account/greeks' -POSITIONS_HISTORY = '/api/v5/account/positions-history' #need add -GET_PM_LIMIT = '/api/v5/account/position-tiers' #need add +POSITIONS_HISTORY = '/api/v5/account/positions-history' +GET_PM_LIMIT = '/api/v5/account/position-tiers' +GET_VIP_INTEREST_ACCRUED_DATA = '/api/v5/account/vip-interest-accrued' +GET_VIP_INTEREST_DEDUCTED_DATA = '/api/v5/account/vip-interest-deducted' +GET_VIP_LOAN_ORDER_LIST= '/api/v5/account/vip-loan-order-list' +GET_VIP_LOAN_ORDER_DETAIL= '/api/v5/account/vip-loan-order-detail' +SET_RISK_OFFSET_TYPE = '/api/v5/account/set-riskOffset-type' +SET_AUTO_LOAN = '/api/v5/account/set-auto-loan' +SET_ACCOUNT_LEVEL = '/api/v5/account/set-account-level' +ACTIVSTE_OPTION = '/api/v5/account/activate-option' +POSITION_BUILDER = '/api/v5/account/position-builder' +GET_INSTRUMENTS = '/api/v5/account/instruments' +BORROWING_LIMIT = '/api/v5/account/fixed-loan/borrowing-limit' +BORROWING_QUOTE = '/api/v5/account/fixed-loan/borrowing-quote' +PLACE_BORROWING_ORDER= '/api/v5/account/fixed-loan/borrowing-order' +AMEND_BORROWING_ORDER='/api/v5/account/fixed-loan/amend-borrowing-order' +MANUAL_REBORROW = '/api/v5/account/fixed-loan/manual-reborrow' +REPAY_BORROWING_ORDER='/api/v5/account/fixed-loan/repay-borrowing-order' +BORROWING_ORDERS_LIST='/api/v5/account/fixed-loan/borrowing-orders-list' +MANUAL_REBORROW_REPAY = '/api/v5/account/spot-manual-borrow-repay' +SET_AUTO_REPAY='/api/v5/account/set-auto-repay' +GET_BORROW_REPAY_HISTORY='/api/v5/account/spot-borrow-repay-history' +SET_AUTO_EARN='/api/v5/account/set-auto-earn' -# funding-complete-testcomplete +# Funding +NON_TRADABLE_ASSETS = '/api/v5/asset/non-tradable-assets' DEPOSIT_ADDRESS = '/api/v5/asset/deposit-address' GET_BALANCES = '/api/v5/asset/balances' FUNDS_TRANSFER = '/api/v5/asset/transfer' TRANSFER_STATE = '/api/v5/asset/transfer-state' WITHDRAWAL_COIN = '/api/v5/asset/withdrawal' -DEPOSIT_HISTORIY = '/api/v5/asset/deposit-history' +DEPOSIT_HISTORY = '/api/v5/asset/deposit-history' CURRENCY_INFO = '/api/v5/asset/currencies' PURCHASE_REDEMPT = '/api/v5/asset/purchase_redempt' BILLS_INFO = '/api/v5/asset/bills' DEPOSIT_LIGHTNING = '/api/v5/asset/deposit-lightning' WITHDRAWAL_LIGHTNING = '/api/v5/asset/withdrawal-lightning' -CANCEL_WITHDRAWAL = '/api/v5/asset/cancel-withdrawal' #need add -WITHDRAWAL_HISTORIY = '/api/v5/asset/withdrawal-history' -CONVERT_DUST_ASSETS = '/api/v5/asset/convert-dust-assets' #need add -ASSET_VALUATION = '/api/v5/asset/asset-valuation' #need add -SET_LENDING_RATE = '/api/v5/asset/set-lending-rate' -LENDING_HISTORY = '/api/v5/asset/lending-history' -LENDING_RATE_HISTORY = '/api/v5/asset/lending-rate-history' -LENDING_RATE_SUMMARY = '/api/v5/asset/lending-rate-summary' -GET_SAVING_BALANCE = '/api/v5/asset/saving-balance' #need to add - - -# Market Data-Complete-testComplete +CANCEL_WITHDRAWAL = '/api/v5/asset/cancel-withdrawal' +CONVERT_DUST_ASSETS = '/api/v5/asset/convert-dust-assets' +ASSET_VALUATION = '/api/v5/asset/asset-valuation' +GET_WITHDRAWAL_HISTORY = '/api/v5/asset/withdrawal-history' +GET_NON_TRADABLE_ASSETS = '/api/v5/asset/non-tradable-assets' +GET_DEPOSIT_WITHDrAW_STATUS = '/api/v5/asset/deposit-withdraw-status' + + +# Market Data TICKERS_INFO = '/api/v5/market/tickers' TICKER_INFO = '/api/v5/market/ticker' INDEX_TICKERS = '/api/v5/market/index-tickers' @@ -81,15 +100,16 @@ MARKPRICE_CANDLES = '/api/v5/market/mark-price-candles' MARKET_TRADES = '/api/v5/market/trades' VOLUMNE = '/api/v5/market/platform-24-volume' -ORACLE = '/api/v5/market/open-oracle' #need to update? if it is open oracle -INDEX_COMPONENTS = '/api/v5/market/index-components' #need to add -EXCHANGE_RATE = '/api/v5/market/exchange-rate' #need to add -HISTORY_TRADES = '/api/v5/market/history-trades' #need to add -BLOCK_TICKERS = '/api/v5/market/block-tickers' #need to add -BLOCK_TICKER = '/api/v5/market/block-ticker'#need to add -BLOCK_TRADES = '/api/v5/market/block-trades'#need to add - -# Public Data-Complete-testComplete +INDEX_COMPONENTS = '/api/v5/market/index-components' +EXCHANGE_RATE = '/api/v5/market/exchange-rate' +HISTORY_TRADES = '/api/v5/market/history-trades' +BLOCK_TICKERS = '/api/v5/market/block-tickers' +BLOCK_TICKER = '/api/v5/market/block-ticker' +BLOCK_TRADES = '/api/v5/market/block-trades' +GET_ORDER_LITE_BOOK = '/api/v5/market/books-lite' +GET_OPTION_TRADES = '/api/v5/market/option/instrument-family-trades' + +# Public Data INSTRUMENT_INFO = '/api/v5/public/instruments' DELIVERY_EXERCISE = '/api/v5/public/delivery-exercise-history' OPEN_INTEREST = '/api/v5/public/open-interest' @@ -103,13 +123,16 @@ LIQUIDATION_ORDERS = '/api/v5/public/liquidation-orders' MARK_PRICE = '/api/v5/public/mark-price' TIER = '/api/v5/public/position-tiers' -INTEREST_LOAN = '/api/v5/public/interest-rate-loan-quota' #need to add -UNDERLYING = '/api/v5/public/underlying' #need to add -VIP_INTEREST_RATE_LOAN_QUOTA = '/api/v5/public/vip-interest-rate-loan-quota' #need to add -INSURANCE_FUND = '/api/v5/public/insurance-fund'#need to add -CONVERT_CONTRACT_COIN = '/api/v5/public/convert-contract-coin' #need to add +INTEREST_LOAN = '/api/v5/public/interest-rate-loan-quota' +UNDERLYING = '/api/v5/public/underlying' +VIP_INTEREST_RATE_LOAN_QUOTA = '/api/v5/public/vip-interest-rate-loan-quota' +INSURANCE_FUND = '/api/v5/public/insurance-fund' +CONVERT_CONTRACT_COIN = '/api/v5/public/convert-contract-coin' +GET_OPTION_TICKBANDS = '/api/v5/public/instrument-tick-bands' +GET_OPTION_TRADES = '/api/v5/public/option-trades' +MARKET_DATA_HISTORY = '/api/v5/public/market-data-history' -# TRADING DATA-COMPLETE +# Trading data SUPPORT_COIN = '/api/v5/rubik/stat/trading-data/support-coin' TAKER_VOLUME = '/api/v5/rubik/stat/taker-volume' MARGIN_LENDING_RATIO = '/api/v5/rubik/stat/margin/loan-ratio' @@ -120,12 +143,13 @@ OPEN_INTEREST_VOLUME_EXPIRY = '/api/v5/rubik/stat/option/open-interest-volume-expiry' INTEREST_VOLUME_STRIKE = '/api/v5/rubik/stat/option/open-interest-volume-strike' TAKER_FLOW = '/api/v5/rubik/stat/option/taker-block-volume' +CONTRACTS_OPEN_INTEREST_HISTORY = '/api/v5/rubik/stat/contracts/open-interest-history' -# TRADE-Complete +# Trade PLACR_ORDER = '/api/v5/trade/order' BATCH_ORDERS = '/api/v5/trade/batch-orders' -CANAEL_ORDER = '/api/v5/trade/cancel-order' -CANAEL_BATCH_ORDERS = '/api/v5/trade/cancel-batch-orders' +CANCEL_ORDER = '/api/v5/trade/cancel-order' +CANCEL_BATCH_ORDERS = '/api/v5/trade/cancel-batch-orders' AMEND_ORDER = '/api/v5/trade/amend-order' AMEND_BATCH_ORDER = '/api/v5/trade/amend-batch-orders' CLOSE_POSITION = '/api/v5/trade/close-position' @@ -137,59 +161,45 @@ ORDERS_FILLS_HISTORY = '/api/v5/trade/fills-history' PLACE_ALGO_ORDER = '/api/v5/trade/order-algo' CANCEL_ALGOS = '/api/v5/trade/cancel-algos' -Cancel_Advance_Algos = '/api/v5/trade/cancel-advance-algos' -ORDERS_ALGO_OENDING = '/api/v5/trade/orders-algo-pending' +ORDERS_ALGO_PENDING = '/api/v5/trade/orders-algo-pending' ORDERS_ALGO_HISTORY = '/api/v5/trade/orders-algo-history' - +GET_ALGO_ORDER_DETAILS = '/api/v5/trade/order-algo' +AMEND_ALGO_ORDER = '/api/v5/trade/amend-algos' EASY_CONVERT_CURRENCY_LIST = '/api/v5/trade/easy-convert-currency-list' EASY_CONVERT = '/api/v5/trade/easy-convert' CONVERT_EASY_HISTORY = '/api/v5/trade/easy-convert-history' ONE_CLICK_REPAY_SUPPORT = '/api/v5/trade/one-click-repay-currency-list' ONE_CLICK_REPAY = '/api/v5/trade/one-click-repay' ONE_CLICK_REPAY_HISTORY = '/api/v5/trade/one-click-repay-history' +ONE_CLICK_REPAY_SUPPORT_V2 = '/api/v5/trade/one-click-repay-currency-list-v2' +ONE_CLICK_REPAY_V2 = '/api/v5/trade/one-click-repay-v2' +ONE_CLICK_REPAY_HISTORY_V2 = '/api/v5/trade/one-click-repay-history-v2' -# SubAccount-complete-testwriteComplete +# SubAccount BALANCE = '/api/v5/account/subaccount/balances' BILLs = '/api/v5/asset/subaccount/bills' RESET = '/api/v5/users/subaccount/modify-apikey' VIEW_LIST = '/api/v5/users/subaccount/list' SUBACCOUNT_TRANSFER = '/api/v5/asset/subaccount/transfer' -ENTRUST_SUBACCOUNT_LIST = '/api/v5/users/entrust-subaccount-list' #need to add -SET_TRSNSFER_OUT = '/api/v5/users/subaccount/set-transfer-out' #need to add -GET_ASSET_SUBACCOUNT_BALANCE = '/api/v5/asset/subaccount/balances' #need to add - -# Broker-all need to implmented-completed -BROKER_INFO = '/api/v5/broker/nd/info' -CREATE_SUBACCOUNT = '/api/v5/broker/nd/create-subaccount' -DELETE_SUBACCOUNT = '/api/v5/broker/nd/delete-subaccount' -SUBACCOUNT_INFO = '/api/v5/broker/nd/subaccount-info' -SET_SUBACCOUNT_LEVEL = '/api/v5/broker/nd/set-subaccount-level' -SET_SUBACCOUNT_FEE_REAT = '/api/v5/broker/nd/set-subaccount-fee-rate' -SUBACCOUNT_DEPOSIT_ADDRESS = '/api/v5/asset/broker/nd/subaccount-deposit-address' -SUBACCOUNT_DEPOSIT_HISTORY = '/api/v5/asset/broker/nd/subaccount-deposit-history' -REBATE_DAILY = '/api/v5/broker/nd/rebate-daily' -ND_CREAET_APIKEY = '/api/v5/broker/nd/subaccount/apikey' -ND_SELECT_APIKEY = '/api/v5/broker/nd/subaccount/apikey' -ND_MODIFY_APIKEY = '/api/v5/broker/nd/subaccount/modify-apikey' -ND_DELETE_APIKEY = '/api/v5/broker/nd/subaccount/delete-apikey' -GET_REBATE_PER_ORDERS = '/api/v5/broker/nd/rebate-per-orders' -REBATE_PER_ORDERS = '/api/v5/broker/nd/rebate-per-orders' -MODIFY_SUBACCOUNT_DEPOSIT_ADDRESS = '/api/v5/asset/broker/nd/modify-subaccount-deposit-address' -GET_SUBACCOUNT_DEPOSIT='/api/v5/asset/broker/nd/subaccount-deposit-address' - -# Convert-Complete +ENTRUST_SUBACCOUNT_LIST = '/api/v5/users/entrust-subaccount-list' +SET_TRSNSFER_OUT = '/api/v5/users/subaccount/set-transfer-out' +GET_ASSET_SUBACCOUNT_BALANCE = '/api/v5/asset/subaccount/balances' +SET_SUB_ACCOUNTS_VIP_LOAN = '/api/v5/account/subaccount/set-loan-allocation' +GET_SUB_ACCOUNT_BORROW_INTEREST_AND_LIMIT = '/api/v5/account/subaccount/interest-limits' + +# Convert GET_CURRENCIES = '/api/v5/asset/convert/currencies' GET_CURRENCY_PAIR = '/api/v5/asset/convert/currency-pair' ESTIMATE_QUOTE = '/api/v5/asset/convert/estimate-quote' CONVERT_TRADE = '/api/v5/asset/convert/trade' CONVERT_HISTORY = '/api/v5/asset/convert/history' -# FDBroker -completed +# FD Broker FD_GET_REBATE_PER_ORDERS = '/api/v5/broker/fd/rebate-per-orders' FD_REBATE_PER_ORDERS = '/api/v5/broker/fd/rebate-per-orders' -# Rfq/BlcokTrading-completed +# BlockTrading COUNTERPARTIES = '/api/v5/rfq/counterparties' CREATE_RFQ = '/api/v5/rfq/create-rfq' CANCEL_RFQ = '/api/v5/rfq/cancel-rfq' @@ -208,7 +218,7 @@ MARKER_INSTRUMENT_SETTING = '/api/v5/rfq/maker-instrument-settings' -# tradingBot-Grid-complete-testcomplete +# Trading Bot GRID_ORDER_ALGO = '/api/v5/tradingBot/grid/order-algo' GRID_AMEND_ORDER_ALGO = '/api/v5/tradingBot/grid/amend-order-algo' GRID_STOP_ORDER_ALGO = '/api/v5/tradingBot/grid/stop-order-algo' @@ -218,18 +228,78 @@ GRID_SUB_ORDERS = '/api/v5/tradingBot/grid/sub-orders' GRID_POSITIONS = '/api/v5/tradingBot/grid/positions' GRID_WITHDRAW_INCOME = '/api/v5/tradingBot/grid/withdraw-income' -#--------need to add: GRID_COMPUTE_MARIGIN_BALANCE = '/api/v5/tradingBot/grid/compute-margin-balance' GRID_MARGIN_BALANCE = '/api/v5/tradingBot/grid/margin-balance' GRID_AI_PARAM = '/api/v5/tradingBot/grid/ai-param' +PLACE_RECURRING_BUY_ORDER = '/api/v5/tradingBot/recurring/order-algo' +AMEND_RECURRING_BUY_ORDER = '/api/v5/tradingBot/recurring/amend-order-algo' +STOP_RECURRING_BUY_ORDER = '/api/v5/tradingBot/recurring/stop-order-algo' +GET_RECURRING_BUY_ORDER_LIST = '/api/v5/tradingBot/recurring/orders-algo-pending' +GET_RECURRING_BUY_ORDER_HISTORY = '/api/v5/tradingBot/recurring/orders-algo-history' +GET_RECURRING_BUY_ORDER_DETAILS = '/api/v5/tradingBot/recurring/orders-algo-details' +GET_RECURRING_BUY_SUB_ORDERS = '/api/v5/tradingBot/recurring/sub-orders' -#stacking - all need to implement-testcomplete +# Stacking STACK_DEFI_OFFERS = '/api/v5/finance/staking-defi/offers' STACK_DEFI_PURCHASE = '/api/v5/finance/staking-defi/purchase' STACK_DEFI_REDEEM = '/api/v5/finance/staking-defi/redeem' STACK_DEFI_CANCEL = '/api/v5/finance/staking-defi/cancel' STACK_DEFI_ORDERS_ACTIVITY = '/api/v5/finance/staking-defi/orders-active' STACK_DEFI_ORDERS_HISTORY = '/api/v5/finance/staking-defi/orders-history' +GET_SAVING_BALANCE = '/api/v5/finance/savings/balance' +SAVING_PURCHASE_REDEMPTION = '/api/v5/finance/savings/purchase-redempt' +SET_LENDING_RATE = '/api/v5/finance/savings/set-lending-rate' +GET_LENDING_HISTORY = '/api/v5/finance/savings/lending-history' +GET_PUBLIC_BORROW_INFO = '/api/v5/finance/savings/lending-rate-summary' +GET_PUBLIC_BORROW_HISTORY = '/api/v5/finance/savings/lending-rate-history' +STACK_ETH_PRODUCT_INFO = '/api/v5/finance/staking-defi/eth/product-info' +STACK_ETH_PURCHASE = '/api/v5/finance/staking-defi/eth/purchase' +STACK_ETH_REDEEM = '/api/v5/finance/staking-defi/eth/redeem' +STACK_ETH_BALANCE = '/api/v5/finance/staking-defi/eth/balance' +STACK_ETH_PURCHASE_REDEEM_HISTORY = '/api/v5/finance/staking-defi/eth/purchase-redeem-history' +STACK_ETH_APY_HISTORY = '/api/v5/finance/staking-defi/eth/apy-history' +STACK_SOL_PURCHASE = '/api/v5/finance/staking-defi/sol/purchase' +STACK_SOL_REDEEM = '/api/v5/finance/staking-defi/sol/redeem' +STACK_SOL_BALANCE = '/api/v5/finance/staking-defi/sol/balance' +STACK_SOL_PURCHASE_REDEEM_HISTORY = '/api/v5/finance/staking-defi/sol/purchase-redeem-history' +STACK_SOL_APY_HISTORY = '/api/v5/finance/staking-defi/sol/apy-history' +STACK_SOL_PRODUCT_INFO = '/api/v5/finance/staking-defi/sol/product-info' -# status-complete +# Status STATUS = '/api/v5/system/status' + +# Copy Trading +GET_EXISTING_LEADING_POSITIONS = '/api/v5/copytrading/current-subpositions' +GET_LEADING_POSITIONS_HISTORY = '/api/v5/copytrading/subpositions-history' +PLACE_LEADING_STOP_ORDER = '/api/v5/copytrading/algo-order' +CLOSE_LEADING_POSITIONS = '/api/v5/copytrading/close-subposition' +GET_LEADING_POSITIONS = '/api/v5/copytrading/instruments' +AMEND_EXISTING_LEADING_POSITIONS = '/api/v5/copytrading/set-instruments' +GET_PROFIT_SHARING_DETAILS = '/api/v5/copytrading/profit-sharing-details' +GET_TOTAL_PROFIT_SHARING = '/api/v5/copytrading/total-profit-sharing' +GET_UNREALIZED_PROFIT_SHARING_DETAILS = '/api/v5/copytrading/unrealized-profit-sharing-details' + +# Spread Trading˚ +SPREAD_PLACE_ORDER= '/api/v5/sprd/order' +SPREAD_CANCEL_ORDER = '/api/v5/sprd/cancel-order' +SPREAD_CANCEL_ALL_ORDERS = '/api/v5/sprd/mass-cancel' +SPREAD_GET_ORDER_DETAILS = '/api/v5/sprd/order' +SPREAD_GET_ACTIVE_ORDERS = '/api/v5/sprd/orders-pending' +SPREAD_GET_ORDERS = '/api/v5/sprd/orders-history' +SPREAD_GET_TRADES = '/api/v5/sprd/trades' +SPREAD_GET_SPREADS = '/api/v5/sprd/spreads' +SPREAD_GET_ORDER_BOOK = '/api/v5/sprd/books' +SPREAD_GET_TICKER = '/api/v5/sprd/ticker' +SPREAD_GET_PUBLIC_TRADES = '/api/v5/sprd/public-trades' + +# Flexible loan +FINANCE_BORROW_CURRENCIES = '/api/v5/finance/flexible-loan/borrow-currencies' +FINANCE_COLLATERAL_ASSETS = '/api/v5/finance/flexible-loan/collateral-assets' +FINANCE_MAX_LOAN = '/api/v5/finance/flexible-loan/max-loan' +FINANCE_MAX_REDEEM = '/api/v5/finance/flexible-loan/max-collateral-redeem-amount' +FINANCE_ADJUST_COLLATERAL = '/api/v5/finance/flexible-loan/adjust-collateral' +FINANCE_LOAN_INFO = '/api/v5/finance/flexible-loan/loan-info' +FINANCE_LOAN_HISTORY = '/api/v5/finance/flexible-loan/loan-history' +FINANCE_INTEREST_ACCRUED = '/api/v5/finance/flexible-loan/interest-accrued' + + diff --git a/okx/exceptions.py b/okx/exceptions.py index e3dd1831..ee8e1803 100644 --- a/okx/exceptions.py +++ b/okx/exceptions.py @@ -4,7 +4,6 @@ class OkxAPIException(Exception): def __init__(self, response): - print(response.text + ', ' + str(response.status_code)) self.code = 0 try: json_res = response.json() diff --git a/okx/okxclient.py b/okx/okxclient.py new file mode 100644 index 00000000..6cd47517 --- /dev/null +++ b/okx/okxclient.py @@ -0,0 +1,73 @@ +import json +import warnings +from datetime import datetime, timezone + +import httpx +from httpx import Client +from datetime import datetime, timezone + +from loguru import logger + +from . import consts as c, utils, exceptions + + +class OkxClient(Client): + + def __init__(self, api_key='-1', api_secret_key='-1', passphrase='-1', use_server_time=None, flag='1',base_api=c.API_URL, debug=False, proxy=None): + # Compatible with different versions of httpx + # New versions (0.24.0+) use proxy, older versions use proxies + try: + super().__init__(base_url=base_api, http2=True, proxy=proxy) + except TypeError: + # Older versions of httpx use proxies parameter + if proxy: + super().__init__(base_url=base_api, http2=True, proxies={'http://': proxy, 'https://': proxy}) + else: + super().__init__(base_url=base_api, http2=True) + self.API_KEY = api_key + self.API_SECRET_KEY = api_secret_key + self.PASSPHRASE = passphrase + self.use_server_time = False + self.flag = flag + self.domain = base_api + self.debug = debug + if use_server_time is not None: + warnings.warn("use_server_time parameter is deprecated. Please remove it.", DeprecationWarning) + + def _request(self, method, request_path, params): + if method == c.GET: + request_path = request_path + utils.parse_params_to_str(params) + timestamp = utils.get_timestamp() + if self.use_server_time: + timestamp = self._get_timestamp() + body = json.dumps(params) if method == c.POST else "" + if self.API_KEY != '-1': + sign = utils.sign(utils.pre_hash(timestamp, method, request_path, str(body), self.debug), self.API_SECRET_KEY) + header = utils.get_header(self.API_KEY, sign, timestamp, self.PASSPHRASE, self.flag, self.debug) + else: + header = utils.get_header_no_sign(self.flag, self.debug) + response = None + if self.debug == True: + logger.debug(f'domain: {self.domain}') + logger.debug(f'url: {request_path}') + logger.debug(f'body:{body}') + if method == c.GET: + response = self.get(request_path, headers=header) + elif method == c.POST: + response = self.post(request_path, data=body, headers=header) + return response.json() + + def _request_without_params(self, method, request_path): + return self._request(method, request_path, {}) + + def _request_with_params(self, method, request_path, params): + return self._request(method, request_path, params) + + def _get_timestamp(self): + request_path = c.API_URL + c.SERVER_TIMESTAMP_URL + response = self.get(request_path) + if response.status_code == 200: + ts = datetime.fromtimestamp(int(response.json()['data'][0]['ts']) / 1000.0, tz=timezone.utc) + return ts.isoformat(timespec='milliseconds').replace('+00:00', 'Z') + else: + return "" diff --git a/okx/utils.py b/okx/utils.py index f130a9b5..a32b50a2 100644 --- a/okx/utils.py +++ b/okx/utils.py @@ -1,6 +1,9 @@ import hmac import base64 import datetime + +from loguru import logger + from . import consts as c @@ -12,7 +15,7 @@ def sign(message, secretKey): def pre_hash(timestamp, method, request_path, body,debug = True): if debug == True: - print('body: ',body) + logger.debug(f'body: {body}') return str(timestamp) + str.upper(method) + request_path + body @@ -25,7 +28,7 @@ def get_header(api_key, sign, timestamp, passphrase, flag,debug = True): header[c.OK_ACCESS_PASSPHRASE] = passphrase header['x-simulated-trading'] = flag if debug == True: - print('header: ',header) + logger.debug(f'header: {header}') return header def get_header_no_sign(flag,debug = True): @@ -33,16 +36,15 @@ def get_header_no_sign(flag,debug = True): header[c.CONTENT_TYPE] = c.APPLICATION_JSON header['x-simulated-trading'] = flag if debug == True: - print('header: ',header) + logger.debug(f'header: {header}') return header def parse_params_to_str(params): url = '?' for key, value in params.items(): - if(value != ''): + if value is not None and value != '': url = url + str(key) + '=' + str(value) + '&' url = url[0:-1] - #print('url:',url) return url diff --git a/okx/websocket/WebSocketFactory.py b/okx/websocket/WebSocketFactory.py new file mode 100644 index 00000000..ff983e6b --- /dev/null +++ b/okx/websocket/WebSocketFactory.py @@ -0,0 +1,32 @@ +import asyncio +import logging +import ssl + +import certifi +import websockets + +logger = logging.getLogger(__name__) + + +class WebSocketFactory: + + def __init__(self, url): + self.url = url + self.websocket = None + self.loop = asyncio.get_event_loop() + + async def connect(self): + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations(certifi.where()) + try: + self.websocket = await websockets.connect(self.url, ssl=ssl_context) + logger.info("WebSocket connection established.") + return self.websocket + except Exception as e: + logger.error(f"Error connecting to WebSocket: {e}") + return None + + async def close(self): + if self.websocket: + await self.websocket.close() + self.websocket = None diff --git a/okx/websocket/WsClientFactory.py b/okx/websocket/WsClientFactory.py deleted file mode 100644 index c016222b..00000000 --- a/okx/websocket/WsClientFactory.py +++ /dev/null @@ -1,49 +0,0 @@ -from autobahn.twisted.websocket import WebSocketClientFactory -from twisted.internet.protocol import ReconnectingClientFactory - -from .WsClientProtocol import * - - -class WsReconnectingClientFactory(ReconnectingClientFactory): - """ - @ivar maxDelay: Maximum number of seconds between connection attempts. - @ivar initialDelay: Delay for the first reconnection attempt. - @ivar maxRetries: Maximum number of consecutive unsuccessful connection - attempts, after which no further connection attempts will be made. If - this is not explicitly set, no maximum is applied. - """ - initialDelay = 0.1 - maxDelay = 2 - maxRetries = 5 - - -class WsClientFactory(WebSocketClientFactory, WsReconnectingClientFactory): - reachMaxRetriesError = {"e": "error", "m": "reached max connect retries"} - - def __init__(self, *args, payload=None, **kwargs): - WebSocketClientFactory.__init__(self, *args, **kwargs) - self.instance = None - self.subscribeSet = set() - self.payload = payload - self.logger = logging.getLogger(__name__) - - def startedConnecting(self, connector): - self.logger.info("WsClientFactory execute startedConnecting") - - def clientConnectionFailed(self, connector, reason): - self.logger.error( - "Can't connect to server. Reason: {}. Retrying: {}".format(reason, self.retries + 1)) - self.retry(connector) - if self.retries > self.maxRetries: - self.callback(self.reachMaxRetriesError) - - def clientConnectionLost(self, connector, reason): - self.logger.error("WsClientFactory execute clientConnectionLost. Reason: {},retried {} times".format(reason, - self.retries + 1)) - self.retry(connector) - if self.retries > self.maxRetries: - self.callback(self.reachMaxRetriesError) - - def buildProtocol(self, addr): - protocol = WsClientProtocol(self, payload=self.payload) - return protocol diff --git a/okx/websocket/WsClientProtocol.py b/okx/websocket/WsClientProtocol.py deleted file mode 100644 index b753215b..00000000 --- a/okx/websocket/WsClientProtocol.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -import logging - -from autobahn.twisted.websocket import WebSocketClientProtocol - - -class WsClientProtocol(WebSocketClientProtocol): - def __init__(self, factory, payload=None): - super().__init__() - self.autoPingInterval = 5 - self.factory = factory - self.payload = payload - self.logger = logging.getLogger(__name__) - - def onOpen(self): - self.factory.instance = self - - def onConnect(self, response): - self.logger.info("WsClientProtocol execute onConnect") - if self.payload: - self.logger.info("WsClientProtocol will Send message to OKX Server") - self.sendMessage(self.payload, isBinary=False) - self.factory.resetDelay() - - def onMessage(self, payload, isBinary): - self.logger.info("WsClientProtocol execute onMessage begin") - if not isBinary: - try: - payload_obj = json.loads(payload.decode("utf8")) - except Exception as e: - self.logger.error("WsClientProtocol onMessage error;e:{}".format(e)) - else: - self.factory.callback(payload_obj) - - def onClose(self, wasClean, code, reason): - self.logger.info( - "WsClientProtocol WS connection will be closed; wasClean={0}, code={1}, reason: {2}".format(wasClean, code, - reason)) - - def onPing(self, payload): - self.logger.info("WsClientProtocol execute onPing") - self.sendPong() - self.logger.info("WsClientProtocol execute onPing finish") - - def onPong(self, payload): - self.logger.info("WsClientProtocol execute onPong") diff --git a/okx/websocket/WsConnectManager.py b/okx/websocket/WsConnectManager.py deleted file mode 100644 index cd71edad..00000000 --- a/okx/websocket/WsConnectManager.py +++ /dev/null @@ -1,136 +0,0 @@ -import threading -import time - -from autobahn.twisted.websocket import connectWS -from twisted.internet import reactor -from twisted.internet.error import ReactorAlreadyRunning - -from . import WsUtils -from .WsClientFactory import * - - -class WsConnectManager(threading.Thread): - - def __init__(self, url, isPrivate): - threading.Thread.__init__(self) - self.factories = {} - self.isPrivate = isPrivate - self._connected_event = threading.Event() - self.url = url - self.conns = {} - self.callback = None - self.logger = logging.getLogger(__name__) - - def subscribeSocket(self, args: list, callback): - channelArgs = {} - channelParamMap = {} - WsUtils.checkSocketParams(args, channelArgs, channelParamMap) - if len(channelArgs) < 1: - return False - for channel in channelArgs: - subSet = channelParamMap.get(channel, set()) - if self.isPrivate: - privateKey = self.getPrivateKey(channel) - if privateKey not in self.factories: - reactor.callFromThread(self.loginSocket, channel) - time.sleep(2) - newFactory = self.initSubscribeFactory(args=channelArgs[channel], subSet=subSet, callback=callback) - reactor.callFromThread(self.resetConnection, newFactory, channel) - continue - factory = self.initSubscribeFactory(args=channelArgs[channel], subSet=subSet, callback=callback) - self.factories[channel] = factory - reactor.callFromThread(self.addConnection, channel) - - def unsubscribeSocket(self, args: list, callback): - channelArgs = {} - channelParamMap = {} - WsUtils.checkSocketParams(args, channelArgs, channelParamMap) - if len(channelArgs) < 1: - return False - for channel in channelArgs: - if self.isPrivate: - privateKey = self.getPrivateKey(channel) - else: - privateKey = channel - if privateKey not in self.factories: - continue - factory = self.factories[privateKey] - ifFiledParams = factory.subscribeSet - channelParamMap[channel] - if len(ifFiledParams) < 1: - self.disconnect(channel) - else: - payload = json.dumps({"op": "unsubscribe", "args": channelArgs[channel]}, ensure_ascii=False).encode( - "utf8") - factory = WsClientFactory(self.url, payload=payload) - factory.client = self - factory.protocol = WsClientProtocol - factory.callback = callback - factory.subscribeSet = ifFiledParams - reactor.callFromThread(self.resetConnection, factory, channel) - - def addConnection(self, channel): - self.conns[channel] = connectWS(self.factories[channel]) - - def disconnect(self, channel): - if channel not in self.conns: - self.logger.error("WsConnectManager disconnect error,channel is not able".format(channel)) - return - self.conns[channel].factory = WebSocketClientFactory(self.url) - self.conns[channel].disconnect() - del self.conns[channel] - privateKey = channel - if self.isPrivate: - privateKey = self.getPrivateKey(channel) - del self.factories[privateKey] - - def initSubscribeFactory(self, args, subSet: set, callback): - payload = json.dumps({"op": "subscribe", "args": args}, ensure_ascii=False).encode( - "utf8") - factory = WsClientFactory(self.url, payload=payload) - factory.payload = payload - factory.protocol = WsClientProtocol - factory.callback = callback - factory.subscribeSet = factory.subscribeSet | subSet - return factory - - def loginSocket(self, channel: str): - payload = WsUtils.initLoginParams(useServerTime=self.useServerTime, apiKey=self.apiKey, - passphrase=self.passphrase, secretKey=self.secretKey) - factory = WsClientFactory(self.url, payload=payload) - factory.protocol = WsClientProtocol - factory.callback = loginSocketCallBack - privateKey = self.getPrivateKey(channel) - self.factories[privateKey] = factory - self.conns[channel] = connectWS(factory) - - def resetConnection(self, newFactory, channel): - if self.isPrivate: - privateKey = self.getPrivateKey(channel) - preFactory = self.factories[privateKey] - else: - preFactory = self.factories[channel] - instance = preFactory.instance - if instance is None: - raise ValueError("instance must not none") - instance.factory = newFactory - instance.payload = newFactory.payload - instance.onConnect(None) - - def getPrivateKey(self, channel) -> str: - return str(self.apiKey) + "@" + channel - - def run(self): - try: - reactor.run(installSignalHandlers=False) - except ReactorAlreadyRunning as e: - self.logger.error("WsConnectManager reactor.run error;e:{}".format(e)) - - def close(self): - keys = set(self.conns.keys()) - for key in keys: - self.closeConnection(key) - self.conns = {} - - -def loginSocketCallBack(message): - print("loginSocket callback:", message) diff --git a/okx/websocket/WsLoginFactory.py b/okx/websocket/WsLoginFactory.py deleted file mode 100644 index e00ac844..00000000 --- a/okx/websocket/WsLoginFactory.py +++ /dev/null @@ -1,59 +0,0 @@ -# import time -# -# import WsUtils -# from WsClientProtocol import * -# from autobahn.twisted.websocket import WebSocketClientFactory -# from twisted.internet.protocol import ReconnectingClientFactory -# -# -# class WsReconnectingClientFactory(ReconnectingClientFactory): -# """ -# @ivar maxDelay: Maximum number of seconds between connection attempts. -# @ivar initialDelay: Delay for the first reconnection attempt. -# @ivar maxRetries: Maximum number of consecutive unsuccessful connection -# attempts, after which no further connection attempts will be made. If -# this is not explicitly set, no maximum is applied. -# """ -# initialDelay = 0.1 -# maxDelay = 1 -# maxRetries = 4 -# -# -# class WsLoginFactory(WebSocketClientFactory, WsReconnectingClientFactory): -# reachMaxRetriesError = {"e": "error", "m": "reached max connect retries"} -# -# def __init__(self, *args, useServerTime: str, apiKey: str, passphrase: str, secretKey: str, **kwargs): -# WebSocketClientFactory.__init__(self, *args, **kwargs) -# self.apiKey = apiKey -# self.passphrase = passphrase -# self.secretKey = secretKey -# self.useServerTime = useServerTime -# self.instance = None -# self.preTime = time.time() -# self.logger = logging.getLogger(__name__) -# -# def startedConnecting(self, connector): -# self.logger.info("WsClientFactory execute startedConnecting") -# -# def clientConnectionFailed(self, connector, reason): -# self.logger.error( -# "Can't connect to server. Reason: {}. Retrying: {}".format(reason, self.retries + 1)) -# self.retry(connector) -# if self.retries > self.maxRetries: -# self.callback(self.reachMaxRetriesError) -# -# def clientConnectionLost(self, connector, reason): -# cur = time.time() -# print("WsClientFactory,pre team=", cur - self.preTime) -# self.preTime = cur -# self.logger.error("WsClientFactory execute clientConnectionLost. Reason: {},retried {} times".format(reason, -# self.retries + 1)) -# self.retry(connector) -# if self.retries > self.maxRetries: -# self.callback(self.reachMaxRetriesError) -# -# def buildProtocol(self, addr): -# payload = WsUtils.initLoginParams(useServerTime=self.useServerTime, apiKey=self.apiKey, -# passphrase=self.passphrase, secretKey=self.secretKey) -# protocol = WsClientProtocol(self, payload=payload) -# return protocol diff --git a/okx/websocket/WsPrivate.py b/okx/websocket/WsPrivate.py deleted file mode 100644 index b1bb189c..00000000 --- a/okx/websocket/WsPrivate.py +++ /dev/null @@ -1,30 +0,0 @@ - -from twisted.internet import reactor - -from . import WsUtils -from .WsConnectManager import WsConnectManager - - -class WsPrivate(WsConnectManager): - def __init__(self, apiKey: str, passphrase: str, secretKey: str, url: str, useServerTime: False): - if ~WsUtils.isNotBlankStr(apiKey) or ~WsUtils.isNotBlankStr(passphrase) or ~WsUtils.isNotBlankStr( - secretKey) or ~WsUtils.isNotBlankStr(url): - return - super().__init__(url, isPrivate=True) - self.apiKey = apiKey - self.passphrase = passphrase - self.secretKey = secretKey - self.useServerTime = useServerTime - - def subscribe(self, params: list, callback): - self.subscribeSocket(params, callback) - - def unsubscribe(self, params: list, callback): - self.unsubscribeSocket(params, callback) - - def stop(self): - try: - self.close() - finally: - reactor.stop() - diff --git a/okx/websocket/WsPrivateAsync.py b/okx/websocket/WsPrivateAsync.py new file mode 100644 index 00000000..b7e328ea --- /dev/null +++ b/okx/websocket/WsPrivateAsync.py @@ -0,0 +1,204 @@ +import asyncio +import json +import logging +import warnings + +from okx.websocket import WsUtils +from okx.websocket.WebSocketFactory import WebSocketFactory + +logger = logging.getLogger(__name__) + + +class WsPrivateAsync: + def __init__(self, apiKey, passphrase, secretKey, url, useServerTime=None, debug=False): + self.url = url + self.subscriptions = set() + self.callback = None + self.loop = asyncio.get_event_loop() + self.factory = WebSocketFactory(url) + self.apiKey = apiKey + self.passphrase = passphrase + self.secretKey = secretKey + self.useServerTime = False + self.websocket = None + self.debug = debug + + # Set log level + if debug: + logger.setLevel(logging.DEBUG) + + # Deprecation warning for useServerTime parameter + if useServerTime is not None: + warnings.warn("useServerTime parameter is deprecated. Please remove it.", DeprecationWarning) + + async def connect(self): + self.websocket = await self.factory.connect() + + async def consume(self): + async for message in self.websocket: + if self.debug: + logger.debug("Received message: {%s}", message) + if self.callback: + self.callback(message) + + async def subscribe(self, params: list, callback, id: str = None): + self.callback = callback + + logRes = await self.login() + await asyncio.sleep(5) + if logRes: + payload_dict = { + "op": "subscribe", + "args": params + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"subscribe: {payload}") + await self.websocket.send(payload) + # await self.consume() + + async def login(self): + loginPayload = WsUtils.initLoginParams( + useServerTime=self.useServerTime, + apiKey=self.apiKey, + passphrase=self.passphrase, + secretKey=self.secretKey + ) + if self.debug: + logger.debug(f"login: {loginPayload}") + await self.websocket.send(loginPayload) + return True + + async def unsubscribe(self, params: list, callback, id: str = None): + self.callback = callback + payload_dict = { + "op": "unsubscribe", + "args": params + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"unsubscribe: {payload}") + else: + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + + async def send(self, op: str, args: list, callback=None, id: str = None): + """ + Generic send method + :param op: Operation type + :param args: Parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + payload_dict = { + "op": op, + "args": args + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"send: {payload}") + await self.websocket.send(payload) + + async def place_order(self, args: list, callback=None, id: str = None): + """ + Place order + :param args: Order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("order", args, id=id) + + async def batch_orders(self, args: list, callback=None, id: str = None): + """ + Batch place orders + :param args: Batch order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("batch-orders", args, id=id) + + async def cancel_order(self, args: list, callback=None, id: str = None): + """ + Cancel order + :param args: Cancel order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("cancel-order", args, id=id) + + async def batch_cancel_orders(self, args: list, callback=None, id: str = None): + """ + Batch cancel orders + :param args: Batch cancel order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("batch-cancel-orders", args, id=id) + + async def amend_order(self, args: list, callback=None, id: str = None): + """ + Amend order + :param args: Amend order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("amend-order", args, id=id) + + async def batch_amend_orders(self, args: list, callback=None, id: str = None): + """ + Batch amend orders + :param args: Batch amend order parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("batch-amend-orders", args, id=id) + + async def mass_cancel(self, args: list, callback=None, id: str = None): + """ + Mass cancel orders + Note: This method is for /ws/v5/business channel, rate limit: 1 request/second + :param args: Cancel parameter list, contains instType and instFamily + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + await self.send("mass-cancel", args, id=id) + + async def stop(self): + await self.factory.close() + + async def start(self): + if self.debug: + logger.debug("Connecting to WebSocket...") + else: + logger.info("Connecting to WebSocket...") + await self.connect() + self.loop.create_task(self.consume()) + + def stop_sync(self): + if self.loop.is_running(): + future = asyncio.run_coroutine_threadsafe(self.stop(), self.loop) + future.result(timeout=10) + else: + self.loop.run_until_complete(self.stop()) diff --git a/okx/websocket/WsPublic.py b/okx/websocket/WsPublic.py deleted file mode 100644 index c38e9d7e..00000000 --- a/okx/websocket/WsPublic.py +++ /dev/null @@ -1,20 +0,0 @@ -from twisted.internet import reactor - -from .WsConnectManager import WsConnectManager - - -class WsPublic(WsConnectManager): - def __init__(self, url): - super().__init__(url, isPrivate=False) - - def subscribe(self, params: list, callback): - self.subscribeSocket(params, callback) - - def unsubscribe(self, params: list, callback): - self.unsubscribeSocket(params, callback) - - def stop(self): - try: - self.close() - finally: - reactor.stop() diff --git a/okx/websocket/WsPublicAsync.py b/okx/websocket/WsPublicAsync.py new file mode 100644 index 00000000..d7eefdcf --- /dev/null +++ b/okx/websocket/WsPublicAsync.py @@ -0,0 +1,125 @@ +import asyncio +import json +import logging + +from okx.websocket import WsUtils +from okx.websocket.WebSocketFactory import WebSocketFactory + +logger = logging.getLogger(__name__) + + +class WsPublicAsync: + def __init__(self, url, apiKey='', passphrase='', secretKey='', debug=False): + self.url = url + self.subscriptions = set() + self.callback = None + self.loop = asyncio.get_event_loop() + self.factory = WebSocketFactory(url) + self.websocket = None + self.debug = debug + # Credentials for business channel login + self.apiKey = apiKey + self.passphrase = passphrase + self.secretKey = secretKey + self.isLoggedIn = False + + # Set log level + if debug: + logger.setLevel(logging.DEBUG) + + async def connect(self): + self.websocket = await self.factory.connect() + + async def consume(self): + async for message in self.websocket: + if self.debug: + logger.debug("Received message: {%s}", message) + if self.callback: + self.callback(message) + + async def login(self): + """ + Login method for business channel that requires authentication (e.g. /ws/v5/business) + """ + if not self.apiKey or not self.secretKey or not self.passphrase: + raise ValueError("apiKey, secretKey and passphrase are required for login") + + loginPayload = WsUtils.initLoginParams( + useServerTime=False, + apiKey=self.apiKey, + passphrase=self.passphrase, + secretKey=self.secretKey + ) + if self.debug: + logger.debug(f"login: {loginPayload}") + await self.websocket.send(loginPayload) + self.isLoggedIn = True + return True + + async def subscribe(self, params: list, callback, id: str = None): + self.callback = callback + payload_dict = { + "op": "subscribe", + "args": params + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"subscribe: {payload}") + await self.websocket.send(payload) + # await self.consume() + + async def unsubscribe(self, params: list, callback, id: str = None): + self.callback = callback + payload_dict = { + "op": "unsubscribe", + "args": params + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"unsubscribe: {payload}") + else: + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + + async def send(self, op: str, args: list, callback=None, id: str = None): + """ + Generic send method + :param op: Operation type + :param args: Parameter list + :param callback: Callback function + :param id: Optional request ID + """ + if callback: + self.callback = callback + payload_dict = { + "op": op, + "args": args + } + if id is not None: + payload_dict["id"] = id + payload = json.dumps(payload_dict) + if self.debug: + logger.debug(f"send: {payload}") + await self.websocket.send(payload) + + async def stop(self): + await self.factory.close() + + async def start(self): + if self.debug: + logger.debug("Connecting to WebSocket...") + else: + logger.info("Connecting to WebSocket...") + await self.connect() + self.loop.create_task(self.consume()) + + def stop_sync(self): + if self.loop.is_running(): + future = asyncio.run_coroutine_threadsafe(self.stop(), self.loop) + future.result(timeout=10) + else: + self.loop.run_until_complete(self.stop()) diff --git a/okx/websocket/WsUtils.py b/okx/websocket/WsUtils.py index 28aa2b91..fcb0db26 100644 --- a/okx/websocket/WsUtils.py +++ b/okx/websocket/WsUtils.py @@ -16,7 +16,7 @@ def initLoginParams(useServerTime: bool, apiKey, passphrase, secretKey): sign = base64.b64encode(d) arg = {"apiKey": apiKey, "passphrase": passphrase, "timestamp": timestamp, "sign": sign.decode("utf-8")} payload = {"op": "login", "args": [arg]} - return json.dumps(payload, ensure_ascii=False).encode("utf8") + return json.dumps(payload) def isNotBlankStr(param: str) -> bool: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..b9a5dafe --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# Dependencies for python-okx +httpx[http2]>=0.24.0 +requests>=2.25.0 +websockets>=10.0 +certifi>=2021.0.0 +loguru>=0.7.0 +python-dotenv>=1.0.0 + +# Development & Testing +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.0.0 +ruff>=0.1.0 +build>=1.0.0 +twine>=4.0.0 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..9b15847b --- /dev/null +++ b/setup.py @@ -0,0 +1,69 @@ +import os +import setuptools + +# Get the directory where setup.py is located +HERE = os.path.dirname(os.path.abspath(__file__)) + +# Read version from package +import okx + +# Read README +with open(os.path.join(HERE, "README.md"), "r", encoding="utf-8") as fh: + long_description = fh.read() + + +def parse_requirements(): + """Parse runtime requirements from requirements.txt. + + Lines from the "# dev" marker onward are skipped, so `pip install + python-okx` only pulls runtime dependencies. `pip install -r + requirements.txt` still installs everything (pip treats the marker as a + plain comment), keeping the dev/test setup unchanged. + """ + requirements = [] + req_path = os.path.join(HERE, "requirements.txt") + + if not os.path.exists(req_path): + return requirements + + with open(req_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + # Stop at the dev-dependency section marker + if line.lower().startswith("# dev"): + break + # Skip empty lines and comments + if not line or line.startswith("#"): + continue + # Handle inline comments + if "#" in line: + line = line.split("#")[0].strip() + requirements.append(line) + + return requirements + + +setuptools.setup( + name="python-okx", + version=okx.__version__, + author="okxv5api", + author_email="api@okg.com", + description="Python SDK for OKX", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://okx.com/docs-v5/", + packages=setuptools.find_packages(exclude=["test", "test.*", "example"]), + python_requires=">=3.7", + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + install_requires=parse_requirements(), +) diff --git a/test/AccountTest.py b/test/AccountTest.py deleted file mode 100644 index 2d088487..00000000 --- a/test/AccountTest.py +++ /dev/null @@ -1,80 +0,0 @@ - -import unittest -from ..okx import Account - -class AccountTest(unittest.TestCase): - def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.AccountAPI = Account.AccountAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') - ''' - POSITIONS_HISTORY = '/api/v5/account/positions-history' #need add - GET_PM_LIMIT = '/api/v5/account/position-tiers' #need add - ACCOUNT_RISK = '/api/v5/account/risk-state' #need add - def test_account_risk(self): - print(self.AccountAPI.get_account_risk()) - - def test_get_pm_limit(self): - print(self.AccountAPI.get_pm_limit("SWAP","BTC-USDT")) - #positions-history - def test_get_positions_history(self): - print(self.AccountAPI.get_positions_history()) - def test_get_user_config(self): - print(self.AccountAPI.get_account_config()) - def test_get_positions(self): - print(self.AccountAPI.get_positions("SWAP")) - def test_get_balance(self): - print(self.AccountAPI.get_account()) - def test_get_positions_risk(self): - print(self.AccountAPI.get_position_risk("SWAP")) - def test_get_bills(self): - print(self.AccountAPI.get_bills_detail()) - - def test_get_bills_arch(self): - print(self.AccountAPI.get_bills_details()) - def test_set_position_mode(self): - print(self.AccountAPI.set_position_mode("long_short_mode")) - def test_set_leverage(self): - print(self.AccountAPI.set_leverage(instId="BTC-USDT",lever="5",mgnMode="isolated")) - def test_get_max_avaliable_size(self): - print(self.AccountAPI.get_max_avail_size(instId="BTC-USDT",tdMode="cash")) - def test_get_max_size(self): - print(self.AccountAPI.get_maximum_trade_size(instId="BTC-USDT",tdMode="cash")) - def test_get_positions(self): - print(self.AccountAPI.get_positions("MARGIN")) - def test_set_margin_balance(self): - print(self.AccountAPI.Adjustment_margin(instId="BTC-USDT",posSide="net",type="add",amt="1")) - def test_get_lev_info(self): - print(self.AccountAPI.get_leverage("BTC-USDT","cross")); - def test_get_max_loan(self): - print(self.AccountAPI.get_max_loan("BTC-USDT","cross","USDT")) - def test_get_trade_fee(self): - print(self.AccountAPI.get_fee_rates("SPOT")) - def test_get_insterested_accrued(self): - print(self.AccountAPI.get_interest_accrued()) - def test_get_interestred_rate(self): - print(self.AccountAPI.get_interest_rate()) - def test_set_greeks(self): - print(self.AccountAPI.set_greeks("BS")) - - def test_set_isolated_mode(self): - print(self.AccountAPI.set_isolated_mode("automatic","MARGIN")) - def test_set_max_withdraw(self): - print(self.AccountAPI.get_max_withdrawal("USDT")) - def test_borrow_repay(self): - print(self.AccountAPI.borrow_repay("BTC","borrow","1.0")) - def test_borrow_repay_history(self): - print(self.AccountAPI.get_borrow_repay_history()) - def test_get_interest_limits(self): - print(self.AccountAPI.get_interest_limits()) - def test_simulated_margin(self): - print(self.AccountAPI.get_simulated_margin()) - def test_get_greeks(self): - print(self.AccountAPI.get_greeks()) - ''' - def test_simulated_margin(self): - print(self.AccountAPI.get_simulated_margin()) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/test/BlockTradingTest.py b/test/BlockTradingTest.py deleted file mode 100644 index 8d86974c..00000000 --- a/test/BlockTradingTest.py +++ /dev/null @@ -1,48 +0,0 @@ - -import unittest -from ..okx import BlockTrading -class BlockTradingTest(unittest.TestCase): - def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.BlockTradingAPI = BlockTrading.BlockTradingAPI(use_server_time=False, flag='1') - - """ - def test_get_counter_parties(self): - print(self.BlockTradingAPI.counterparties()) - def test_create_rfqs(self): - counterparties=['HWZ'] - legs =[{ - 'instId':"BTC-USDT", - 'sz':'25', - 'side':'buy' - }] - print(self.BlockTradingAPI.create_rfq(counterparties,legs = legs)) - def test_cancel_rfq(self):###'rfqId': '3I1MK3O' - print(self.BlockTradingAPI.cancel_rfq(rfqId='3I1MK3O')) - def test_cancel_batch_rfqs(self): - #3I1MK40 - #3I1MK48 - print(self.BlockTradingAPI.cancel_batch_rfqs(["3I1MK40","3I1MK48"])) - def test_cancel_all_rfqs(self): - print(self.BlockTradingAPI.cancel_all_rfqs()) - def test_execute_quotes(self): - print(self.BlockTradingAPI.execute_quote("3I1MJE0","AC1233")) - def test_create_quotes(self): - print(self.BlockTradingAPI.create_quote("3I1MJE0",)) - def test_get_rfqs(self): - print(self.BlockTradingAPI.get_rfqs()) - def test_get_quotes(self): - print(self.BlockTradingAPI.get_quotes()) - def test_get_public_trades(self): - print(self.BlockTradingAPI.get_public_trades()) - def test_get_trade(self): - print(self.BlockTradingAPI.get_trades()) - """ - - - def test_get_public_trades(self): - print(self.BlockTradingAPI.get_public_trades()) -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/test/BrokerTest.py b/test/BrokerTest.py deleted file mode 100644 index e4a034cd..00000000 --- a/test/BrokerTest.py +++ /dev/null @@ -1,70 +0,0 @@ - -import unittest -from ..okx import NDBroker - -class BrokerTest(unittest.TestCase): - def setUp(self): - ''' - - 52c37310-a8b0-454a-8191-3250acff2626 - EC37534156E6B8C32E78FE8D8C1D506B - Hanhao0.0 - ''' - api_key = '52c37310-a8b0-454a-8191-3250acff2626' - api_secret_key = 'EC37534156E6B8C32E78FE8D8C1D506B' - passphrase = 'Hanhao0.0' - self.NDBrokerAPI = NDBroker.NDBrokerAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') - - ''' - def test_get_broker_info(self): - result = self.NDBrokerAPI.get_broker_info() - print(result) - - def test_create_subAccount(self): - print(self.NDBrokerAPI.create_subaccount("","")) - #{'code': '0', 'data': [{'acctLv': '1', 'label': 'unitTest', 'subAcct': 'unitTest1298', 'ts': '1660789737257', 'uid': '346146586377719875'}], 'msg': ''} - - - - def test_get_subaccount_info(self): - print(self.NDBrokerAPI.get_subaccount_info()) - - def test_subaccount_create_apikey(self): - print(self.NDBrokerAPI.create_subaccount_apikey("unitTest1298",'test2222',"114514A.bc","142.112.128.63","trade")) - #{'code': '0', 'data': [{'apiKey': 'faf24bd1-dc25-45ab-9f78-ebfea58614bd', 'ip': '142.112.128.63', 'label': 'test2222', 'passphrase': '114514A.bc', 'perm': 'read_only,trade', 'secretKey': 'DB4F607380AA04313BB0DEDBFE576FF1', 'subAcct': 'unitTest1298', 'ts': '1660793476848'}], 'msg': ''} - - def test_subaccount_get_apikey(self): - print(self.NDBrokerAPI.get_subaccount_apikey("unitTest1298","faf24bd1-dc25-45ab-9f78-ebfea58614bd")) - - def test_delete_subAccount(self): - print(self.NDBrokerAPI.delete_subaccount("hanhaoBras1234")) - - - def test_modifiy_subaccount_apikey(self): - print(self.NDBrokerAPI.reset_subaccount_apikey("unitTest1298","faf24bd1-dc25-45ab-9f78-ebfea58614bd","csuihssssiani",perm="trade",ip = "192.168.1.1")) - - def test_delete_subaccount_apikey(self): - print(self.NDBrokerAPI.delete_subaccount_apikey("unitTest1298","faf24bd1-dc25-45ab-9f78-ebfea58614bd")) - - def test_set_account_lv(self): - print(self.NDBrokerAPI.set_subaccount_level("unitTest1298","4")) - - def test_delete_subaccount_apikey(self): - print(self.NDBrokerAPI.delete_subaccount_apikey("unitTest1298","faf24bd1-dc25-45ab-9f78-ebfea58614bd")) - - def test_set_fee_rate(self): - print(self.NDBrokerAPI.set_subaccount_fee_rate("unitTest1298","SPOT","absolute","90","90")) - def test_create_desposit(self): - print(self.NDBrokerAPI.create_subaccount_deposit_address("unitTest1298","ETH")) - def test_rebate_daily(self): - print(self.NDBrokerAPI.get_rebate_daily()) - - - - def test_create_rebate_per_order(self): - print(self.NDBrokerAPI.generate_rebate_per_orders("20220501","20220801")) - ''' - def test_get_rebate_per_order(self): - print(self.NDBrokerAPI.get_rebate_per_orders("false",begin="20220501",end = "20220801")) -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/test/GridTest.py b/test/GridTest.py deleted file mode 100644 index 072e67cb..00000000 --- a/test/GridTest.py +++ /dev/null @@ -1,55 +0,0 @@ - -import unittest -from ..okx import Grid - -class GridTest(unittest.TestCase): - def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.GridAPI = Grid.GridAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1', debug=False) - """ - GRID_COMPUTE_MARIGIN_BALANCE = '/api/v5/tradingBot/grid/compute-margin-balance' - GRID_MARGIN_BALANCE = '/api/v5/tradingBot/grid/margin-balance' - GRID_AI_PARAM = '/api/v5/tradingBot/grid/ai-param' - - def test_ai_param(self): - print(self.GridAPI.grid_ai_param("grid","BTC-USDT")) - - def test_order_algo(self): - print(self.GridAPI.grid_order_algo("BTC-USDT","grid","45000","20000","100","1",quoteSz="50")) - #479973849967362048 - def test_grid_margin_balance(self): - print(self.GridAPI.grid_adjust_margin_balance()) - def test_compute_margin_balance(self): - print(self.GridAPI.grid_compute_margin_balance("479978879210491904","add","100")) - - def test_pending_grid_order(self): - print(self.GridAPI.grid_orders_algo_pending("grid")) - def test_amend_order_algo(self): - print(self.GridAPI.grid_amend_order_algo('485238792325173248','BTC-USDT-SWAP',tpTriggerPx='50000')) - def test_stop_order_algo(self): - print(self.GridAPI.grid_stop_order_algo('485238792325173248','BTC-USDT-SWAP','contract_grid','1')) - def test_pending_grid_order(self): - print(self.GridAPI.grid_orders_algo_pending("grid")) - def test_algo_history(self): - print(self.GridAPI.grid_orders_algo_history('contract_grid')) - def test_orders_algo_details(self): - print(self.GridAPI.grid_orders_algo_details('contract_grid','485238792325173248')) - def test_get_sub_orders(self): - print(self.GridAPI.grid_sub_orders('485238792325173248','contract_grid','filled')) - def test_order_algo2(self): - print(self.GridAPI.grid_order_algo("BTC-USDT-SWAP","contract_grid","45000","20000","100","1",sz='3000',direction='long',lever='3.0')) - def test_get_positions(self): - print(self.GridAPI.grid_positions('contract_grid','485379848832286720')) - def test_withdrawl_profits(self): - print(self.GridAPI.grid_withdraw_income('11111')) - def test_withdrawl_profits(self): - print(self.GridAPI.grid_withdraw_income('485380442313723904')) - """ - - - def test_order_algo(self): - print(self.GridAPI.grid_order_algo("BTC-USDT","grid","45000","20000","100","1",quoteSz="50")) -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/test/StackingTest.py b/test/StackingTest.py deleted file mode 100644 index 1d6a8755..00000000 --- a/test/StackingTest.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest -from ..okx import Status - -class StackingTest(unittest.TestCase): - def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.StackingAPI = Status.StackingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') - ''' - STACK_DEFI_OFFERS = '/api/v5/finance/staking-defi/offers' - STACK_DEFI_PURCHASE = '/api/v5/finance/staking-defi/purchase' - STACK_DEFI_REDEEM = '/api/v5/finance/staking-defi/redeem' - STACK_DEFI_CANCEL = '/api/v5/finance/staking-defi/cancel' - STACK_DEFI_ORDERS_ACTIVITY = '/api/v5/finance/staking-defi/orders-active' - STACK_DEFI_ORDERS_HISTORY = '/api/v5/finance/staking-defi/orders-history' - ''' - def test_get_offers(self): - print(self.StackingAPI.get_offers(ccy="USDT")) - - - - def test_purcase(self): - print(self.StackingAPI.purchase(1456,"USDT","100","0")) - def test_redeem(self): - print(self.StackingAPI.redeem()) - def test_cencel(self): - print(self.StackingAPI.cancel()) - def test_order_activity(self): - print(self.StackingAPI.get_activity_orders()) - def test_order_history(self): - print(self.StackingAPI.stack_get_order_history()) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/test/TradeTest.py b/test/TradeTest.py deleted file mode 100644 index d963787c..00000000 --- a/test/TradeTest.py +++ /dev/null @@ -1,121 +0,0 @@ -import unittest -from ..okx import Trade -class TradeTest(unittest.TestCase): - def setUp(self): - api_key = '35d8f27e-63cc-45bc-a578-45d76363d47f' - api_secret_key = '0B7C968025BC2D4D71CF74771EA0E15C' - passphrase = '123456' - self.tradeApi = Trade.TradeAPI(api_key, api_secret_key, passphrase, False, '1') - """ - def test_place_order(self): - print(self.tradeApi.place_order("BTC-USDT",tdMode="cross",clOrdId="asCai1",side="buy",ordType="limit",sz="0.01",px="18000")) - def test_cancel_order(self): - print(self.tradeApi.cancel_order(instId="ETH-USDT",ordId="480702180748558336")) - - def test_batch_order(self): - orderData = [ { - "instId":"ETH-USDT", - "tdMode":"cross", - "clOrdId":"b151121", - "side":"buy", - "ordType":"limit", - "px":"2.15", - "sz":"2" - }, - { - "instId":"BTC-USDT", - "tdMode":"cross", - "clOrdId":"b152233", - "side":"buy", - "ordType":"limit", - "px":"2.15", - "sz":"2" - }] - print(self.tradeApi.place_multiple_orders(orderData)) - #480702180748558336 - def test_cancel_batch_orders(self): - data=[ - { - 'instId':"ETH-USDT", - 'ordId':"480702885353881600" - }, - { - 'instId':"BTC-USDT", - 'ordId':'480702885353881601' - } - ] - print(self.tradeApi.cancel_multiple_orders(data)) - def test_amend_order(self): - print(self.tradeApi.amend_order("BTC-USDT",ordId="480706017781743616",newSz="0.03")) - def test_amend_order_batch(self): - orderData = [ - { - 'instId':'ETH-USDT', - 'ordId':'480707205436669952', - 'newSz':'0.02' - }, - { - 'instId':'BTC-USDT', - 'ordId':'480707205436669953', - 'newPx':'3.0' - } - ] - print(self.tradeApi.amend_multiple_orders(orderData)) - def test_close_all_positions(self): - print(self.tradeApi.close_positions("BTC-USDT",mgnMode="cross")) - def test_get_order_info(self): - print(self.tradeApi.get_orders("ETH-USDT","480707205436669952")) - def test_get_order_pending(self): - print(self.tradeApi.get_order_list("SPOT")) - def test_get_order_history(self): - print(self.tradeApi.get_orders_history("SPOT")) - def test_get_order_histry_archive(self): - print(self.tradeApi.orders_history_archive("SPOT")) - def test_get_fills(self): - print(self.tradeApi.get_fills("SPOT")) - def test_get_fills_history(self): - print(self.tradeApi.get_fills_history("SPOT")) - def test_get_order_algo_pending(self): - print(self.tradeApi.order_algos_list('oco')) - def test_order_algo(self): - print(self.tradeApi.place_algo_order('BTC-USDT-SWAP', 'cross', side='buy', ordType='trigger', posSide='long', - sz='100', triggerPx='22000', triggerPxType ='index', orderPx='-1')) - def test_cancel_algos(self): - params = [{ - 'algoId': '485903392536264704', - 'instId': 'BTC-USDT-SWAP' - }] - - - print(self.tradeApi.cancel_algo_order(params)) - def test_cancel_adv_algos(self): - params = [{ - 'algoId': '485936482235191296', - 'instId': 'BTC-USDT-SWAP' - }] - - print(self.tradeApi.cancel_advance_algos(params))) - def test_orders_algo_pending(self): - print(self.tradeApi.order_algos_list(ordType='iceberg')) - def test_algo_order_history(self): - print(self.tradeApi.order_algos_history(algoId='485903392536264704',ordType='conditional')) - def test_get_easy_convert_list(self): - print(self.tradeApi.get_easy_convert_currency_list()) - def test_easy_convert(self): - print(self.tradeApi.easy_convert(fromCcy=['BTC'],toCcy='OKB')) - def test_get_convert_history(self): - print(self.tradeApi.get_easy_convert_history()) - def test_get_oneclick_repay_support_list(self): - print(self.tradeApi.get_oneclick_repay_list('cross')) - def test_oneclick_repay(self): - print(self.tradeApi.oneclick_repay(['BTC'],'USDT')) -""" -#485903392536264704 - #485936482235191296 - def test_oneclick_repay_history(self): - print(self.tradeApi.oneclick_repay_history()) - - - -if __name__=='__main__': - unittest.main() diff --git a/test/WsPrivateTest.py b/test/WsPrivateTest.py deleted file mode 100644 index af12427b..00000000 --- a/test/WsPrivateTest.py +++ /dev/null @@ -1,32 +0,0 @@ -import time - -from okx.websocket.WsPrivate import WsPrivate - -def privateCallback(message): - print("WsPrivate subscribe callback:", message) - - -if __name__ == '__main__': - url = "wss://ws.okx.com:8443/ws/v5/private" - ws = WsPrivate(apiKey="your_apiKey", - passphrase="your_passphrase", - secretKey="your_secretKey", - url=url, - useServerTime=False) - ws.start() - args = [] - arg1 = {"channel": "account", "instType": "BTC"} - arg2 = {"channel": "orders", "instType": "ANY"} - arg3 = {"channel": "balance_and_position"} - args.append(arg1) - args.append(arg2) - args.append(arg3) - ws.subscribe(args, callback=privateCallback) - time.sleep(30) - print("-----------------------------------------unsubscribe--------------------------------------------") - args2 = [arg2] - ws.unsubscribe(args2, callback=privateCallback) - time.sleep(30) - print("-----------------------------------------unsubscribe all--------------------------------------------") - args3 = [arg1, arg3] - ws.unsubscribe(args3, callback=privateCallback) diff --git a/test/WsPublicTest.py b/test/WsPublicTest.py deleted file mode 100644 index 6a56990e..00000000 --- a/test/WsPublicTest.py +++ /dev/null @@ -1,29 +0,0 @@ -import time -from okx.websocket.WsPublic import WsPublic - -def publicCallback(message): - print("publicCallback", message) - - -if __name__ == '__main__': - url = "wss://wspri.coinall.ltd:8443/ws/v5/public?brokerId=9999" - ws = WsPublic(url=url) - ws.start() - args = [] - arg1 = {"channel": "instruments", "instType": "FUTURES"} - arg2 = {"channel": "instruments", "instType": "SPOT"} - arg3 = {"channel": "tickers", "instId": "BTC-USDT"} - arg4 = {"channel": "tickers", "instId": "ETH-USDT"} - args.append(arg1) - args.append(arg2) - args.append(arg3) - args.append(arg4) - ws.subscribe(args, publicCallback) - time.sleep(10) - print("-----------------------------------------unsubscribe--------------------------------------------") - args2 = [arg4] - ws.unsubscribe(args2, publicCallback) - time.sleep(10) - print("-----------------------------------------unsubscribe all--------------------------------------------") - args3 = [arg1, arg2, arg3] - ws.unsubscribe(args3, publicCallback) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/config.py b/test/config.py new file mode 100644 index 00000000..78885b32 --- /dev/null +++ b/test/config.py @@ -0,0 +1,58 @@ +""" +Test configuration module - loads API credentials from environment variables. + +Usage: + from test.config import get_api_credentials + + api_key, api_secret, passphrase, flag = get_api_credentials() +""" +import os +import logging +from pathlib import Path + +logger = logging.getLogger(__name__) + +# Flag to ensure .env is loaded only once +_env_loaded = False + + +def _load_env_once(): + """Load .env file only once, log any exceptions.""" + global _env_loaded + if _env_loaded: + return + + _env_loaded = True + env_path = Path(__file__).parent.parent / '.env' + + try: + from dotenv import load_dotenv + if env_path.exists(): + load_dotenv(env_path) + logger.debug(f"Loaded .env file from: {env_path}") + else: + logger.warning(f".env file not found at: {env_path}") + except ImportError: + logger.warning("python-dotenv not installed, relying on system environment variables") + except Exception as e: + logger.error(f"Failed to load .env file: {e}") + + +# Load .env when module is imported +_load_env_once() + + +def get_api_credentials(): + """ + Get API credentials from environment variables. + + Returns: + tuple: (api_key, api_secret, passphrase, flag) + """ + api_key = os.getenv('OKX_API_KEY', '') + api_secret = os.getenv('OKX_API_SECRET', '') + passphrase = os.getenv('OKX_PASSPHRASE', '') + flag = os.getenv('OKX_FLAG', '1') # Default to demo trading + + return api_key, api_secret, passphrase, flag + diff --git a/test/test_account.py b/test/test_account.py new file mode 100644 index 00000000..ef711815 --- /dev/null +++ b/test/test_account.py @@ -0,0 +1,159 @@ +import unittest + +from loguru import logger + +from okx import Account +from test.config import get_api_credentials + + +class AccountTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.AccountAPI = Account.AccountAPI(api_key, api_secret_key, passphrase, flag=flag) + + # ''' + # POSITIONS_HISTORY = '/api/v5/account/positions-history' #need add + # GET_PM_LIMIT = '/api/v5/account/position-tiers' #need add + # ACCOUNT_RISK = '/api/v5/account/risk-state' #need add + # def test_account_risk(self): + # print(self.AccountAPI.get_account_risk()) + # + # def test_get_pm_limit(self): + # print(self.AccountAPI.get_pm_limit("SWAP","BTC-USDT")) + # positions-history + # def test_get_positions_history(self): + # print(self.AccountAPI.get_positions_history()) + # def test_get_instruments(self): + # print(self.AccountAPI.get_instruments(instType='SPOT')) + # def test_get_account_bills_archive(self): + # print(self.AccountAPI.get_account_bills_archive(begin='1715780962300',end='1716998400000')) + # def test_positions_builder(self): + # print("Both real and virtual positions and assets are calculated") + # sim_pos = [{'instId': 'BTC-USDT-SWAP', 'pos': '10','avgPx': '1'}] + # sim_asset = [{'ccy': 'USDT', 'amt': '100'}] + # print(self.AccountAPI.position_builder(inclRealPosAndEq=False, greeksType='CASH', + # simPos=sim_pos, simAsset=sim_asset)) + # + # print("Only existing real positions are calculated") + # print(self.AccountAPI.position_builder(inclRealPosAndEq=True, greeksType='CASH')) + # + # print("Only virtual positions are calculated") + # print(self.AccountAPI.position_builder(inclRealPosAndEq=False, simPos=sim_pos)) + + # def test_get_user_config(self): + # print(self.AccountAPI.get_account_config()) + # def test_get_positions(self): + # print(self.AccountAPI.get_positions("SWAP")) + # def test_get_balance(self): + # print(self.AccountAPI.get_account()) + # def test_get_positions_risk(self): + # print(self.AccountAPI.get_position_risk("SWAP")) + # def test_get_bills(self): + # print(self.AccountAPI.get_bills_detail()) + # + # def test_get_bills_arch(self): + # print(self.AccountAPI.get_bills_details()) + # def test_set_position_mode(self): + # print(self.AccountAPI.set_position_mode("long_short_mode")) + # def test_set_leverage(self): + # print(self.AccountAPI.set_leverage(instId="BTC-USDT",lever="5",mgnMode="isolated")) + # def test_get_max_avaliable_size(self): + # print(self.AccountAPI.get_max_avail_size(instId="BTC-USDT",tdMode="cash")) + # def test_get_max_size(self): + # print(self.AccountAPI.get_maximum_trade_size(instId="BTC-USDT",tdMode="cash")) + # def test_get_positions(self): + # print(self.AccountAPI.get_positions("MARGIN")) + # def test_set_margin_balance(self): + # print(self.AccountAPI.Adjustment_margin(instId="BTC-USDT",posSide="net",type="add",amt="1")) + # def test_get_lev_info(self): + # print(self.AccountAPI.get_leverage("BTC-USDT","cross")); + # def test_get_max_loan(self): + # print(self.AccountAPI.get_max_loan("BTC-USDT","cross","USDT")) + # def test_get_trade_fee(self): + # print(self.AccountAPI.get_fee_rates("SPOT")) + # def test_get_insterested_accrued(self): + # print(self.AccountAPI.get_interest_accrued()) + # def test_get_interestred_rate(self): + # print(self.AccountAPI.get_interest_rate()) + # def test_set_greeks(self): + # print(self.AccountAPI.set_greeks("BS")) + # + # def test_set_isolated_mode(self): + # print(self.AccountAPI.set_isolated_mode("automatic","MARGIN")) + # def test_set_max_withdraw(self): + # print(self.AccountAPI.get_max_withdrawal("USDT")) + # def test_borrow_repay(self): + # print(self.AccountAPI.borrow_repay("BTC","borrow","1.0")) + # def test_borrow_repay_history(self): + # print(self.AccountAPI.get_borrow_repay_history()) + # def test_get_interest_limits(self): + # print(self.AccountAPI.get_interest_limits()) + # def test_simulated_margin(self): + # print(self.AccountAPI.get_simulated_margin()) + # def test_get_greeks(self): + # print(self.AccountAPI.get_greeks()) + # ''' + # def test_simulated_margin(self): + # print(self.AccountAPI.get_simulated_margin()) + + # def test_get_VIP_interest_accrued_data(self): + # print(self.AccountAPI.get_VIP_interest_accrued_data()) + + # def test_get_VIP_interest_deducted_data(self): + # print(self.AccountAPI.get_VIP_interest_deducted_data()) + + # def test_get_VIP_loan_order_list(self): + # print(self.AccountAPI.get_VIP_loan_order_list()) + + # def test_get_VIP_loan_order_detail(self): + # print(self.AccountAPI.get_VIP_loan_order_detail(ordId='1')) + + # def test_set_risk_offset_typel(self): + # print(self.AccountAPI.set_risk_offset_typel(type='1')) + # + # def test_set_auto_loan(self): + # print(self.AccountAPI.set_auto_loan()) + # + # def test_activate_option(self): + # print(self.AccountAPI.activate_option()) + + # def test_get_max_avaliable_size(self): + # print(self.AccountAPI.get_max_avail_size(instId="BTC-USDT",tdMode="cash",quickMgnType='manual')) + # def test_borrow_repay(self): + # print(self.AccountAPI.borrow_repay("BTC", "borrow", "1.0")) + + # def test_simulated_margin(self): + # print(self.AccountAPI.get_simulated_margin(spotOffsetType='3')) + # def test_get_fix_loan_borrowing_limit(self): + # logger.debug(f'{self.AccountAPI.get_fix_loan_borrowing_limit()}') + # def test_get_fix_loan_borrowing_quote(self): + # logger.debug(f'{self.AccountAPI.get_fix_loan_borrowing_quote(type="normal")}') + # def test_place_fix_loan_borrowing_order(self): + # logger.debug(f'{self.AccountAPI.place_fix_loan_borrowing_order(ccy="BTC", amt="0.1515", maxRate="0.001", term="30D", reborrow=True, reborrowRate="0.01")}') + # def test_amend_fix_loan_borrowing_order(self): + # logger.debug(f'{self.AccountAPI.amend_fix_loan_borrowing_order(ordId="2407301043344857",reborrow=True,renewMaxRate="0.01")}') + # def test_fix_loan_manual_reborrow(self): + # logger.debug(f'{self.AccountAPI.fix_loan_manual_reborrow(ordId="2407301043344857",maxRate="0.1")}') + # def test_repay_fix_loan_borrowing_order(self): + # logger.info(f'{self.AccountAPI.repay_fix_loan_borrowing_order(ordId="2407301054407907")}') + # def test_get_fix_loan_borrowing_orders_list(self): + # logger.debug(self.AccountAPI.get_fix_loan_borrowing_orders_list(ordId="2407301054407907")) + + # def test_spot_manual_borrow_repay(self): + # logger.debug(f'{self.AccountAPI.spot_manual_borrow_repay(ccy="USDT",side="borrow",amt=1)}') + # def test_set_auto_repay(self): + # logger.info(f'{self.AccountAPI.set_auto_repay(autoRepay=True)}') + # def test_spot_borrow_repay_history(self): + # logger.debug(self.AccountAPI.spot_borrow_repay_history(ccy="USDT",type="auto_borrow",after="1597026383085")) + # def test_set_auto_earn(self): + # logger.debug(self.AccountAPI.set_auto_earn(ccy="USDT", action="turn_on", earnType='0')) + #def test_get_max_loan_with_trade_quote_ccy(self): + # logger.debug(self.AccountAPI.get_max_loan( + # instId="BTC-USDT", + # mgnMode="isolated", + # mgnCcy="USDT", + # tradeQuoteCcy="USDT" + # )) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_block_trading.py b/test/test_block_trading.py new file mode 100644 index 00000000..1c025421 --- /dev/null +++ b/test/test_block_trading.py @@ -0,0 +1,88 @@ + +import unittest +from okx import BlockTrading +from test.config import get_api_credentials + + +class BlockTradingTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.BlockTradingAPI = BlockTrading.BlockTradingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) + + """ + def test_get_counter_parties(self): + print(self.BlockTradingAPI.counterparties()) + def test_create_rfqs(self): + counterparties=['HWZ'] + legs =[{ + 'instId':"BTC-USDT", + 'sz':'25', + 'side':'buy' + }] + print(self.BlockTradingAPI.create_rfq(counterparties,legs = legs)) + def test_cancel_rfq(self):###'rfqId': '3I1MK3O' + print(self.BlockTradingAPI.cancel_rfq(rfqId='3I1MK3O')) + def test_cancel_batch_rfqs(self): + #3I1MK40 + #3I1MK48 + print(self.BlockTradingAPI.cancel_batch_rfqs(["3I1MK40","3I1MK48"])) + def test_cancel_all_rfqs(self): + print(self.BlockTradingAPI.cancel_all_rfqs()) + def test_execute_quotes(self): + print(self.BlockTradingAPI.execute_quote("3I1MJE0","AC1233")) + def test_create_quotes(self): + print(self.BlockTradingAPI.create_quote("3I1MJE0",)) + def test_get_rfqs(self): + print(self.BlockTradingAPI.get_rfqs()) + def test_get_quotes(self): + print(self.BlockTradingAPI.get_quotes()) + def test_get_public_trades(self): + print(self.BlockTradingAPI.get_public_trades()) + def test_get_trade(self): + print(self.BlockTradingAPI.get_trades()) + """ + + + # def test_get_public_trades(self): + # print(self.BlockTradingAPI.get_public_trades()) + + # def test_get_quote_products(self): + # print(self.BlockTradingAPI.get_quote_products()) + + def test_create_rfqs(self): + counterparties=['8924'] + legs =[{ + 'instId':"BTC-USDT", + 'sz':'25', + 'side':'buy', + 'posSide':'net', + 'tdMode':'cross', + 'ccy':'USDT' + }] + print(self.BlockTradingAPI.create_rfq(counterparties,allowPartialExecution='true',tag='1234',legs = legs)) + + # def test_execute_quotes(self): + # legs = [{ + # 'instId':"BTC-USDT", + # 'sz':'0.0001', + # }] + # print(self.BlockTradingAPI.execute_quote("3IR9E68","3IR9E80",legs)) + + # def test_create_quotes(self): + # legs = [{ + # 'instId': "BTC-USDT", + # 'sz': '25', + # 'side': 'buy', + # 'posSide': 'net', + # 'tdMode': 'cross', + # 'ccy': 'USDT' + # }] + # print(self.BlockTradingAPI.create_quote(rfqId='3IR9BT8',quoteSide='buy',legs=legs)) + + # def test_get_trade(self): + # print(self.BlockTradingAPI.get_trades()) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/ConvertTest.py b/test/test_convert.py similarity index 81% rename from test/ConvertTest.py rename to test/test_convert.py index d4faa28f..fc1f0486 100644 --- a/test/ConvertTest.py +++ b/test/test_convert.py @@ -1,11 +1,12 @@ import unittest -from ..okx import Convert +from okx import Convert +from test.config import get_api_credentials + + class ConvertTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.ConvertAPI = Convert.ConvertAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.ConvertAPI = Convert.ConvertAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) ''' def test_get_currencies(self): diff --git a/test/test_copy_trading.py b/test/test_copy_trading.py new file mode 100644 index 00000000..b516bc5e --- /dev/null +++ b/test/test_copy_trading.py @@ -0,0 +1,40 @@ +import unittest +from okx import CopyTrading +from test.config import get_api_credentials + + +class CopyTradingTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.StackingAPI = CopyTrading.CopyTradingAPI(api_key, api_secret_key, passphrase, use_server_time=False, + flag=flag) + + # def test_get_existing_leading_positions(self): + # print(self.StackingAPI.get_existing_leading_positions(instId='DOGE-USDT-SWAP')) + + # def test_get_leading_position_history(self): + # print(self.StackingAPI.get_leading_position_history()) + + # def test_place_leading_stop_order(self): + # print(self.StackingAPI.place_leading_stop_order(subPosId='581247467976732672',tpTriggerPx='1')) + # + # def test_close_leading_position(self): + # print(self.StackingAPI.close_leading_position(subPosId='581247467976732672')) + + # def test_get_leading_instruments(self): + # print(self.StackingAPI.get_leading_instruments()) + + # def test_amend_leading_instruments(self): + # print(self.StackingAPI.amend_leading_instruments(instId='AAVE-USDT-SWAP')) + # + # def test_get_profit_sharing_details(self): + # print(self.StackingAPI.get_profit_sharing_details()) + # + # def test_get_total_profit_sharing(self): + # print(self.StackingAPI.get_total_profit_sharing()) + # + def test_get_unrealized_profit_sharing_details(self): + print(self.StackingAPI.get_unrealized_profit_sharing_details()) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/test_eth_staking.py b/test/test_eth_staking.py new file mode 100644 index 00000000..f280d1d6 --- /dev/null +++ b/test/test_eth_staking.py @@ -0,0 +1,30 @@ +import unittest +from okx.Finance import EthStaking +from test.config import get_api_credentials + + +class EthStakingTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.StackingAPI = EthStaking.EthStakingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) + + def test_eth_product_info(self): + print(self.StackingAPI.eth_product_info()) + + def test_eth_purchase(self): + print(self.StackingAPI.eth_purchase(amt="1")) + + def test_eth_redeem(self): + print(self.StackingAPI.eth_redeem(amt="1")) + + def test_eth_balance(self): + print(self.StackingAPI.eth_balance()) + + def test_eth_purchase_redeem_history(self): + print(self.StackingAPI.eth_purchase_redeem_history(type="", status="", after="", before="", limit="")) + + def test_eth_apy_history(self): + print(self.StackingAPI.eth_apy_history(days="7")) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/test_flexible_loan.py b/test/test_flexible_loan.py new file mode 100644 index 00000000..517b194e --- /dev/null +++ b/test/test_flexible_loan.py @@ -0,0 +1,36 @@ +import unittest +from okx.Finance import FlexibleLoan +from test.config import get_api_credentials + + +class FlexibleLoanTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.FlexibleLoanAPI = FlexibleLoan.FlexibleLoanAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) + + def test_borrow_currencies(self): + print(self.FlexibleLoanAPI.borrow_currencies()) + + def test_collateral_assets(self): + print(self.FlexibleLoanAPI.collateral_assets()) + + def test_max_loan(self): + print(self.FlexibleLoanAPI.max_loan(borrowCcy='USDT')) + + def test_max_collateral_redeem_amount(self): + print(self.FlexibleLoanAPI.max_collateral_redeem_amount(ccy='USDT')) + + def test_adjust_collateral(self): + print(self.FlexibleLoanAPI.adjust_collateral(type="add", collateralCcy="USDT", collateralAmt="1")) + + def test_loan_info(self): + print(self.FlexibleLoanAPI.loan_info()) + + def test_loan_history(self): + print(self.FlexibleLoanAPI.loan_history()) + + def test_interest_accrued(self): + print(self.FlexibleLoanAPI.interest_accrued()) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/FundingTest.py b/test/test_funding.py similarity index 61% rename from test/FundingTest.py rename to test/test_funding.py index 2cce1e1e..3247f653 100644 --- a/test/FundingTest.py +++ b/test/test_funding.py @@ -1,13 +1,13 @@ import unittest -from ..okx import Funding +from okx import Funding +from test.config import get_api_credentials + class FundingTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.FundingAPI = Funding.FundingAPI(use_server_time=False, flag='0') + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.FundingAPI = Funding.FundingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) """ CANCEL_WITHDRAWAL = '/api/v5/asset/cancel-withdrawal' #need add CONVERT_DUST_ASSETS = '/api/v5/asset/convert-dust-assets' #need add @@ -58,9 +58,36 @@ def test_get_lending_summary(self): print(self.FundingAPI.get_lending_rate_summary('BTC')) """ - def test_get_lending_summary(self): - print(self.FundingAPI.get_lending_rate_summary('BTC')) - def test_get_lending_rate_history(self): - print(self.FundingAPI.get_lending_rate_history()) + # def test_get_non_tradable_assets(self): + # print(self.FundingAPI.get_non_tradable_assets()) + # def test_get_lending_summary(self): + # print(self.FundingAPI.get_lending_rate_summary('BTC')) + # def test_get_lending_rate_history(self): + # print(self.FundingAPI.get_lending_rate_history()) + + # def test_get_non_tradable_assets(self): + # print(self.FundingAPI.get_non_tradable_assets()) + + # def test_get_deposit_withdraw_status(self): + # print(self.FundingAPI.get_deposit_withdraw_status(wdId='84804812')) + + # def test_get_withdrawal_history(self): + # print(self.FundingAPI.get_withdrawal_history()) + + # def test_get_deposit_history(self): + # print(self.FundingAPI.get_deposit_history()) + + def test_withdrawal(self): + # toAddrType: Address type + # 1: Wallet address, email, phone number or login account + # 2: UID (only applicable when dest=3) + print(self.FundingAPI.withdrawal(ccy='USDT', amt='1', dest='3', toAddr='18740405107', areaCode='86', toAddrType='1')) + + def test_get_withdrawal_history_with_toAddrType(self): + # toAddrType: Address type filter + # 1: Wallet address, email, phone number or login account + # 2: UID + print(self.FundingAPI.get_withdrawal_history(toAddrType='1')) + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/test/test_grid.py b/test/test_grid.py new file mode 100644 index 00000000..fb14c951 --- /dev/null +++ b/test/test_grid.py @@ -0,0 +1,109 @@ + +import unittest +from okx import Grid +from test.config import get_api_credentials + + +class GridTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.GridAPI = Grid.GridAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag, debug=False) + """ + GRID_COMPUTE_MARIGIN_BALANCE = '/api/v5/tradingBot/grid/compute-margin-balance' + GRID_MARGIN_BALANCE = '/api/v5/tradingBot/grid/margin-balance' + GRID_AI_PARAM = '/api/v5/tradingBot/grid/ai-param' + + def test_ai_param(self): + print(self.GridAPI.grid_ai_param("grid","BTC-USDT")) + + def test_order_algo(self): + print(self.GridAPI.grid_order_algo("BTC-USDT","grid","45000","20000","100","1",quoteSz="50")) + #479973849967362048 + def test_grid_margin_balance(self): + print(self.GridAPI.grid_adjust_margin_balance()) + def test_compute_margin_balance(self): + print(self.GridAPI.grid_compute_margin_balance("479978879210491904","add","100")) + + def test_pending_grid_order(self): + print(self.GridAPI.grid_orders_algo_pending("grid")) + def test_amend_order_algo(self): + print(self.GridAPI.grid_amend_order_algo('485238792325173248','BTC-USDT-SWAP',tpTriggerPx='50000')) + def test_stop_order_algo(self): + print(self.GridAPI.grid_stop_order_algo('485238792325173248','BTC-USDT-SWAP','contract_grid','1')) + def test_pending_grid_order(self): + print(self.GridAPI.grid_orders_algo_pending("grid")) + def test_algo_history(self): + print(self.GridAPI.grid_orders_algo_history('contract_grid')) + def test_orders_algo_details(self): + print(self.GridAPI.grid_orders_algo_details('contract_grid','485238792325173248')) + def test_get_sub_orders(self): + print(self.GridAPI.grid_sub_orders('485238792325173248','contract_grid','filled')) + def test_order_algo2(self): + print(self.GridAPI.grid_order_algo("BTC-USDT-SWAP","contract_grid","45000","20000","100","1",sz='3000',direction='long',lever='3.0')) + def test_get_positions(self): + print(self.GridAPI.grid_positions('contract_grid','485379848832286720')) + def test_withdrawl_profits(self): + print(self.GridAPI.grid_withdraw_income('11111')) + def test_withdrawl_profits(self): + print(self.GridAPI.grid_withdraw_income('485380442313723904')) + """ + + + # def test_order_algo(self): + # print(self.GridAPI.grid_order_algo("BTC-USDT","grid","45000","20000","100","1",quoteSz="50")) + + # def test_place_recurring_buy_order(self): + # print(self.GridAPI.place_recurring_buy_order(stgyName="jzhtest",recurringList=[{ + # 'ccy':"ETH", + # 'ratio':'1' + # }],period="daily",recurringDay='1',recurringTime='0',timeZone='8',amt='100',investmentCcy='USDT',tdMode='cross')) + + # def test_amend_recurring_buy_order(self): + # print(self.GridAPI.amend_recurring_buy_order(algoId="581185292170952704",stgyName="changtest")) + + # def test_stop_recurring_buy_order(self): + # orderData = [{ + # "algoId": "581190894481838080" + # }] + # print(self.GridAPI.stop_recurring_buy_order(orderData)) + + # def test_get_recurring_buy_order_list(self): + # print(self.GridAPI.get_recurring_buy_order_list()) + + # def test_get_recurring_buy_order_history(self): + # print(self.GridAPI.get_recurring_buy_order_history()) + + # def test_get_recurring_buy_order_details(self): + # print(self.GridAPI.get_recurring_buy_order_details(algoId="581191143417970688")) + + # def test_get_recurring_buy_sub_orders(self): + # print(self.GridAPI.get_recurring_buy_sub_orders(algoId="581191143417970688")) + + #def test_grid_order_algo_with_trade_quote_ccy(self): + # print(self.GridAPI.grid_order_algo( + # instId="BTC-USDT", + # algoOrdType="grid", + # maxPx="45000", + # minPx="20000", + # gridNum="100", + # runType="1", + # quoteSz="50", + # tradeQuoteCcy="USDT" + # )) + + #def test_place_recurring_buy_order_with_trade_quote_ccy(self): + # print(self.GridAPI.place_recurring_buy_order( + # stgyName="test_strategy", + # recurringList=[{'ccy': 'ETH', 'ratio': '1'}], + # period="daily", + # recurringDay='1', + # recurringTime='0', + # timeZone='8', + # amt='100', + # investmentCcy='USDT', + # tdMode='cash', + # tradeQuoteCcy="USDT" + # )) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/MarketTest.py b/test/test_market.py similarity index 84% rename from test/MarketTest.py rename to test/test_market.py index fdf8df77..f3b6acff 100644 --- a/test/MarketTest.py +++ b/test/test_market.py @@ -1,6 +1,6 @@ import unittest -from ..okx import MarketData +from okx import MarketData ''' ORACLE = '/api/v5/market/open-oracle' #need to update? if it is open oracle @@ -12,16 +12,15 @@ BLOCK_TRADES = '/api/v5/market/block-trades'#need to add ''' +from test.config import get_api_credentials + + class MarketAPITest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.MarketApi = MarketData.MarketAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.MarketApi = MarketData.MarketAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) ''' - def test_oracle(self): - print(self.MarketApi.get_oracle()) def test_index_component(self): print(self.MarketApi.get_index_components("BTC-USDT")) def test_exchange_rate(self): @@ -58,7 +57,11 @@ def test_get_platform_24_volume(self): print(self.MarketApi.get_volume()) ''' + # def test_get_order_lite_book(self): + # print(self.MarketApi.get_order_lite_book(instId='BTC-USDT')) + def test_get_option_trades(self): + print(self.MarketApi.get_option_trades(instFamily='BTC-USD')) if __name__ == "__main__": diff --git a/test/PublicDataTest.py b/test/test_public_data.py similarity index 68% rename from test/PublicDataTest.py rename to test/test_public_data.py index f0b51389..79e5d350 100644 --- a/test/PublicDataTest.py +++ b/test/test_public_data.py @@ -1,11 +1,12 @@ import unittest -from ..okx import PublicData +from okx import PublicData +from test.config import get_api_credentials + + class publicDataTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' - self.publicDataApi = PublicData.PublicAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.publicDataApi = PublicData.PublicAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) ''' TestCase For: INTEREST_LOAN = '/api/v5/public/interest-rate-loan-quota' #need to add @@ -50,8 +51,29 @@ def test_get_mark_price(self): print(self.publicDataApi.get_mark_price('SWAP')) ''' - def test_position_tier(self): - print(self.publicDataApi.get_position_tiers('SWAP','cross',uly='ETH-USD')) + # def test_position_tier(self): + # print(self.publicDataApi.get_position_tiers('SWAP','cross',uly='ETH-USD')) + + # def test_get_option_tickBands(self): + # print(self.publicDataApi.get_option_tick_bands(instType='OPTION')) + + # def test_get_option_trades(self): + # print(self.publicDataApi.get_option_trades(instFamily='BTC-USD')) + + def test_get_market_data_history(self): + # module: 数据模块类型 + # 1: Trade history, 2: 1-minute candlestick, 3: Funding rate + # 5: 5000-level orderbook (from Nov 1, 2025), 6: 50-level orderbook + # instType: SPOT, FUTURES, SWAP, OPTION + # dateAggrType: daily, monthly + print(self.publicDataApi.get_market_data_history( + module='6', + instType='SPOT', + dateAggrType='daily', + begin='1761274032000', + end='1761883371133', + instIdList='BTC-USDT' + )) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/test/test_savings.py b/test/test_savings.py new file mode 100644 index 00000000..9baf6632 --- /dev/null +++ b/test/test_savings.py @@ -0,0 +1,31 @@ +import unittest +from okx.Finance import Savings +from test.config import get_api_credentials + + +class SavingsTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.StackingAPI = Savings.SavingsAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) + + + def test_get_saving_balance(self): + print(self.StackingAPI.get_saving_balance(ccy='USDT')) + + def test_savings_purchase_redemption(self): + print(self.StackingAPI.savings_purchase_redemption(ccy='USDT',amt="0.1",side="redempt",rate="1")) + + def test_set_lending_rate(self): + print(self.StackingAPI.set_lending_rate(ccy='USDT',rate="1")) + + def test_get_lending_history(self): + print(self.StackingAPI.get_lending_history(ccy='USDT')) + + def test_get_public_borrow_history(self): + print(self.StackingAPI.get_public_borrow_history(ccy='USDT')) + + def test_get_public_borrow_info(self): + print(self.StackingAPI.get_public_borrow_info(ccy='BTC')) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/test_sol_staking.py b/test/test_sol_staking.py new file mode 100644 index 00000000..110fb67d --- /dev/null +++ b/test/test_sol_staking.py @@ -0,0 +1,30 @@ +import unittest +from okx.Finance import SolStaking +from test.config import get_api_credentials + + +class SolStakingTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.StackingAPI = SolStaking.SolStakingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) + + def test_sol_purchase(self): + print(self.StackingAPI.sol_purchase(amt="1")) + + def test_sol_redeem(self): + print(self.StackingAPI.sol_redeem(amt="1")) + + def test_sol_balance(self): + print(self.StackingAPI.sol_balance()) + + def test_sol_purchase_redeem_history(self): + print(self.StackingAPI.sol_purchase_redeem_history(type="purchase", status="", after="", before="", limit="")) + + def test_sol_apy_history(self): + print(self.StackingAPI.sol_apy_history(days="7")) + + def test_sol_product_info(self): + print(self.StackingAPI.sol_product_info()) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/test_spread.py b/test/test_spread.py new file mode 100644 index 00000000..25a596b2 --- /dev/null +++ b/test/test_spread.py @@ -0,0 +1,47 @@ +import unittest +from okx import SpreadTrading +from test.config import get_api_credentials + + +class TradeTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.tradeApi = SpreadTrading.SpreadTradingAPI(api_key, api_secret_key, passphrase, False, flag) + + # def test_place_order(self): + # print(self.tradeApi.place_order(sprdId='BTC-USDT_BTC-USDT-SWAP',clOrdId='b15',side='buy',ordType='limit', + # px='2',sz='2')) + #{'code': '0', 'msg': '', 'data': [{'ordId': '1899422086260064256', 'clOrdId': 'b15', 'tag': '', 'sCode': '0', 'sMsg': ''}]} + + # def test_cancel_order(self): + # print(self.tradeApi.cancel_order(ordId='1899422086260064256')) + + # def test_cancel_all_orders(self): + # print(self.tradeApi.cancel_all_orders(sprdId='BTC-USDT_BTC-USDT-SWAP')) + + + #{'code': '0', 'msg': '','data': [{'ordId': '1899453539647750144', 'clOrdId': 'b15', 'tag': '', 'sCode': '0', + # 'sMsg': ''}]} + # def test_get_order_details(self): + # print(self.tradeApi.get_order_details(ordId='1899453539647750144')) + + # def test_get_active_orders(self): + # print(self.tradeApi.get_active_orders()) + + # def test_get_orders(self): + # print(self.tradeApi.get_orders()) + + # def test_get_spreads(self): + # print(self.tradeApi.get_spreads()) + + # def test_get_order_book(self): + # print(self.tradeApi.get_order_book(sprdId='ETH-USDT-SWAP_ETH-USDT-230929')) + # + # def test_get_ticker(self): + # print(self.tradeApi.get_ticker(sprdId='ETH-USDT-SWAP_ETH-USDT-230929')) + # + def test_get_public_trades(self): + print(self.tradeApi.get_public_trades(sprdId='ETH-USDT-SWAP_ETH-USDT-230929')) + +if __name__=='__main__': + unittest.main() diff --git a/test/test_staking_defi.py b/test/test_staking_defi.py new file mode 100644 index 00000000..bfe908a6 --- /dev/null +++ b/test/test_staking_defi.py @@ -0,0 +1,35 @@ +import unittest +from okx.Finance import StakingDefi +from test.config import get_api_credentials + + +class StakingDefiTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.StackingAPI = StakingDefi.StakingDefiAPI(api_key, api_secret_key, passphrase, use_server_time=False, + flag=flag) + + def test_get_offers(self): + print(self.StackingAPI.get_offers(ccy="USDT")) + + def test_purcase(self): + print(self.StackingAPI.purchase(1456, [{ + "ccy":"USDT", + "amt":"100" + }], "100", "0")) + + def test_redeem(self): + print(self.StackingAPI.redeem(1456,"defi")) + + def test_cancel(self): + print(self.StackingAPI.cancel(1456,"defi")) + + def test_get_activity_orders(self): + print(self.StackingAPI.get_activity_orders()) + + def test_get_orders_history(self): + print(self.StackingAPI.get_orders_history()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/SubAccountTest.py b/test/test_sub_account.py similarity index 65% rename from test/SubAccountTest.py rename to test/test_sub_account.py index c251c0b8..8682194e 100644 --- a/test/SubAccountTest.py +++ b/test/test_sub_account.py @@ -1,12 +1,12 @@ import unittest -from ..okx import SubAccount +from okx import SubAccount +from test.config import get_api_credentials + class SubAccountTest(unittest.TestCase): def setUp(self): - api_key = '52c37310-a8b0-454a-8191-3250acff2626' - api_secret_key = 'EC37534156E6B8C32E78FE8D8C1D506B' - passphrase = 'Hanhao0.0' - self.SubAccountApi = SubAccount.SubAccountAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.SubAccountApi = SubAccount.SubAccountAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag=flag) ''' ENTRUST_SUBACCOUNT_LIST = '/api/v5/users/entrust-subaccount-list' #need to add SET_TRSNSFER_OUT = '/api/v5/users/subaccount/set-transfer-out' #need to add @@ -36,5 +36,15 @@ def test_subaccount_transfer(self): ''' + # def test_set_sub_accounts_VIP_loan(self): + # print(self.SubAccountApi.set_sub_accounts_VIP_loan(enable='true',alloc=[{'subAcct':'coretrading7', + # 'loanAlloc':'1'}])) + + def test_get_sub_account_borrow_interest_and_limit(self): + print(self.SubAccountApi.get_sub_account_borrow_interest_and_limit(subAcct='coretrading7')) + + # def test_get_history_of_managed_subAccount_transfer(self): + # print(self.SubAccountApi.get_history_of_managed_subAccount_transfer()) + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/test/test_trade.py b/test/test_trade.py new file mode 100644 index 00000000..6da7a54c --- /dev/null +++ b/test/test_trade.py @@ -0,0 +1,298 @@ +import unittest + +from okx import Trade +from test.config import get_api_credentials + + +class TradeTest(unittest.TestCase): + def setUp(self): + api_key, api_secret_key, passphrase, flag = get_api_credentials() + self.tradeApi = Trade.TradeAPI(api_key, api_secret_key, passphrase, False, flag) + + # # """ + # def test_place_order(self): + # attachAlgoOrds = [{'tpTriggerPx': '49000.0', 'tpOrdPx': '-1', 'sz': '1', 'tpTriggerPxType': 'last'}]; + # print(self.tradeApi.place_order( + # "BTC-USDT-SWAP", tdMode="isolated", clOrdId="asCai1234", side='buy', posSide="long", ordType="limit", + # sz="1", + # px="30000.0", + # attachAlgoOrds=attachAlgoOrds) + # ); + # + # attachAlgoOrds = [{'slTriggerPx': '25000.0', 'slOrdPx': '-1', 'sz': '1', 'slTriggerPxType': 'last'}]; + # print(self.tradeApi.place_order( + # "BTC-USDT-SWAP", tdMode="isolated", clOrdId="asCai1234", side='buy', posSide="long", ordType="limit", + # sz="1", + # px="30000.0", + # attachAlgoOrds=attachAlgoOrds) + # ); + # + # attachAlgoOrds = [ + # { + # "tpTriggerPxType": "last", + # "tpOrdPx": "-1", + # "tpTriggerPx": "34000", + # "sz": "1" + # }, + # { + # "tpTriggerPxType": "last", + # "tpOrdPx": "-1", + # "tpTriggerPx": "35000", + # "sz": "1" + # }, + # { + # "slTriggerPxType": "last", + # "slOrdPx": "-1", + # "slTriggerPx": "20000", + # "sz": "3" + # } + # ] + # print(self.tradeApi.place_order( + # "BTC-USDT-SWAP", tdMode="isolated", clOrdId="asCai1234", side='buy', posSide="long", ordType="limit", + # sz="1", + # px="30000.0", + # attachAlgoOrds=attachAlgoOrds) + # ); + + # def test_cancel_order(self): + # print(self.tradeApi.cancel_order(instId="ETH-USDT",ordId="480702180748558336")) + + # def test_batch_order(self): + # orderData = [{ + # "instId": "BTC-USDT-SWAP", + # "tdMode": "isolated", + # "clOrdId": "b15112122", + # "side": "buy", + # "posSide": "long", + # "ordType": "limit", + # "px": "30000.0", + # "sz": "2", + # "attachAlgoOrds": [{'tpTriggerPx': '50000.0', 'tpOrdPx': '-1', 'sz': '1', 'tpTriggerPxType': 'last'}] + # }, + # { + # "instId": "BTC-USDT-SWAP", + # "tdMode": "isolated", + # "clOrdId": "b15112111", + # "side": "buy", + # "posSide": "long", + # "ordType": "limit", + # "px": "31000.0", + # "sz": "2", + # "attachAlgoOrds": [{'tpTriggerPx': '51000.0', 'tpOrdPx': '-1', 'sz': '1', 'tpTriggerPxType': 'last'}] + # } + # ] + # + # print(self.tradeApi.place_multiple_orders(orderData)) + + # 480702180748558336 + # def test_cancel_batch_orders(self): + # data=[ + # { + # 'instId':"ETH-USDT", + # 'ordId':"480702885353881600" + # }, + # { + # 'instId':"BTC-USDT", + # 'ordId':'480702885353881601' + # } + # ] + # print(self.tradeApi.cancel_multiple_orders(data)) + # def test_amend_order(self): + # attachAlgoOrds = [{'attachAlgoId': '672081789170569217', 'newTpTriggerPx': '55000.0'}]; + # print(self.tradeApi.amend_order("BTC-USDT-SWAP", ordId="672081789170569216", newSz="1", + # attachAlgoOrds=attachAlgoOrds)) + + # def test_amend_order_batch(self): + # orderData = [ + # { + # 'instId': 'BTC-USDT-SWAP', + # 'ordId': '672081789170569216', + # 'newSz': '1', + # "attachAlgoOrds": [{'attachAlgoId': '672081789170569217', 'newTpTriggerPx': '53000.0'}] + # } + # ] + # + # print(self.tradeApi.amend_multiple_orders(orderData)) + + # def test_close_all_positions(self): + # print(self.tradeApi.close_positions("BTC-USDT",mgnMode="cross")) + # def test_get_order_info(self): + # print(self.tradeApi.get_orders("ETH-USDT","480707205436669952")) + # def test_get_order_pending(self): + # print(self.tradeApi.get_order_list("SPOT")) + # def test_get_order_history(self): + # print(self.tradeApi.get_orders_history("SPOT")) + # def test_get_order_histry_archive(self): + # print(self.tradeApi.orders_history_archive("SPOT")) + # def test_get_fills(self): + # print(self.tradeApi.get_fills(begin='1717045609000',end='1717045609100')) + # def test_get_fills_history(self): + # print(self.tradeApi.get_fills_history("SPOT")) + # def test_get_order_algo_pending(self): + # print(self.tradeApi.order_algos_list('oco')) + # def test_order_algo(self): + # print(self.tradeApi.place_algo_order('BTC-USDT-SWAP', 'cross', side='buy', ordType='trigger', posSide='long', + # sz='100', triggerPx='22000', triggerPxType ='index', orderPx='-1')) + # def test_cancel_algos(self): + # params = [{ + # 'algoId': '485903392536264704', + # 'instId': 'BTC-USDT-SWAP' + # }] + # + # + # print(self.tradeApi.cancel_algo_order(params)) + # def test_orders_algo_pending(self): + # print(self.tradeApi.order_algos_list(ordType='iceberg')) + # def test_algo_order_history(self): + # print(self.tradeApi.order_algos_history(algoId='485903392536264704',ordType='conditional')) + # def test_get_easy_convert_list(self): + # print(self.tradeApi.get_easy_convert_currency_list()) + # def test_easy_convert(self): + # print(self.tradeApi.easy_convert(fromCcy=['BTC'],toCcy='OKB')) + # def test_get_convert_history(self): + # print(self.tradeApi.get_easy_convert_history()) + # def test_get_oneclick_repay_support_list(self): + # print(self.tradeApi.get_oneclick_repay_list('cross')) + # def test_oneclick_repay(self): + # print(self.tradeApi.oneclick_repay(['BTC'],'USDT')) + # 485903392536264704 + # 485936482235191296 + # def test_oneclick_repay_history(self): + # print(self.tradeApi.oneclick_repay_history()) + # def test_order_algo(self): + # print(self.tradeApi.place_algo_order(instId='BTC-USDT-SWAP', tdMode='cross', side='buy', ordType='conditional', \ + # tpTriggerPx='15', tpOrdPx='18',sz='2')) + + # 581628185981308928 + # def test_get_algo_order_details(self): + # print(self.tradeApi.get_algo_order_details(algoId='581628185981308928')) + + # 581628185981308928 + # def test_amend_algo_order(self): + # print(self.tradeApi.amend_algo_order(instId='BTC-USDT-SWAP', algoId='581628185981308928',newSz='3')) + + # def test_get_order_history(self): + # print(self.tradeApi.get_orders_history(instType="SPOT",begin='1684857629313',end='1684857629313')) + + # def test_get_order_histry_archive(self): + # print(self.tradeApi.get_orders_history_archive(instType="SPOT",begin='1684857629313',end='1684857629313')) + # def test_place_order(self): + # print(self.tradeApi.place_order("BTC-USDT", tdMode="cross", clOrdId="asCai1", side="buy", ordType="limit", + # sz="0.01", px="18000")) + # def test_batch_order(self): + # orderData = [{ + # "instId": "ETH-USDT", + # "tdMode": "cross", + # "clOrdId": "b151121", + # "side": "buy", + # "ordType": "limit", + # "px": "2.15", + # "sz": "2" + # }, + # { + # "instId": "BTC-USDT", + # "tdMode": "cross", + # "clOrdId": "b152233", + # "side": "buy", + # "ordType": "limit", + # "px": "2.15", + # "sz": "2" + # }] + # print(self.tradeApi.place_multiple_orders(orderData)) + + # 581616258865516544 + # 581616258865516545 + # def test_amend_order(self): + # print(self.tradeApi.amend_order("BTC-USDT", ordId="581616258865516544", newSz="0.03")) + # def test_amend_order_batch(self): + # orderData = [ + # { + # 'instId': 'ETH-USDT', + # 'ordId': '581616258865516544', + # 'newSz': '0.02' + # }, + # { + # 'instId': 'BTC-USDT', + # 'ordId': '581616258865516545', + # 'newPx': '3.0' + # } + # ] + # print(self.tradeApi.amend_multiple_orders(orderData)) + + # def test_order_algo(self): + # + # print(self.tradeApi.place_algo_order(instId='BTC-USDT-SWAP', tdMode='cross', side='buy', ordType='conditional', \ + # tpTriggerPx='15', tpOrdPx='18', sz='2',algoClOrdId='7678687',quickMgnType='manual')) + + # def test_order_algos_list(self): + # print(self.tradeApi.order_algos_list(ordType='conditional')) + + # def test_order_algo(self): + # print(self.tradeApi.place_order(instId='BTC-USDT-SWAP', tdMode='cross', side='buy',px='121',sz='2', + # clOrdId='234234565535',ordType='market')) + # def test_close_all_positions(self): + # print(self.tradeApi.close_positions(instId="BTC-USDT-SWAP", mgnMode="cross",clOrdId='1213124')) + + #def test_get_oneclick_repay_list_v2(self): + # print(self.tradeApi.get_oneclick_repay_list_v2()) + #def test_oneclick_repay_v2(self): + # print(self.tradeApi.oneclick_repay_v2('BTC',['USDT'])) + #def test_oneclick_repay_history_v2(self): + # print(self.tradeApi.oneclick_repay_history_v2()) + + #def test_place_order_with_trade_quote_ccy(self): + # print(self.tradeApi.place_order( + # instId="BTC-USDT", + # tdMode="cash", + # side="buy", + # ordType="limit", + # sz="0.01", + # px="30000", + # tradeQuoteCcy="USDT" + # )) + + #def test_place_order_with_px_amend_type(self): + # print(self.tradeApi.place_order( + # instId="BTC-USDT-SWAP", + # tdMode="cash", + # side="buy", + # ordType="limit", + # sz="1", + # px="30000", + # pxAmendType="1" + # )) + + #def test_amend_order_with_px_amend_type(self): + # print(self.tradeApi.amend_order( + # instId="BTC-USDT-SWAP", + # ordId="123", + # newPx="30500", + # pxAmendType="1" + # )) + + #def test_place_algo_order_with_trade_quote_ccy(self): + # print(self.tradeApi.place_algo_order( + # instId="BTC-USDT-SWAP", + # tdMode="cash", + # side="buy", + # ordType="trigger", + # sz="1", + # triggerPx="30000", + # orderPx="-1", + # tradeQuoteCcy="USDT" + # )) + + #def test_place_algo_order_with_px_amend_type(self): + # print(self.tradeApi.place_algo_order( + # instId="BTC-USDT-SWAP", + # tdMode="cash", + # side="buy", + # ordType="trigger", + # sz="1", + # triggerPx="30000", + # orderPx="-1", + # pxAmendType="1" + # )) + +if __name__ == '__main__': + unittest.main() diff --git a/test/TradingDataTest.py b/test/test_trading_data.py similarity index 68% rename from test/TradingDataTest.py rename to test/test_trading_data.py index e3083fdf..99eaabd4 100644 --- a/test/TradingDataTest.py +++ b/test/test_trading_data.py @@ -1,14 +1,13 @@ import unittest -from ..okx import TradingData +from okx import TradingData +from test.config import get_api_credentials class TradingDataTest(unittest.TestCase): def setUp(self): - api_key = '52c37310-a8b0-454a-8191-3250acff2626' - api_secret_key = 'EC37534156E6B8C32E78FE8D8C1D506B' - passphrase = 'Hanhao0.0' + api_key, api_secret_key, passphrase, flag = get_api_credentials() self.TradingDataAPI = TradingData.TradingDataAPI(api_key, api_secret_key, passphrase, use_server_time=False, - flag='1') + flag=flag) """ def test_get_support_coins(self): print(self.TradingDataAPI.get_support_coin()) @@ -30,8 +29,16 @@ def test_open_interest_volume_strike(self): print(self.TradingDataAPI.get_interest_volume_strike(ccy="BTC",expTime="20220901")) """ - def test_taker_block_vol(self): - print(self.TradingDataAPI.get_taker_flow(ccy='BTC')) + + # def test_get_open_interest_history(self): + # print(self.TradingDataAPI.get_open_interest_history(instId='BTC-USDT-SWAP')) + # + # def test_get_open_interest_history_with_params(self): + # print(self.TradingDataAPI.get_open_interest_history( + # instId='BTC-USDT-SWAP', + # period='1H', + # limit='50' + # )) if __name__ == "__main__": unittest.main() diff --git a/test/test_ws_private_async.py b/test/test_ws_private_async.py new file mode 100644 index 00000000..082f61ad --- /dev/null +++ b/test/test_ws_private_async.py @@ -0,0 +1,274 @@ +import asyncio + +from okx.websocket.WsPrivateAsync import WsPrivateAsync +from test.config import get_api_credentials + +# Test constants +WS_PRIVATE_URL = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" +WS_BUSINESS_URL = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" + + +def privateCallback(message): + print("privateCallback", message) + + +async def main(): + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + args = [] + arg1 = {"channel": "account", "ccy": "BTC"} + arg2 = {"channel": "orders", "instType": "ANY"} + arg3 = {"channel": "balance_and_position"} + # Withdrawal info channel subscription example, supporting the toAddrType parameter + # toAddrType: Address type + # 1: Wallet address, email, phone number or login account name + # 2: UID (applicable only when dest=3) + arg4 = {"channel": "withdrawal-info", "ccy": "USDT", "toAddrType": "1"} + args.append(arg1) + args.append(arg2) + args.append(arg3) + args.append(arg4) + await ws.subscribe(args, callback=privateCallback) + await asyncio.sleep(30) + print("-----------------------------------------unsubscribe--------------------------------------------") + args2 = [arg2] + # Use id parameter to identify unsubscribe request + await ws.unsubscribe(args2, callback=privateCallback, id="privateUnsub001") + await asyncio.sleep(5) + print("-----------------------------------------unsubscribe all--------------------------------------------") + args3 = [arg1, arg3] + await ws.unsubscribe(args3, callback=privateCallback) + await asyncio.sleep(1) + await ws.stop() + + +async def test_place_order(): + """ + Test place order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Order parameters + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "clOrdId": "client_order_001", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + await ws.place_order(order_args, callback=privateCallback, id="order001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_batch_orders(): + """ + Test batch orders functionality + URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Batch order parameters (max 20) + order_args = [ + { + "instId": "BTC-USDT", + "tdMode": "cash", + "clOrdId": "batch_order_001", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }, + { + "instId": "ETH-USDT", + "tdMode": "cash", + "clOrdId": "batch_order_002", + "side": "buy", + "ordType": "limit", + "sz": "0.01", + "px": "2000" + } + ] + await ws.batch_orders(order_args, callback=privateCallback, id="batchOrder001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_cancel_order(): + """ + Test cancel order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Cancel order parameters (either ordId or clOrdId must be provided) + cancel_args = [{ + "instId": "BTC-USDT", + "ordId": "your_order_id" + # Or use "clOrdId": "client_order_001" + }] + await ws.cancel_order(cancel_args, callback=privateCallback, id="cancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_batch_cancel_orders(): + """ + Test batch cancel orders functionality + URL: /ws/v5/private (Rate limit: 60 requests/second, max 20 orders) + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + cancel_args = [ + {"instId": "BTC-USDT", "ordId": "order_id_1"}, + {"instId": "ETH-USDT", "ordId": "order_id_2"} + ] + await ws.batch_cancel_orders(cancel_args, callback=privateCallback, id="batchCancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_amend_order(): + """ + Test amend order functionality + URL: /ws/v5/private (Rate limit: 60 requests/second) + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Amend order parameters + amend_args = [{ + "instId": "BTC-USDT", + "ordId": "your_order_id", + "newSz": "0.002", + "newPx": "31000" + }] + await ws.amend_order(amend_args, callback=privateCallback, id="amend001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_mass_cancel(): + """ + Test mass cancel functionality + URL: /ws/v5/business (Rate limit: 1 request/second) + Note: This function uses the business channel + """ + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_BUSINESS_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Mass cancel parameters + mass_cancel_args = [{ + "instType": "SPOT", + "instFamily": "BTC-USDT" + }] + await ws.mass_cancel(mass_cancel_args, callback=privateCallback, id="massCancel001") + await asyncio.sleep(5) + await ws.stop() + + +async def test_send_method(): + """Test generic send method""" + api_key, api_secret_key, passphrase, _ = get_api_credentials() + ws = WsPrivateAsync( + apiKey=api_key, + passphrase=passphrase, + secretKey=api_secret_key, + url=WS_PRIVATE_URL, + debug=True + ) + await ws.start() + await ws.login() + await asyncio.sleep(5) + + # Use generic send method to place order - callback must be provided to receive response + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + await ws.send("order", order_args, callback=privateCallback, id="send001") + await asyncio.sleep(5) + await ws.stop() + + +if __name__ == '__main__': + # asyncio.run(main()) + asyncio.run(test_place_order()) + asyncio.run(test_batch_orders()) + asyncio.run(test_cancel_order()) + asyncio.run(test_batch_cancel_orders()) + asyncio.run(test_amend_order()) + asyncio.run(test_mass_cancel()) # Note: uses business channel + asyncio.run(test_send_method()) diff --git a/test/test_ws_public_async.py b/test/test_ws_public_async.py new file mode 100644 index 00000000..dac6c84f --- /dev/null +++ b/test/test_ws_public_async.py @@ -0,0 +1,81 @@ +import asyncio + +from okx.websocket.WsPublicAsync import WsPublicAsync + + +def publicCallback(message): + print("publicCallback", message) + + +async def main(): + # url = "wss://wspap.okex.com:8443/ws/v5/public?brokerId=9999" + url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" + ws = WsPublicAsync(url=url, debug=True) # Enable debug logging + await ws.start() + args = [] + arg1 = {"channel": "instruments", "instType": "FUTURES"} + arg2 = {"channel": "instruments", "instType": "SPOT"} + arg3 = {"channel": "tickers", "instId": "BTC-USDT-SWAP"} + arg4 = {"channel": "tickers", "instId": "ETH-USDT"} + args.append(arg1) + args.append(arg2) + args.append(arg3) + args.append(arg4) + # Use id parameter to identify subscribe request, the same id will be returned in response + await ws.subscribe(args, publicCallback, id="sub001") + await asyncio.sleep(5) + print("-----------------------------------------unsubscribe--------------------------------------------") + args2 = [arg4] + # Use id parameter to identify unsubscribe request + await ws.unsubscribe(args2, publicCallback, id="unsub001") + await asyncio.sleep(5) + print("-----------------------------------------unsubscribe all--------------------------------------------") + args3 = [arg1, arg2, arg3] + await ws.unsubscribe(args3, publicCallback) + await asyncio.sleep(1) + await ws.stop() + + +async def test_business_channel_with_login(): + """ + Test business channel login functionality + Business channel requires login to subscribe to certain private data + """ + url = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999" + ws = WsPublicAsync( + url=url, + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + debug=True + ) + await ws.start() + + # Login + await ws.login() + await asyncio.sleep(5) + + # Subscribe to channels that require login + args = [{"channel": "candle1m", "instId": "BTC-USDT"}] + await ws.subscribe(args, publicCallback) + await asyncio.sleep(30) + await ws.stop() + + +async def test_send_method(): + """Test generic send method""" + url = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999" + ws = WsPublicAsync(url=url, debug=True) + await ws.start() + + # Use generic send method to subscribe - callback must be provided to receive response + args = [{"channel": "tickers", "instId": "BTC-USDT"}] + await ws.send("subscribe", args, callback=publicCallback, id="send001") + await asyncio.sleep(10) + await ws.stop() + + +if __name__ == '__main__': + # asyncio.run(main()) + # asyncio.run(test_business_channel_with_login()) + asyncio.run(test_send_method()) diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..940cd5ef --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,10 @@ +""" +Unit tests package + +Unit tests mirror the source code structure for easy navigation. + +Example: + okx/Account.py -> test/unit/okx/test_account.py + okx/Trade.py -> test/unit/okx/test_trade.py + okx/Finance/Savings.py -> test/unit/okx/Finance/test_savings.py +""" diff --git a/test/unit/okx/__init__.py b/test/unit/okx/__init__.py new file mode 100644 index 00000000..2b43621a --- /dev/null +++ b/test/unit/okx/__init__.py @@ -0,0 +1,2 @@ +"""Unit tests for okx package""" + diff --git a/test/unit/okx/test_account.py b/test/unit/okx/test_account.py new file mode 100644 index 00000000..949a1897 --- /dev/null +++ b/test/unit/okx/test_account.py @@ -0,0 +1,700 @@ +""" +Unit tests for okx.Account module + +Mirrors the structure: okx/Account.py -> test/unit/okx/test_account.py +""" +import unittest +from unittest.mock import patch +from okx.Account import AccountAPI +from okx import consts as c + +# Test constants +IDX_VOL_NEGATIVE_5_PERCENT = '-0.05' + + +class TestAccountAPIPositionBuilder(unittest.TestCase): + """Unit tests for the position_builder method""" + + def setUp(self): + """Set up test fixtures""" + self.api_key = 'test_api_key' + self.api_secret = 'test_api_secret' + self.passphrase = 'test_passphrase' + self.account_api = AccountAPI( + api_key=self.api_key, + api_secret_key=self.api_secret, + passphrase=self.passphrase, + flag='0' + ) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_all_parameters(self, mock_request): + """Test position_builder with all parameters provided""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [{ + 'mmr': '1000', + 'imr': '2000', + 'mmrBf': '900', + 'imrBf': '1900' + }] + } + mock_request.return_value = mock_response + + sim_pos = [{'instId': 'BTC-USDT-SWAP', 'pos': '10', 'avgPx': '50000'}] + sim_asset = [{'ccy': 'USDT', 'amt': '10000'}] + + # Act + result = self.account_api.position_builder( + acctLv='2', + inclRealPosAndEq=True, + lever='5', + greeksType='PA', + simPos=sim_pos, + simAsset=sim_asset, + idxVol='0.05' + ) + + # Assert + expected_params = { + 'acctLv': '2', + 'inclRealPosAndEq': True, + 'lever': '5', + 'greeksType': 'PA', + 'simPos': sim_pos, + 'simAsset': sim_asset, + 'idxVol': '0.05' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_idxVol_only(self, mock_request): + """Test position_builder with only idxVol parameter""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [] + } + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(idxVol='0.1') + + # Assert + expected_params = { + 'idxVol': '0.1' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_negative_idxVol(self, mock_request): + """Test position_builder with negative idxVol (price decrease)""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [] + } + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(idxVol=IDX_VOL_NEGATIVE_5_PERCENT) + + # Assert + expected_params = { + 'idxVol': IDX_VOL_NEGATIVE_5_PERCENT + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_no_parameters(self, mock_request): + """Test position_builder with no parameters (all None)""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [] + } + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder() + + # Assert + # Should pass empty params dict + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, {}) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_simulated_positions(self, mock_request): + """Test position_builder with simulated positions and assets""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [{ + 'mmr': '5000', + 'imr': '10000' + }] + } + mock_request.return_value = mock_response + + sim_pos = [ + {'instId': 'BTC-USDT-SWAP', 'pos': '10', 'avgPx': '50000'}, + {'instId': 'ETH-USDT-SWAP', 'pos': '100', 'avgPx': '3000'} + ] + sim_asset = [ + {'ccy': 'USDT', 'amt': '100000'}, + {'ccy': 'BTC', 'amt': '1'} + ] + + # Act + result = self.account_api.position_builder( + inclRealPosAndEq=False, + simPos=sim_pos, + simAsset=sim_asset, + idxVol='0.1' + ) + + # Assert + expected_params = { + 'inclRealPosAndEq': False, + 'simPos': sim_pos, + 'simAsset': sim_asset, + 'idxVol': '0.1' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_greeks_type_pa(self, mock_request): + """Test position_builder with greeksType PA""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(greeksType='PA') + + # Assert + expected_params = {'greeksType': 'PA'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_greeks_type_bs(self, mock_request): + """Test position_builder with greeksType BS""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(greeksType='BS') + + # Assert + expected_params = {'greeksType': 'BS'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_includes_real_positions(self, mock_request): + """Test position_builder with inclRealPosAndEq=True""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder( + inclRealPosAndEq=True, + idxVol='0.05' + ) + + # Assert + expected_params = { + 'inclRealPosAndEq': True, + 'idxVol': '0.05' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_excludes_real_positions(self, mock_request): + """Test position_builder with inclRealPosAndEq=False (only virtual positions)""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + sim_pos = [{'instId': 'BTC-USDT-SWAP', 'pos': '5', 'avgPx': '60000'}] + + # Act + result = self.account_api.position_builder( + inclRealPosAndEq=False, + simPos=sim_pos + ) + + # Assert + expected_params = { + 'inclRealPosAndEq': False, + 'simPos': sim_pos + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_account_level(self, mock_request): + """Test position_builder with specific account level""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(acctLv='3') + + # Assert + expected_params = {'acctLv': '3'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_with_leverage(self, mock_request): + """Test position_builder with leverage parameter""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(lever='10') + + # Assert + expected_params = {'lever': '10'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_extreme_volatility_positive(self, mock_request): + """Test position_builder with maximum positive volatility""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(idxVol='1') + + # Assert + expected_params = {'idxVol': '1'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_extreme_volatility_negative(self, mock_request): + """Test position_builder with maximum negative volatility""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(idxVol='-0.99') + + # Assert + expected_params = {'idxVol': '-0.99'} + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_position_builder_complex_scenario(self, mock_request): + """Test position_builder with a complex realistic scenario""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [{ + 'mmr': '15000', + 'imr': '30000', + 'mmrBf': '14000', + 'imrBf': '28000', + 'markPxBf': '49500' + }] + } + mock_request.return_value = mock_response + + sim_pos = [ + {'instId': 'BTC-USDT-SWAP', 'pos': '10', 'avgPx': '50000'}, + {'instId': 'ETH-USDT-SWAP', 'pos': '-50', 'avgPx': '3000'} + ] + sim_asset = [{'ccy': 'USDT', 'amt': '50000'}] + + # Act - Simulate a 5% market drop + result = self.account_api.position_builder( + acctLv='2', + inclRealPosAndEq=False, + lever='5', + greeksType='PA', + simPos=sim_pos, + simAsset=sim_asset, + idxVol=IDX_VOL_NEGATIVE_5_PERCENT + ) + + # Assert + expected_params = { + 'acctLv': '2', + 'inclRealPosAndEq': False, + 'lever': '5', + 'greeksType': 'PA', + 'simPos': sim_pos, + 'simAsset': sim_asset, + 'idxVol': IDX_VOL_NEGATIVE_5_PERCENT + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + self.assertEqual(result['code'], '0') + self.assertIn('mmrBf', result['data'][0]) + self.assertIn('imrBf', result['data'][0]) + + +class TestAccountAPIPositionBuilderParameterHandling(unittest.TestCase): + """Test parameter handling and edge cases""" + + def setUp(self): + """Set up test fixtures""" + self.account_api = AccountAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(AccountAPI, '_request_with_params') + def test_none_parameters_are_excluded(self, mock_request): + """Test that None parameters are not included in the request""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder( + acctLv='2', + inclRealPosAndEq=None, # Should be excluded + lever=None, # Should be excluded + greeksType='PA', + simPos=None, # Should be excluded + simAsset=None, # Should be excluded + idxVol='0.05' + ) + + # Assert - Only non-None params should be in the call + expected_params = { + 'acctLv': '2', + 'greeksType': 'PA', + 'idxVol': '0.05' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_false_value_for_inclRealPosAndEq_is_included(self, mock_request): + """Test that False value for inclRealPosAndEq is included (not treated as None)""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(inclRealPosAndEq=False) + + # Assert - False should be included + expected_params = { + 'inclRealPosAndEq': False + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_empty_lists_are_included(self, mock_request): + """Test that empty lists are included in the request""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder( + simPos=[], + simAsset=[] + ) + + # Assert + expected_params = { + 'simPos': [], + 'simAsset': [] + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_zero_idxVol_is_included(self, mock_request): + """Test that zero idxVol is included (represents no volatility change)""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.position_builder(idxVol='0') + + # Assert + expected_params = { + 'idxVol': '0' + } + mock_request.assert_called_once_with(c.POST, c.POSITION_BUILDER, expected_params) + + +class TestAccountAPISetAutoEarn(unittest.TestCase): + """Unit tests for the set_auto_earn method""" + + def setUp(self): + """Set up test fixtures""" + self.account_api = AccountAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(AccountAPI, '_request_with_params') + def test_set_auto_earn_with_all_params(self, mock_request): + """Test set_auto_earn with all parameters provided""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.set_auto_earn( + ccy='USDT', + action='turn_on', + earnType='0' + ) + + # Assert + expected_params = { + 'ccy': 'USDT', + 'action': 'turn_on', + 'earnType': '0' + } + mock_request.assert_called_once_with(c.POST, c.SET_AUTO_EARN, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_set_auto_earn_turn_on_action(self, mock_request): + """Test set_auto_earn with turn_on action""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.set_auto_earn( + ccy='BTC', + action='turn_on', + earnType='0' + ) + + # Assert + expected_params = { + 'ccy': 'BTC', + 'action': 'turn_on', + 'earnType': '0' + } + mock_request.assert_called_once_with(c.POST, c.SET_AUTO_EARN, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_set_auto_earn_turn_off_action(self, mock_request): + """Test set_auto_earn with turn_off action""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.set_auto_earn( + ccy='ETH', + action='turn_off', + earnType='0' + ) + + # Assert + expected_params = { + 'ccy': 'ETH', + 'action': 'turn_off', + 'earnType': '0' + } + mock_request.assert_called_once_with(c.POST, c.SET_AUTO_EARN, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_set_auto_earn_with_required_params_only(self, mock_request): + """Test set_auto_earn with required parameters only (ccy, action)""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.account_api.set_auto_earn(ccy='USDT', action='turn_on') + + # Assert + expected_params = { + 'ccy': 'USDT', + 'action': 'turn_on' + } + mock_request.assert_called_once_with(c.POST, c.SET_AUTO_EARN, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_set_auto_earn_different_currencies(self, mock_request): + """Test set_auto_earn with different currencies""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + currencies = ['USDT', 'BTC', 'ETH', 'USDC'] + + for ccy in currencies: + mock_request.reset_mock() + result = self.account_api.set_auto_earn( + ccy=ccy, + action='turn_on', + earnType='0' + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['ccy'], ccy) + + +class TestAccountAPIGetMaxOrderSize(unittest.TestCase): + """Unit tests for the get_max_order_size method""" + + def setUp(self): + """Set up test fixtures""" + self.account_api = AccountAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_order_size_with_required_params(self, mock_request): + """Test get_max_order_size with required parameters only""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_order_size( + instId='BTC-USDT', + tdMode='cash' + ) + + expected_params = { + 'instId': 'BTC-USDT', + 'tdMode': 'cash', + 'ccy': '', + 'px': '' + } + mock_request.assert_called_once_with(c.GET, c.MAX_TRADE_SIZE, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_order_size_with_tradeQuoteCcy(self, mock_request): + """Test get_max_order_size with tradeQuoteCcy parameter for Unified USD Orderbook""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_order_size( + instId='BTC-USD', + tdMode='cash', + tradeQuoteCcy='USDC' + ) + + expected_params = { + 'instId': 'BTC-USD', + 'tdMode': 'cash', + 'ccy': '', + 'px': '', + 'tradeQuoteCcy': 'USDC' + } + mock_request.assert_called_once_with(c.GET, c.MAX_TRADE_SIZE, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_order_size_without_tradeQuoteCcy(self, mock_request): + """Test get_max_order_size without tradeQuoteCcy (should not include in params)""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_order_size( + instId='BTC-USDT', + tdMode='cash' + ) + + call_args = mock_request.call_args[0][2] + self.assertNotIn('tradeQuoteCcy', call_args) + + +class TestAccountAPIGetMaxAvailSize(unittest.TestCase): + """Unit tests for the get_max_avail_size method""" + + def setUp(self): + """Set up test fixtures""" + self.account_api = AccountAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_avail_size_with_required_params(self, mock_request): + """Test get_max_avail_size with required parameters only""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_avail_size( + instId='BTC-USDT', + tdMode='cash' + ) + + expected_params = { + 'instId': 'BTC-USDT', + 'tdMode': 'cash', + 'ccy': '', + 'reduceOnly': '', + 'unSpotOffset': '', + 'quickMgnType': '' + } + mock_request.assert_called_once_with(c.GET, c.MAX_AVAIL_SIZE, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_avail_size_with_tradeQuoteCcy(self, mock_request): + """Test get_max_avail_size with tradeQuoteCcy parameter for Unified USD Orderbook""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_avail_size( + instId='BTC-USD', + tdMode='cash', + tradeQuoteCcy='USDC' + ) + + expected_params = { + 'instId': 'BTC-USD', + 'tdMode': 'cash', + 'ccy': '', + 'reduceOnly': '', + 'unSpotOffset': '', + 'quickMgnType': '', + 'tradeQuoteCcy': 'USDC' + } + mock_request.assert_called_once_with(c.GET, c.MAX_AVAIL_SIZE, expected_params) + + @patch.object(AccountAPI, '_request_with_params') + def test_get_max_avail_size_without_tradeQuoteCcy(self, mock_request): + """Test get_max_avail_size without tradeQuoteCcy (should not include in params)""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + result = self.account_api.get_max_avail_size( + instId='BTC-USDT', + tdMode='cash' + ) + + call_args = mock_request.call_args[0][2] + self.assertNotIn('tradeQuoteCcy', call_args) + + +if __name__ == '__main__': + unittest.main() + diff --git a/test/unit/okx/test_funding.py b/test/unit/okx/test_funding.py new file mode 100644 index 00000000..61fb7019 --- /dev/null +++ b/test/unit/okx/test_funding.py @@ -0,0 +1,225 @@ +""" +Unit tests for okx.Funding module + +Mirrors the structure: okx/Funding.py -> test/unit/okx/test_funding.py +""" +import unittest +from unittest.mock import patch +from okx.Funding import FundingAPI +from okx import consts as c + + +class TestFundingAPIWithdrawal(unittest.TestCase): + """Unit tests for the withdrawal method""" + + def setUp(self): + """Set up test fixtures""" + self.funding_api = FundingAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(FundingAPI, '_request_with_params') + def test_withdrawal_with_required_params(self, mock_request): + """Test withdrawal with required parameters only""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': [{'wdId': '12345'}]} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.withdrawal( + ccy='USDT', + amt='100', + dest='4', + toAddr='0x1234567890abcdef' + ) + + # Assert + expected_params = { + 'ccy': 'USDT', + 'amt': '100', + 'dest': '4', + 'toAddr': '0x1234567890abcdef', + 'chain': '', + 'areaCode': '', + 'clientId': '' + } + mock_request.assert_called_once_with(c.POST, c.WITHDRAWAL_COIN, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(FundingAPI, '_request_with_params') + def test_withdrawal_with_all_params(self, mock_request): + """Test withdrawal with all parameters provided""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': [{'wdId': '12345'}]} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.withdrawal( + ccy='USDT', + amt='100', + dest='4', + toAddr='0x1234567890abcdef', + chain='USDT-TRC20', + areaCode='86', + clientId='client123', + toAddrType='1' + ) + + # Assert + expected_params = { + 'ccy': 'USDT', + 'amt': '100', + 'dest': '4', + 'toAddr': '0x1234567890abcdef', + 'chain': 'USDT-TRC20', + 'areaCode': '86', + 'clientId': 'client123', + 'toAddrType': '1' + } + mock_request.assert_called_once_with(c.POST, c.WITHDRAWAL_COIN, expected_params) + + @patch.object(FundingAPI, '_request_with_params') + def test_withdrawal_with_toAddrType_okx_account(self, mock_request): + """Test withdrawal with toAddrType for OKX account""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.withdrawal( + ccy='USDT', + amt='50', + dest='3', + toAddr='user@example.com', + toAddrType='1' # OKX account + ) + + # Assert + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['toAddrType'], '1') + + @patch.object(FundingAPI, '_request_with_params') + def test_withdrawal_with_toAddrType_external(self, mock_request): + """Test withdrawal with toAddrType for external address""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.withdrawal( + ccy='BTC', + amt='0.1', + dest='4', + toAddr='bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', + toAddrType='2' # External address + ) + + # Assert + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['toAddrType'], '2') + + +class TestFundingAPIGetWithdrawalHistory(unittest.TestCase): + """Unit tests for the get_withdrawal_history method""" + + def setUp(self): + """Set up test fixtures""" + self.funding_api = FundingAPI( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + @patch.object(FundingAPI, '_request_with_params') + def test_get_withdrawal_history_with_no_params(self, mock_request): + """Test get_withdrawal_history with no parameters""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.get_withdrawal_history() + + # Assert + call_args = mock_request.call_args[0][2] + self.assertNotIn('toAddrType', call_args) + self.assertEqual(result, mock_response) + + @patch.object(FundingAPI, '_request_with_params') + def test_get_withdrawal_history_with_toAddrType(self, mock_request): + """Test get_withdrawal_history with toAddrType parameter""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.get_withdrawal_history( + ccy='USDT', + toAddrType='1' + ) + + # Assert + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['ccy'], 'USDT') + self.assertEqual(call_args['toAddrType'], '1') + + @patch.object(FundingAPI, '_request_with_params') + def test_get_withdrawal_history_with_all_params(self, mock_request): + """Test get_withdrawal_history with all parameters""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.funding_api.get_withdrawal_history( + ccy='BTC', + wdId='12345', + clientId='client123', + txId='tx123', + type='1', + state='2', + after='1609459200000', + before='1609545600000', + limit='10', + toAddrType='2' + ) + + # Assert + expected_params = { + 'ccy': 'BTC', + 'wdId': '12345', + 'clientId': 'client123', + 'txId': 'tx123', + 'type': '1', + 'state': '2', + 'after': '1609459200000', + 'before': '1609545600000', + 'limit': '10', + 'toAddrType': '2' + } + mock_request.assert_called_once_with(c.GET, c.GET_WITHDRAWAL_HISTORY, expected_params) + + @patch.object(FundingAPI, '_request_with_params') + def test_get_withdrawal_history_filter_by_state(self, mock_request): + """Test get_withdrawal_history filtering by state""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + states = ['0', '1', '2', '3', '4', '5'] + + for state in states: + mock_request.reset_mock() + result = self.funding_api.get_withdrawal_history(state=state) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['state'], state) + + +if __name__ == '__main__': + unittest.main() + diff --git a/test/unit/okx/test_okxclient.py b/test/unit/okx/test_okxclient.py new file mode 100644 index 00000000..da8f9006 --- /dev/null +++ b/test/unit/okx/test_okxclient.py @@ -0,0 +1,191 @@ +""" +Unit tests for okx.okxclient module + +Mirrors the structure: okx/okxclient.py -> test/unit/okx/test_okxclient.py +""" +import unittest +import warnings +from unittest.mock import patch, MagicMock + +# Test constants +MOCK_CLIENT_INIT = 'okx.okxclient.Client.__init__' +TEST_PROXY_URL = 'http://proxy.example.com:8080' +TEST_API_ENDPOINT = '/api/v5/test' + + +class TestOkxClientInit(unittest.TestCase): + """Unit tests for OkxClient initialization""" + + def test_init_with_default_parameters(self): + """Test initialization with default parameters""" + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.return_value = None + + from okx.okxclient import OkxClient + client = OkxClient() + + self.assertEqual(client.API_KEY, '-1') + self.assertEqual(client.API_SECRET_KEY, '-1') + self.assertEqual(client.PASSPHRASE, '-1') + self.assertEqual(client.flag, '1') + self.assertFalse(client.debug) + + def test_init_with_custom_parameters(self): + """Test initialization with custom parameters""" + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.return_value = None + + from okx.okxclient import OkxClient + client = OkxClient( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0', + debug=True + ) + + self.assertEqual(client.API_KEY, 'test_key') + self.assertEqual(client.API_SECRET_KEY, 'test_secret') + self.assertEqual(client.PASSPHRASE, 'test_pass') + self.assertEqual(client.flag, '0') + self.assertTrue(client.debug) + + def test_init_with_deprecated_use_server_time_shows_warning(self): + """Test that using deprecated use_server_time parameter shows warning""" + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.return_value = None + + from okx.okxclient import OkxClient + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + client = OkxClient(use_server_time=True) + + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, DeprecationWarning)) + self.assertIn("use_server_time parameter is deprecated", str(w[0].message)) + + +class TestOkxClientHttpxCompatibility(unittest.TestCase): + """Unit tests for httpx version compatibility in OkxClient""" + + def test_init_with_new_httpx_proxy_parameter(self): + """Test initialization with new httpx version using proxy parameter""" + with patch(MOCK_CLIENT_INIT) as mock_init: + # Simulate new httpx version (accepts proxy parameter) + mock_init.return_value = None + + from okx.okxclient import OkxClient + client = OkxClient(proxy=TEST_PROXY_URL) + + # Should call super().__init__ with proxy parameter + mock_init.assert_called_once() + call_kwargs = mock_init.call_args + self.assertIn('proxy', call_kwargs.kwargs) + self.assertEqual(call_kwargs.kwargs['proxy'], TEST_PROXY_URL) + + def test_init_with_old_httpx_falls_back_to_proxies(self): + """Test initialization falls back to proxies for old httpx version""" + call_count = [0] + + def mock_init_side_effect(*args, **kwargs): + call_count[0] += 1 + if call_count[0] == 1 and 'proxy' in kwargs: + # First call with proxy parameter - simulate old httpx raising TypeError + raise TypeError("__init__() got an unexpected keyword argument 'proxy'") + # Second call should work (with proxies or without) + return None + + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.side_effect = mock_init_side_effect + + from okx.okxclient import OkxClient + client = OkxClient(proxy=TEST_PROXY_URL) + + # Should have been called twice + self.assertEqual(mock_init.call_count, 2) + + # Second call should use proxies parameter + second_call = mock_init.call_args_list[1] + self.assertIn('proxies', second_call.kwargs) + expected_proxies = { + 'http://': TEST_PROXY_URL, + 'https://': TEST_PROXY_URL + } + self.assertEqual(second_call.kwargs['proxies'], expected_proxies) + + def test_init_with_old_httpx_no_proxy(self): + """Test initialization with old httpx version without proxy""" + call_count = [0] + + def mock_init_side_effect(*args, **kwargs): + call_count[0] += 1 + if call_count[0] == 1 and 'proxy' in kwargs: + # First call with proxy parameter - simulate old httpx raising TypeError + raise TypeError("__init__() got an unexpected keyword argument 'proxy'") + return None + + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.side_effect = mock_init_side_effect + + from okx.okxclient import OkxClient + client = OkxClient() # No proxy + + # Should have been called twice + self.assertEqual(mock_init.call_count, 2) + + # Second call should not have proxies parameter + second_call = mock_init.call_args_list[1] + self.assertNotIn('proxies', second_call.kwargs) + + def test_init_without_proxy(self): + """Test initialization without proxy parameter""" + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.return_value = None + + from okx.okxclient import OkxClient + client = OkxClient() + + mock_init.assert_called_once() + call_kwargs = mock_init.call_args.kwargs + self.assertEqual(call_kwargs.get('proxy'), None) + + +class TestOkxClientRequest(unittest.TestCase): + """Unit tests for OkxClient request methods""" + + def setUp(self): + """Set up test fixtures""" + with patch(MOCK_CLIENT_INIT) as mock_init: + mock_init.return_value = None + from okx.okxclient import OkxClient + self.client = OkxClient( + api_key='test_key', + api_secret_key='test_secret', + passphrase='test_pass', + flag='0' + ) + + def test_request_without_params(self): + """Test _request_without_params calls _request with empty dict""" + with patch.object(self.client, '_request') as mock_request: + mock_request.return_value = {'code': '0'} + + result = self.client._request_without_params('GET', TEST_API_ENDPOINT) + + mock_request.assert_called_once_with('GET', TEST_API_ENDPOINT, {}) + + def test_request_with_params(self): + """Test _request_with_params passes params correctly""" + with patch.object(self.client, '_request') as mock_request: + mock_request.return_value = {'code': '0'} + params = {'instId': 'BTC-USDT'} + + result = self.client._request_with_params('GET', TEST_API_ENDPOINT, params) + + mock_request.assert_called_once_with('GET', TEST_API_ENDPOINT, params) + + +if __name__ == '__main__': + unittest.main() + diff --git a/test/unit/okx/test_public_data.py b/test/unit/okx/test_public_data.py new file mode 100644 index 00000000..abe68245 --- /dev/null +++ b/test/unit/okx/test_public_data.py @@ -0,0 +1,208 @@ +""" +Unit tests for okx.PublicData module + +Mirrors the structure: okx/PublicData.py -> test/unit/okx/test_public_data.py +""" +import unittest +from unittest.mock import patch +from okx.PublicData import PublicAPI +from okx import consts as c + + +class TestPublicAPIMarketDataHistory(unittest.TestCase): + """Unit tests for the get_market_data_history method""" + + def setUp(self): + """Set up test fixtures""" + self.public_api = PublicAPI(flag='0') + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_with_required_params(self, mock_request): + """Test get_market_data_history with required parameters only""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [{'ts': '1234567890', 'vol': '1000'}] + } + mock_request.return_value = mock_response + + # Act + result = self.public_api.get_market_data_history( + module='volume', + instType='SPOT', + dateAggrType='1D', + begin='1609459200000', + end='1609545600000' + ) + + # Assert + expected_params = { + 'module': 'volume', + 'instType': 'SPOT', + 'dateAggrType': '1D', + 'begin': '1609459200000', + 'end': '1609545600000' + } + mock_request.assert_called_once_with(c.GET, c.MARKET_DATA_HISTORY, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_with_all_params(self, mock_request): + """Test get_market_data_history with all parameters provided""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [{'ts': '1234567890', 'vol': '1000'}] + } + mock_request.return_value = mock_response + + # Act + result = self.public_api.get_market_data_history( + module='volume', + instType='SWAP', + dateAggrType='1W', + begin='1609459200000', + end='1609545600000', + instIdList='BTC-USDT-SWAP,ETH-USDT-SWAP', + instFamilyList='BTC-USDT,ETH-USDT' + ) + + # Assert + expected_params = { + 'module': 'volume', + 'instType': 'SWAP', + 'dateAggrType': '1W', + 'begin': '1609459200000', + 'end': '1609545600000', + 'instIdList': 'BTC-USDT-SWAP,ETH-USDT-SWAP', + 'instFamilyList': 'BTC-USDT,ETH-USDT' + } + mock_request.assert_called_once_with(c.GET, c.MARKET_DATA_HISTORY, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_with_inst_id_list(self, mock_request): + """Test get_market_data_history with instIdList parameter""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.public_api.get_market_data_history( + module='volume', + instType='SPOT', + dateAggrType='1D', + begin='1609459200000', + end='1609545600000', + instIdList='BTC-USDT' + ) + + # Assert + expected_params = { + 'module': 'volume', + 'instType': 'SPOT', + 'dateAggrType': '1D', + 'begin': '1609459200000', + 'end': '1609545600000', + 'instIdList': 'BTC-USDT' + } + mock_request.assert_called_once_with(c.GET, c.MARKET_DATA_HISTORY, expected_params) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_with_inst_family_list(self, mock_request): + """Test get_market_data_history with instFamilyList parameter""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.public_api.get_market_data_history( + module='openInterest', + instType='FUTURES', + dateAggrType='1M', + begin='1609459200000', + end='1612137600000', + instFamilyList='BTC-USD' + ) + + # Assert + expected_params = { + 'module': 'openInterest', + 'instType': 'FUTURES', + 'dateAggrType': '1M', + 'begin': '1609459200000', + 'end': '1612137600000', + 'instFamilyList': 'BTC-USD' + } + mock_request.assert_called_once_with(c.GET, c.MARKET_DATA_HISTORY, expected_params) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_different_inst_types(self, mock_request): + """Test get_market_data_history with different instType values""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + inst_types = ['SPOT', 'SWAP', 'FUTURES', 'OPTION'] + + for inst_type in inst_types: + mock_request.reset_mock() + result = self.public_api.get_market_data_history( + module='volume', + instType=inst_type, + dateAggrType='1D', + begin='1609459200000', + end='1609545600000' + ) + + call_args = mock_request.call_args + self.assertEqual(call_args[0][1], c.MARKET_DATA_HISTORY) + self.assertEqual(call_args[1]['instType'] if call_args[1] else call_args[0][2]['instType'], inst_type) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_different_date_aggr_types(self, mock_request): + """Test get_market_data_history with different dateAggrType values""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + date_aggr_types = ['1D', '1W', '1M'] + + for aggr_type in date_aggr_types: + mock_request.reset_mock() + result = self.public_api.get_market_data_history( + module='volume', + instType='SPOT', + dateAggrType=aggr_type, + begin='1609459200000', + end='1609545600000' + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['dateAggrType'], aggr_type) + + @patch.object(PublicAPI, '_request_with_params') + def test_get_market_data_history_different_modules(self, mock_request): + """Test get_market_data_history with different module values""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + modules = ['volume', 'openInterest', 'tradeCount'] + + for module in modules: + mock_request.reset_mock() + result = self.public_api.get_market_data_history( + module=module, + instType='SPOT', + dateAggrType='1D', + begin='1609459200000', + end='1609545600000' + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['module'], module) + + +if __name__ == '__main__': + unittest.main() + diff --git a/test/unit/okx/test_trading_data.py b/test/unit/okx/test_trading_data.py new file mode 100644 index 00000000..db81f8c0 --- /dev/null +++ b/test/unit/okx/test_trading_data.py @@ -0,0 +1,177 @@ +""" +Unit tests for okx.TradingData module + +Mirrors the structure: okx/TradingData.py -> test/unit/okx/test_trading_data.py +""" +import unittest +from unittest.mock import patch +from okx.TradingData import TradingDataAPI +from okx import consts as c + + +class TestTradingDataAPIContractsOpenInterestHistory(unittest.TestCase): + """Unit tests for the get_open_interest_history method""" + + def setUp(self): + """Set up test fixtures""" + self.trading_data_api = TradingDataAPI(flag='0') + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_with_required_params(self, mock_request): + """Test ge_open_interest_history with required parameters only""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [ + {'ts': '1609459200000', 'oi': '100000', 'oiCcy': '10'} + ] + } + mock_request.return_value = mock_response + + # Act + result = self.trading_data_api.get_open_interest_history( + instId='BTC-USDT-SWAP' + ) + + # Assert + expected_params = { + 'instId': 'BTC-USDT-SWAP' + } + mock_request.assert_called_once_with(c.GET, c.CONTRACTS_OPEN_INTEREST_HISTORY, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_with_all_params(self, mock_request): + """Test get_open_interest_history with all parameters provided""" + # Arrange + mock_response = { + 'code': '0', + 'msg': '', + 'data': [ + {'ts': '1609459200000', 'oi': '100000', 'oiCcy': '10'} + ] + } + mock_request.return_value = mock_response + + # Act + result = self.trading_data_api.get_open_interest_history( + instId='BTC-USDT-SWAP', + period='1H', + begin='1609459200000', + end='1609545600000', + limit='50' + ) + + # Assert + expected_params = { + 'instId': 'BTC-USDT-SWAP', + 'period': '1H', + 'begin': '1609459200000', + 'end': '1609545600000', + 'limit': '50' + } + mock_request.assert_called_once_with(c.GET, c.CONTRACTS_OPEN_INTEREST_HISTORY, expected_params) + self.assertEqual(result, mock_response) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_with_period(self, mock_request): + """Test get_open_interest_history with period parameter""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.trading_data_api.get_open_interest_history( + instId='ETH-USDT-SWAP', + period='5m' + ) + + # Assert + expected_params = { + 'instId': 'ETH-USDT-SWAP', + 'period': '5m' + } + mock_request.assert_called_once_with(c.GET, c.CONTRACTS_OPEN_INTEREST_HISTORY, expected_params) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_different_periods(self, mock_request): + """Test get_open_interest_history with different period values""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + periods = ['5m', '15m', '30m', '1H', '2H', '4H', '6H', '12H', '1D', '2D', '3D', '5D', '1W', '1M', '3M'] + + for period in periods: + mock_request.reset_mock() + result = self.trading_data_api.get_open_interest_history( + instId='BTC-USDT-SWAP', + period=period + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['period'], period) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_different_inst_ids(self, mock_request): + """Test get_open_interest_history with different instrument IDs""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + inst_ids = ['BTC-USDT-SWAP', 'ETH-USDT-SWAP', 'BTC-USD-SWAP', 'BTC-USDT-240329'] + + for inst_id in inst_ids: + mock_request.reset_mock() + result = self.trading_data_api.get_open_interest_history( + instId=inst_id + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['instId'], inst_id) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_with_pagination(self, mock_request): + """Test get_open_interest_history with pagination parameters""" + # Arrange + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + # Act + result = self.trading_data_api.get_open_interest_history( + instId='BTC-USDT-SWAP', + begin='1609459200000', + end='1609545600000', + limit='100' + ) + + # Assert + expected_params = { + 'instId': 'BTC-USDT-SWAP', + 'begin': '1609459200000', + 'end': '1609545600000', + 'limit': '100' + } + mock_request.assert_called_once_with(c.GET, c.CONTRACTS_OPEN_INTEREST_HISTORY, expected_params) + + @patch.object(TradingDataAPI, '_request_with_params') + def test_get_open_interest_history_utc_periods(self, mock_request): + """Test get_open_interest_history with UTC+0 period values""" + mock_response = {'code': '0', 'msg': '', 'data': []} + mock_request.return_value = mock_response + + utc_periods = ['6Hutc', '12Hutc', '1Dutc', '2Dutc', '3Dutc', '5Dutc', '1Wutc', '1Mutc', '3Mutc'] + + for period in utc_periods: + mock_request.reset_mock() + result = self.trading_data_api.get_open_interest_history( + instId='BTC-USDT-SWAP', + period=period + ) + + call_args = mock_request.call_args[0][2] + self.assertEqual(call_args['period'], period) + + +if __name__ == '__main__': + unittest.main() + diff --git a/test/unit/okx/websocket/__init__.py b/test/unit/okx/websocket/__init__.py new file mode 100644 index 00000000..b0061bd3 --- /dev/null +++ b/test/unit/okx/websocket/__init__.py @@ -0,0 +1,2 @@ +# Unit tests for okx.websocket module + diff --git a/test/unit/okx/websocket/test_ws_private_async.py b/test/unit/okx/websocket/test_ws_private_async.py new file mode 100644 index 00000000..96419095 --- /dev/null +++ b/test/unit/okx/websocket/test_ws_private_async.py @@ -0,0 +1,586 @@ +""" +Unit tests for okx.websocket.WsPrivateAsync module + +Mirrors the structure: okx/websocket/WsPrivateAsync.py -> test/unit/okx/websocket/test_ws_private_async.py +""" +import json +import unittest +import asyncio +import warnings +from unittest.mock import patch, MagicMock, AsyncMock + +# Import the module first so patch can resolve the path +import okx.websocket.WsPrivateAsync as ws_private_module +from okx.websocket.WsPrivateAsync import WsPrivateAsync + +# Test constants +TEST_WS_URL = 'wss://test.example.com' +MOCK_WS_FACTORY = 'okx.websocket.WsPrivateAsync.WebSocketFactory' + + +class TestWsPrivateAsyncInit(unittest.TestCase): + """Unit tests for WsPrivateAsync initialization""" + + def test_init_with_required_params(self): + """Test initialization with required parameters""" + with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory: + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + + self.assertEqual(ws.apiKey, "test_api_key") + self.assertEqual(ws.passphrase, "test_passphrase") + self.assertEqual(ws.secretKey, "test_secret_key") + self.assertEqual(ws.url, TEST_WS_URL) + self.assertFalse(ws.useServerTime) + self.assertFalse(ws.debug) + mock_factory.assert_called_once_with(TEST_WS_URL) + + def test_init_with_debug_enabled(self): + """Test initialization with debug mode enabled""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL, + debug=True + ) + + self.assertTrue(ws.debug) + + def test_init_with_deprecated_useServerTime_shows_warning(self): + """Test that using deprecated useServerTime parameter shows warning""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL, + useServerTime=True + ) + + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, DeprecationWarning)) + self.assertIn("useServerTime parameter is deprecated", str(w[0].message)) + + def test_init_without_useServerTime_no_warning(self): + """Test that not using useServerTime parameter shows no warning""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + + # No deprecation warning expected + deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)] + self.assertEqual(len(deprecation_warnings), 0) + + +class TestWsPrivateAsyncSubscribe(unittest.TestCase): + """Unit tests for WsPrivateAsync subscribe method""" + + def test_subscribe_sends_correct_payload(self): + """Test subscribe sends correct payload after login""" + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \ + patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock): + + mock_ws_utils.initLoginParams.return_value = '{"op":"login"}' + + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.subscribe(params, callback) + self.assertEqual(ws.callback, callback) + # Second call should be the subscribe (first is login) + subscribe_call = mock_websocket.send.call_args_list[1] + payload = json.loads(subscribe_call[0][0]) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_id(self): + """Test subscribe with id parameter""" + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \ + patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock): + + mock_ws_utils.initLoginParams.return_value = '{"op":"login"}' + + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.subscribe(params, callback, id="sub001") + # Second call should be the subscribe (first is login) + subscribe_call = mock_websocket.send.call_args_list[1] + payload = json.loads(subscribe_call[0][0]) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["id"], "sub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncUnsubscribe(unittest.TestCase): + """Unit tests for WsPrivateAsync unsubscribe method""" + + def test_unsubscribe_sends_correct_payload(self): + """Test unsubscribe sends correct payload""" + with patch.object(ws_private_module, 'WebSocketFactory'): + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.unsubscribe(params, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_unsubscribe_with_id(self): + """Test unsubscribe with id parameter""" + with patch.object(ws_private_module, 'WebSocketFactory'): + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "account", "ccy": "BTC"}] + + async def run_test(): + await ws.unsubscribe(params, callback, id="unsub001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["id"], "unsub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncSend(unittest.TestCase): + """Unit tests for WsPrivateAsync generic send method""" + + def test_send_without_id(self): + """Test generic send method without id""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=callback) + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["args"], args) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_id(self): + """Test generic send method with id""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, id="send001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["id"], "send001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncOrderMethods(unittest.TestCase): + """Unit tests for WsPrivateAsync order-related methods""" + + def _create_ws_instance(self): + """Helper to create WsPrivateAsync instance with mocked websocket""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPrivateAsync import WsPrivateAsync + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + return ws, mock_websocket + + def test_place_order_sends_correct_payload(self): + """Test place_order sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + order_args = [{ + "instId": "BTC-USDT", + "tdMode": "cash", + "side": "buy", + "ordType": "limit", + "sz": "0.001", + "px": "30000" + }] + + async def run_test(): + await ws.place_order(order_args, callback=callback, id="order001") + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "order") + self.assertEqual(payload["args"], order_args) + self.assertEqual(payload["id"], "order001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_place_order_without_id(self): + """Test place_order without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + order_args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.place_order(order_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_orders_sends_correct_payload(self): + """Test batch_orders sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + order_args = [ + {"instId": "BTC-USDT", "side": "buy", "sz": "0.001", "px": "30000"}, + {"instId": "ETH-USDT", "side": "buy", "sz": "0.01", "px": "2000"} + ] + + async def run_test(): + await ws.batch_orders(order_args, callback=callback, id="batch001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-orders") + self.assertEqual(payload["args"], order_args) + self.assertEqual(payload["id"], "batch001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_orders_without_id(self): + """Test batch_orders without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + order_args = [{"instId": "BTC-USDT"}, {"instId": "ETH-USDT"}] + + async def run_test(): + await ws.batch_orders(order_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_cancel_order_sends_correct_payload(self): + """Test cancel_order sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.cancel_order(cancel_args, callback=callback, id="cancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "cancel-order") + self.assertEqual(payload["args"], cancel_args) + self.assertEqual(payload["id"], "cancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_cancel_order_without_id(self): + """Test cancel_order without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.cancel_order(cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "cancel-order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_cancel_orders_sends_correct_payload(self): + """Test batch_cancel_orders sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + cancel_args = [ + {"instId": "BTC-USDT", "ordId": "12345"}, + {"instId": "ETH-USDT", "ordId": "67890"} + ] + + async def run_test(): + await ws.batch_cancel_orders(cancel_args, callback=callback, id="batchCancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-cancel-orders") + self.assertEqual(payload["args"], cancel_args) + self.assertEqual(payload["id"], "batchCancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_cancel_orders_without_id(self): + """Test batch_cancel_orders without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + cancel_args = [{"instId": "BTC-USDT", "ordId": "12345"}] + + async def run_test(): + await ws.batch_cancel_orders(cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-cancel-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_amend_order_sends_correct_payload(self): + """Test amend_order sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + amend_args = [{ + "instId": "BTC-USDT", + "ordId": "12345", + "newSz": "0.002", + "newPx": "31000" + }] + + async def run_test(): + await ws.amend_order(amend_args, callback=callback, id="amend001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "amend-order") + self.assertEqual(payload["args"], amend_args) + self.assertEqual(payload["id"], "amend001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_amend_order_without_id(self): + """Test amend_order without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + amend_args = [{"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}] + + async def run_test(): + await ws.amend_order(amend_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "amend-order") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_amend_orders_sends_correct_payload(self): + """Test batch_amend_orders sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + amend_args = [ + {"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}, + {"instId": "ETH-USDT", "ordId": "67890", "newPx": "2100"} + ] + + async def run_test(): + await ws.batch_amend_orders(amend_args, callback=callback, id="batchAmend001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-amend-orders") + self.assertEqual(payload["args"], amend_args) + self.assertEqual(payload["id"], "batchAmend001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_batch_amend_orders_without_id(self): + """Test batch_amend_orders without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + amend_args = [{"instId": "BTC-USDT", "ordId": "12345", "newSz": "0.002"}] + + async def run_test(): + await ws.batch_amend_orders(amend_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "batch-amend-orders") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_mass_cancel_sends_correct_payload(self): + """Test mass_cancel sends correct operation""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + callback = MagicMock() + mass_cancel_args = [{ + "instType": "SPOT", + "instFamily": "BTC-USDT" + }] + + async def run_test(): + await ws.mass_cancel(mass_cancel_args, callback=callback, id="massCancel001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "mass-cancel") + self.assertEqual(payload["args"], mass_cancel_args) + self.assertEqual(payload["id"], "massCancel001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_mass_cancel_without_id(self): + """Test mass_cancel without id parameter""" + with patch(MOCK_WS_FACTORY): + ws, mock_websocket = self._create_ws_instance() + mass_cancel_args = [{"instType": "SPOT", "instFamily": "BTC-USDT"}] + + async def run_test(): + await ws.mass_cancel(mass_cancel_args) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "mass-cancel") + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncLogin(unittest.TestCase): + """Unit tests for WsPrivateAsync login method""" + + def test_login_calls_init_login_params(self): + """Test login calls WsUtils.initLoginParams with correct parameters""" + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils: + + mock_ws_utils.initLoginParams.return_value = '{"op":"login","args":[...]}' + + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + + async def run_test(): + result = await ws.login() + self.assertTrue(result) + mock_ws_utils.initLoginParams.assert_called_once_with( + useServerTime=False, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPrivateAsyncStartStop(unittest.TestCase): + """Unit tests for WsPrivateAsync start and stop methods""" + + def test_stop(self): + """Test stop method closes the factory""" + with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory_class: + mock_factory_instance = MagicMock() + mock_factory_instance.close = AsyncMock() + mock_factory_class.return_value = mock_factory_instance + + ws = WsPrivateAsync( + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key", + url=TEST_WS_URL + ) + + async def run_test(): + await ws.stop() + mock_factory_instance.close.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/okx/websocket/test_ws_public_async.py b/test/unit/okx/websocket/test_ws_public_async.py new file mode 100644 index 00000000..86bef231 --- /dev/null +++ b/test/unit/okx/websocket/test_ws_public_async.py @@ -0,0 +1,325 @@ +""" +Unit tests for okx.websocket.WsPublicAsync module + +Mirrors the structure: okx/websocket/WsPublicAsync.py -> test/unit/okx/websocket/test_ws_public_async.py +""" +import json +import unittest +import asyncio +from unittest.mock import patch, MagicMock, AsyncMock + +# Import the module first so patch can resolve the path +import okx.websocket.WsPublicAsync as ws_public_module +from okx.websocket.WsPublicAsync import WsPublicAsync + +# Test constants +TEST_WS_URL = 'wss://test.example.com' +MOCK_WS_FACTORY = 'okx.websocket.WsPublicAsync.WebSocketFactory' + + +class TestWsPublicAsyncInit(unittest.TestCase): + """Unit tests for WsPublicAsync initialization""" + + def test_init_with_url(self): + """Test initialization with url parameter""" + with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory: + ws = WsPublicAsync(url=TEST_WS_URL) + + self.assertEqual(ws.url, TEST_WS_URL) + self.assertEqual(ws.apiKey, '') + self.assertEqual(ws.passphrase, '') + self.assertEqual(ws.secretKey, '') + self.assertFalse(ws.debug) + self.assertFalse(ws.isLoggedIn) + + def test_init_with_credentials(self): + """Test initialization with all credentials for business channel""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync( + url=TEST_WS_URL, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + + self.assertEqual(ws.apiKey, "test_api_key") + self.assertEqual(ws.passphrase, "test_passphrase") + self.assertEqual(ws.secretKey, "test_secret_key") + + def test_init_with_debug_enabled(self): + """Test initialization with debug mode enabled""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL, debug=True) + + self.assertTrue(ws.debug) + + def test_init_with_debug_disabled(self): + """Test initialization with debug mode disabled (default)""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL, debug=False) + + self.assertFalse(ws.debug) + + +class TestWsPublicAsyncLogin(unittest.TestCase): + """Unit tests for WsPublicAsync login method""" + + def test_login_without_credentials_raises_error(self): + """Test that login raises ValueError when credentials are missing""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + + async def run_test(): + with self.assertRaises(ValueError) as context: + await ws.login() + self.assertIn("apiKey, secretKey and passphrase are required for login", str(context.exception)) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_login_with_credentials_success(self): + """Test successful login with valid credentials""" + with patch(MOCK_WS_FACTORY) as mock_factory, \ + patch('okx.websocket.WsPublicAsync.WsUtils.initLoginParams') as mock_init_login: + + mock_init_login.return_value = '{"op":"login","args":[...]}' + + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync( + url=TEST_WS_URL, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + + async def run_test(): + result = await ws.login() + self.assertTrue(result) + self.assertTrue(ws.isLoggedIn) + mock_init_login.assert_called_once_with( + useServerTime=False, + apiKey="test_api_key", + passphrase="test_passphrase", + secretKey="test_secret_key" + ) + mock_websocket.send.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncSubscribe(unittest.TestCase): + """Unit tests for WsPublicAsync subscribe method""" + + def test_subscribe_without_id(self): + """Test subscribe without id parameter""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.subscribe(params, callback) + self.assertEqual(ws.callback, callback) + mock_websocket.send.assert_called_once() + + # Verify the payload + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_id(self): + """Test subscribe with id parameter""" + with patch.object(ws_public_module, 'WebSocketFactory'): + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.subscribe(params, callback, id="sub001") + + # Verify the payload includes id + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "subscribe") + self.assertEqual(payload["args"], params) + self.assertEqual(payload["id"], "sub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_subscribe_with_multiple_channels(self): + """Test subscribe with multiple channels""" + with patch.object(ws_public_module, 'WebSocketFactory'): + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [ + {"channel": "tickers", "instId": "BTC-USDT"}, + {"channel": "tickers", "instId": "ETH-USDT"} + ] + + async def run_test(): + await ws.subscribe(params, callback, id="multi001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(len(payload["args"]), 2) + self.assertEqual(payload["id"], "multi001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncUnsubscribe(unittest.TestCase): + """Unit tests for WsPublicAsync unsubscribe method""" + + def test_unsubscribe_without_id(self): + """Test unsubscribe without id parameter""" + with patch.object(ws_public_module, 'WebSocketFactory'): + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.unsubscribe(params, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["args"], params) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_unsubscribe_with_id(self): + """Test unsubscribe with id parameter""" + with patch.object(ws_public_module, 'WebSocketFactory'): + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + params = [{"channel": "tickers", "instId": "BTC-USDT"}] + + async def run_test(): + await ws.unsubscribe(params, callback, id="unsub001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "unsubscribe") + self.assertEqual(payload["id"], "unsub001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncSend(unittest.TestCase): + """Unit tests for WsPublicAsync send method""" + + def test_send_without_id(self): + """Test generic send method without id""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + callback = MagicMock() + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=callback) + self.assertEqual(ws.callback, callback) + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["args"], args) + self.assertNotIn("id", payload) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_id(self): + """Test generic send method with id""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, id="send001") + call_args = mock_websocket.send.call_args[0][0] + payload = json.loads(call_args) + self.assertEqual(payload["op"], "custom_op") + self.assertEqual(payload["id"], "send001") + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_without_callback(self): + """Test send method without callback (preserves existing callback)""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + existing_callback = MagicMock() + ws.callback = existing_callback + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args) + # Callback should remain unchanged + self.assertEqual(ws.callback, existing_callback) + + asyncio.get_event_loop().run_until_complete(run_test()) + + def test_send_with_new_callback_replaces_existing(self): + """Test send method with new callback replaces existing callback""" + with patch(MOCK_WS_FACTORY): + from okx.websocket.WsPublicAsync import WsPublicAsync + ws = WsPublicAsync(url=TEST_WS_URL) + mock_websocket = AsyncMock() + ws.websocket = mock_websocket + old_callback = MagicMock() + new_callback = MagicMock() + ws.callback = old_callback + args = [{"instId": "BTC-USDT"}] + + async def run_test(): + await ws.send("custom_op", args, callback=new_callback) + self.assertEqual(ws.callback, new_callback) + + asyncio.get_event_loop().run_until_complete(run_test()) + + +class TestWsPublicAsyncStartStop(unittest.TestCase): + """Unit tests for WsPublicAsync start and stop methods""" + + def test_stop(self): + """Test stop method closes the factory""" + with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory_class: + mock_factory_instance = MagicMock() + mock_factory_instance.close = AsyncMock() + mock_factory_class.return_value = mock_factory_instance + + ws = WsPublicAsync(url=TEST_WS_URL) + + async def run_test(): + await ws.stop() + mock_factory_instance.close.assert_called_once() + + asyncio.get_event_loop().run_until_complete(run_test()) + + +if __name__ == '__main__': + unittest.main() diff --git a/websocket_example.py b/websocket_example.py deleted file mode 100644 index 2b83cb07..00000000 --- a/websocket_example.py +++ /dev/null @@ -1,522 +0,0 @@ -import asyncio -import base64 -import datetime -import hmac -import json -import time -import zlib - -import requests -import websockets - - -def get_timestamp(): - now = datetime.datetime.now() - t = now.isoformat("T", "milliseconds") - return t + "Z" - - -def get_server_time(): - url = "https://www.okx.com/api/v5/public/time" - response = requests.get(url) - if response.status_code == 200: - return response.json()['data'][0]['ts'] - else: - return "" - - -def get_local_timestamp(): - return int(time.time()) - - -def login_params(timestamp, api_key, passphrase, secret_key): - message = timestamp + 'GET' + '/users/self/verify' - - mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256') - d = mac.digest() - sign = base64.b64encode(d) - - login_param = {"op": "login", "args": [{"apiKey": api_key, - "passphrase": passphrase, - "timestamp": timestamp, - "sign": sign.decode("utf-8")}]} - login_str = json.dumps(login_param) - return login_str - - -def partial(res): - data_obj = res['data'][0] - bids = data_obj['bids'] - asks = data_obj['asks'] - instrument_id = res['arg']['instId'] - # print('全量数据bids为:' + str(bids)) - # print('档数为:' + str(len(bids))) - # print('全量数据asks为:' + str(asks)) - # print('档数为:' + str(len(asks))) - return bids, asks, instrument_id - - -def update_bids(res, bids_p): - # 获取增量bids数据 - bids_u = res['data'][0]['bids'] - # print('增量数据bids为:' + str(bids_u)) - # print('档数为:' + str(len(bids_u))) - # bids合并 - for i in bids_u: - bid_price = i[0] - for j in bids_p: - if bid_price == j[0]: - if i[1] == '0': - bids_p.remove(j) - break - else: - del j[1] - j.insert(1, i[1]) - break - else: - if i[1] != "0": - bids_p.append(i) - else: - bids_p.sort(key=lambda price: sort_num(price[0]), reverse=True) - # print('合并后的bids为:' + str(bids_p) + ',档数为:' + str(len(bids_p))) - return bids_p - - -def update_asks(res, asks_p): - # 获取增量asks数据 - asks_u = res['data'][0]['asks'] - # print('增量数据asks为:' + str(asks_u)) - # print('档数为:' + str(len(asks_u))) - # asks合并 - for i in asks_u: - ask_price = i[0] - for j in asks_p: - if ask_price == j[0]: - if i[1] == '0': - asks_p.remove(j) - break - else: - del j[1] - j.insert(1, i[1]) - break - else: - if i[1] != "0": - asks_p.append(i) - else: - asks_p.sort(key=lambda price: sort_num(price[0])) - # print('合并后的asks为:' + str(asks_p) + ',档数为:' + str(len(asks_p))) - return asks_p - - -def sort_num(n): - if n.isdigit(): - return int(n) - else: - return float(n) - - -def check(bids, asks): - # 获取bid档str - bids_l = [] - bid_l = [] - count_bid = 1 - while count_bid <= 25: - if count_bid > len(bids): - break - bids_l.append(bids[count_bid - 1]) - count_bid += 1 - for j in bids_l: - str_bid = ':'.join(j[0: 2]) - bid_l.append(str_bid) - # 获取ask档str - asks_l = [] - ask_l = [] - count_ask = 1 - while count_ask <= 25: - if count_ask > len(asks): - break - asks_l.append(asks[count_ask - 1]) - count_ask += 1 - for k in asks_l: - str_ask = ':'.join(k[0: 2]) - ask_l.append(str_ask) - # 拼接str - num = '' - if len(bid_l) == len(ask_l): - for m in range(len(bid_l)): - num += bid_l[m] + ':' + ask_l[m] + ':' - elif len(bid_l) > len(ask_l): - # bid档比ask档多 - for n in range(len(ask_l)): - num += bid_l[n] + ':' + ask_l[n] + ':' - for l in range(len(ask_l), len(bid_l)): - num += bid_l[l] + ':' - elif len(bid_l) < len(ask_l): - # ask档比bid档多 - for n in range(len(bid_l)): - num += bid_l[n] + ':' + ask_l[n] + ':' - for l in range(len(bid_l), len(ask_l)): - num += ask_l[l] + ':' - - new_num = num[:-1] - int_checksum = zlib.crc32(new_num.encode()) - fina = change(int_checksum) - return fina - - -def change(num_old): - num = pow(2, 31) - 1 - if num_old > num: - out = num_old - num * 2 - 2 - else: - out = num_old - return out - - -# subscribe channels un_need login -async def subscribe_without_login(url, channels): - l = [] - while True: - try: - async with websockets.connect(url) as ws: - sub_param = {"op": "subscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - while True: - try: - res = await asyncio.wait_for(ws.recv(), timeout=25) - except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed) as e: - try: - await ws.send('ping') - res = await ws.recv() - print(res) - continue - except Exception as e: - print("连接关闭,正在重连……") - break - - print(get_timestamp() + res) - res = eval(res) - if 'event' in res: - continue - for i in res['arg']: - if 'books' in res['arg'][i] and 'books5' not in res['arg'][i]: - # 订阅频道是深度频道 - if res['action'] == 'snapshot': - for m in l: - if res['arg']['instId'] == m['instrument_id']: - l.remove(m) - # 获取首次全量深度数据 - bids_p, asks_p, instrument_id = partial(res) - d = {} - d['instrument_id'] = instrument_id - d['bids_p'] = bids_p - d['asks_p'] = asks_p - l.append(d) - - # 校验checksum - checksum = res['data'][0]['checksum'] - # print('推送数据的checksum为:' + str(checksum)) - check_num = check(bids_p, asks_p) - # print('校验后的checksum为:' + str(check_num)) - if check_num == checksum: - print("校验结果为:True") - else: - print("校验结果为:False,正在重新订阅……") - - # 取消订阅 - await unsubscribe_without_login(url, channels) - # 发送订阅 - async with websockets.connect(url) as ws: - sub_param = {"op": "subscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - elif res['action'] == 'update': - for j in l: - if res['arg']['instId'] == j['instrument_id']: - # 获取全量数据 - bids_p = j['bids_p'] - asks_p = j['asks_p'] - # 获取合并后数据 - bids_p = update_bids(res, bids_p) - asks_p = update_asks(res, asks_p) - - # 校验checksum - checksum = res['data'][0]['checksum'] - # print('推送数据的checksum为:' + str(checksum)) - check_num = check(bids_p, asks_p) - # print('校验后的checksum为:' + str(check_num)) - if check_num == checksum: - print("校验结果为:True") - else: - print("校验结果为:False,正在重新订阅……") - - # 取消订阅 - await unsubscribe_without_login(url, channels) - # 发送订阅 - async with websockets.connect(url) as ws: - sub_param = {"op": "subscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - except Exception as e: - print(e) - print("连接断开,正在重连……") - continue - - -# subscribe channels need login -async def subscribe(url, api_key, passphrase, secret_key, channels): - while True: - try: - async with websockets.connect(url) as ws: - # login - timestamp = str(get_local_timestamp()) - login_str = login_params(timestamp, api_key, passphrase, secret_key) - await ws.send(login_str) - # print(f"send: {login_str}") - res = await ws.recv() - print(res) - - # subscribe - sub_param = {"op": "subscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - while True: - try: - res = await asyncio.wait_for(ws.recv(), timeout=25) - except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed) as e: - try: - await ws.send('ping') - res = await ws.recv() - print(res) - continue - except Exception as e: - print("连接关闭,正在重连……") - break - - print(get_timestamp() + res) - - except Exception as e: - print("连接断开,正在重连……") - continue - - -# trade -async def trade(url, api_key, passphrase, secret_key, trade_param): - while True: - try: - async with websockets.connect(url) as ws: - # login - timestamp = str(get_local_timestamp()) - login_str = login_params(timestamp, api_key, passphrase, secret_key) - await ws.send(login_str) - # print(f"send: {login_str}") - res = await ws.recv() - print(res) - - # trade - sub_str = json.dumps(trade_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - while True: - try: - res = await asyncio.wait_for(ws.recv(), timeout=25) - except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed) as e: - try: - await ws.send('ping') - res = await ws.recv() - print(res) - continue - except Exception as e: - print("连接关闭,正在重连……") - break - - print(get_timestamp() + res) - - except Exception as e: - print("连接断开,正在重连……") - continue - - -# unsubscribe channels -async def unsubscribe(url, api_key, passphrase, secret_key, channels): - async with websockets.connect(url) as ws: - # login - timestamp = str(get_local_timestamp()) - login_str = login_params(timestamp, api_key, passphrase, secret_key) - await ws.send(login_str) - # print(f"send: {login_str}") - - res = await ws.recv() - print(f"recv: {res}") - - # unsubscribe - sub_param = {"op": "unsubscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - res = await ws.recv() - print(f"recv: {res}") - - -# unsubscribe channels -async def unsubscribe_without_login(url, channels): - async with websockets.connect(url) as ws: - # unsubscribe - sub_param = {"op": "unsubscribe", "args": channels} - sub_str = json.dumps(sub_param) - await ws.send(sub_str) - print(f"send: {sub_str}") - - res = await ws.recv() - print(f"recv: {res}") - - -api_key = "" -secret_key = "" -passphrase = "" - -# WebSocket公共频道 public channels -# 实盘 real trading -# url = "wss://ws.okx.com:8443/ws/v5/public" -# 模拟盘 demo trading -# url = "wss://wspap.okx.com:8443/ws/v5/public" - -# WebSocket私有频道 private channels -# 实盘 real trading -# url = "wss://ws.okx.com:8443/ws/v5/private" -# 模拟盘 demo trading -# url = "wss://wspap.okx.com:8443/ws/v5/private" - -''' -公共频道 public channel -:param channel: 频道名 -:param instType: 产品类型 -:param instId: 产品ID -:param uly: 合约标的指数 - -''' - -# 产品频道 Instruments Channel -# channels = [{"channel": "instruments", "instType": "FUTURES"}] -# 行情频道 tickers channel -# channels = [{"channel": "tickers", "instId": "BTC-USDT"}, {"channel": "tickers", "instId": "ETH-USDT"}] -# 持仓总量频道 Open interest Channel -# channels = [{"channel": "open-interest", "instId": "BTC-USD-210326"}] -# K线频道 Candlesticks Channel -# channels = [{"channel": "candle1m", "instId": "BTC-USD-210326"}] -# 交易频道 Trades Channel -# channels = [{"channel": "trades", "instId": "BTC-USD-201225"}] -# 预估交割/行权价格频道 Estimated delivery/exercise Price Channel -# channels = [{"channel": "estimated-price", "instType": "FUTURES", "uly": "BTC-USD"}] -# 标记价格频道 Mark Price Channel -# channels = [{"channel": "mark-price", "instId": "BTC-USDT-210326"}] -# 标记价格K线频道 Mark Price Candlesticks Channel -# channels = [{"channel": "mark-price-candle1D", "instId": "BTC-USD-201225"}] -# 限价频道 Price Limit Channel -# channels = [{"channel": "price-limit", "instId": "BTC-USD-201225"}] -# 深度频道 Order Book Channel -# channels = [{"channel": "books", "instId": "BTC-USD-SWAP"}] -# 期权定价频道 OPTION Summary Channel -# channels = [{"channel": "opt-summary", "uly": "BTC-USD"}] -# 资金费率频道 Funding Rate Channel -# channels = [{"channel": "funding-rate", "instId": "BTC-USD-SWAP"}] -# 指数K线频道 Index Candlesticks Channel -# channels = [{"channel": "index-candle1m", "instId": "BTC-USDT"}] -# 指数行情频道 Index Tickers Channel -# channels = [{"channel": "index-tickers", "instId": "BTC-USDT"}] -# status频道 Status Channel -# channels = [{"channel": "status"}] -# 公共大宗交易频道 Public block trading channel -# channels = [{"channel": "public-struc-block-trades"}] -# 大宗交易行情频道 Block trading market channel -# channels = [{"channel": "block-tickers", "instId":"BTC-USDT-SWAP"}] - -''' -私有频道 private channel -:param channel: 频道名 -:param ccy: 币种 -:param instType: 产品类型 -:param uly: 合约标的指数 -:param instId: 产品ID - -''' - -# 账户频道 Account Channel -# channels = [{"channel": "account", "ccy": "BTC"}] -# 持仓频道 Positions Channel -# channels = [{"channel": "positions", "instType": "FUTURES", "uly": "BTC-USDT", "instId": "BTC-USDT-210326"}] -# 余额和持仓频道 Balance and Position Channel -# channels = [{"channel": "balance_and_position"}] -# 订单频道 Order Channel -# channels = [{"channel": "orders", "instType": "FUTURES", "uly": "BTC-USD", "instId": "BTC-USD-201225"}] -# 策略委托订单频道 Algo Orders Channel -# channels = [{"channel": "orders-algo", "instType": "FUTURES", "uly": "BTC-USD", "instId": "BTC-USD-201225"}] -# 高级策略委托订单频道 Cancel Advance Algos -# channels = [{"channel": "algo-advance", "instType": "SPOT","instId": "BTC-USD-201225","algoId":"12345678"}] -# 爆仓风险预警推送频道 -# channels = [{"channel": "liquidation-warning", "instType": "SWAP","instType": "","uly":"","instId":""}] -# 账户greeks频道 -# channels = [{"channel": "account-greeks", "ccy": "BTC"}] -# 询价频道 Inquiry channel -# channels = [{"channel": "rfqs"}] -# 报价频道 Quote channel -# channels = [{"channel": "quotes"}] -# 大宗交易频道 Block trading channel -# channels = [{"channel": "struc-block-trades"}] -# 现货网格策略委托订单频道 Consignment order channel of spot grid strategy -# channels = [{"channel": "grid-orders-spot", "instType": "ANY"}] -# 合约网格策略委托订单频道 Spot grid policy delegated order channel contract grid policy delegated order channel -# channels = [{"channel": "grid-orders-contract", "instType": "ANY"}] -# 合约网格持仓频道 Contract grid position channel -# channels = [{"channel": "grid-positions", "algoId": ""}] -# 网格策略子订单频道 Grid policy suborder channel -# channels = [{"channel": "grid-sub-orders", "algoId": ""}] -''' -交易 trade -''' - -# 下单 Place Order -# trade_param = {"id": "1512", "op": "order", "args": [{"side": "buy", "instId": "BTC-USDT", "tdMode": "isolated", "ordType": "limit", "px": "19777", "sz": "1"}]} -# 批量下单 Place Multiple Orders -# trade_param = {"id": "1512", "op": "batch-orders", "args": [ -# {"side": "buy", "instId": "BTC-USDT", "tdMode": "isolated", "ordType": "limit", "px": "19666", "sz": "1"}, -# {"side": "buy", "instId": "BTC-USDT", "tdMode": "isolated", "ordType": "limit", "px": "19633", "sz": "1"} -# ]} -# 撤单 Cancel Order -# trade_param = {"id": "1512", "op": "cancel-order", "args": [{"instId": "BTC-USDT", "ordId": "259424589042823169"}]} -# 批量撤单 Cancel Multiple Orders -# trade_param = {"id": "1512", "op": "batch-cancel-orders", "args": [ -# {"instId": "BTC-USDT", "ordId": ""}, -# {"instId": "BTC-USDT", "ordId": ""} -# ]} -# 改单 Amend Order -# trade_param = {"id": "1512", "op": "amend-order", "args": [{"instId": "BTC-USDT", "ordId": "259432767558135808", "newSz": "2"}]} -# 批量改单 Amend Multiple Orders -# trade_param = {"id": "1512", "op": "batch-amend-orders", "args": [ -# {"instId": "BTC-USDT", "ordId": "", "newSz": "2"}, -# {"instId": "BTC-USDT", "ordId": "", "newSz": "3"} -# ]} - - -loop = asyncio.get_event_loop() - -# 公共频道 不需要登录(行情,持仓总量,K线,标记价格,深度,资金费率等)subscribe public channel -loop.run_until_complete(subscribe_without_login(url, channels)) - -# 私有频道 需要登录(账户,持仓,订单等)subscribe private channel -# loop.run_until_complete(subscribe(url, api_key, passphrase, secret_key, channels)) - -# 交易(下单,撤单,改单等)trade -# loop.run_until_complete(trade(url, api_key, passphrase, secret_key, trade_param)) - -loop.close()