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/.gitignore b/.gitignore index c45a7445..10c5a421 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,4 @@ build/ ### VS Code ### .vscode/ -setup.py \ No newline at end of file +id_rsa* \ No newline at end of file diff --git a/README.md b/README.md index e67d4b95..19aac314 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,64 @@ -[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` - -* 将SDK目录`Clone`或者`Download`到本地,选择使用`okx-python-sdk-api-v5`即可 - -1.2 安装所需库 - -```python -pip install requests -pip install autobahn\[twisted\] -pip install pyOpenSSL -``` - -#### 第二步:配置个人信息 - -2.1 如果还未有API,可[点击](https://www.okx.com/account/users/myApi)前往官网进行申请 - -```python -api_key = "" -secret_key = "" -passphrase = "" -``` - -#### 第三步:调用接口 - -* 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 - - - -### How to use ? +### Quick start +#### Prerequisites `python version:>=3.9` -`WebSocketAPI: autobahn.twisted>=22.10.0` - -#### Step 1: Download the SDK and install the necessary libraries - -1.1 Download python SDK +`WebSocketAPI: websockets package advise version 6.0` -- `Clone` or `Download` the SDK directory to your local directory,choose to use `okx-python-sdk-api-v5` +#### 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 -1.2 Install the necessary libraries +#### Step 2: install python-okx ```python -pip install requests -pip install autobahn\[twisted\] -pip install pyOpenSSL +pip install python-okx ``` -#### 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 - -```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" -``` + - 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 -P.S. +Note -- If you know little about API, advise consulting the offical [API document](https://www.okx.com/docs-v5/en/) +- To learn more about OKX API, visit official [OKX API documentation](https://www.okx.com/docs-v5/en/) -- User with RestAPI can configure parameter `flag` in `example.py` in to choose to access to real trading or demo trading - -- 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 +74,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..911cf9b8 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,40 @@ 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=False, lever=None, greeksType=None, simPos=None, + simAsset=None): + params = {} + if acctLv is not None: + params['acctLv'] = acctLv + if inclRealPosAndEq is not None: + params['inclRealPosAndEq'] = inclRealPosAndEq + if lever is not None: + params['spotOffsetType'] = lever + if greeksType is not None: + params['greksType'] = greeksType + if simPos is not None: + params['simPos'] = simPos + if simAsset is not None: + params['simAsset'] = simAsset + 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 @@ -61,28 +79,34 @@ def get_max_order_size(self, instId, tdMode, ccy='', px=''): 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=''): + params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'reduceOnly': reduceOnly, + 'unSpotOffset': unSpotOffset, 'quickMgnType': quickMgnType} 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=''): params = {'instId': instId, 'mgnMode': mgnMode, 'mgnCcy': mgnCcy} 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 +125,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 +135,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 +159,167 @@ 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) 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..6ca0344d 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,21 @@ 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=''): + params = {'ccy': ccy, 'amt': amt, 'dest': dest, 'toAddr': toAddr, 'chain': chain, + 'areaCode': areaCode, 'clientId': clientId} 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) + def get_withdrawal_history(self, ccy='', wdId='', state='', after='', before='', limit='',txId=''): + params = {'ccy': ccy, 'wdId': wdId, 'state': state, 'after': after, 'before': before, 'limit': limit,'txId':txId} + return self._request_with_params(GET, WITHDRAWAL_HISTORY, params) # Get Currencies def get_currencies(self, ccy=''): @@ -72,31 +79,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 +100,20 @@ 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=''): + params = {'ccy': ccy, 'wdId': wdId, 'clientId': clientId, 'txId': txId, 'type': type, 'state': state, 'after': after, 'before': before, 'limit': limit} + return self._request_with_params(GET, GET_WITHDRAWAL_HISTORY, params) + diff --git a/okx/Grid.py b/okx/Grid.py index 6cf49914..74b6a9b5 100644 --- a/okx/Grid.py +++ b/okx/Grid.py @@ -1,10 +1,10 @@ -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=''): @@ -76,3 +76,57 @@ 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=''): + params = {'stgyName': stgyName, 'recurringList': recurringList, 'period': period, 'recurringDay': recurringDay, + 'recurringTime': recurringTime, + 'timeZone': timeZone, 'amt': amt, 'investmentCcy': investmentCcy, 'tdMode': tdMode, + 'algoClOrdId': algoClOrdId, 'tag': tag} + 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..b3bb9614 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,19 @@ 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) 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..db994ae6 100644 --- a/okx/Trade.py +++ b/okx/Trade.py @@ -1,18 +1,22 @@ -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=''): 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, 'tradeQuoteCcy': tradeQuoteCcy} + params['attachAlgoOrds'] = attachAlgoOrds return self._request_with_params(POST, PLACR_ORDER, params) # Place Multiple Orders @@ -22,17 +26,21 @@ 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=''): + 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} + params['attachAlgoOrds'] = attachAlgoOrds return self._request_with_params(POST, AMEND_ORDER, params) # Amend Multiple Orders @@ -40,8 +48,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 +59,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 +94,31 @@ 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='', tpOrdKind='', cxlOnClosePos='' + , chaseType='', chaseVal='', maxChaseType='', maxChaseVal='', attachAlgoOrds=[]): 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, 'tradeQuoteCcy': tradeQuoteCcy, + 'tpOrdKind': tpOrdKind, 'cxlOnClosePos': cxlOnClosePos, 'chaseType': chaseType, 'chaseVal': chaseVal, + 'maxChaseType': maxChaseType, 'maxChaseVal': maxChaseVal, 'attachAlgoOrds': attachAlgoOrds} 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 +127,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..7100ef49 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): diff --git a/okx/__init__.py b/okx/__init__.py index 71c25d8f..2dbeeb8b 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.0" \ 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/consts.py b/okx/consts.py index 833e44f5..aae07780 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' -# 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' +WITHDRAWAL_HISTORY = '/api/v5/asset/withdrawal-history' +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,15 @@ 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' -# 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' @@ -121,11 +143,11 @@ INTEREST_VOLUME_STRIKE = '/api/v5/rubik/stat/option/open-interest-volume-strike' TAKER_FLOW = '/api/v5/rubik/stat/option/taker-block-volume' -# 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 +159,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 +216,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 +226,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/client.py b/okx/okxclient.py similarity index 58% rename from okx/client.py rename to okx/okxclient.py index 26ae6033..e88e3a7d 100644 --- a/okx/client.py +++ b/okx/okxclient.py @@ -1,22 +1,29 @@ import json +import warnings +from datetime import datetime, timezone import httpx +from httpx import Client +from datetime import datetime, timezone -from . import consts as c, utils, exceptions +from loguru import logger +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'): +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): + super().__init__(base_url=base_api, http2=True, proxy=proxy) self.API_KEY = api_key self.API_SECRET_KEY = api_secret_key self.PASSPHRASE = passphrase - self.use_server_time = use_server_time + self.use_server_time = False self.flag = flag self.domain = base_api self.debug = debug - self.client = httpx.Client(base_url=base_api, http2=True) + 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: @@ -32,14 +39,13 @@ def _request(self, method, request_path, params): header = utils.get_header_no_sign(self.flag, self.debug) response = None if self.debug == True: - print('domain:',self.domain) - print('url:',request_path) + logger.debug(f'domain: {self.domain}') + logger.debug(f'url: {request_path}') + logger.debug(f'body:{body}') if method == c.GET: - response = self.client.get(request_path, headers=header) + response = self.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) + response = self.post(request_path, data=body, headers=header) return response.json() def _request_without_params(self, method, request_path): @@ -50,8 +56,9 @@ def _request_with_params(self, method, request_path, params): def _get_timestamp(self): request_path = c.API_URL + c.SERVER_TIMESTAMP_URL - response = self.client.get(request_path) + response = self.get(request_path) if response.status_code == 200: - return response.json()['ts'] + 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..c5359aa2 --- /dev/null +++ b/okx/websocket/WsPrivateAsync.py @@ -0,0 +1,77 @@ +import asyncio +import json +import logging + +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): + 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 = useServerTime + self.websocket = None + + async def connect(self): + self.websocket = await self.factory.connect() + + async def consume(self): + async for message in self.websocket: + logger.debug("Received message: {%s}", message) + if self.callback: + self.callback(message) + + async def subscribe(self, params: list, callback): + self.callback = callback + + logRes = await self.login() + await asyncio.sleep(5) + if logRes: + payload = json.dumps({ + "op": "subscribe", + "args": params + }) + 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 + ) + await self.websocket.send(loginPayload) + return True + + async def unsubscribe(self, params: list, callback): + self.callback = callback + payload = json.dumps({ + "op": "unsubscribe", + "args": params + }) + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + # for param in params: + # self.subscriptions.discard(param) + + async def stop(self): + await self.factory.close() + self.loop.stop() + + async def start(self): + logger.info("Connecting to WebSocket...") + await self.connect() + self.loop.create_task(self.consume()) + + def stop_sync(self): + 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..e576d658 --- /dev/null +++ b/okx/websocket/WsPublicAsync.py @@ -0,0 +1,56 @@ +import asyncio +import json +import logging + +from okx.websocket.WebSocketFactory import WebSocketFactory + +logger = logging.getLogger(__name__) + + +class WsPublicAsync: + def __init__(self, url): + self.url = url + self.subscriptions = set() + self.callback = None + self.loop = asyncio.get_event_loop() + self.factory = WebSocketFactory(url) + self.websocket = None + + async def connect(self): + self.websocket = await self.factory.connect() + + async def consume(self): + async for message in self.websocket: + logger.debug("Received message: {%s}", message) + if self.callback: + self.callback(message) + + async def subscribe(self, params: list, callback): + self.callback = callback + payload = json.dumps({ + "op": "subscribe", + "args": params + }) + await self.websocket.send(payload) + # await self.consume() + + async def unsubscribe(self, params: list, callback): + self.callback = callback + payload = json.dumps({ + "op": "unsubscribe", + "args": params + }) + logger.info(f"unsubscribe: {payload}") + await self.websocket.send(payload) + + async def stop(self): + await self.factory.close() + self.loop.stop() + + async def start(self): + logger.info("Connecting to WebSocket...") + await self.connect() + self.loop.create_task(self.consume()) + + def stop_sync(self): + 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/setup.py b/setup.py new file mode 100644 index 00000000..b91e2374 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +import setuptools +import okx +with open("README.md", "r",encoding="utf-8") as fh: + long_description = fh.read() + +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(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + install_requires=[ + "importlib-metadata", + "httpx[http2]", + "keyring", + "loguru", + "requests", + "Twisted", + "pyOpenSSL" + ] +) \ No newline at end of file diff --git a/test/AccountTest.py b/test/AccountTest.py index 2d088487..724112b8 100644 --- a/test/AccountTest.py +++ b/test/AccountTest.py @@ -1,80 +1,151 @@ - import unittest -from ..okx import Account + +from loguru import logger + +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()) + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.AccountAPI = Account.AccountAPI(api_key, api_secret_key, passphrase, 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_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_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_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_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_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_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_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")) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/test/BlockTradingTest.py b/test/BlockTradingTest.py index 8d86974c..e8b0e88d 100644 --- a/test/BlockTradingTest.py +++ b/test/BlockTradingTest.py @@ -1,12 +1,12 @@ import unittest -from ..okx import BlockTrading +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') + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.BlockTradingAPI = BlockTrading.BlockTradingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') """ def test_get_counter_parties(self): @@ -42,7 +42,46 @@ def test_get_trade(self): """ - def test_get_public_trades(self): - print(self.BlockTradingAPI.get_public_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/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/ConvertTest.py b/test/ConvertTest.py index d4faa28f..aa1d175c 100644 --- a/test/ConvertTest.py +++ b/test/ConvertTest.py @@ -2,9 +2,9 @@ from ..okx import Convert class ConvertTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' self.ConvertAPI = Convert.ConvertAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') ''' diff --git a/test/CopyTradingTest.py b/test/CopyTradingTest.py new file mode 100644 index 00000000..95c17396 --- /dev/null +++ b/test/CopyTradingTest.py @@ -0,0 +1,40 @@ +import unittest +from okx import CopyTrading + +class CopyTradingTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.StackingAPI = CopyTrading.CopyTradingAPI(api_key, api_secret_key, passphrase, use_server_time=False, + flag='0') + + # 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/EthStakingTest.py b/test/EthStakingTest.py new file mode 100644 index 00000000..3be48603 --- /dev/null +++ b/test/EthStakingTest.py @@ -0,0 +1,30 @@ +import unittest +from okx.Finance import EthStaking + +class EthStakingTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.StackingAPI = EthStaking.EthStakingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + + 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/FlexibleLoanTest.py b/test/FlexibleLoanTest.py new file mode 100644 index 00000000..b4320a5c --- /dev/null +++ b/test/FlexibleLoanTest.py @@ -0,0 +1,36 @@ +import unittest +from okx.Finance import FlexibleLoan + +class FlexibleLoanTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.FlexibleLoanAPI = FlexibleLoan.FlexibleLoanAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + + 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/FundingTest.py index 2cce1e1e..e87bb76d 100644 --- a/test/FundingTest.py +++ b/test/FundingTest.py @@ -1,13 +1,13 @@ import unittest -from ..okx import Funding +from okx import Funding 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 = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.FundingAPI = Funding.FundingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='0') """ CANCEL_WITHDRAWAL = '/api/v5/asset/cancel-withdrawal' #need add CONVERT_DUST_ASSETS = '/api/v5/asset/convert-dust-assets' #need add @@ -58,9 +58,27 @@ 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): + print(self.FundingAPI.withdrawal(ccy='USDT',amt='1',dest='3',toAddr='18740405107',areaCode='86')) + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/test/GridTest.py b/test/GridTest.py index 072e67cb..6e8f7da1 100644 --- a/test/GridTest.py +++ b/test/GridTest.py @@ -1,12 +1,12 @@ import unittest -from ..okx import Grid +from okx import Grid class GridTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' 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' @@ -49,7 +49,37 @@ def test_withdrawl_profits(self): """ - def test_order_algo(self): - print(self.GridAPI.grid_order_algo("BTC-USDT","grid","45000","20000","100","1",quoteSz="50")) + # 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")) + + #581191143417970688 + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/test/MarketTest.py b/test/MarketTest.py index fdf8df77..2a45d7f9 100644 --- a/test/MarketTest.py +++ b/test/MarketTest.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 @@ -14,14 +14,12 @@ class MarketAPITest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' self.MarketApi = MarketData.MarketAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') ''' - 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 +56,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/PublicDataTest.py index f0b51389..7d7449dd 100644 --- a/test/PublicDataTest.py +++ b/test/PublicDataTest.py @@ -1,10 +1,10 @@ import unittest -from ..okx import PublicData +from okx import PublicData class publicDataTest(unittest.TestCase): def setUp(self): - api_key = 'ef06bf27-6a01-4797-b801-e3897031e45d' - api_secret_key = 'D3620B2660203350EEE80FDF5BE0C960' - passphrase = 'Beijing123' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' self.publicDataApi = PublicData.PublicAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') ''' TestCase For: @@ -50,8 +50,14 @@ 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')) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/test/SavingsTest.py b/test/SavingsTest.py new file mode 100644 index 00000000..9dac910a --- /dev/null +++ b/test/SavingsTest.py @@ -0,0 +1,31 @@ +import unittest +from okx.Finance import Savings + +class SavingsTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.StackingAPI = Savings.SavingsAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + + + 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/SolStakingTest.py b/test/SolStakingTest.py new file mode 100644 index 00000000..c3899d43 --- /dev/null +++ b/test/SolStakingTest.py @@ -0,0 +1,30 @@ +import unittest +from okx.Finance import SolStaking + +class SolStakingTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.StackingAPI = SolStaking.SolStakingAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') + + 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/SpreadTest.py b/test/SpreadTest.py new file mode 100644 index 00000000..cbaf6e8e --- /dev/null +++ b/test/SpreadTest.py @@ -0,0 +1,46 @@ +import unittest +from okx import SpreadTrading +class TradeTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.tradeApi = SpreadTrading.SpreadTradingAPI(api_key, api_secret_key, passphrase, False, '1') + + # 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/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/StakingDefiTest.py b/test/StakingDefiTest.py new file mode 100644 index 00000000..29ac0453 --- /dev/null +++ b/test/StakingDefiTest.py @@ -0,0 +1,36 @@ +import unittest +from okx.Finance import StakingDefi + + +class StakingDefiTest(unittest.TestCase): + def setUp(self): + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' + self.StackingAPI = StakingDefi.StakingDefiAPI(api_key, api_secret_key, passphrase, use_server_time=False, + flag='1') + + 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/SubAccountTest.py index c251c0b8..b4281bd4 100644 --- a/test/SubAccountTest.py +++ b/test/SubAccountTest.py @@ -1,11 +1,11 @@ import unittest -from ..okx import SubAccount +from okx import SubAccount class SubAccountTest(unittest.TestCase): def setUp(self): - api_key = '52c37310-a8b0-454a-8191-3250acff2626' - api_secret_key = 'EC37534156E6B8C32E78FE8D8C1D506B' - passphrase = 'Hanhao0.0' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' self.SubAccountApi = SubAccount.SubAccountAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') ''' ENTRUST_SUBACCOUNT_LIST = '/api/v5/users/entrust-subaccount-list' #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/TradeTest.py b/test/TradeTest.py index d963787c..3f1b601c 100644 --- a/test/TradeTest.py +++ b/test/TradeTest.py @@ -1,121 +1,245 @@ import unittest -from ..okx import Trade + +from okx import Trade + + class TradeTest(unittest.TestCase): def setUp(self): - api_key = '35d8f27e-63cc-45bc-a578-45d76363d47f' - api_secret_key = '0B7C968025BC2D4D71CF74771EA0E15C' - passphrase = '123456' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' 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__': + + # # """ + # 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()) + +if __name__ == '__main__': unittest.main() diff --git a/test/TradingDataTest.py b/test/TradingDataTest.py index e3083fdf..ac318b60 100644 --- a/test/TradingDataTest.py +++ b/test/TradingDataTest.py @@ -4,9 +4,9 @@ class TradingDataTest(unittest.TestCase): def setUp(self): - api_key = '52c37310-a8b0-454a-8191-3250acff2626' - api_secret_key = 'EC37534156E6B8C32E78FE8D8C1D506B' - passphrase = 'Hanhao0.0' + api_key = 'your_apiKey' + api_secret_key = 'your_secretKey' + passphrase = 'your_secretKey' self.TradingDataAPI = TradingData.TradingDataAPI(api_key, api_secret_key, passphrase, use_server_time=False, flag='1') """ diff --git a/test/WsPrivateAsyncTest.py b/test/WsPrivateAsyncTest.py new file mode 100644 index 00000000..ba7fcff0 --- /dev/null +++ b/test/WsPrivateAsyncTest.py @@ -0,0 +1,39 @@ +import asyncio + +from okx.websocket.WsPrivateAsync import WsPrivateAsync + + +def privateCallback(message): + print("privateCallback", message) + + +async def main(): + url = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999" + ws = WsPrivateAsync( + apiKey="your apiKey", + passphrase="your passphrase", + secretKey="your secretKey", + url=url, + useServerTime=False + ) + await ws.start() + args = [] + arg1 = {"channel": "account", "ccy": "BTC"} + arg2 = {"channel": "orders", "instType": "ANY"} + arg3 = {"channel": "balance_and_position"} + args.append(arg1) + args.append(arg2) + args.append(arg3) + await ws.subscribe(args, callback=privateCallback) + await asyncio.sleep(30) + print("-----------------------------------------unsubscribe--------------------------------------------") + args2 = [arg2] + await ws.unsubscribe(args2, callback=privateCallback) + await asyncio.sleep(30) + print("-----------------------------------------unsubscribe all--------------------------------------------") + args3 = [arg1, arg3] + await ws.unsubscribe(args3, callback=privateCallback) + + +if __name__ == '__main__': + asyncio.run(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/WsPublicAsyncTest.py similarity index 53% rename from test/WsPublicTest.py rename to test/WsPublicAsyncTest.py index 6a56990e..14276a06 100644 --- a/test/WsPublicTest.py +++ b/test/WsPublicAsyncTest.py @@ -1,29 +1,37 @@ -import time -from okx.websocket.WsPublic import WsPublic +import asyncio + +from okx.websocket.WsPublicAsync import WsPublicAsync + 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() +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) + await ws.start() args = [] arg1 = {"channel": "instruments", "instType": "FUTURES"} arg2 = {"channel": "instruments", "instType": "SPOT"} - arg3 = {"channel": "tickers", "instId": "BTC-USDT"} + 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) - ws.subscribe(args, publicCallback) - time.sleep(10) + await ws.subscribe(args, publicCallback) + await asyncio.sleep(5) print("-----------------------------------------unsubscribe--------------------------------------------") args2 = [arg4] - ws.unsubscribe(args2, publicCallback) - time.sleep(10) + await ws.unsubscribe(args2, publicCallback) + await asyncio.sleep(5) print("-----------------------------------------unsubscribe all--------------------------------------------") args3 = [arg1, arg2, arg3] - ws.unsubscribe(args3, publicCallback) + await ws.unsubscribe(args3, publicCallback) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/test/eth_trade_bot b/test/eth_trade_bot new file mode 100644 index 00000000..d1e1956d --- /dev/null +++ b/test/eth_trade_bot @@ -0,0 +1,1215 @@ +# ======================== 导入必要的库 ======================== +import asyncio +import time +import json +import logging +import string +import random +import os +from datetime import datetime, timedelta +from collections import defaultdict +import pytz +import pandas as pd +import numpy as np +import requests +from okx.websocket.WsPrivateAsync import WsPrivateAsync as PrivateWs +from okx.websocket.WsPublicAsync import WsPublicAsync as PublicWs +import okx.Account as Account +import okx.Trade as Trade +import okx.MarketData as MarketData +from supertrend_lib1 import SupertrendAnalyzer # 使用您提供的库 + +from decimal import Decimal, getcontext, ROUND_HALF_UP + +# ======================== Decimal 配置 ======================== +getcontext().prec = 28 # 高精度以避免价格/数量浮点误差 + +# ======================== 日志系统配置 ======================== +def setup_logging(): + os.makedirs("logs", exist_ok=True) + logger = logging.getLogger("intelligent_trading_bot") + logger.setLevel(logging.DEBUG) + + file_handler = logging.FileHandler("logs/trading.log") + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(logging.Formatter('%(message)s')) + + # 避免重复添加 handler(在多次导入时) + if not logger.handlers: + logger.addHandler(file_handler) + logger.addHandler(console_handler) + return logger + + +logger = setup_logging() + + +def log_action(action, details, level="info", extra_data=None, exc_info=False): + symbols = { + "debug": "🔵", + "info": "🟢", + "warning": "🟠", + "error": "🔴", + "critical": "⛔" + } + symbol = symbols.get(level, "⚪") + header = f"\n{'-' * 80}\n[{datetime.now().strftime('%H:%M:%S.%f')}] {symbol} {action}" + log_line = header + f"\n • {details}" + + if extra_data: + try: + if isinstance(extra_data, dict): + log_line += f"\n • 附加数据: {json.dumps(extra_data, indent=2, ensure_ascii=False)}" + else: + log_line += f"\n • 附加数据: {extra_data}" + except Exception: + log_line += f"\n • 附加数据: [无法序列化]" + + log_line += f"\n{'-' * 80}" + + # 适配 debug 等级 + if level == "debug": + logger.debug(log_line, exc_info=exc_info) + elif level == "info": + logger.info(log_line, exc_info=exc_info) + elif level == "warning": + logger.warning(log_line, exc_info=exc_info) + elif level == "error": + logger.error(log_line, exc_info=exc_info) + elif level == "critical": + logger.critical(log_line, exc_info=exc_info) + else: + logger.info(log_line, exc_info=exc_info) + + +def log_state_transition(current_state, new_state, reason): + """记录状态转换 - 增加更多上下文信息""" + global trading_phase + + # 获取当前仓位信息 + long_key = get_position_key(SYMBOL, "long") + short_key = get_position_key(SYMBOL, "short") + long_pos = position_info[long_key]["pos"] + short_pos = position_info[short_key]["pos"] + + # 计算ETH价值 + long_eth = calculate_contract_value(long_pos) + short_eth = calculate_contract_value(short_pos) + + logger.info(f"\n{'=' * 80}") + logger.info(f"🔄 状态变更: [{current_state}] → [{new_state}]") + logger.info(f" 原因: {reason}") + logger.info(f" 当前方向: {trading_direction.upper()}") + logger.info(f" 多仓: {long_pos}张 ({long_eth:.6f} ETH) | 空仓: {short_pos}张 ({short_eth:.6f} ETH)") + logger.info(f" 当前价格: ${current_price:.4f}") + logger.info(f" 账户净值: ${account_equity:.2f}") + logger.info(f" 活跃订单: {len(active_orders)}个") + logger.info(f" 最后趋势检查: {datetime.fromtimestamp(last_trend_check).strftime('%H:%M:%S')}") + logger.info(f"{'=' * 80}\n") + + +# ======================== 基础配置 ======================== +API_KEY = "a3a2a008-576a-4548-a1c0-f28ac940bd6b" +SECRET_KEY = "3F9170BCBE9C88EDFA6675438CD0DBAA" +PASSPHRASE = "Aa123414.." + +# ======================== 交易对基础信息(可由 API 获取覆盖) ======================== +CONTRACT_INFO = { + "symbol": "ETH-USDT-SWAP", + "lotSz": 1, # 下单数量精度 + "minSz": 1, # 最小下单数量 + "ctVal": 0.01, # 合约面值 (每张合约代表 0.01 ETH) -- 已修正注释 + "tickSz": 0.1, # 价格精度 + "ctValCcy": "ETH", # 合约价值货币 + "instType": "SWAP" # 合约类型 +} + +# 使用合约信息配置全局变量(会在启动时尝试从 API 更新) +SYMBOL = CONTRACT_INFO["symbol"] +TICK_SIZE = CONTRACT_INFO["tickSz"] + +# Supertrend策略配置 +SUPERTREND_CONFIG = { + 'symbol': SYMBOL, + 'timeframe': '1H', + 'atr_period': 7, + 'multiplier': 2.5, + 'change_atr': True, + 'min_data': 50, + 'data_delay': 1 +} + +# ======================== 全局配置常量 ======================== +PING_INTERVAL = 25 # WebSocket心跳间隔 +PONG_TIMEOUT = 5 # Pong响应超时时间 + +# 交易策略配置 +TRADE_STRATEGY = { + "price_offset": 0.015, # 价格偏移 15%(例如 0.015 表示当前价格的 15%) + "eth_position": 0.01, # 目标 ETH 持仓量(以 ETH 计) + "leverage": 10, # 杠杆倍数 + "atr_multiplier": 0.7, # ATR系数 + "order_increment": 0, # 开仓单在基础仓位上增加的合约张数;0 表示不启用增仓 + "fixed_trend_direction": "long", # 固定趋势方向 (long/short) + "trend_mode": "fixed" # "fixed" 或 "auto" +} + +# 全局变量 +account_equity = 0.0 +initial_equity = 0.0 +current_price = 0.0 +last_price = 0.0 +trading_direction = TRADE_STRATEGY.get("fixed_trend_direction", "long") +last_trend_check = 0 +last_ws_price_update = 0 +last_api_price_update = 0 +price_source = "unknown" +# 日志 +last_status_log_time = 0 +STATUS_LOG_INTERVAL = 60 # 60秒记录一次摘要 + +# 订单和仓位管理 +active_orders = {} # 使用 cl_ord_id 作为键 +position_info = defaultdict(lambda: { + "pos": 0.0, # 合约张数 + "eth_value": 0.0, # ETH价值 + "avg_px": 0.0, + "upl": 0.0, + "entry_time": 0 +}) + +# 订单对映射 +order_pair_mapping = {} # 使用 pair_id 作为键,存储订单对信息 + +# API客户端 +flag = "0" +account_api = Account.AccountAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, flag) +trade_api = Trade.TradeAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, flag) +market_api = MarketData.MarketAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, flag) + +# WebSocket实例(占位) +ws_private = None +ws_public = None + +# 初始化Supertrend分析器 +supertrend_analyzer = SupertrendAnalyzer(SUPERTREND_CONFIG) + + +# ======================== ETH合约工具函数 ======================== +def calculate_contract_value(sz): + """计算合约实际价值 (ETH数量)""" + return sz * CONTRACT_INFO["ctVal"] + + +def calculate_contract_size(eth_amount): + """根据ETH数量计算合约张数""" + return eth_amount / CONTRACT_INFO["ctVal"] + + +def round_to_min_size(size): + """调整数量到最小交易单位的整数倍""" + min_lot = CONTRACT_INFO["minSz"] + # 保证返回与 min_lot 同单位(整数倍) + if min_lot == 0: + return size + # 使用 Decimal 以获得更好精度 + s = Decimal(str(size)) + m = Decimal(str(min_lot)) + rounded = (s / m).quantize(Decimal('1'), rounding=ROUND_HALF_UP) * m + return float(rounded) + + +def validate_position_size(size): + """验证仓位大小是否符合要求""" + min_size = CONTRACT_INFO["minSz"] + if size < min_size: + log_action("风控", f"仓位大小{size}小于最小值{min_size},自动修正", "warning") + return min_size + + # 检查是否为最小单位的整数倍 + if not (size / min_size).is_integer(): + log_action("风控", f"仓位大小{size}不是最小单位{min_size}的整数倍", "warning") + return round_to_min_size(size) + + return size + + +def get_position_size(): + """根据ETH目标持仓计算合约张数""" + eth_amount = TRADE_STRATEGY["eth_position"] + contract_size = calculate_contract_size(eth_amount) + return round_to_min_size(contract_size) # 确保符合最小单位 + + +def get_size_precision(): + """获取数量精度的小数位数""" + min_sz = CONTRACT_INFO["minSz"] + # 计算需要的小数位数 + if min_sz >= 1: + return 0 + elif min_sz >= 0.1: + return 1 + elif min_sz >= 0.01: + return 2 + elif min_sz >= 0.001: + return 3 + else: + return 4 # 默认4位小数 + + +def get_price_precision(): + """获取价格精度的小数位数""" + tick_sz = CONTRACT_INFO["tickSz"] + # 计算需要的小数位数 + if tick_sz >= 1: + return 0 + elif tick_sz >= 0.1: + return 1 + elif tick_sz >= 0.01: + return 2 + elif tick_sz >= 0.001: + return 3 + else: + return 4 # 默认4位小数 + + +def validate_order_increment(): + """验证开仓增量配置是否有效。允许为 0(表示不启用增仓)。""" + increment = TRADE_STRATEGY.get("order_increment", 0) + min_sz = CONTRACT_INFO["minSz"] + + if increment < 0: + log_action("配置验证", "开仓增量不能为负数", "error") + return False + + if increment == 0: + log_action("配置验证", "开仓增量为0,表示不启用分批增仓", "info") + return True + + # 如果小于最小单位,自动修正到最小单位 + if increment < min_sz: + log_action("配置验证", + f"开仓增量({increment})小于最小交易单位({min_sz}),自动修正为最小单位", + "warning") + TRADE_STRATEGY["order_increment"] = min_sz + return True + + # 检查是否为最小单位的整数倍 + if not (increment / min_sz).is_integer(): + log_action("配置验证", + f"开仓增量({increment})不是最小单位({min_sz})的整数倍,自动四舍五入", + "warning") + TRADE_STRATEGY["order_increment"] = round_to_min_size(increment) + return True + + return True + + +# ======================== 交易阶段定义 ======================== +class TradingPhase: + INIT = "INIT" + A1_POSITION_SETUP = "A1" # 新增: 仓位建立阶段 + A2_ORDER_PAIR = "A2" # 订单对阶段 + B2_WAIT_PAIR = "B2" # 订单对监控阶段 + + +# 当前交易阶段 +trading_phase = TradingPhase.INIT +phase_start_time = 0 + + +# ======================== 核心功能函数 ======================== +def get_position_key(inst_id, pos_side): + """获取持仓唯一键""" + return f"{inst_id}-{pos_side.lower()}" + + +def generate_order_id(prefix): + """生成符合OKX要求的订单ID""" + clean_prefix = ''.join(c for c in prefix if c.isalnum()) + rand_part = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + return (clean_prefix + rand_part)[:32] + + +def round_price(price): + """根据 tick 精度调整价格(使用 Decimal 以避免 float 精度问题)""" + tick = Decimal(str(TICK_SIZE)) + p = Decimal(str(price)) + quant = (p / tick).quantize(Decimal('1'), rounding=ROUND_HALF_UP) + rounded = (quant * tick).normalize() + return float(rounded) + + +def safe_float(value, default=0.0): + """安全转换浮点数""" + try: + return float(value) if value else default + except (ValueError, TypeError): + return default + + +# ======================== 账户与市场数据 ======================== +async def fetch_account_balance(): + """获取账户余额 - 使用线程池执行阻塞 API 调用""" + global account_equity, initial_equity + + try: + log_action("账户查询", "发送账户余额请求", "debug") + # 尝试直接按 SDK 常用签名调用,放到线程池以防阻塞 + response = await asyncio.to_thread(account_api.get_account_balance, ccy="USDT") + + # 记录完整响应 + log_action("账户查询", "收到账户余额响应", "debug", response) + + if response.get("code", "") == "0" and response.get("data"): + for detail_group in response["data"]: + for detail in detail_group.get("details", []): + if detail.get("ccy") == "USDT": + equity = float(detail.get("eq", 0.0)) + account_equity = equity + if initial_equity == 0: + initial_equity = equity + log_action("账户初始化", f"初始权益: ${initial_equity:.2f}") + return True + return False + except Exception as e: + log_action("账户查询", f"请求失败: {str(e)}", "error", exc_info=True) + return False + + +def log_periodic_status(): + """记录周期性状态摘要""" + global last_status_log_time + + current_time = time.time() + if current_time - last_status_log_time >= STATUS_LOG_INTERVAL: + last_status_log_time = current_time + + long_key = get_position_key(SYMBOL, "long") + short_key = get_position_key(SYMBOL, "short") + long_pos = position_info[long_key]["pos"] + short_pos = position_info[short_key]["pos"] + + # 计算ETH价值 + long_eth = calculate_contract_value(long_pos) + short_eth = calculate_contract_value(short_pos) + + logger.info(f"\n{'=' * 80}") + logger.info(f"📊 当前状态摘要 - {trading_phase}") + logger.info(f" 当前方向: {trading_direction.upper()}") + logger.info(f" 多仓: {long_pos}张 ({long_eth:.6f} ETH) | 空仓: {short_pos}张 ({short_eth:.6f} ETH)") + logger.info(f" 当前价格: ${current_price:.4f}") + logger.info(f" 账户净值: ${account_equity:.2f}") + logger.info(f" 活跃订单: {len(active_orders)}个") + logger.info(f" 最后趋势检查: {datetime.fromtimestamp(last_trend_check).strftime('%H:%M:%S')}") + logger.info(f" 开仓增量配置: {TRADE_STRATEGY['order_increment']}张") + logger.info(f"{'=' * 80}\n") + + +async def update_position_info(): + """获取持仓信息 - 使用线程池执行阻塞 API 调用""" + try: + log_action("仓位查询", "发送仓位查询请求", "debug") + response = await asyncio.to_thread(account_api.get_positions, instType="SWAP") + + log_action("仓位查询", "收到仓位查询响应", "debug", response) + + if response.get("code") == "0": + positions = response.get("data", []) + + # 重置仓位信息 + for key in list(position_info.keys()): + position_info[key]["pos"] = 0.0 + position_info[key]["eth_value"] = 0.0 + position_info[key]["avg_px"] = 0.0 + position_info[key]["upl"] = 0.0 + position_info[key]["entry_time"] = 0 + + # 更新有效仓位 + for pos in positions: + if isinstance(pos, dict) and pos.get("instId") == SYMBOL: + pos_side = pos.get("posSide", "net").lower() + pos_key = get_position_key(SYMBOL, pos_side) + + contract_size = float(pos.get("pos", "0")) + eth_value = calculate_contract_value(contract_size) + + position_info[pos_key]["pos"] = contract_size + position_info[pos_key]["eth_value"] = eth_value + position_info[pos_key]["avg_px"] = float(pos.get("avgPx", "0")) + position_info[pos_key]["upl"] = float(pos.get("upl", "0")) + + log_action("仓位更新", + f"{pos_side}仓: {contract_size}张 ({eth_value:.6f} ETH)", + "debug") + return True + return False + except Exception as e: + log_action("仓位查询", f"请求失败: {str(e)}", "error", exc_info=True) + return False + + +async def update_current_price(): + """获取当前价格 - 通过API备选 - 使用线程池执行阻塞调用""" + global current_price, last_price, last_api_price_update, price_source + + try: + log_action("价格查询", "发送价格查询请求", "debug") + response = await asyncio.to_thread(market_api.get_ticker, instId=SYMBOL) + + log_action("价格查询", "收到价格查询响应", "debug", response) + + if response.get("code") == "0" and response.get("data"): + price = float(response["data"][0]["last"]) + + # 更新价格和时间戳 + last_price = current_price + current_price = price + last_api_price_update = time.time() + price_source = "rest_api" + + log_action("API价格更新", f"${last_price:.4f} → ${price:.4f}", "info") + return True + return False + except Exception as e: + log_action("价格查询", f"请求失败: {str(e)}", "error", exc_info=True) + return False + + +async def ensure_price_freshness(): + """确保价格足够新鲜 - 增加详细日志""" + global price_source + + # 记录当前状态 + ws_freshness = time.time() - last_ws_price_update + api_freshness = time.time() - last_api_price_update + log_action("价格新鲜度", + f"WS新鲜度: {ws_freshness:.2f}s | API新鲜度: {api_freshness:.2f}s", + "debug") + + # 如果WebSocket价格在2秒内更新过,使用WebSocket价格 + if ws_freshness <= 2.0: + price_source = "websocket" + log_action("价格新鲜度", f"使用WS价格(新鲜度:{ws_freshness:.2f}s)", "debug") + return True + + # 如果API价格在5秒内更新过,使用API价格 + if api_freshness <= 5.0: + price_source = "rest_api" + log_action("价格新鲜度", f"使用API价格(新鲜度:{api_freshness:.2f}s)", "debug") + return True + + # 主动更新价格 + log_action("价格更新", "价格数据过期,主动获取最新价格", "warning") + return await update_current_price() + + +# ======================== 订单管理 ======================== +class RateLimiter: + """OKX API速率限制管理器 - 增加详细日志""" + + def __init__(self): + self.last_request_time = 0 + self.request_count = 0 + self.window_start = time.time() + # 默认通用上限(保守),但支持自定义短时间窗口限制 + self.max_orders_per_window = 300 # 每2秒300个订单(可视为上限,谨慎使用) + self.window_seconds = 2 + + async def check_limit(self, orders_count, max_per_window=None, window_seconds=None): + """检查并遵守API速率限制。允许传入自定义限速(用于特定端点)""" + # 支持自定义值 + max_orders = max_per_window if max_per_window is not None else self.max_orders_per_window + win_seconds = window_seconds if window_seconds is not None else self.window_seconds + + current_time = time.time() + + # 检查当前时间窗口 + window_elapsed = current_time - self.window_start + if window_elapsed > win_seconds: + log_action("限速器", f"窗口重置 (已过{window_elapsed:.2f}s > {win_seconds}s)", "debug") + self.request_count = 0 + self.window_start = current_time + window_elapsed = 0 + + # 预测请求后是否超出限制 + predicted_count = self.request_count + orders_count + while predicted_count > max_orders: + # 计算需要等待的时间 + wait_time = max(0.0, win_seconds - window_elapsed + 0.1) + log_action("限速等待", + f"需等待{wait_time:.2f}秒 (当前{self.request_count}/{max_orders},请求后{predicted_count})", + "warning") + await asyncio.sleep(wait_time) + current_time = time.time() + window_elapsed = current_time - self.window_start + + if window_elapsed > win_seconds: + log_action("限速器", f"等待后窗口重置 (已过{window_elapsed:.2f}s > {win_seconds}s)", "debug") + self.request_count = 0 + self.window_start = current_time + break + + predicted_count = self.request_count + orders_count + + # 避免请求过快(即使未达限制) + if current_time - self.last_request_time < 0.1: + wait_time = 0.1 - (current_time - self.last_request_time) + log_action("限速等待", f"避免请求过快,等待{wait_time:.3f}s", "debug") + await asyncio.sleep(wait_time) + + # 更新计数器 + self.request_count += orders_count + self.last_request_time = time.time() + log_action("限速器", f"请求计数: {self.request_count}/{max_orders}", "debug", + {"订单数": orders_count, "窗口秒数": win_seconds}) + + +# 全局速率限制器实例 +rate_limiter = RateLimiter() + + +async def check_rate_limit(orders_count, max_per_window=None, window_seconds=None): + """应用速率限制""" + await rate_limiter.check_limit(orders_count, max_per_window=max_per_window, window_seconds=window_seconds) + + +async def fetch_instrument_info_from_api(): + """使用 OKX API 获取指定合约的基础信息并更新 CONTRACT_INFO(遵守 20 requests / 2s 限速)""" + global CONTRACT_INFO, TICK_SIZE, SYMBOL + try: + # OKX 文档:获取交易产品基础信息,限速 20 次/2s(User ID + Instrument Type) + await check_rate_limit(1, max_per_window=20, window_seconds=2) + + log_action("合约信息", f"请求合约信息: instType={CONTRACT_INFO.get('instType','SWAP')} instId={CONTRACT_INFO.get('symbol')}", "debug") + # account_api.get_instruments(instType="SWAP") or with instId + response = await asyncio.to_thread(account_api.get_instruments, instType=CONTRACT_INFO.get("instType", "SWAP")) + log_action("合约信息", "收到合约信息响应", "debug", response) + + if response.get("code") == "0" and response.get("data"): + # 寻找匹配的 instId(SYMBOL 形如 ETH-USDT-SWAP) + inst_list = response.get("data", []) + # Normalize symbol: OKX api uses instId like 'ETH-USDT-SWAP' depending on API; try match by prefix + target = CONTRACT_INFO.get("symbol") + found = None + for item in inst_list: + # fields often instId, tickSz, minSz, ctVal, lotSz, ctValCcy + inst_id = item.get("instId") or item.get("inst_id") or "" + if inst_id == target or inst_id.startswith(target.split('-')[0] + '-'): + found = item + break + if not found: + # fallback: try exact match where instId may be without '-SWAP' + for item in inst_list: + inst_id = item.get("instId") or "" + if target.split('-')[0] in inst_id and 'USDT' in inst_id: + found = item + break + + if found: + # Parse and update CONTRACT_INFO fields if present + minSz = safe_float(found.get("minSz", CONTRACT_INFO["minSz"])) + tickSz = safe_float(found.get("tickSz", CONTRACT_INFO["tickSz"])) + ctVal = found.get("ctVal", CONTRACT_INFO["ctVal"]) + try: + ctVal = float(ctVal) if ctVal not in (None, "", []) else CONTRACT_INFO["ctVal"] + except Exception: + ctVal = CONTRACT_INFO["ctVal"] + lotSz = found.get("lotSz", CONTRACT_INFO["lotSz"]) + try: + lotSz = float(lotSz) if lotSz not in (None, "", []) else CONTRACT_INFO["lotSz"] + except Exception: + lotSz = CONTRACT_INFO["lotSz"] + ctValCcy = found.get("ctValCcy", CONTRACT_INFO.get("ctValCcy", "ETH")) + + CONTRACT_INFO.update({ + "minSz": minSz, + "tickSz": tickSz, + "ctVal": ctVal, + "lotSz": lotSz, + "ctValCcy": ctValCcy + }) + + # Update derived globals + TICK_SIZE = CONTRACT_INFO["tickSz"] + SYMBOL = CONTRACT_INFO["symbol"] + + log_action("合约信息", f"已更新 CONTRACT_INFO: minSz={minSz}, tickSz={tickSz}, ctVal={ctVal}, lotSz={lotSz}", "info") + return True + else: + log_action("合约信息", f"未在返回列表中找到匹配合约: {CONTRACT_INFO.get('symbol')}", "warning", {"returned_count": len(inst_list)}) + return False + else: + log_action("合约信息", f"获取合约信息失败: {response.get('msg','')}", "warning", response) + return False + except Exception as e: + log_action("合约信息", f"获取合约信息异常: {e}", "error", exc_info=True) + return False + + +async def cancel_all_orders(): + """取消所有活跃订单 - 增加详细日志""" + try: + if active_orders: + order_ids = list(active_orders.keys()) + order_count = len(order_ids) + log_action("订单取消", f"取消{order_count}个订单", "info") + + # 应用速率限制 + await check_rate_limit(order_count) + + # 批量取消请求 + cancel_reqs = [{"instId": SYMBOL, "clOrdId": cl_ord_id} for cl_ord_id in order_ids] + log_action("批量取消", "发送批量取消请求", "debug", {"订单列表": order_ids}) + # trade_api.cancel_multiple_orders likely blocking; run in thread + response = await asyncio.to_thread(trade_api.cancel_multiple_orders, cancel_reqs) + + # 记录完整响应 + log_action("批量取消", "收到批量取消响应", "debug", response) + + if response.get("code") == "0": + active_orders.clear() + return True + else: + log_action("批量取消", "批量取消返回非 0 code,未清除本地订单", "warning", response) + # Do not clear locally unless confirmed; attempt best-effort update + return False + return True # 没有订单也算成功 + except Exception as e: + log_action("取消订单", f"取消失败: {str(e)}", "error", exc_info=True) + return False + + +async def cancel_single_order(cl_ord_id): + """取消单个订单 - 增加详细日志""" + try: + # 应用速率限制 + await check_rate_limit(1) + + request = {"instId": SYMBOL, "clOrdId": cl_ord_id} + log_action("取消订单", f"发送取消请求: {cl_ord_id}", "debug", request) + response = await asyncio.to_thread(trade_api.cancel_order, **request) + + # 记录完整响应 + log_action("取消订单", f"收到取消响应: {cl_ord_id}", "debug", response) + + if str(response.get("code", "")) == "0": + log_action("订单取消", f"订单 {cl_ord_id} 取消成功") + # 更新本地 active_orders + if cl_ord_id in active_orders: + del active_orders[cl_ord_id] + return True + else: + log_action("订单取消", f"订单 {cl_ord_id} 取消失败: {response.get('msg', '未知错误')}", "warning") + return False + except Exception as e: + log_action("取消订单", f"取消订单 {cl_ord_id} 失败: {str(e)}", "error") + return False + + +# ======================== 新增: A1阶段 - 仓位建立 ======================== +async def place_market_setup_order(adjust_size): + """根据当前趋势方向市价建仓 - 使用线程池执行阻塞 trade_api 调用""" + # 确保数量有效 + if adjust_size <= 0: + log_action("A1下单", "调整量为0,无需建仓", "warning") + return True + + try: + # 确定下单方向 + side = "buy" if trading_direction == "long" else "sell" + + # 生成订单ID + cl_ord_id = generate_order_id(f"A1_{trading_direction[:1]}") + + # 验证并调整仓位大小 + validated_size = validate_position_size(adjust_size) + eth_value = calculate_contract_value(validated_size) + + request = { + "instId": SYMBOL, + "tdMode": "isolated", + "clOrdId": cl_ord_id, + "side": side, + "posSide": trading_direction, + "ordType": "market", + "sz": str(validated_size) + } + + # 应用速率限制 + await check_rate_limit(1) + + # 发送下单请求(线程池) + log_action("A1下单", "发送市价开仓请求", "debug", request) + response = await asyncio.to_thread(trade_api.place_order, **request) + + # 记录完整响应 + log_action("A1下单", "收到下单响应", "debug", response) + + if str(response.get("code", "")) == "0": + # 记录活跃订单 + ord_id = response['data'][0].get('ordId', '') + active_orders[cl_ord_id] = { + "ord_id": ord_id, + "state": "live", + "type": "market", + "tag": "A1_SETUP", + "create_time": time.time(), + "side": side, + "posSide": trading_direction + } + + log_action("A1下单", f"市价{trading_direction}建仓单已提交", "info", { + "调整仓位": f"{validated_size}张 ({eth_value:.6f} ETH)", + "订单ID": cl_ord_id + }) + return True + + # 处理下单失败 + log_action("A1下单", f"市价建仓失败: {response.get('msg', '未知错误')}", "error", response) + return False + except Exception as e: + log_action("A1下单", f"市价建仓异常: {str(e)}", "error", exc_info=True) + return False + + +# ======================== 重构: A2阶段 - 挂订单对 (OKX批量下单) ======================== +async def calculate_dynamic_offset(): + """计算价格偏移 - 简化版本,只使用固定偏移""" + base_offset = TRADE_STRATEGY["price_offset"] + # 记录简单的调试信息 + log_action("价格偏移", f"使用固定偏移: {base_offset * 100:.2f}% ({base_offset})", "debug") + return base_offset + + +async def place_open_order_only(): + """只挂开仓单的逻辑""" + # 生成订单ID + open_cl_ord_id = generate_order_id("OPEN_ONLY") + + # 计算开仓价格 + offset_percent = await calculate_dynamic_offset() + # 使用 Decimal 以提升精度 + if trading_direction == "long": + open_price = round_price(Decimal(str(current_price)) * (Decimal('1') - Decimal(str(offset_percent)))) + else: + open_price = round_price(Decimal(str(current_price)) * (Decimal('1') + Decimal(str(offset_percent)))) + + # 获取仓位大小 + contract_size = get_position_size() + eth_value = calculate_contract_value(contract_size) + + # 创建开仓单请求 + request = { + "instId": SYMBOL, + "tdMode": "isolated", + "clOrdId": open_cl_ord_id, + "side": "buy" if trading_direction == "long" else "sell", + "posSide": trading_direction, + "ordType": "limit", + "px": str(open_price), + "sz": str(contract_size), + "reduceOnly": False + } + + try: + await check_rate_limit(1) + response = await asyncio.to_thread(trade_api.place_order, **request) + + if str(response.get("code", "")) == "0": + ord_id = response['data'][0].get('ordId', '') + active_orders[open_cl_ord_id] = { + "ord_id": ord_id, + "state": "live", + "type": "limit", + "tag": "OPEN_ONLY", + "create_time": time.time(), + "side": request["side"], + "posSide": trading_direction, + "px": open_price, + "sz": contract_size + } + log_action("开仓单", f"开仓单已提交 {open_cl_ord_id}", "info", { + "价格": open_price, + "方向": trading_direction, + "数量": f"{contract_size}张 ({eth_value:.6f} ETH)" + }) + return True + log_action("开仓单", f"开仓单失败: {response.get('msg', '未知错误')}", "error", response) + return False + except Exception as e: + log_action("开仓单", f"挂单失败: {str(e)}", "error", exc_info=True) + return False + + +async def place_full_order_pair(offset_percent): + """挂完整订单对""" + # 生成订单对ID + pair_id = generate_order_id("PAIR") + open_tag = "BL" if trading_direction == "long" else "SS" # BL = Buy Long, SS = Sell Short + close_tag = "SL" if trading_direction == "short" else "SC" # SL = Sell Long, SC = Cover Short + + # 计算开仓和平仓价格(使用 Decimal 再转 float via round_price) + if trading_direction == "long": + open_price = round_price(Decimal(str(current_price)) * (Decimal('1') - Decimal(str(offset_percent)))) + close_price = round_price(Decimal(str(current_price)) * (Decimal('1') + Decimal(str(offset_percent)))) + else: + open_price = round_price(Decimal(str(current_price)) * (Decimal('1') + Decimal(str(offset_percent)))) + close_price = round_price(Decimal(str(current_price)) * (Decimal('1') - Decimal(str(offset_percent)))) + + # 获取基础仓位大小 + base_contract_size = get_position_size() + + # 获取开仓增量配置值 + order_increment = TRADE_STRATEGY["order_increment"] + + # 开仓单增加配置的增量 + open_contract_size = base_contract_size + order_increment + open_contract_size = validate_position_size(open_contract_size) + + # 平仓单保持原大小 + close_contract_size = base_contract_size + + # 计算ETH价值 + base_eth_value = calculate_contract_value(base_contract_size) + increment_eth_value = calculate_contract_value(order_increment) + open_eth_value = calculate_contract_value(open_contract_size) + close_eth_value = calculate_contract_value(close_contract_size) + + # 创建开仓单 + open_cl_ord_id = generate_order_id(f"{open_tag}_{pair_id}") + open_request = { + "instId": SYMBOL, + "tdMode": "isolated", + "clOrdId": open_cl_ord_id, + "side": "buy" if trading_direction == "long" else "sell", + "posSide": trading_direction, + "ordType": "limit", + "px": str(open_price), + "sz": str(open_contract_size), # 使用修改后的开仓数量 + "reduceOnly": False + } + + # 创建平仓单 + close_cl_ord_id = generate_order_id(f"{close_tag}_{pair_id}") + close_request = { + "instId": SYMBOL, + "tdMode": "isolated", + "clOrdId": close_cl_ord_id, + "side": "sell" if trading_direction == "long" else "buy", + "posSide": trading_direction, + "ordType": "limit", + "px": str(close_price), + "sz": str(close_contract_size), # 使用原始平仓数量 + "reduceOnly": True + } + + # 批量订单请求 + batch_requests = [open_request, close_request] + + # 记录订单对关系(提前记录以处理回调) + order_pair_mapping[pair_id] = { + "open_cl_ord_id": open_cl_ord_id, + "close_cl_ord_id": close_cl_ord_id, + "status": "pending", + "create_time": time.time() + } + + try: + # 应用速率限制 (2个订单) + await check_rate_limit(2) + + # 提交批量订单 + log_action("批量下单", "提交批量订单请求", "info", { + "订单对ID": pair_id, + "开仓请求": open_request, + "平仓请求": close_request, + "价格偏移": f"{offset_percent * 100:.2f}%", + "当前价格": current_price, + "基础仓位": f"{base_contract_size}张 ({base_eth_value:.6f} ETH)", + "开仓增量": f"{order_increment}张 ({increment_eth_value:.6f} ETH)", + "开仓总量": f"{open_contract_size}张 ({open_eth_value:.6f} ETH)" + }) + response = await asyncio.to_thread(trade_api.place_multiple_orders, batch_requests) + + # 记录完整响应 + log_action("批量下单", "收到批量下单响应", "debug", response) + + # 检查主响应代码 + if response.get("code") != "0": + log_action("批量下单", "批量接口主响应错误", "error", response) + del order_pair_mapping[pair_id] + return False + + # 处理每个订单的响应 + order_data = response.get("data", []) + success_orders = [] + failure_orders = [] + + for result in order_data: + cl_ord_id = result.get("clOrdId", "") + s_code = result.get("sCode", "") + ord_id = result.get("ordId", "") # 服务器分配的订单ID + s_msg = result.get("sMsg", "") + + if s_code == "0": # 成功 + success_orders.append({ + "cl_ord_id": cl_ord_id, + "ord_id": ord_id, + "request": next((r for r in batch_requests if r["clOrdId"] == cl_ord_id), None) + }) + else: + failure_orders.append({ + "cl_ord_id": cl_ord_id, + "s_code": s_code, + "s_msg": s_msg, + "ord_id": ord_id + }) + + # 处理部分失败情况 + if failure_orders: + log_action("批量下单", f"{len(failure_orders)}个订单失败", "error", failure_orders) + + # 取消已成功的订单 + if success_orders: + for order in success_orders: + await cancel_single_order(order["cl_ord_id"]) + log_action("批量下单", f"已取消{len(success_orders)}个成功订单", "warning") + + # 清理订单对映射 + if pair_id in order_pair_mapping: + del order_pair_mapping[pair_id] + return False + + # 记录活跃订单 + for order in success_orders: + # 确定订单类型(开仓单还是平仓单) + if open_tag in order["cl_ord_id"]: + order_type = "open" + tag = open_tag + else: + order_type = "close" + tag = close_tag + + # 记录到活跃订单 + active_orders[order["cl_ord_id"]] = { + "ord_id": order["ord_id"], + "state": "live", + "type": "limit", + "tag": tag, + "create_time": time.time(), + "side": order["request"]["side"], + "posSide": trading_direction, + "pair_id": pair_id, + "px": order["request"]["px"], + "sz": order["request"]["sz"] + } + + log_action("订单记录", f"{order_type}订单已记录", "info", { + "cl_ord_id": order["cl_ord_id"], + "ord_id": order["ord_id"], + "price": order["request"]["px"], + "size": f"{order['request']['sz']}张" + }) + + # 更新订单对状态 + order_pair_mapping[pair_id]["status"] = "active" + log_action("批量下单", "✅ 订单对挂单成功", "info", { + "pair_id": pair_id, + "开仓价": open_price, + "平仓价": close_price, + "价格偏移": f"{offset_percent * 100:.2f}%", + "当前价格": current_price, + "基础仓位": f"{base_contract_size}张 ({base_eth_value:.6f} ETH)", + "开仓增量": f"{order_increment}张 ({increment_eth_value:.6f} ETH)", + "开仓总量": f"{open_contract_size}张 ({open_eth_value:.6f} ETH)" + }) + return True + + except Exception as e: + log_action("批量下单", f"批量下单异常: {str(e)}", "error", exc_info=True) + if pair_id in order_pair_mapping: + del order_pair_mapping[pair_id] + return False + + +async def place_order_pair(): + """挂完整订单对 - 简化版本,不考虑持仓均价""" + # 确保价格新鲜 + if not await ensure_price_freshness(): + log_action("A2下单", "无法获取最新价格", "error") + return False + + # 计算动态偏移量 + offset_percent = await calculate_dynamic_offset() + + # 直接挂完整订单对 + log_action("订单对", "无价格限制,挂完整订单对", "info") + return await place_full_order_pair(offset_percent) + + +# ======================== 趋势分析 ======================== +async def analyze_trend(): + """分析Supertrend趋势并返回方向 - 支持 fixed 或 auto 模式""" + global trading_direction, last_trend_check + + # 更新最后趋势检查时间 + last_trend_check = time.time() + + mode = TRADE_STRATEGY.get("trend_mode", "fixed") + if mode == "fixed": + fixed_direction = TRADE_STRATEGY.get("fixed_trend_direction", "long") + if trading_direction != fixed_direction: + log_action("趋势更新", + f"更新趋势方向: {trading_direction} → {fixed_direction}", + "warning", + {"reason": "使用配置的固定趋势方向"}) + trading_direction = fixed_direction + # 返回 False 表示没有触发方向变化事件外部需处理 + return False + else: + # auto 模式:调用 supertrend_analyzer(假设存在同步接口 get_direction 或 analyze) + try: + # 将可能阻塞的计算放到线程池 + direction = await asyncio.to_thread(supertrend_analyzer.get_direction) + # direction 应返回 'long' 或 'short' + if direction not in ["long", "short"]: + log_action("趋势分析", f"Supertrend 返回的方向不在预期集合: {direction}", "error") + return False + if direction != trading_direction: + log_action("趋势更新", f"自动趋势更新: {trading_direction} → {direction}", "info", {"source": "supertrend"}) + trading_direction = direction + return True + return False + except Exception as e: + log_action("趋势分析", f"Supertrend 分析失败: {e}", "error") + return False + + +async def close_position(pos_side): + """平掉指定方向的仓位 - 增加详细日志""" + if pos_side not in ["long", "short"]: + log_action("平仓", f"非法平仓方向: {pos_side}", "error") + return False + + pos_key = get_position_key(SYMBOL, pos_side) + pos_size = position_info[pos_key]["pos"] + + if pos_size <= 0: + log_action("平仓", f"{pos_side} 仓位为0,无需平仓", "info") + return True + + try: + # 市价平仓 + side = "sell" if pos_side == "long" else "buy" + cl_ord_id = generate_order_id(f"MC_{pos_side[:1]}") + + # 验证仓位大小 + validated_size = validate_position_size(pos_size) + eth_value = calculate_contract_value(validated_size) + + request = { + "instId": SYMBOL, + "tdMode": "isolated", + "clOrdId": cl_ord_id, + "side": side, + "posSide": pos_side, + "ordType": "market", + "sz": str(validated_size), + "reduceOnly": True + } + + # 发送下单请求(线程池) + log_action("平仓下单", "发送平仓请求", "debug", request) + response = await asyncio.to_thread(trade_api.place_order, **request) + + # 记录完整响应 + log_action("平仓下单", "收到平仓响应", "debug", response) + + if str(response.get("code", "")) == "0": + log_action("平仓下单", f"{pos_side} 仓位市价平仓提交成功", "info", { + "数量": f"{validated_size}张 ({eth_value:.6f} ETH)", + "订单ID": cl_ord_id + }) + # 在本地更新仓位信息为 0(由于我们没有等待 websocket 真实成交通知) + position_info[pos_key]["pos"] = 0.0 + return True + else: + log_action("平仓下单", f"市价平仓失败: {response.get('msg', '未知错误')}", "error", response) + return False + except Exception as e: + log_action("平仓下单", f"市价平仓异常: {str(e)}", "error", exc_info=True) + return False + + +# ======================== WebSocket 及回调占位(测试用) ======================== +# 为了测试流程,提供最小的 WebSocket 占位(不做实际连接) +async def ws_price_update_simulator(): + """模拟 WebSocket 价格更新 - 在测试时可启动""" + global current_price, last_ws_price_update + while True: + # 模拟价格微动 + if current_price == 0: + current_price = 1000.0 # 初始测试价格 + else: + current_price = current_price * (1 + (random.random() - 0.5) * 0.001) + last_ws_price_update = time.time() + await asyncio.sleep(0.5) + + +# ======================== 主流程入口(测试/调试专用) ======================== +async def main_loop_once(): + """单次运行逻辑,用于测试脚本在没有真实 websocket 回调的环境下运行""" + # 尝试从 API 获取合约信息以覆盖本地 CONTRACT_INFO(如果成功则会修正 minSz/tickSz/ctVal 等) + await fetch_instrument_info_from_api() + + # 刷新账户、仓位、价格 + await update_current_price() + await fetch_account_balance() + await update_position_info() + + # 确保 order_increment 配置有效 + validate_order_increment() + + # 分析趋势(fixed 或 auto) + await analyze_trend() + + # 根据阶段与策略做简单动作(示例) + # 如果没有活跃订单,挂一对订单作为测试 + if not active_orders: + log_action("主流程", "当前无活跃订单,尝试挂一对订单", "info") + success = await place_order_pair() + log_action("主流程", f"挂单对结果: {success}", "info") + else: + log_action("主流程", f"已有活跃订单: {len(active_orders)},跳过挂单", "debug") + + # 记录周期状态 + log_periodic_status() + + +async def main(run_forever=False): + """主入口,用于测试与调试""" + # 启动一个价格模拟器(测试用) + sim_task = asyncio.create_task(ws_price_update_simulator()) + + try: + # 单次运行并退出,或持续运行 + if run_forever: + while True: + await main_loop_once() + await asyncio.sleep(5) + else: + await main_loop_once() + finally: + sim_task.cancel() + try: + await sim_task + except asyncio.CancelledError: + pass + + +if __name__ == "__main__": + # 便于测试:运行一次主流程 + asyncio.run(main(run_forever=False)) diff --git a/test/eth_trade_bot.py b/test/eth_trade_bot.py new file mode 100644 index 00000000..cd088df9 --- /dev/null +++ b/test/eth_trade_bot.py @@ -0,0 +1,1122 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +ETH perpetual trading bot for OKX (SDK-only, testnet-ready). + +Security: +- Do NOT hard-code API keys into source files. +- This script reads credentials from environment variables: + OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE, OKX_FLAG +- For quick local testing, export/set these env vars in your shell before running. + +This file includes: + - SDK init (Account/Trade/MarketData, PrivateWs/PublicWs) + - REST helpers (fetch instrument, balances, positions, orders) + - WebSocket handling (private channels: account, orders, fills, positions, balance_and_position; public: tickers) + - Strategy helpers: continuous-eaten protection, gatekeeper, dynamic offset, place_pair_if_ok +""" + +import asyncio +import time +import json +import logging +import random +import os +import sys +from datetime import datetime, timezone +from collections import defaultdict, deque +from decimal import Decimal, getcontext, ROUND_HALF_UP +from typing import Optional, Dict, Any, List +import math + +# ---------------- Credentials (from environment) ---------------- +# API_KEY = os.getenv("OKX_API_KEY", "") +# SECRET_KEY = os.getenv("OKX_SECRET_KEY", "") +# PASSPHRASE = os.getenv("OKX_PASSPHRASE", "") +# OKX_FLAG = os.getenv("OKX_FLAG", "1") # "1" for testnet, "0" for mainnet + +API_KEY = os.getenv("OKX_API_KEY", "52c6b3db-8827-477d-8e25-9c8b14d816e7") +SECRET_KEY = os.getenv("OKX_SECRET_KEY", "6AA11170CBC857418B3FEA38127703CA") +PASSPHRASE = os.getenv("OKX_PASSPHRASE", "Jinquan169..") +OKX_FLAG = os.getenv("OKX_FLAG", "1") # "1" for testnet, "0" for mainnet + +# ---------------- SDK imports ---------------- +try: + from okx.websocket.WsPrivateAsync import WsPrivateAsync as PrivateWs + from okx.websocket.WsPublicAsync import WsPublicAsync as PublicWs + import okx.Account as Account + import okx.Trade as Trade + import okx.MarketData as MarketData +except Exception: + PrivateWs = None + PublicWs = None + Account = None + Trade = None + MarketData = None + +getcontext().prec = 28 + +# ---------------- Logging ---------------- +LOG_DIR = "logs" +os.makedirs(LOG_DIR, exist_ok=True) +LOG_FILE = os.path.join(LOG_DIR, "trading.log") + +logger = logging.getLogger("eth_trade_bot") +logger.setLevel(logging.DEBUG) +for h in list(logger.handlers): + logger.removeHandler(h) +fh = logging.FileHandler(LOG_FILE, encoding="utf-8") +fh.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) +logger.addHandler(fh) +try: + if hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass +except Exception: + pass +ch = logging.StreamHandler(sys.stdout) +ch.setFormatter(logging.Formatter('%(message)s')) +logger.addHandler(ch) + + +def log_action(action: str, details: str, level: str = "info", extra: Optional[dict] = None, exc_info: bool = False): + prefix = { + "debug": "🔵", + "info": "🟢", + "warning": "🟠", + "error": "🔴", + "critical": "⛔" + }.get(level, "⚪") + ts = datetime.now(timezone.utc).isoformat() + msg = f"[{ts}] {prefix} {action} - {details}" + if extra: + try: + msg += " | " + json.dumps(extra, ensure_ascii=False) + except Exception: + msg += f" | {extra}" + try: + if level == "debug": + logger.debug(msg, exc_info=exc_info) + elif level == "warning": + logger.warning(msg, exc_info=exc_info) + elif level == "error": + logger.error(msg, exc_info=exc_info) + elif level == "critical": + logger.critical(msg, exc_info=exc_info) + else: + logger.info(msg, exc_info=exc_info) + except UnicodeEncodeError: + safe_msg = msg.encode("utf-8", errors="replace").decode("utf-8") + if level == "debug": + logger.debug(safe_msg, exc_info=exc_info) + elif level == "warning": + logger.warning(safe_msg, exc_info=exc_info) + elif level == "error": + logger.error(safe_msg, exc_info=exc_info) + elif level == "critical": + logger.critical(safe_msg, exc_info=exc_info) + else: + logger.info(safe_msg, exc_info=exc_info) + +# ---------------- Globals & Config ---------------- +SYMBOL = "ETH-USDT-SWAP" +CONTRACT_INFO: Dict[str, Any] = {"symbol": SYMBOL, "minSz": 0.0, "tickSz": 0.0, "ctVal": 0.0, "ctValCcy": "ETH", "instType": "SWAP"} +TICK_SIZE = CONTRACT_INFO["tickSz"] + +STRATEGY = { + "base_notional_fraction": 0.25, + "leverage": 5, + "price_offset": 0.001, + "expected_hold_seconds": 300, + "expected_slippage_pct": 0.0002, + "order_type": "limit" +} + +# Runtime state +account_equity_usdt = 0.0 +initial_equity_usdt = 0.0 +current_price = 0.0 +last_price = 0.0 +price_source = "unknown" + +active_orders: Dict[str, dict] = {} +position_info = defaultdict(lambda: {"pos": 0.0, "avg_px": 0.0, "usdt_value": 0.0}) + +# pairs & lock +active_pairs: Dict[str, dict] = {} +orders_lock = asyncio.Lock() + +# SDK clients +account_api = None +trade_api = None +market_api = None + +# WS instances +_ws_instance = None +_public_ws_instance = None + +# Dedup sets +seen_trade_ids = set() +seen_filled_ordids = set() +seen_reqids = set() + +# Public ticker best bid/ask +best_bid = 0.0 +best_ask = 0.0 + +# Controls +ENABLE_FILLS_CHANNEL = False +ENABLE_PUBLIC_TICKER = True + +# ---------------- Utilities ---------------- +def safe_float(v, default=0.0): + try: + if v is None or v == "": + return default + return float(v) + except Exception: + return default + + +def generate_order_id(prefix: str = "o"): + suffix = ''.join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=10)) + ts = int(time.time() * 1000) % 1000000 + return f"{prefix}{ts}{suffix}"[:32] + + +def round_price_by_tick(p: float, tick: float): + try: + if tick <= 0: + return p + q = Decimal(str(p)) / Decimal(str(tick)) + qr = q.quantize(Decimal("1"), rounding=ROUND_HALF_UP) + return float((qr * Decimal(str(tick))).normalize()) + except Exception: + return p + + +def round_to_min_size(size: float) -> float: + min_sz = CONTRACT_INFO.get("minSz", 0) or 0 + try: + m = Decimal(str(min_sz)) + s = Decimal(str(size)) + if m == 0: + return float(s) + q = (s / m).quantize(Decimal("1"), rounding=ROUND_HALF_UP) + rounded = (q * m).normalize() + return float(rounded) + except Exception: + return float(size) + + +# ---------------- Rate limiter ---------------- +class RateLimiter: + def __init__(self): + self.window_start = time.time() + self.count = 0 + self.window_seconds = 2 + self.max_per_window = 300 + self.last_request_time = 0.0 + + async def wait_for(self, n=1, max_per_window=None, window_seconds=None): + max_w = max_per_window if max_per_window is not None else self.max_per_window + win = window_seconds if window_seconds is not None else self.window_seconds + while True: + now = time.time() + if now - self.window_start >= win: + self.window_start = now + self.count = 0 + if self.count + n <= max_w: + if now - self.last_request_time < 0.05: + await asyncio.sleep(max(0.0, 0.05 - (now - self.last_request_time))) + self.count += n + self.last_request_time = time.time() + return + await asyncio.sleep(0.05) + + +rate_limiter = RateLimiter() + +# ---------------- Initialization ---------------- +def initialize_clients(): + global account_api, trade_api, market_api + if Account is None or Trade is None or MarketData is None: + log_action("初始化", "OKX SDK 未安装,请 pip install python-okx", "error") + raise RuntimeError("OKX SDK not installed") + account_api = Account.AccountAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, OKX_FLAG) + trade_api = Trade.TradeAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, OKX_FLAG) + market_api = MarketData.MarketAPI(API_KEY, SECRET_KEY, PASSPHRASE, False, OKX_FLAG) + log_action("初始化", f"OKX SDK 已初始化 (flag={OKX_FLAG})", "info") + + +# ---------------- REST helpers ---------------- +async def fetch_instrument_info(): + global CONTRACT_INFO, TICK_SIZE, SYMBOL + await rate_limiter.wait_for(1, max_per_window=20, window_seconds=2) + try: + resp = await asyncio.to_thread(account_api.get_instruments, instType=CONTRACT_INFO.get("instType", "SWAP"), instId=SYMBOL) + except TypeError: + resp = await asyncio.to_thread(account_api.get_instruments, CONTRACT_INFO.get("instType", "SWAP"), SYMBOL) + log_action("合约", "拿到合约信息", "debug", {"resp_code": resp.get("code") if isinstance(resp, dict) else None}) + if not isinstance(resp, dict) or str(resp.get("code", "")) != "0" or not resp.get("data"): + log_action("合约", "获取合约信息失败", "warning", resp) + return False + data = resp.get("data", []) + found = None + for it in data: + if it.get("instId") == SYMBOL: + found = it + break + if not found: + for it in data: + if it.get("instId", "").startswith(SYMBOL.split("-")[0]) and "USDT" in it.get("instId", ""): + found = it + break + if not found: + log_action("合约", "没有找到合约信息", "error", {"symbol": SYMBOL}) + return False + CONTRACT_INFO["minSz"] = safe_float(found.get("minSz", CONTRACT_INFO.get("minSz", 0))) + CONTRACT_INFO["tickSz"] = safe_float(found.get("tickSz", CONTRACT_INFO.get("tickSz", 0))) + CONTRACT_INFO["ctVal"] = safe_float(found.get("ctVal", CONTRACT_INFO.get("ctVal", 0))) + CONTRACT_INFO["ctValCcy"] = found.get("ctValCcy", CONTRACT_INFO.get("ctValCcy")) + TICK_SIZE = CONTRACT_INFO["tickSz"] + log_action("合约", "合约信息更新成功", "info", CONTRACT_INFO) + return True + + +async def update_price_from_rest(): + global current_price, last_price, price_source + await rate_limiter.wait_for(1) + try: + resp = await asyncio.to_thread(market_api.get_ticker, instId=SYMBOL) + except TypeError: + resp = await asyncio.to_thread(market_api.get_ticker, SYMBOL) + if isinstance(resp, dict) and str(resp.get("code", "")) in ("0", 0) and resp.get("data"): + d = resp["data"][0] + p = None + for k in ("last", "lastPx", "price"): + if k in d: + p = safe_float(d.get(k)) + break + if p is not None: + last_price = current_price + current_price = p + price_source = "rest" + log_action("价格", f"REST 价格更新 {current_price}", "debug") + return True + return False + + +async def fetch_account_and_positions(): + global account_equity_usdt, initial_equity_usdt + await rate_limiter.wait_for(1) + try: + resp = await asyncio.to_thread(account_api.get_account_balance, ccy="") + except TypeError: + resp = await asyncio.to_thread(account_api.get_account_balance, "") + log_action("账户", "余额获取(简略)", "debug") + try: + if isinstance(resp, dict) and resp.get("data"): + total = 0 + for grp in resp.get("data", []): + for d in grp.get("details", []) if grp.get("details") else []: + if (d.get("ccy") or d.get("currency") or "").upper() == "USDT": + total += safe_float(d.get("eq", d.get("availEq", 0)), 0.0) + if total > 0: + account_equity_usdt = float(total) + if initial_equity_usdt == 0.0: + initial_equity_usdt = account_equity_usdt + except Exception: + pass + try: + resp2 = await asyncio.to_thread(account_api.get_positions, instType=CONTRACT_INFO.get("instType", "SWAP"), instId=SYMBOL) + log_action("仓位", "仓位查询返回(简略)", "debug", resp2) + except Exception: + pass + + +# ---------------- Order helpers ---------------- +async def place_order_simple(side: str, pos_side: str, ord_type: str, sz: str, px: Optional[str] = None, cl_ord_id: Optional[str] = None): + cl = cl_ord_id or generate_order_id("cl") + try: + szf = float(sz) + except Exception: + szf = float(sz) + minsz = CONTRACT_INFO.get("minSz", 0) or 0.0 + if minsz > 0 and szf < minsz: + szf = minsz + sz_str = str(szf) + px_str = None + if px is not None: + px_str = str(round_price_by_tick(float(px), CONTRACT_INFO.get("tickSz", 0.0))) + req = {"instId": SYMBOL, "tdMode": "isolated", "clOrdId": cl, "side": side, "ordType": ord_type, "sz": sz_str} + if pos_side: + req["posSide"] = pos_side + if px_str: + req["px"] = px_str + await rate_limiter.wait_for(1, max_per_window=60, window_seconds=2) + try: + resp = await asyncio.to_thread(trade_api.place_order, **req) + except Exception as e: + log_action("下单", f"下单异常: {e}", "error", exc_info=True) + prev = active_orders.get(cl, {}) + prev.update({"ord_id": "", "cl": cl, "px": px_str, "sz": sz_str, "state": "error", "raw": str(e)}) + active_orders[cl] = prev + return {"code": "-1", "msg": str(e)} + if isinstance(resp, dict): + data0 = (resp.get("data") or [{}])[0] + ord_id = data0.get("ordId") or "" + prev = active_orders.get(cl, {}) + prev.update({"ord_id": ord_id, "cl": cl, "px": px_str, "sz": sz_str, "state": "accepted", "raw": data0}) + active_orders[cl] = prev + else: + prev = active_orders.get(cl, {}) + prev.update({"ord_id": "", "cl": cl, "px": px_str, "sz": sz_str, "state": "unknown", "raw": resp}) + active_orders[cl] = prev + log_action("下单", f"已提交订单 cl={cl}", "info", resp) + return resp + + +async def cancel_order_by_cl(cl: str): + await rate_limiter.wait_for(1, max_per_window=60, window_seconds=2) + try: + resp = await asyncio.to_thread(trade_api.cancel_order, instId=SYMBOL, clOrdId=cl) + log_action("撤单", f"撤单请求提交 cl={cl}", "info", resp) + return resp + except Exception as e: + log_action("撤单", f"撤单异常: {e}", "error", exc_info=True) + return {"code": "-1", "msg": str(e)} + + +# ---------------- Helper: find other cl in same pair ---------------- +def get_other_cl_of_active_pair(cl: str) -> Optional[str]: + """ + From active_pairs mapping return the other cl in the same pair, or None. + """ + try: + for pid, p in active_pairs.items(): + buy = p.get("buy") or {} + sell = p.get("sell") or {} + if buy.get("cl") == cl: + return sell.get("cl") + if sell.get("cl") == cl: + return buy.get("cl") + except Exception: + return None + return None + + +# ---------------- Exposure helper ---------------- +def net_exposure_exceeds_threshold(threshold_fraction: float = 0.3) -> bool: + """ + Compute an approximate net exposure in USDT and check if it exceeds threshold_fraction of equity. + Uses position_info[*]['usdt_value'] when available, otherwise estimates as pos*avg_px*ctVal. + """ + try: + equity = float(account_equity_usdt or 0.0) + if equity <= 0: + return False + net = 0.0 + for k, v in position_info.items(): + usdt_val = v.get("usdt_value", None) + if usdt_val is None: + pos = safe_float(v.get("pos", 0.0)) + avg_px = safe_float(v.get("avg_px", current_price or 0.0)) + ct = safe_float(v.get("ctVal", CONTRACT_INFO.get("ctVal", 1.0))) + est = pos * avg_px * (ct or 1.0) + usdt_val = est + net += safe_float(usdt_val, 0.0) + return abs(net) > (threshold_fraction * equity) + except Exception: + return False + + +# ---------------- Price ticks buffer helper ---------------- +_price_ticks = deque() + + +def feed_price_tick(price: float) -> None: + """ + Append a price tick (timestamp, price) into the rolling deque _price_ticks. + Keeps data for VOL_WINDOW_SECONDS seconds (used by estimate_short_volatility). + """ + try: + ts = time.time() + _price_ticks.append((ts, float(price))) + while _price_ticks and _price_ticks[0][0] < ts - VOL_WINDOW_SECONDS: + _price_ticks.popleft() + except Exception: + return + + +# ---------------- Reduce positions to safe level (implementation) ---------------- +async def reduce_positions_to_safe_level(target_fraction: float = 0.1): + """ + 将净暴露降到 equity * target_fraction 以内(示例实现)。 + - target_fraction: 目标净暴露占 equity 的比例,例如 0.1 表示 10%。 + 注意:该实现为示例,使用市价/IOC 下单可能导致滑点,请在 testnet 验证并按需改用限价分批。 + """ + try: + equity = float(account_equity_usdt or initial_equity_usdt or 0.0) + if equity <= 0: + log_action("风控", "账户权益未知或为0,无法降仓", "warning") + return + # compute current net exposure (USDT) + net = 0.0 + positions_to_reduce = [] + for k, v in position_info.items(): + # skip balance-only entries + if str(k).startswith("BAL-"): + continue + usdt_val = v.get("usdt_value", None) + if usdt_val is None: + pos = safe_float(v.get("pos", 0.0)) + avg_px = safe_float(v.get("avg_px", current_price or 0.0)) + ct = safe_float(v.get("ctVal", CONTRACT_INFO.get("ctVal", 1.0))) + est = pos * avg_px * (ct or 1.0) + usdt_val = est + net += safe_float(usdt_val, 0.0) + positions_to_reduce.append((k, v)) + target_exposure = equity * target_fraction + if abs(net) <= target_exposure: + log_action("风控", "净暴露在目标范围内,无需降仓", "info", {"net": net, "target": target_exposure}) + return + reduce_amount = abs(net) - target_exposure + log_action("风控", f"开始降仓,目标减仓金额约 {reduce_amount:.4f} USDT", "warning") + remaining = reduce_amount + # Naive reduction: iterate positions and submit opposite market IOC orders until reduce_amount <= 0 + for (k, v) in positions_to_reduce: + inst = k.split("-")[0] if "-" in k else SYMBOL + pos_size = safe_float(v.get("pos", 0.0)) + if pos_size == 0: + continue + avg_px = safe_float(v.get("avg_px", current_price or 0.0)) + notional = abs(pos_size) * avg_px * v.get("ctVal", CONTRACT_INFO.get("ctVal", 1.0)) if avg_px and pos_size else 0.0 + if pos_size > 0: + side = "sell" + else: + side = "buy" + proportion = min(1.0, remaining / (notional + 1e-12)) + sz_to_close = abs(pos_size) * proportion + sz_s = str(round_to_min_size(sz_to_close)) + if float(sz_s) <= 0: + continue + try: + await rate_limiter.wait_for(1, max_per_window=60, window_seconds=2) + resp = await asyncio.to_thread(trade_api.place_order, instId=inst, tdMode="isolated", side=side, ordType="market", sz=sz_s) + log_action("风控", f"降仓下单 inst={inst} side={side} sz={sz_s}", "info", resp) + except Exception as e: + log_action("风控", f"降仓下单异常: {e}", "error", exc_info=True) + remaining -= notional * proportion + if remaining <= 0: + break + log_action("风控", "降仓动作完成(示例实现)", "info") + except Exception as e: + log_action("风控", f"降仓异常: {e}", "error", exc_info=True) + + +# ---------------- WebSocket message handlers ---------------- +def _handle_account_ws_entry(entry: dict): + try: + data = entry.get("data", []) or [] + if not data: + return + snapshot = data[0] + total_eq = snapshot.get("totalEq") or snapshot.get("adjEq") or "" + if total_eq: + try: + global account_equity_usdt, initial_equity_usdt + account_equity_usdt = float(total_eq) + if initial_equity_usdt == 0.0: + initial_equity_usdt = account_equity_usdt + except Exception: + pass + details = snapshot.get("details", []) or [] + for d in details: + ccy = d.get("ccy") + if not ccy: + continue + avail = safe_float(d.get("availBal", 0.0)) + eq = safe_float(d.get("eq", 0.0)) + try: + position_info[f"BAL-{ccy}"]["pos"] = avail + position_info[f"BAL-{ccy}"]["avg_px"] = 0.0 + position_info[f"BAL-{ccy}"]["usdt_value"] = eq + except Exception: + position_info[f"BAL-{ccy}"] = {"pos": avail, "avg_px": 0.0, "usdt_value": eq} + log_action("WS账户", "处理 account 推送", "debug", {"totalEq": total_eq}) + except Exception as e: + log_action("WS账户处理", f"异常: {e}", "error", exc_info=True) + + +def _handle_order_ws_entry(entry: dict): + try: + ord_id = entry.get("ordId") or "" + cl = entry.get("clOrdId") or "" + trade_id = entry.get("tradeId") or "" + state = entry.get("state") or "" + fill_sz = safe_float(entry.get("fillSz", 0)) + acc_fill = safe_float(entry.get("accFillSz", 0)) + req_id = entry.get("reqId") or "" + if req_id and req_id in seen_reqids: + return + if req_id: + seen_reqids.add(req_id) + if trade_id: + if trade_id in seen_trade_ids: + return + seen_trade_ids.add(trade_id) + if not trade_id and state == "filled" and ord_id: + if ord_id in seen_filled_ordids: + return + seen_filled_ordids.add(ord_id) + key = cl or ord_id or generate_order_id("ws") + ao = active_orders.get(key, {}) + ao.update({ + "ord_id": ord_id, + "cl": cl, + "state": state, + "fillSz": fill_sz, + "accFillSz": acc_fill, + "tradeId": trade_id, + "raw": entry, + "last_update": time.time() + }) + active_orders[key] = ao + log_action("WS订单", f"更新订单 {key} state={state}", "debug", ao) + if entry.get("tradeId") or str(state).lower() in ("filled", "partially_filled", "partial-filled"): + try: + order_side = (entry.get("side") or "").lower() or "buy" + our_filled_side = order_side + cl_local = cl or ao.get("cl") or "" + async def _update_and_handle(): + async with orders_lock: + ao = active_orders.get(cl_local, {}) + ao.update({"raw_ws": entry, "state_ws": state, "last_update_ws": time.time()}) + active_orders[cl_local] = ao + try: + record_fill_event(our_filled_side, entry) + asyncio.create_task(on_fill_event(cl_local, our_filled_side, entry)) + except Exception: + pass + asyncio.create_task(_update_and_handle()) + except Exception: + pass + except Exception as e: + log_action("WS订单处理", f"异常: {e}", "error", exc_info=True) + + +def _handle_positions_ws_entry(entry: dict): + try: + inst = entry.get("instId") or SYMBOL + pos = safe_float(entry.get("pos", 0)) + pos_side = (entry.get("posSide") or "net").lower() + if pos_side == "net": + side = "long" if pos >= 0 else "short" + size = abs(pos) + else: + side = pos_side + size = abs(pos) + key = f"{inst}-{side}" + # store pos and avg_px + avg_px = safe_float(entry.get("avgPx", 0)) + position_info[key]["pos"] = pos if pos_side == "net" else size + position_info[key]["avg_px"] = avg_px + # compute signed usdt_value: long positive, short negative + try: + ct = safe_float(entry.get("ctVal", CONTRACT_INFO.get("ctVal", 1.0))) + except Exception: + ct = CONTRACT_INFO.get("ctVal", 1.0) or 1.0 + # signed exposure: pos (can be negative) * avg_px * ct + signed_usdt = float(pos) * float(avg_px or current_price or 0.0) * float(ct) + position_info[key]["usdt_value"] = signed_usdt + position_info[key]["ctVal"] = ct + log_action("WS仓位", f"{key} -> {size}", "debug", position_info[key]) + except Exception as e: + log_action("WS仓位处理", f"异常: {e}", "error", exc_info=True) + + +def _handle_balance_ws_entry(entry: dict): + log_action("WS余额", "收到 balance_and_position 更新", "debug", entry) + + +def _handle_fills_ws_entry(entry: dict): + trade_id = entry.get("tradeId") + if not trade_id: + return + if trade_id in seen_trade_ids: + return + seen_trade_ids.add(trade_id) + ord_id = entry.get("ordId") or "" + cl = entry.get("clOrdId") or "" + key = cl or ord_id + ao = active_orders.get(key, {}) + ao.update({ + "tradeId": trade_id, + "fillSz": safe_float(entry.get("fillSz", 0)), + "fillPx": safe_float(entry.get("fillPx", 0)), + "last_update": time.time() + }) + active_orders[key] = ao + try: + record_fill_event(entry.get("side") or "buy", entry) + asyncio.create_task(on_fill_event(cl or ao.get("cl",""), entry.get("side") or "buy", entry)) + except Exception: + pass + + +def _ws_message_callback(message: Any): + try: + data = message + if isinstance(message, str): + try: + data = json.loads(message) + except Exception: + log_action("WS", "非 JSON 消息", "debug", {"raw": message}) + return + if "event" in data and data.get("event"): + log_action("WS 事件", f"event={data.get('event')}", "debug", data.get("arg")) + arg = data.get("arg") or {} + channel = arg.get("channel") or data.get("channel") + if channel == "orders": + payloads = data.get("data", []) or [] + if isinstance(payloads, dict): + payloads = [payloads] + for p in payloads: + _handle_order_ws_entry(p) + return + if channel == "positions": + payloads = data.get("data", []) or [] + if isinstance(payloads, dict): + payloads = [payloads] + for p in payloads: + _handle_positions_ws_entry(p) + return + if channel == "balance_and_position": + payloads = data.get("data", []) or [] + for p in payloads: + _handle_balance_ws_entry(p) + return + if channel == "fills": + payloads = data.get("data", []) or [] + for p in payloads: + _handle_fills_ws_entry(p) + return + if channel == "account": + _handle_account_ws_entry(data) + return + log_action("WS 未知消息", "未处理的频道/消息", "debug", data) + except Exception as e: + log_action("WS 回调", f"异常: {e}", "error", exc_info=True) + + +# ---------------- WS keepalive ---------------- +_ws_last_recv = time.time() +_ws_ping_task: Optional[asyncio.Task] = None +_WS_PING_INTERVAL = 20 +_WS_PONG_WAIT = 5 + + +def _ws_mark_recv(): + global _ws_last_recv + _ws_last_recv = time.time() + + +async def _ws_ping_loop(get_ws_callable, interval=_WS_PING_INTERVAL, pong_wait=_WS_PONG_WAIT): + """ + Robust ping/pong loop: + - Try multiple ways to send ping (ws.ping, inner._ws, ws.ws, fallback). + - If cannot send or no pong received within pong_wait, attempt reconnect with simple backoff. + - Avoid raising out of loop; handle exceptions and continue. + """ + backoff_seconds = 1.0 + max_backoff = 30.0 + try: + while True: + await asyncio.sleep(interval) + ws = get_ws_callable() + if ws is None: + # no instance, try reconnect with backoff + log_action("WS 保活", "没有 WS 实例,等待并重试", "debug") + await asyncio.sleep(backoff_seconds) + backoff_seconds = min(max_backoff, backoff_seconds * 1.5) + continue + # reset backoff when we have an instance + backoff_seconds = 1.0 + # 近期已有消息则跳过 ping + if time.time() - _ws_last_recv < interval: + continue + try: + sent = False + # 1) prefer coroutine ping() + if hasattr(ws, "ping") and asyncio.iscoroutinefunction(getattr(ws, "ping")): + await ws.ping() + sent = True + log_action("WS 保活", "通过 ws.ping() 发送 ping", "debug") + elif hasattr(ws, "ping") and callable(getattr(ws, "ping")): + await asyncio.to_thread(ws.ping) + sent = True + log_action("WS 保活", "通过 ws.ping() (sync) 发送 ping", "debug") + # 2) inner attributes commonly used by wrappers + elif hasattr(ws, "_ws"): + inner = getattr(ws, "_ws") + if inner is not None: + if hasattr(inner, "ping"): + if asyncio.iscoroutinefunction(getattr(inner, "ping")): + await inner.ping() + else: + await asyncio.to_thread(inner.ping) + sent = True + log_action("WS 保活", "通过 ws._ws.ping() 发送 ping", "debug") + elif hasattr(inner, "send"): + if asyncio.iscoroutinefunction(getattr(inner, "send")): + await inner.send("ping") + else: + await asyncio.to_thread(inner.send, "ping") + sent = True + log_action("WS 保活", "通过 ws._ws.send() 发送 ping", "debug") + elif hasattr(ws, "ws"): + inner = getattr(ws, "ws") + if inner is not None: + if hasattr(inner, "ping"): + if asyncio.iscoroutinefunction(getattr(inner, "ping")): + await inner.ping() + else: + await asyncio.to_thread(inner.ping) + sent = True + log_action("WS 保活", "通过 ws.ws.ping() 发送 ping", "debug") + elif hasattr(inner, "send"): + if asyncio.iscoroutinefunction(getattr(inner, "send")): + await inner.send("ping") + else: + await asyncio.to_thread(inner.send, "ping") + sent = True + log_action("WS 保活", "通过 ws.ws.send() 发送 ping", "debug") + # 3) fallback to ws.send if exists + elif hasattr(ws, "send"): + if asyncio.iscoroutinefunction(getattr(ws, "send")): + await ws.send("ping") + else: + await asyncio.to_thread(ws.send, "ping") + sent = True + log_action("WS 保活", "通过 ws.send() 发送 ping (fallback)", "debug") + + if not sent: + # 无法发送 ping:记录并尝试重建连接(但用 backoff 防止风暴) + log_action("WS 保活", "WS 实例不支持发送 ping (no ping/send/_ws/ws)", "warning") + try: + await stop_private_ws() + except Exception: + pass + await asyncio.sleep(backoff_seconds) + asyncio.create_task(start_private_ws()) + backoff_seconds = min(max_backoff, backoff_seconds * 1.5) + continue + except Exception as e: + # 发送失败:记录并重连(带 backoff) + log_action("WS 保活", f"发送 ping 失败: {e}", "warning", exc_info=True) + try: + await stop_private_ws() + except Exception: + pass + await asyncio.sleep(backoff_seconds) + asyncio.create_task(start_private_ws()) + backoff_seconds = min(max_backoff, backoff_seconds * 1.5) + continue + + # 等待 pong_wait,看是否有任何消息/心跳到达 + await asyncio.sleep(pong_wait) + if time.time() - _ws_last_recv >= pong_wait: + log_action("WS 保活", "未收到 pong/消息,重连", "warning") + try: + await stop_private_ws() + except Exception: + pass + await asyncio.sleep(backoff_seconds) + asyncio.create_task(start_private_ws()) + backoff_seconds = min(max_backoff, backoff_seconds * 1.5) + except asyncio.CancelledError: + return + except Exception as e: + log_action("WS 保活", f"异常: {e}", "error", exc_info=True) + + +# ---------------- Public / Private WS start/stop ---------------- +async def start_private_ws(): + global _ws_instance, _ws_ping_task + if PrivateWs is None: + log_action("WS", "PrivateWs SDK 未安装,无法启动私有 WS", "error") + return None + try: + if str(OKX_FLAG) == "1": + ws_url = "wss://wspap.okx.com:8443/ws/v5/private" + else: + ws_url = "wss://ws.okx.com:8443/ws/v5/private" + log_action("WS", f"Connecting private WS to {ws_url}", "info") + ws = PrivateWs(apiKey=API_KEY, passphrase=PASSPHRASE, secretKey=SECRET_KEY, url=ws_url, useServerTime=False) + await ws.start() + _ws_instance = ws + log_action("WS", "私有 WS 已启动", "info") + args = [ + {"channel": "positions", "instType": "ANY"}, + {"channel": "balance_and_position"}, + {"channel": "orders", "instType": "ANY"}, + {"channel": "account"} + ] + if ENABLE_FILLS_CHANNEL: + args.append({"channel": "fills"}) + await ws.subscribe(args, callback=_ws_message_callback) + log_action("WS", "已订阅私有频道", "info", {"args": args}) + if _ws_ping_task is None or _ws_ping_task.done(): + _ws_ping_task = asyncio.create_task(_ws_ping_loop(lambda: _ws_instance)) + return ws + except Exception as e: + log_action("WS 启动", f"失败: {e}", "error", exc_info=True) + return None + + +async def stop_private_ws(): + global _ws_instance, _ws_ping_task + try: + if _ws_instance: + try: + await _ws_instance.stop() + except Exception: + pass + _ws_instance = None + if _ws_ping_task: + _ws_ping_task.cancel() + _ws_ping_task = None + log_action("WS", "私有 WS 已停止", "info") + except Exception as e: + log_action("WS 停止", f"异常: {e}", "warning", exc_info=True) + + +async def start_public_ws(): + global _public_ws_instance + if PublicWs is None: + log_action("Public WS", "PublicWs SDK 未安装,跳过", "warning") + return None + try: + if str(OKX_FLAG) == "1": + public_url = "wss://wspap.okx.com:8443/ws/v5/public" + else: + public_url = "wss://ws.okx.com:8443/ws/v5/public" + ws = PublicWs(url=public_url) + await ws.start() + _public_ws_instance = ws + args = [{"channel": "tickers", "instId": SYMBOL}] + await ws.subscribe(args, callback=_public_ws_ticker_callback) + log_action("Public WS", "已订阅 tickers", "info", {"inst": SYMBOL, "url": public_url}) + return ws + except Exception as e: + log_action("Public WS", f"启动失败: {e}", "error", exc_info=True) + return None + + +async def stop_public_ws(): + global _public_ws_instance + try: + if _public_ws_instance: + try: + await _public_ws_instance.stop() + except Exception: + pass + _public_ws_instance = None + log_action("Public WS", "已停止", "info") + except Exception as e: + log_action("Public WS 停止", f"异常: {e}", "warning", exc_info=True) + + +def _public_ws_ticker_callback(message: Any): + global current_price, last_price, price_source, best_bid, best_ask + try: + data = message + if isinstance(message, str): + try: + data = json.loads(message) + except Exception: + return + if "event" in data: + return + arg = data.get("arg") or {} + channel = arg.get("channel") or data.get("channel") + if channel != "tickers": + return + payloads = data.get("data", []) or [] + if isinstance(payloads, dict): + payloads = [payloads] + for p in payloads: + price = None + for k in ("last", "lastPx", "price"): + if k in p: + price = safe_float(p.get(k)) + break + if price is None: + bid = safe_float(p.get("bidPx", 0)) + ask = safe_float(p.get("askPx", 0)) + if bid and ask: + price = (bid + ask) / 2.0 + elif bid: + price = bid + elif ask: + price = ask + if price is None: + continue + best_bid = safe_float(p.get("bidPx", best_bid)) + best_ask = safe_float(p.get("askPx", best_ask)) + last_price = current_price + current_price = float(price) + price_source = "ws_ticker" + try: + feed_price_tick(current_price) + except Exception: + pass + except Exception: + pass + + +# ---------------- Strategy helpers (continuous-eaten, gatekeeper, dynamic offset) ---------------- +N_CONSEC = 3 +WINDOW_SECONDS = 60 +MIN_FILLS_WINDOW = 5 +P_THRESHOLD = 0.7 +PAUSE_SECONDS_AFTER_CONSEC = 120 +SCALE_DOWN_FACTOR = 0.3 + +SAFETY_MARGIN_USDT = 0.5 +MAX_REQUIRED_MOVE_PCT = 0.015 + +MIN_OFFSET_PCT = 0.0008 +VOL_K = 1.0 +VOL_WINDOW_SECONDS = 60 + +_recent_fills = deque() +_consec_same_side = 0 +_last_fill_side: Optional[str] = None +_paused_until = 0.0 +_trend_watch_until = 0.0 +_mark_scale_down_next = False + +_price_ticks = deque() + +def record_fill_event(side: str, fill_info: Dict[str, Any]) -> None: + global _consec_same_side, _last_fill_side, _recent_fills + ts = time.time() + if _last_fill_side == side: + _consec_same_side += 1 + else: + _consec_same_side = 1 + _last_fill_side = side + _recent_fills.append((ts, side, fill_info)) + while _recent_fills and _recent_fills[0][0] < ts - WINDOW_SECONDS: + _recent_fills.popleft() + +def check_window_rule() -> bool: + if len(_recent_fills) < MIN_FILLS_WINDOW: + return False + same = sum(1 for (_, s, _) in _recent_fills if s == _last_fill_side) + frac = same / len(_recent_fills) + return frac >= P_THRESHOLD + +async def on_fill_event(cl: str, side: str, fill_info: Dict[str, Any]): + global _paused_until, _trend_watch_until, _mark_scale_down_next, _consec_same_side + record_fill_event(side, fill_info) + window_trigger = check_window_rule() + if _consec_same_side >= N_CONSEC or window_trigger: + _paused_until = time.time() + PAUSE_SECONDS_AFTER_CONSEC + _trend_watch_until = max(_trend_watch_until, time.time() + PAUSE_SECONDS_AFTER_CONSEC) + _mark_scale_down_next = True + try: + other_cl = get_other_cl_of_active_pair(cl) + if other_cl: + await cancel_order_by_cl(other_cl) + except Exception: + pass + try: + log_action("风控", f"连续被吃保护触发 side={side} consec={_consec_same_side}", "warning", {"fill": fill_info}) + except Exception: + pass + try: + if net_exposure_exceeds_threshold(): + asyncio.create_task(reduce_positions_to_safe_level()) + except Exception: + pass + return + return + +def is_paused() -> bool: + return time.time() < _paused_until + +def mark_and_consume_scale_down() -> bool: + global _mark_scale_down_next + if _mark_scale_down_next: + _mark_scale_down_next = False + return True + return False + +def should_place_pair(position_value: float, + fee_maker: float, + fee_taker: float, + funding_rate_per_8h: float, + expected_hold_seconds: float, + expected_slippage_pct: float, + safety_margin_usdt: float = SAFETY_MARGIN_USDT, + max_required_move_pct: float = MAX_REQUIRED_MOVE_PCT) -> Dict[str, Any]: + roundtrip_fee_usdt = position_value * (fee_maker + fee_maker) + funding_usdt = position_value * funding_rate_per_8h * (expected_hold_seconds / (8*3600)) + slippage_usdt = position_value * expected_slippage_pct + total_cost_usdt = roundtrip_fee_usdt + funding_usdt + slippage_usdt + safety_margin_usdt + required_move_pct = total_cost_usdt / position_value + can_place = (required_move_pct < max_required_move_pct) + return { + "roundtrip_fee_usdt": roundtrip_fee_usdt, + "funding_usdt": funding_usdt, + "slippage_usdt": slippage_usdt, + "total_cost_usdt": total_cost_usdt, + "required_move_pct": required_move_pct, + "can_place": can_place + } + +# ---------------- Main loops ---------------- +async def main_loop_once(): + ok = await fetch_instrument_info() + if not ok: + log_action("主流程", "未能加载合约信息,退出", "error") + return + await update_price_from_rest() + await fetch_account_and_positions() + log_action("主流程", "一次循环完成", "info") + + +async def main_loop_continuous(): + private_task = asyncio.create_task(start_private_ws()) + public_task = None + if ENABLE_PUBLIC_TICKER: + public_task = asyncio.create_task(start_public_ws()) + try: + while True: + try: + await main_loop_once() + except Exception as e: + log_action("主循环", f"异常: {e}", "error", exc_info=True) + await asyncio.sleep(5) + finally: + try: + await stop_private_ws() + except Exception: + pass + if public_task: + try: + await stop_public_ws() + except Exception: + pass + + +# ---------------- Entrypoint ---------------- +if __name__ == "__main__": + initialize_clients() + log_action("启动", f"OKX_FLAG={OKX_FLAG} (测试网=1)", "info") + asyncio.run(main_loop_continuous()) 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()